diff --git a/CHANGELOG.md b/CHANGELOG.md index 1661439..6563af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 82a59cd..d13b8c6 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,10 @@ dotpm --url http://127.0.0.1:27140 --token status | Command | Does | | --- | --- | -| `projects` | List every project | +| `projects [--archived]` | List every project | | `project [--tasks] [--archived]` | One project: team, custom fields, allowed status and priority ids | | `create-project --title ` | New project at the root or under `--parent` | +| `archive-project [--restore]` | Archive a project and its sub-projects, or bring it back | | `tasks [--archived]` | A project's tasks in tree order | | `task ` | One task with its description | | `search [text] [--project] [--status] [--assignee] [--archived] [--limit]` | Tasks across every project | diff --git a/package.json b/package.json index 50ba13a..71b5202 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28f7246..0417037 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: 4.0.0 version: 4.0.0(typescript@7.0.2) '@dotpm/api': - specifier: 0.1.0 - version: 0.1.0(typescript@7.0.2) + specifier: 0.2.0 + version: 0.2.0(typescript@7.0.2) '@types/node': specifier: 26.5.1 version: 26.5.1 @@ -80,11 +80,11 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@dotpm/api@0.1.0': - resolution: {integrity: sha512-BGMEKuUZMnLIUqpCj4sKZs7PDMsIX660uowFuZeBHcQ42Yhep/w700LDXQpb5tombm/Tqw/Z3v2zBTl+uVMC8A==} + '@dotpm/api@0.2.0': + resolution: {integrity: sha512-ZyL7UMQwV2eXIWx/gjR1MJD8azVLjifLgMnlgCHAPMMfskJC80zweHlgtGVkV4hV6DxaOD7+Ul43m5lCYzVUIg==} - '@dotpm/core@0.1.0': - resolution: {integrity: sha512-cmNpUAAOTAK7NZbIp/HiX9jWqx3p+2JVFaGqzEP42727QJ7O5tdfxbMZgpOnKsaqlDbccGkOmts/+vn8ah5wRA==} + '@dotpm/core@0.2.0': + resolution: {integrity: sha512-r+WAfNrFan2dnlgAczco2/hX+I0EHbErpxMtl/VL/I1dFA/sZCE/vidn982FT4PuiN0ryJph2GlwcPaptLgk3A==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} @@ -1304,9 +1304,9 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@dotpm/api@0.1.0(typescript@7.0.2)': + '@dotpm/api@0.2.0(typescript@7.0.2)': dependencies: - '@dotpm/core': 0.1.0 + '@dotpm/core': 0.2.0 '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@7.0.2)) hono: 4.13.7 mcp-lite: 0.10.0 @@ -1314,7 +1314,7 @@ snapshots: transitivePeerDependencies: - typescript - '@dotpm/core@0.1.0': + '@dotpm/core@0.2.0': dependencies: temporal-polyfill: 1.0.4 diff --git a/skills/dotpm/SKILL.md b/skills/dotpm/SKILL.md index b68d553..3c50095 100644 --- a/skills/dotpm/SKILL.md +++ b/skills/dotpm/SKILL.md @@ -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. @@ -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 @@ -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 ` | | `create_project` | `dotpm create-project --title [--description ] [--icon ] [--color ] [--member ]... [--parent ]` | +| `archive_project` | `dotpm archive-project [--restore]` | | `list_tasks` | `dotpm tasks ` | | `get_task` | `dotpm task ` | | `search_tasks` | `dotpm search [text] [--project ] [--status ] [--assignee ]` | diff --git a/source/cli.test.ts b/source/cli.test.ts index 3d1b534..de8e830 100644 --- a/source/cli.test.ts +++ b/source/cli.test.ts @@ -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"}') diff --git a/source/cli.ts b/source/cli.ts index 4d43dbd..d80a4ea 100644 --- a/source/cli.ts +++ b/source/cli.ts @@ -189,8 +189,11 @@ const COMMANDS: Record = { 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: '', @@ -241,6 +244,15 @@ const COMMANDS: Record = { return { kind: 'project', value: await api.createProject(body) } } }, + 'archive-project': { + args: '', + 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: '', summary: "List a project's tasks in tree order", diff --git a/source/render.ts b/source/render.ts index d38da9d..958b4c2 100644 --- a/source/render.ts +++ b/source/render.ts @@ -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(', ')], @@ -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)