dsh-checklists 0.3.2 → 0.4.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
@@ -28,6 +28,8 @@ If the agent forgets to mark something, you say so in chat and it fixes it.
28
28
  | `checklist_edit` | Changes an item's text (not its state) |
29
29
  | `checklist_list` | Full tree with ids and states, for when the digest is truncated |
30
30
  | `checklist_delete` | Deletes a list (by id or exact title) |
31
+ | `checklist_remove_item` | Removes one item and everything nested under it |
32
+ | `checklist_rename` | Renames a list; titles stay unique within the session |
31
33
  | `checklist_template_save` | Saves a list as a reusable template for future sessions |
32
34
 
33
35
  Items are plain strings. **Two leading spaces per level** nest them:
@@ -48,7 +50,7 @@ From a local build or tarball:
48
50
  npm install
49
51
  npm run build
50
52
  npm pack
51
- dsh plugin --profile dev add ./dsh-checklists-0.3.2.tgz
53
+ dsh plugin --profile dev add ./dsh-checklists-0.4.0.tgz
52
54
  dsh --profile dev --dump-config # the tool-checklist row should appear
53
55
  ```
54
56
 
@@ -88,6 +90,8 @@ document degrades to an empty list instead of failing the turn.
88
90
 
89
91
  ## Sidebar tab
90
92
 
93
+ ![Checklist sidebar tab](assets/screenshot-1.png)
94
+
91
95
  Open the right sidebar, then the **guide** (`+`) and pick **Checklist**.
92
96
 
93
97
  | Action | How |
Binary file
package/lib/model.js CHANGED
@@ -147,6 +147,56 @@ export function findItemById(lists, id) {
147
147
  }
148
148
  return undefined;
149
149
  }
150
+ /**
151
+ * Remove one item (and its whole subtree) by exact id.
152
+ *
153
+ * Mutates the array in place, so callers must pass a document they own — the
154
+ * store's `update` already hands out a clone.
155
+ *
156
+ * @param items - sibling list to search, at any depth.
157
+ * @param id - exact item id.
158
+ * @returns the removed node, or `undefined` when no item has that id.
159
+ */
160
+ export function removeItemById(items, id) {
161
+ const index = items.findIndex((entry) => entry.id === id);
162
+ if (index >= 0)
163
+ return items.splice(index, 1)[0];
164
+ for (const item of items) {
165
+ const removed = removeItemById(item.children, id);
166
+ if (removed)
167
+ return removed;
168
+ }
169
+ return undefined;
170
+ }
171
+ /**
172
+ * Validate a checklist rename before applying it.
173
+ *
174
+ * Titles are how the user reads the sidebar and how `findList` resolves a
175
+ * reference, so a rename must never create a second list with the same title.
176
+ * Renaming a list to its own title (any case) is a no-op, not a conflict.
177
+ *
178
+ * @param lists - the session's checklists.
179
+ * @param ref - checklist id or exact title.
180
+ * @param nextTitle - the requested new title.
181
+ * @returns the resolved list, or the reason the rename cannot proceed.
182
+ */
183
+ export function checkRename(lists, ref, nextTitle) {
184
+ const to = nextTitle.trim();
185
+ if (to.length === 0)
186
+ return { kind: 'empty' };
187
+ const list = findList(lists, ref);
188
+ if (!list) {
189
+ return { kind: 'missing', available: lists.map((entry) => `${entry.id} (${entry.title})`) };
190
+ }
191
+ const lower = to.toLowerCase();
192
+ if (list.title.trim().toLowerCase() === lower) {
193
+ return { kind: 'ok', list, from: list.title, to };
194
+ }
195
+ const clash = lists.find((entry) => entry !== list && entry.title.trim().toLowerCase() === lower);
196
+ if (clash)
197
+ return { kind: 'duplicate', existing: clash.title };
198
+ return { kind: 'ok', list, from: list.title, to };
199
+ }
150
200
  /**
151
201
  * The next item to work on, in depth-first order.
152
202
  *
package/lib/tools.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * the single source of truth and the sidebar (phase 2) is a read-only mirror.
6
6
  */
7
7
  import { defineTool } from '@deepseek-ai/dsh-tools';
8
- import { buildItems, countItems, findList, flatten, ITEM_STATES, isItemState, mintId, nextItem, resolveRef, toLines, } from './model.js';
8
+ import { buildItems, checkRename, countItems, findList, flatten, ITEM_STATES, isItemState, mintId, nextItem, removeItemById, resolveRef, toLines, } from './model.js';
9
9
  /** Ships with the plugin so a first run has something to instantiate. */
10
10
  const DEFAULT_TEMPLATES = {
11
11
  release: [
@@ -482,6 +482,138 @@ export function registerChecklistTools(ctx, store, config = {}) {
482
482
  });
483
483
  },
484
484
  })));
485
+ ctx.effect(() => ctx.tools.register(defineTool({
486
+ name: 'checklist_rename',
487
+ description: 'Rename an existing checklist in the CURRENT session. Titles must be unique within the session. '
488
+ + 'Pass the id from checklist_list, or the exact current title.',
489
+ parameters: {
490
+ listId: {
491
+ type: 'string',
492
+ required: true,
493
+ description: 'Checklist id, or its exact current title.',
494
+ },
495
+ title: {
496
+ type: 'string',
497
+ required: true,
498
+ description: 'New title for the checklist.',
499
+ },
500
+ },
501
+ output: {
502
+ schema: {
503
+ type: 'object',
504
+ additionalProperties: false,
505
+ properties: {
506
+ listId: { type: 'string', required: true },
507
+ from: { type: 'string', required: true },
508
+ to: { type: 'string', required: true },
509
+ },
510
+ },
511
+ render: (_args, value) => {
512
+ const result = value;
513
+ return [{ type: 'text', text: `Renamed "${result.from}" → "${result.to}".` }];
514
+ },
515
+ },
516
+ timeoutMs: 10_000,
517
+ async execute(args, exec) {
518
+ const input = args;
519
+ const sessionId = sessionOf(exec);
520
+ const ref = text(input.listId).trim();
521
+ const nextTitle = text(input.title).trim();
522
+ if (ref.length === 0)
523
+ throw new Error('checklist_rename: listId must not be empty');
524
+ if (nextTitle.length === 0)
525
+ throw new Error('checklist_rename: title must not be empty');
526
+ if (nextTitle.length > 120)
527
+ throw new Error('checklist_rename: title is longer than 120 characters');
528
+ return store.update(sessionId, (doc) => {
529
+ const check = checkRename(doc.lists, ref, nextTitle);
530
+ if (check.kind === 'empty')
531
+ throw new Error('checklist_rename: title must not be empty');
532
+ if (check.kind === 'missing') {
533
+ const available = check.available.join(', ');
534
+ throw new Error(`checklist_rename: no checklist "${ref}" in this session. `
535
+ + `Available: ${available.length > 0 ? available : 'none'}`);
536
+ }
537
+ if (check.kind === 'duplicate') {
538
+ throw new Error(`checklist_rename: a checklist titled "${check.existing}" already exists in this session`);
539
+ }
540
+ check.list.title = check.to;
541
+ return { listId: check.list.id, from: check.from, to: check.to };
542
+ });
543
+ },
544
+ })));
545
+ ctx.effect(() => ctx.tools.register(defineTool({
546
+ name: 'checklist_remove_item',
547
+ description: 'Remove one item — and everything nested under it — from a checklist in the CURRENT session. '
548
+ + 'Match it by id or exact text. Use it to drop work that no longer applies; '
549
+ + 'to change the item instead, use checklist_edit.',
550
+ parameters: {
551
+ listId: {
552
+ type: 'string',
553
+ required: true,
554
+ description: 'Checklist id, or its exact title.',
555
+ },
556
+ item: {
557
+ type: 'string',
558
+ required: true,
559
+ description: 'Item id or exact item text.',
560
+ },
561
+ },
562
+ output: {
563
+ schema: {
564
+ type: 'object',
565
+ additionalProperties: false,
566
+ properties: {
567
+ listId: { type: 'string', required: true },
568
+ itemId: { type: 'string', required: true },
569
+ text: { type: 'string', required: true },
570
+ removed: { type: 'integer', required: true },
571
+ },
572
+ },
573
+ render: (_args, value) => {
574
+ const result = value;
575
+ const extra = result.removed > 1 ? ` (+${result.removed - 1} nested)` : '';
576
+ return [{ type: 'text', text: `Removed "${result.text}"${extra}.` }];
577
+ },
578
+ },
579
+ timeoutMs: 10_000,
580
+ async execute(args, exec) {
581
+ const input = args;
582
+ const sessionId = sessionOf(exec);
583
+ const listRef = text(input.listId).trim();
584
+ const itemRef = text(input.item).trim();
585
+ if (listRef.length === 0)
586
+ throw new Error('checklist_remove_item: listId must not be empty');
587
+ if (itemRef.length === 0)
588
+ throw new Error('checklist_remove_item: item must not be empty');
589
+ return store.update(sessionId, (doc) => {
590
+ const list = findList(doc.lists, listRef);
591
+ if (!list) {
592
+ const available = doc.lists.map((entry) => `${entry.id} (${entry.title})`).join(', ');
593
+ throw new Error(`checklist_remove_item: no checklist "${listRef}" in this session. `
594
+ + `Available: ${available.length > 0 ? available : 'none'}`);
595
+ }
596
+ const found = resolveRef([list], itemRef);
597
+ if (found.kind === 'ambiguous') {
598
+ const options = found.candidates.map((entry) => `${entry.item.id} (${entry.item.text})`).join(', ');
599
+ throw new Error(`checklist_remove_item: "${itemRef}" is ambiguous; use an id: ${options}`);
600
+ }
601
+ if (found.kind === 'miss') {
602
+ throw new Error(`checklist_remove_item: no item "${itemRef}" in "${list.title}". Call checklist_list for ids.`);
603
+ }
604
+ const removed = removeItemById(list.items, found.hit.item.id);
605
+ if (!removed) {
606
+ throw new Error(`checklist_remove_item: item "${itemRef}" disappeared from "${list.title}"`);
607
+ }
608
+ return {
609
+ listId: list.id,
610
+ itemId: removed.id,
611
+ text: removed.text,
612
+ removed: countItems([removed]).total,
613
+ };
614
+ });
615
+ },
616
+ })));
485
617
  ctx.effect(() => ctx.tools.register(defineTool({
486
618
  name: 'checklist_delete',
487
619
  description: 'Delete one checklist from the CURRENT session. The list and its items are gone; there is no archive. '
@@ -97,6 +97,44 @@ export declare function findItemById(lists: readonly Checklist[], id: string): {
97
97
  list: Checklist;
98
98
  item: ChecklistItem;
99
99
  } | undefined;
100
+ /**
101
+ * Remove one item (and its whole subtree) by exact id.
102
+ *
103
+ * Mutates the array in place, so callers must pass a document they own — the
104
+ * store's `update` already hands out a clone.
105
+ *
106
+ * @param items - sibling list to search, at any depth.
107
+ * @param id - exact item id.
108
+ * @returns the removed node, or `undefined` when no item has that id.
109
+ */
110
+ export declare function removeItemById(items: ChecklistItem[], id: string): ChecklistItem | undefined;
111
+ export type RenameCheck = {
112
+ kind: 'ok';
113
+ list: Checklist;
114
+ from: string;
115
+ to: string;
116
+ } | {
117
+ kind: 'empty';
118
+ } | {
119
+ kind: 'missing';
120
+ available: string[];
121
+ } | {
122
+ kind: 'duplicate';
123
+ existing: string;
124
+ };
125
+ /**
126
+ * Validate a checklist rename before applying it.
127
+ *
128
+ * Titles are how the user reads the sidebar and how `findList` resolves a
129
+ * reference, so a rename must never create a second list with the same title.
130
+ * Renaming a list to its own title (any case) is a no-op, not a conflict.
131
+ *
132
+ * @param lists - the session's checklists.
133
+ * @param ref - checklist id or exact title.
134
+ * @param nextTitle - the requested new title.
135
+ * @returns the resolved list, or the reason the rename cannot proceed.
136
+ */
137
+ export declare function checkRename(lists: readonly Checklist[], ref: string, nextTitle: string): RenameCheck;
100
138
  export interface NextItem {
101
139
  list: Checklist;
102
140
  item: ChecklistItem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-checklists",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Session-scoped checklists for DeepSeek Harness: the agent tracks work as a tree, the model always sees a compact digest, and a read-only sidebar tab mirrors it.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -19,6 +19,7 @@
19
19
  "files": [
20
20
  "lib",
21
21
  "client",
22
+ "assets",
22
23
  "cordis.patch.yml",
23
24
  "README.md"
24
25
  ],