opencode-backlog 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,22 +4,30 @@ A persistent project backlog for OpenCode V2 agents and the TUI.
4
4
 
5
5
  `opencode-backlog` gives the agent tools to manage project tasks. It also adds an interactive backlog to the OpenCode sidebar and command palette.
6
6
 
7
+ Published package: [opencode-backlog on npm](https://www.npmjs.com/package/opencode-backlog).
8
+
7
9
  ![Backlog sidebar in OpenCode](docs/images/opencode-backlog.jpg)
8
10
 
9
11
  ## What It Does
10
12
 
11
- The plugin stores tasks in `BACKLOG.json` at the project root. Each task has a title, optional notes, and one Kanban state:
13
+ The plugin stores tasks and ordered categories in `BACKLOG.json` at the project root.
14
+
15
+ New backlogs contain these categories:
12
16
 
13
17
  - `todo`
14
18
  - `doing`
15
19
  - `done`
16
20
 
17
- The server plugin lets the agent list, add, edit, move, reorder, and remove tasks. The TUI plugin provides:
21
+ You can add, rename, reorder, purge, and remove categories. Each category has a stable ID and a modifiable title.
22
+
23
+ The server plugin manages tasks and categories. The TUI plugin provides:
18
24
 
19
25
  - A live backlog summary below the sidebar context.
20
26
  - A `Browse backlog` command in the command palette.
21
27
  - `/backlog` and `/tasks` slash commands.
22
- - Task details and status changes from the TUI.
28
+ - Task details and category changes from the TUI.
29
+ - `/backlog-purge` to remove every task from a selected category.
30
+ - `/backlog-categories` to manage categories.
23
31
 
24
32
  ## Requirements
25
33
 
@@ -119,35 +127,57 @@ Reopen the TUI to load the TUI plugin.
119
127
  Ask the agent to manage tasks in normal language:
120
128
 
121
129
  ```text
122
- Add a todo task to document the release process.
123
- Move the release task to doing.
124
- List only the todo tasks.
125
- Mark the current task as done.
130
+ Add a Todo task to document the release process.
131
+ Add a Blocked category after Doing.
132
+ Move the release task to Blocked.
133
+ Purge all tasks in Done.
126
134
  ```
127
135
 
128
- Open the command palette and select `Browse backlog` to inspect tasks. Select a task to view its ID, notes, and current state. Then select its new state.
136
+ Open the command palette and select `Browse backlog` to inspect tasks. Select a task to view its ID, notes, and category.
129
137
 
130
138
  Run `/backlog` or `/tasks` to open the same browser directly.
131
139
 
140
+ The backlog browser provides these shortcuts:
141
+
142
+ - `Enter` opens the selected task details.
143
+ - `n` creates a task.
144
+ - `c` changes the selected task category.
145
+ - `e` edits the selected task.
146
+ - `d` deletes the selected task after confirmation.
147
+ - `p` purges all tasks from a selected category after confirmation.
148
+
149
+ Click a sidebar task to open its details. The detail dialog provides `c`, `e`, and `d` for the same actions.
150
+
132
151
  ## Agent Tools
133
152
 
134
153
  | Tool | Purpose |
135
154
  | --- | --- |
136
- | `backlog_list` | List all tasks or filter by `todo`, `doing`, or `done`. |
137
- | `backlog_add` | Add a task at an optional state and position. |
155
+ | `backlog_list` | List categories and tasks, with an optional category ID filter. |
156
+ | `backlog_add` | Add a task at an optional category and position. |
138
157
  | `backlog_update` | Change a task title or notes. |
139
- | `backlog_move` | Change a task state or position. |
158
+ | `backlog_move` | Change a task category or position. |
140
159
  | `backlog_remove` | Permanently remove a task. |
160
+ | `backlog_category_add` | Add a category with a stable ID and title. |
161
+ | `backlog_category_update` | Change a category title. |
162
+ | `backlog_category_move` | Change a category position. |
163
+ | `backlog_category_remove` | Remove an empty category. |
164
+ | `backlog_category_purge` | Permanently remove all tasks from a category. |
141
165
 
142
166
  The agent receives task IDs from `backlog_list` and from each mutation result.
143
167
 
144
168
  ## Backlog File
145
169
 
146
- The plugin creates `BACKLOG.json` when the first task is added:
170
+ The plugin creates `BACKLOG.json` when the first task or category is added:
147
171
 
148
172
  ```json
149
173
  {
150
- "version": 1,
174
+ "version": 2,
175
+ "categories": [
176
+ { "id": "todo", "title": "Todo" },
177
+ { "id": "doing", "title": "Doing" },
178
+ { "id": "blocked", "title": "Blocked" },
179
+ { "id": "done", "title": "Done" }
180
+ ],
151
181
  "items": [
152
182
  {
153
183
  "id": "c50437e3-2a57-424d-9257-32ec432145a9",
@@ -159,7 +189,11 @@ The plugin creates `BACKLOG.json` when the first task is added:
159
189
  }
160
190
  ```
161
191
 
162
- Items stay grouped by state. Their array order defines their position inside each state.
192
+ The category array defines category order. The item array defines task order inside each category.
193
+
194
+ Category IDs stay stable when titles change. The plugin rejects tasks that reference an unknown category ID.
195
+
196
+ The plugin reads version 1 files with the default categories. The next backlog change writes the file as version 2.
163
197
 
164
198
  Commit `BACKLOG.json` when the backlog must follow the project. Ignore it when the backlog must remain local.
165
199
 
package/dist/backlog.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export declare const BACKLOG_FILE = "BACKLOG.json";
2
- export declare const STATUSES: readonly ["todo", "doing", "done"];
3
- export type Status = (typeof STATUSES)[number];
2
+ export interface Category {
3
+ readonly id: string;
4
+ readonly title: string;
5
+ }
6
+ export type Status = string;
4
7
  export interface BacklogItem {
5
8
  readonly id: string;
6
9
  readonly title: string;
@@ -8,12 +11,19 @@ export interface BacklogItem {
8
11
  readonly status: Status;
9
12
  }
10
13
  export interface Backlog {
11
- readonly version: 1;
14
+ readonly version: 2;
15
+ readonly categories: readonly Category[];
12
16
  readonly items: readonly BacklogItem[];
13
17
  }
18
+ export declare const DEFAULT_CATEGORIES: readonly Category[];
14
19
  export declare const EMPTY_BACKLOG: Backlog;
15
- export declare function isStatus(value: unknown): value is Status;
20
+ export declare function isStatus(backlog: Backlog, value: unknown): value is Status;
16
21
  export declare function parseBacklog(value: unknown): Backlog;
17
- export declare function sortByStatus(items: readonly BacklogItem[]): BacklogItem[];
18
- export declare function moveItem(items: readonly BacklogItem[], id: string, status: Status | undefined, position: number | undefined): BacklogItem[];
22
+ export declare function sortByStatus(items: readonly BacklogItem[], categories: readonly Category[]): BacklogItem[];
23
+ export declare function moveItem(items: readonly BacklogItem[], id: string, status: Status | undefined, position: number | undefined, categories: readonly Category[]): BacklogItem[];
19
24
  export declare function describeBacklog(backlog: Backlog, statuses?: readonly Status[]): string;
25
+ export declare function addCategory(backlog: Backlog, category: Category, position?: number): Backlog;
26
+ export declare function renameCategory(backlog: Backlog, id: string, title: string): Backlog;
27
+ export declare function moveCategory(backlog: Backlog, id: string, position: number): Backlog;
28
+ export declare function removeCategory(backlog: Backlog, id: string): Backlog;
29
+ export declare function purgeCategory(backlog: Backlog, id: string): Backlog;
package/dist/backlog.js CHANGED
@@ -1,21 +1,46 @@
1
1
  export const BACKLOG_FILE = "BACKLOG.json";
2
- export const STATUSES = ["todo", "doing", "done"];
3
- export const EMPTY_BACKLOG = { version: 1, items: [] };
2
+ export const DEFAULT_CATEGORIES = [
3
+ { id: "todo", title: "Todo" },
4
+ { id: "doing", title: "Doing" },
5
+ { id: "done", title: "Done" },
6
+ ];
7
+ export const EMPTY_BACKLOG = { version: 2, categories: DEFAULT_CATEGORIES, items: [] };
4
8
  function isRecord(value) {
5
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
6
10
  }
7
- export function isStatus(value) {
8
- return typeof value === "string" && STATUSES.includes(value);
11
+ function validateCategory(category, index) {
12
+ const label = index === undefined ? "Category" : `Backlog category ${index}`;
13
+ if (!isRecord(category))
14
+ throw new Error(`${label} must be an object`);
15
+ if (typeof category.id !== "string" || category.id.trim().length === 0) {
16
+ throw new Error(`${label} must have a non-empty id`);
17
+ }
18
+ if (typeof category.title !== "string" || category.title.trim().length === 0) {
19
+ throw new Error(`${label} must have a non-empty title`);
20
+ }
21
+ return { id: category.id, title: category.title };
9
22
  }
10
- export function parseBacklog(value) {
11
- if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.items)) {
12
- throw new Error("BACKLOG.json must contain a version 1 backlog");
23
+ function validateCategories(value) {
24
+ if (!Array.isArray(value) || value.length === 0) {
25
+ throw new Error("A version 2 backlog must have at least one category");
13
26
  }
14
27
  const ids = new Set();
15
- const items = value.items.map((item, index) => {
28
+ return value.map((value, index) => {
29
+ const category = validateCategory(value, index);
30
+ if (ids.has(category.id))
31
+ throw new Error(`Backlog category id ${category.id} is duplicated`);
32
+ ids.add(category.id);
33
+ return category;
34
+ });
35
+ }
36
+ function validateItems(value, categories) {
37
+ if (!Array.isArray(value))
38
+ throw new Error("Backlog items must be an array");
39
+ const ids = new Set();
40
+ return value.map((item, index) => {
16
41
  if (!isRecord(item))
17
42
  throw new Error(`Backlog item ${index} must be an object`);
18
- if (typeof item.id !== "string" || item.id.length === 0) {
43
+ if (typeof item.id !== "string" || item.id.trim().length === 0) {
19
44
  throw new Error(`Backlog item ${index} must have a non-empty id`);
20
45
  }
21
46
  if (ids.has(item.id))
@@ -26,8 +51,9 @@ export function parseBacklog(value) {
26
51
  if (item.notes !== undefined && typeof item.notes !== "string") {
27
52
  throw new Error(`Backlog item ${item.id} has invalid notes`);
28
53
  }
29
- if (!isStatus(item.status))
54
+ if (typeof item.status !== "string" || !categories.some(({ id }) => id === item.status)) {
30
55
  throw new Error(`Backlog item ${item.id} has an invalid status`);
56
+ }
31
57
  ids.add(item.id);
32
58
  return {
33
59
  id: item.id,
@@ -36,35 +62,109 @@ export function parseBacklog(value) {
36
62
  status: item.status,
37
63
  };
38
64
  });
39
- return { version: 1, items };
40
65
  }
41
- export function sortByStatus(items) {
42
- return STATUSES.flatMap((status) => items.filter((item) => item.status === status));
66
+ export function isStatus(backlog, value) {
67
+ return typeof value === "string" && backlog.categories.some(({ id }) => id === value);
43
68
  }
44
- export function moveItem(items, id, status, position) {
69
+ export function parseBacklog(value) {
70
+ if (!isRecord(value))
71
+ throw new Error("BACKLOG.json must contain a backlog object");
72
+ if (value.version === 1) {
73
+ return {
74
+ version: 2,
75
+ categories: DEFAULT_CATEGORIES,
76
+ items: validateItems(value.items, DEFAULT_CATEGORIES),
77
+ };
78
+ }
79
+ if (value.version !== 2)
80
+ throw new Error(`Unsupported backlog version ${String(value.version)}`);
81
+ const categories = validateCategories(value.categories);
82
+ return { version: 2, categories, items: validateItems(value.items, categories) };
83
+ }
84
+ export function sortByStatus(items, categories) {
85
+ return categories.flatMap(({ id }) => items.filter((item) => item.status === id));
86
+ }
87
+ export function moveItem(items, id, status, position, categories) {
45
88
  const item = items.find((candidate) => candidate.id === id);
46
89
  if (!item)
47
90
  throw new Error(`Backlog item ${id} does not exist`);
48
91
  const targetStatus = status ?? item.status;
92
+ if (!categories.some((category) => category.id === targetStatus)) {
93
+ throw new Error(`Backlog category ${targetStatus} does not exist`);
94
+ }
49
95
  const remaining = items.filter((candidate) => candidate.id !== id);
50
96
  const target = remaining.filter((candidate) => candidate.status === targetStatus);
51
- const targetPosition = Math.min(position ?? target.length, target.length);
52
- target.splice(targetPosition, 0, { ...item, status: targetStatus });
53
- return STATUSES.flatMap((candidateStatus) => candidateStatus === targetStatus
97
+ target.splice(clampPosition(position, target.length), 0, { ...item, status: targetStatus });
98
+ return categories.flatMap(({ id: categoryId }) => categoryId === targetStatus
54
99
  ? target
55
- : remaining.filter((candidate) => candidate.status === candidateStatus));
100
+ : remaining.filter((candidate) => candidate.status === categoryId));
56
101
  }
57
- export function describeBacklog(backlog, statuses = STATUSES) {
58
- if (backlog.items.length === 0 && statuses.length === STATUSES.length) {
59
- return "The project backlog is empty.";
60
- }
61
- return statuses.map((status) => {
102
+ export function describeBacklog(backlog, statuses) {
103
+ const selected = statuses ?? backlog.categories.map(({ id }) => id);
104
+ return selected.map((status) => {
105
+ const category = backlog.categories.find(({ id }) => id === status);
106
+ if (!category)
107
+ throw new Error(`Backlog category ${status} does not exist`);
62
108
  const items = backlog.items.filter((item) => item.status === status);
63
109
  const lines = items.map((item, index) => {
64
110
  const notes = item.notes ? `\n Notes: ${item.notes}` : "";
65
111
  return `${index}. ${item.id}: ${item.title}${notes}`;
66
112
  });
67
- return `${status.toUpperCase()} (${items.length})\n${lines.length > 0 ? lines.join("\n") : "Empty"}`;
113
+ return `${category.title.toUpperCase()} [${category.id}] (${items.length})\n${lines.length > 0 ? lines.join("\n") : "Empty"}`;
68
114
  }).join("\n\n");
69
115
  }
116
+ export function addCategory(backlog, category, position) {
117
+ const added = validateCategory(category);
118
+ if (backlog.categories.some(({ id }) => id === added.id)) {
119
+ throw new Error(`Backlog category id ${added.id} is duplicated`);
120
+ }
121
+ const categories = [...backlog.categories];
122
+ categories.splice(clampPosition(position, categories.length), 0, added);
123
+ return { ...backlog, categories };
124
+ }
125
+ export function renameCategory(backlog, id, title) {
126
+ if (title.trim().length === 0)
127
+ throw new Error("Category must have a non-empty title");
128
+ if (!backlog.categories.some((category) => category.id === id)) {
129
+ throw new Error(`Backlog category ${id} does not exist`);
130
+ }
131
+ return {
132
+ ...backlog,
133
+ categories: backlog.categories.map((category) => category.id === id ? { ...category, title } : category),
134
+ };
135
+ }
136
+ export function moveCategory(backlog, id, position) {
137
+ const category = backlog.categories.find((candidate) => candidate.id === id);
138
+ if (!category)
139
+ throw new Error(`Backlog category ${id} does not exist`);
140
+ const categories = backlog.categories.filter((candidate) => candidate.id !== id);
141
+ categories.splice(clampPosition(position, categories.length), 0, category);
142
+ return { ...backlog, categories, items: sortByStatus(backlog.items, categories) };
143
+ }
144
+ export function removeCategory(backlog, id) {
145
+ requireRemovableCategory(backlog, id);
146
+ if (backlog.items.some((item) => item.status === id)) {
147
+ throw new Error(`Backlog category ${id} is not empty`);
148
+ }
149
+ return { ...backlog, categories: backlog.categories.filter((category) => category.id !== id) };
150
+ }
151
+ export function purgeCategory(backlog, id) {
152
+ if (!backlog.categories.some((category) => category.id === id)) {
153
+ throw new Error(`Backlog category ${id} does not exist`);
154
+ }
155
+ return {
156
+ ...backlog,
157
+ items: backlog.items.filter((item) => item.status !== id),
158
+ };
159
+ }
160
+ function requireRemovableCategory(backlog, id) {
161
+ if (!backlog.categories.some((category) => category.id === id)) {
162
+ throw new Error(`Backlog category ${id} does not exist`);
163
+ }
164
+ if (backlog.categories.length === 1)
165
+ throw new Error("Cannot remove the final backlog category");
166
+ }
167
+ function clampPosition(position, length) {
168
+ return Math.max(0, Math.min(position ?? length, length));
169
+ }
70
170
  //# sourceMappingURL=backlog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"backlog.js","sourceRoot":"","sources":["../src/backlog.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAA;AAC1C,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAU,CAAA;AAgB1D,MAAM,CAAC,MAAM,aAAa,GAAY,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;AAE/D,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC7E,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAe,CAAC,CAAA;AACxE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAe,EAAE;QACzD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,oBAAoB,CAAC,CAAA;QAC/E,IAAI,OAAO,IAAI,CAAC,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,2BAA2B,CAAC,CAAA;QACnE,CAAC;QACD,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,EAAE,gBAAgB,CAAC,CAAA;QACjF,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrE,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,EAAE,8BAA8B,CAAC,CAAA;QACxE,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,EAAE,oBAAoB,CAAC,CAAA;QAC9D,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,CAAC,EAAE,wBAAwB,CAAC,CAAA;QAE5F,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAChB,OAAO;YACL,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1D,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAA;AAC9B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAA6B;IACxD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAA;AACrF,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,KAA6B,EAC7B,EAAU,EACV,MAA0B,EAC1B,QAA4B;IAE5B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;IAC3D,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAA;IAE/D,MAAM,YAAY,GAAG,MAAM,IAAI,IAAI,CAAC,MAAM,CAAA;IAC1C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;IAClE,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,KAAK,YAAY,CAAC,CAAA;IACjF,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACzE,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;IAEnE,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE,CAC1C,eAAe,KAAK,YAAY;QAC9B,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,KAAK,eAAe,CAAC,CAC1E,CAAA;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,WAA8B,QAAQ;IACtF,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QACtE,OAAO,+BAA+B,CAAA;IACxC,CAAC;IAED,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAA;QACpE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;YACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;YAC3D,OAAO,GAAG,KAAK,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,KAAK,GAAG,KAAK,EAAE,CAAA;QACtD,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,MAAM,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA;IACtG,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AACjB,CAAC"}
1
+ {"version":3,"file":"backlog.js","sourceRoot":"","sources":["../src/backlog.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import { Plugin } from "@opencode-ai/plugin";
4
- import { BACKLOG_FILE, describeBacklog, moveItem, sortByStatus, } from "./backlog.js";
4
+ import { BACKLOG_FILE, addCategory, describeBacklog, moveItem, moveCategory, purgeCategory, removeCategory, renameCategory, sortByStatus, } from "./backlog.js";
5
5
  import { optionalNullableString, optionalPosition, optionalStatus, record, requiredString, } from "./input.js";
6
6
  import { readBacklog, updateBacklog } from "./store.js";
7
7
  const idInput = {
@@ -10,6 +10,15 @@ const idInput = {
10
10
  required: ["id"],
11
11
  additionalProperties: false,
12
12
  };
13
+ const categoryTitleInput = {
14
+ type: "object",
15
+ properties: {
16
+ id: { type: "string", minLength: 1 },
17
+ title: { type: "string", minLength: 1 },
18
+ },
19
+ required: ["id", "title"],
20
+ additionalProperties: false,
21
+ };
13
22
  async function backlogPath(context, sessionID) {
14
23
  const session = await context.session.get({ sessionID });
15
24
  return join(session.location.directory, BACKLOG_FILE);
@@ -33,29 +42,30 @@ export default Plugin.define({
33
42
  await context.tool.transform((tools) => {
34
43
  tools.add({
35
44
  name: "backlog_list",
36
- description: "List the ordered project backlog, optionally filtered by Kanban state.",
45
+ description: "List the ordered project backlog, optionally filtered by category ID.",
37
46
  input: {
38
47
  type: "object",
39
- properties: { status: { type: "string", enum: ["todo", "doing", "done"] } },
48
+ properties: { status: { type: "string", minLength: 1 } },
40
49
  additionalProperties: false,
41
50
  },
42
51
  options: { codemode: false },
43
52
  execute: async (input, toolContext) => {
44
- const status = optionalStatus(record(input));
53
+ const backlog = await readForSession(context, toolContext.sessionID);
54
+ const status = optionalStatus(record(input), backlog);
45
55
  return {
46
- content: describeBacklog(await readForSession(context, toolContext.sessionID), status === undefined ? undefined : [status]),
56
+ content: describeBacklog(backlog, status === undefined ? undefined : [status]),
47
57
  };
48
58
  },
49
59
  });
50
60
  tools.add({
51
61
  name: "backlog_add",
52
- description: "Add a task to the project backlog at a position within a Kanban state.",
62
+ description: "Add a task at a zero-based position within a backlog category.",
53
63
  input: {
54
64
  type: "object",
55
65
  properties: {
56
66
  title: { type: "string", minLength: 1 },
57
67
  notes: { type: "string" },
58
- status: { type: "string", enum: ["todo", "doing", "done"] },
68
+ status: { type: "string", minLength: 1 },
59
69
  position: { type: "integer", minimum: 0 },
60
70
  },
61
71
  required: ["title"],
@@ -66,18 +76,27 @@ export default Plugin.define({
66
76
  const values = record(input);
67
77
  const title = requiredString(values, "title");
68
78
  const notes = optionalNullableString(values, "notes") ?? undefined;
69
- const status = optionalStatus(values) ?? "todo";
70
79
  const position = optionalPosition(values);
71
- const item = {
72
- id: randomUUID(),
73
- title,
74
- ...(notes === undefined ? {} : { notes }),
75
- status,
76
- };
77
- const backlog = await updateForSession(context, toolContext.sessionID, (current) => ({
78
- version: 1,
79
- items: moveItem([...current.items, item], item.id, status, position),
80
- }));
80
+ let item;
81
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => {
82
+ const firstCategory = current.categories[0];
83
+ if (!firstCategory)
84
+ throw new Error("The backlog must have at least one category");
85
+ const status = optionalStatus(values, current) ?? firstCategory.id;
86
+ item = {
87
+ id: randomUUID(),
88
+ title,
89
+ ...(notes === undefined ? {} : { notes }),
90
+ status,
91
+ };
92
+ return {
93
+ version: 2,
94
+ categories: current.categories,
95
+ items: moveItem([...current.items, item], item.id, status, position, current.categories),
96
+ };
97
+ });
98
+ if (!item)
99
+ throw new Error("Backlog update did not add an item");
81
100
  return { content: `Added ${item.id}.\n\n${describeBacklog(backlog)}` };
82
101
  },
83
102
  });
@@ -113,19 +132,23 @@ export default Plugin.define({
113
132
  ...(title === undefined ? {} : { title }),
114
133
  ...(notes === undefined || notes === null ? {} : { notes }),
115
134
  };
116
- return { version: 1, items: sortByStatus(replaceItem(current.items, replacement)) };
135
+ return {
136
+ version: 2,
137
+ categories: current.categories,
138
+ items: sortByStatus(replaceItem(current.items, replacement), current.categories),
139
+ };
117
140
  });
118
141
  return { content: `Updated ${id}.\n\n${describeBacklog(backlog)}` };
119
142
  },
120
143
  });
121
144
  tools.add({
122
145
  name: "backlog_move",
123
- description: "Change a task Kanban state or its zero-based position within that state.",
146
+ description: "Change a task category or its zero-based position within that category.",
124
147
  input: {
125
148
  type: "object",
126
149
  properties: {
127
150
  id: { type: "string", minLength: 1 },
128
- status: { type: "string", enum: ["todo", "doing", "done"] },
151
+ status: { type: "string", minLength: 1 },
129
152
  position: { type: "integer", minimum: 0 },
130
153
  },
131
154
  required: ["id"],
@@ -135,15 +158,18 @@ export default Plugin.define({
135
158
  execute: async (input, toolContext) => {
136
159
  const values = record(input);
137
160
  const id = requiredString(values, "id");
138
- const status = optionalStatus(values);
139
161
  const position = optionalPosition(values);
140
- if (status === undefined && position === undefined) {
141
- throw new Error("backlog_move requires a status or position change");
142
- }
143
- const backlog = await updateForSession(context, toolContext.sessionID, (current) => ({
144
- version: 1,
145
- items: moveItem(current.items, id, status, position),
146
- }));
162
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => {
163
+ const status = optionalStatus(values, current);
164
+ if (status === undefined && position === undefined) {
165
+ throw new Error("backlog_move requires a status or position change");
166
+ }
167
+ return {
168
+ version: 2,
169
+ categories: current.categories,
170
+ items: moveItem(current.items, id, status, position, current.categories),
171
+ };
172
+ });
147
173
  return { content: `Moved ${id}.\n\n${describeBacklog(backlog)}` };
148
174
  },
149
175
  });
@@ -158,11 +184,96 @@ export default Plugin.define({
158
184
  if (!current.items.some((item) => item.id === id)) {
159
185
  throw new Error(`Backlog item ${id} does not exist`);
160
186
  }
161
- return { version: 1, items: current.items.filter((item) => item.id !== id) };
187
+ return {
188
+ version: 2,
189
+ categories: current.categories,
190
+ items: current.items.filter((item) => item.id !== id),
191
+ };
162
192
  });
163
193
  return { content: `Removed ${id}.\n\n${describeBacklog(backlog)}` };
164
194
  },
165
195
  });
196
+ tools.add({
197
+ name: "backlog_category_add",
198
+ description: "Add a backlog category with a unique ID, title, and optional zero-based position.",
199
+ input: {
200
+ type: "object",
201
+ properties: {
202
+ id: { type: "string", minLength: 1 },
203
+ title: { type: "string", minLength: 1 },
204
+ position: { type: "integer", minimum: 0 },
205
+ },
206
+ required: ["id", "title"],
207
+ additionalProperties: false,
208
+ },
209
+ options: { codemode: false },
210
+ execute: async (input, toolContext) => {
211
+ const values = record(input);
212
+ const id = requiredString(values, "id");
213
+ const title = requiredString(values, "title");
214
+ const position = optionalPosition(values);
215
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => addCategory(current, { id, title }, position));
216
+ return { content: `Added category ${id}.\n\n${describeBacklog(backlog)}` };
217
+ },
218
+ });
219
+ tools.add({
220
+ name: "backlog_category_update",
221
+ description: "Change a backlog category title.",
222
+ input: categoryTitleInput,
223
+ options: { codemode: false },
224
+ execute: async (input, toolContext) => {
225
+ const values = record(input);
226
+ const id = requiredString(values, "id");
227
+ const title = requiredString(values, "title");
228
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => renameCategory(current, id, title));
229
+ return { content: `Updated category ${id}.\n\n${describeBacklog(backlog)}` };
230
+ },
231
+ });
232
+ tools.add({
233
+ name: "backlog_category_move",
234
+ description: "Move a backlog category to a zero-based position.",
235
+ input: {
236
+ type: "object",
237
+ properties: {
238
+ id: { type: "string", minLength: 1 },
239
+ position: { type: "integer", minimum: 0 },
240
+ },
241
+ required: ["id", "position"],
242
+ additionalProperties: false,
243
+ },
244
+ options: { codemode: false },
245
+ execute: async (input, toolContext) => {
246
+ const values = record(input);
247
+ const id = requiredString(values, "id");
248
+ const position = optionalPosition(values);
249
+ if (position === undefined)
250
+ throw new Error("position is required");
251
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => moveCategory(current, id, position));
252
+ return { content: `Moved category ${id}.\n\n${describeBacklog(backlog)}` };
253
+ },
254
+ });
255
+ tools.add({
256
+ name: "backlog_category_remove",
257
+ description: "Remove an empty backlog category.",
258
+ input: idInput,
259
+ options: { codemode: false },
260
+ execute: async (input, toolContext) => {
261
+ const id = requiredString(record(input), "id");
262
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => removeCategory(current, id));
263
+ return { content: `Removed category ${id}.\n\n${describeBacklog(backlog)}` };
264
+ },
265
+ });
266
+ tools.add({
267
+ name: "backlog_category_purge",
268
+ description: "Permanently remove all tasks in a backlog category while retaining the category.",
269
+ input: idInput,
270
+ options: { codemode: false },
271
+ execute: async (input, toolContext) => {
272
+ const id = requiredString(record(input), "id");
273
+ const backlog = await updateForSession(context, toolContext.sessionID, (current) => purgeCategory(current, id));
274
+ return { content: `Purged tasks from category ${id}.\n\n${describeBacklog(backlog)}` };
275
+ },
276
+ });
166
277
  });
167
278
  },
168
279
  });