Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added the `archive-project` command to archive a project with its sub-projects, or restore it with `--restore`
- Added `--archived` to `projects`; archived projects are left out otherwise

## [0.1.1] - 2026-09-15

### Changed
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ dotpm --url http://127.0.0.1:27140 --token <token> status

| Command | Does |
| --- | --- |
| `projects` | List every project |
| `projects [--archived]` | List every project |
| `project <id> [--tasks] [--archived]` | One project: team, custom fields, allowed status and priority ids |
| `create-project --title <text>` | New project at the root or under `--parent` |
| `archive-project <id> [--restore]` | Archive a project and its sub-projects, or bring it back |
| `tasks <projectId> [--archived]` | A project's tasks in tree order |
| `task <id>` | One task with its description |
| `search [text] [--project] [--status] [--assignee] [--archived] [--limit]` | Tasks across every project |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"devDependencies": {
"@2bad/axiom": "2.1.0",
"@2bad/tsconfig": "4.0.0",
"@dotpm/api": "0.1.0",
"@dotpm/api": "0.2.0",
"@types/node": "26.5.1",
"@vitest/coverage-v8": "5.0.0",
"npm-run-all2": "9.0.3",
Expand Down
18 changes: 9 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions skills/dotpm/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Projects and tasks in this vault are Markdown notes managed by the dotpm Obsidia

- Don't create, edit, rename, move or delete notes in a `_tasks` folder, and don't edit a project note's frontmatter, even with file access. The plugin keeps ids, parent and subtask links, positions and the archive folder in step across several notes. A hand edit breaks that.
- Read the project before writing to it. `get_project` lists the status and priority ids its tasks may carry, plus its custom fields and team. Use only those ids. Don't assume `done` or `high` exist.
- `create_project` is the only project write. It starts with the global statuses and priorities. Renaming a project or changing its palettes and custom fields is done in Obsidian.
- `create_project` and `archive_project` are the only project writes. A new project starts with the global statuses and priorities. Renaming a project or changing its palettes and custom fields is done in Obsidian.
- Archiving a project takes its sub-projects with it, and `list_projects` leaves archived projects out unless `includeArchived` (`--archived`) is set. Only the project that was archived itself can be brought back; a sub-project follows its parent.
- Take ids from earlier results. Don't build ids or paths yourself.
- A refused write names the allowed values. Fix the call and retry. Don't edit the note instead.
- `assignees`, `tags` and `dependencies` replace the whole list. To add one tag, send every tag the task should have.
Expand All @@ -25,7 +26,7 @@ Projects and tasks in this vault are Markdown notes managed by the dotpm Obsidia
1. `list_projects` to find the project, or `create_project` when it does not exist yet, at the root or under `parentId`.
2. `get_project` for its status ids, priority ids, custom fields and team.
3. Find the tasks. `list_tasks` when you know the project and need the tree, with `parentId` and `position`. `search_tasks` when you only have a title, a status or an assignee, or the project is unknown. `get_task` only when you need the description. Both lists leave archived tasks out unless `includeArchived` (`--archived`) is set.
4. Make the change. `create_task` for a new task, at the top level or under `parentId`. `update_task` to change fields in place. `move_task` to re-parent (`parentId`, null for top level), move to another project or reorder among siblings with `before` or `after`. `archive_task` to take a task and its subtasks out of the way, `archived: false` to bring them back.
4. Make the change. `create_task` for a new task, at the top level or under `parentId`. `update_task` to change fields in place. `move_task` to re-parent (`parentId`, null for top level), move to another project or reorder among siblings with `before` or `after`. `archive_task` to take a task and its subtasks out of the way, `archived: false` to bring them back. `archive_project` when a whole project is done or no longer relevant.
5. Tell the user what changed, by task title.

## Commands
Expand All @@ -34,9 +35,10 @@ The same operations from a terminal:

| MCP tool | Command |
| --- | --- |
| `list_projects` | `dotpm projects` |
| `list_projects` | `dotpm projects [--archived]` |
| `get_project` | `dotpm project <projectId>` |
| `create_project` | `dotpm create-project --title <text> [--description <markdown>] [--icon <emoji>] [--color <hex>] [--member <person>]... [--parent <projectId>]` |
| `archive_project` | `dotpm archive-project <projectId> [--restore]` |
| `list_tasks` | `dotpm tasks <projectId>` |
| `get_task` | `dotpm task <taskId>` |
| `search_tasks` | `dotpm search [text] [--project <id>] [--status <id>] [--assignee <person>]` |
Expand Down
12 changes: 12 additions & 0 deletions source/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ describe('runCli', () => {
expect(JSON.parse(matched.err)).toEqual({ error: { code: 'conflict', message: 'task changed' } })
})

it('archives a project and lists archived projects only when asked', async () => {
expect((await run(['archive-project', 'p1'])).json).toMatchObject({ id: 'p1', archived: true })
expect(host.calls.at(-1)).toBe('archiveProject p1 true')
expect((await run(['projects'])).json).toEqual([])
expect((await run(['projects', '--archived'])).json).toMatchObject([{ id: 'p1', archived: true }])
tty = true
expect((await run(['projects', '--archived'])).out).toContain('Demo (archived)')
expect((await run(['project', 'p1'])).out).toContain('archived yes')
expect((await run(['--json', 'archive-project', 'p1', '--restore'])).json).toMatchObject({ archived: false })
expect((await run(['projects'])).out).toContain('p1 1/2 Demo')
})

it('moves, archives and deletes', async () => {
await run(['move', 't2', '--top', '--after', 't1'])
expect(host.calls.at(-1)).toBe('moveTask t2 {"parentId":null,"after":"t1"}')
Expand Down
16 changes: 14 additions & 2 deletions source/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,11 @@ const COMMANDS: Record<string, Command> = {
projects: {
args: '',
summary: 'List every project',
flags: {},
run: async ({ api }) => ({ kind: 'projects', value: await api.listProjects() })
flags: { archived: { type: 'boolean', help: 'Include archived projects' } },
run: async ({ api }, _positionals, values) => ({
kind: 'projects',
value: await api.listProjects(values['archived'] === true)
})
},
project: {
args: '<projectId>',
Expand Down Expand Up @@ -241,6 +244,15 @@ const COMMANDS: Record<string, Command> = {
return { kind: 'project', value: await api.createProject(body) }
}
},
'archive-project': {
args: '<projectId>',
summary: 'Archive a project and its sub-projects, or bring it back',
flags: { restore: { type: 'boolean', help: 'Unarchive instead' } },
run: async ({ api }, positionals, values) => ({
kind: 'project',
value: await api.archiveProject(need(positionals, 0, 'projectId'), values['restore'] !== true)
})
},
tasks: {
args: '<projectId>',
summary: "List a project's tasks in tree order",
Expand Down
8 changes: 7 additions & 1 deletion source/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function project(value: ProjectResource & { tasks?: TaskResource[] }): string {
['title', value.title],
['path', value.path],
['parent', value.parentId ?? ''],
['archived', value.archived ? 'yes' : 'no'],
['tasks', `${value.doneCount} of ${value.taskCount} done`],
['team', value.teamMembers.join(', ')],
['statuses', value.statuses.map((status) => status.id).join(', ')],
Expand Down Expand Up @@ -108,7 +109,12 @@ export function render(output: Output): string {
? 'no projects'
: table(
['ID', 'TASKS', 'TITLE', 'PATH'],
output.value.map((p) => [p.id, `${p.doneCount}/${p.taskCount}`, p.title, p.path])
output.value.map((p) => [
p.id,
`${p.doneCount}/${p.taskCount}`,
`${p.title}${p.archived ? ' (archived)' : ''}`,
p.path
])
)
case 'project':
return project(output.value)
Expand Down
Loading