dsh-checklists 0.6.0 → 0.7.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 +29 -1
- package/client/index.js +240 -4
- package/lib/index.js +12 -1
- package/lib/route.js +44 -0
- package/lib/store.js +16 -0
- package/lib/tools.js +64 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/route.d.ts +14 -0
- package/lib/types/store.d.ts +7 -0
- package/lib/types/tools.d.ts +4 -0
- package/lib/types/workspace.d.ts +53 -0
- package/lib/workspace.js +134 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ If the agent forgets to mark something, you say so in chat and it fixes it.
|
|
|
30
30
|
| `checklist_delete` | Deletes a list (by id or exact title) |
|
|
31
31
|
| `checklist_remove_item` | Removes one item and everything nested under it |
|
|
32
32
|
| `checklist_rename` | Renames a list; titles stay unique within the session |
|
|
33
|
+
| `checklist_workspace` | Lists checklists from other sessions in the same workspace (read-only) |
|
|
33
34
|
| `checklist_template_save` | Saves a list as a reusable template for future sessions |
|
|
34
35
|
|
|
35
36
|
Items are plain strings. **Two leading spaces per level** nest them:
|
|
@@ -50,7 +51,7 @@ From a local build or tarball:
|
|
|
50
51
|
npm install
|
|
51
52
|
npm run build
|
|
52
53
|
npm pack
|
|
53
|
-
dsh plugin --profile dev add ./dsh-checklists-0.
|
|
54
|
+
dsh plugin --profile dev add ./dsh-checklists-0.7.0.tgz
|
|
54
55
|
dsh --profile dev --dump-config # the tool-checklist row should appear
|
|
55
56
|
```
|
|
56
57
|
|
|
@@ -69,6 +70,8 @@ Override the defaults on the plugin's entry in the profile's `cordis.patch.yml`:
|
|
|
69
70
|
maxItemsPerList: 200 # per-list item cap
|
|
70
71
|
pruneDays: 14 # prune untouched sessions after N days (0 = never)
|
|
71
72
|
nudge: steer # steer | observe | off — see "Turn-end gate"
|
|
73
|
+
workspaceView: on-demand # off | on-demand | always — see "Workspace lens"
|
|
74
|
+
workspaceViewLimit: 10 # sibling checklists returned
|
|
72
75
|
# storageRoot: "D:/data/dsh-checklists"
|
|
73
76
|
# templates:
|
|
74
77
|
# my-flow: ["Step 1", " Sub-step", "Step 2"]
|
|
@@ -115,6 +118,7 @@ the agent's edit.
|
|
|
115
118
|
| `/dsh-checklist/state` | GET | One session's state (read-only, `no-store`) |
|
|
116
119
|
| `/dsh-checklist/item` | POST | Edit an item's text, with a conflict guard |
|
|
117
120
|
| `/dsh-checklist/nudge` | GET | Recent turn-end gate decisions |
|
|
121
|
+
| `/dsh-checklist/workspace` | GET | Checklists of sibling sessions in the same workspace |
|
|
118
122
|
| `/dsh-checklist/client-probe` | GET | Client load diagnostics |
|
|
119
123
|
|
|
120
124
|
## Turn-end gate
|
|
@@ -150,6 +154,30 @@ Inspect recent decisions at `GET /dsh-checklist/nudge`:
|
|
|
150
154
|
|
|
151
155
|
`nudge: observe` records without interrupting; `nudge: off` disables the gate.
|
|
152
156
|
|
|
157
|
+
## Workspace lens
|
|
158
|
+
|
|
159
|
+
Checklists stay per session. The lens is a **read-only** way to see the
|
|
160
|
+
checklists of other sessions in the same project — available on request, and
|
|
161
|
+
never part of the prompt.
|
|
162
|
+
|
|
163
|
+
| Mode | Behaviour |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `on-demand` (default) | The tool works; the sidebar section starts collapsed |
|
|
166
|
+
| `always` | The sidebar section starts expanded |
|
|
167
|
+
| `off` | No tool, no route |
|
|
168
|
+
|
|
169
|
+
Clicking an entry expands it into a read-only tree, fetched once from that
|
|
170
|
+
session's own state route. Nothing there is editable — the lens can look, not
|
|
171
|
+
touch.
|
|
172
|
+
|
|
173
|
+
Membership comes from disk: a session belongs to the workspace directory that
|
|
174
|
+
contains it under `$DSH_HOME/sessions/<workspace>/<sessionId>/`. The host's
|
|
175
|
+
`workspace.json` is only used for a friendly title, because it is not always
|
|
176
|
+
complete — on the machine this was written on it was missing a workspace.
|
|
177
|
+
|
|
178
|
+
Nothing here is writable. To change another session's checklist, open that
|
|
179
|
+
session: two writers on one list would give the list two truths.
|
|
180
|
+
|
|
153
181
|
## Language
|
|
154
182
|
|
|
155
183
|
The panel follows the harness locale: **Spanish** when `locale` starts with `es`,
|
package/client/index.js
CHANGED
|
@@ -28,6 +28,8 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const COLOR = { done: '#3fb950', doing: '#d29922', blocked: '#f85149', todo: 'inherit' }
|
|
29
29
|
|
|
30
30
|
const MUTED = { opacity: 0.72, fontSize: 13, padding: '10px 12px', lineHeight: 1.5 }
|
|
31
|
+
/** Stable empty map: read-only trees never collapse, so one shared object is enough. */
|
|
32
|
+
const EMPTY_COLLAPSED = {}
|
|
31
33
|
const BUTTON = {
|
|
32
34
|
background: 'transparent',
|
|
33
35
|
border: 'none',
|
|
@@ -61,6 +63,9 @@ window.__ModuleLoader__.load({
|
|
|
61
63
|
hideDoneHint: 'Hide or show completed items',
|
|
62
64
|
copy: 'Copy',
|
|
63
65
|
copyHint: 'Copy the checklist as markdown',
|
|
66
|
+
workspace: 'Workspace',
|
|
67
|
+
workspaceEmpty: 'No other session in this workspace has a checklist.',
|
|
68
|
+
loadingShort: 'Loading…',
|
|
64
69
|
copied: '✓ Copied',
|
|
65
70
|
copyFailed: 'Could not copy',
|
|
66
71
|
noClipboard: 'Clipboard unavailable',
|
|
@@ -86,6 +91,9 @@ window.__ModuleLoader__.load({
|
|
|
86
91
|
hideDoneHint: 'Ocultar o mostrar los ítems hechos',
|
|
87
92
|
copy: 'Copiar',
|
|
88
93
|
copyHint: 'Copiar el checklist como markdown',
|
|
94
|
+
workspace: 'Proyecto',
|
|
95
|
+
workspaceEmpty: 'Ninguna otra sesión de este proyecto tiene checklist.',
|
|
96
|
+
loadingShort: 'Cargando…',
|
|
89
97
|
copied: '✓ Copiado',
|
|
90
98
|
copyFailed: 'No se pudo copiar',
|
|
91
99
|
noClipboard: 'Portapapeles no disponible',
|
|
@@ -293,6 +301,7 @@ window.__ModuleLoader__.load({
|
|
|
293
301
|
const nodeKey = `${props.listId}/${item.id}`
|
|
294
302
|
const isCollapsed = Boolean(props.collapsed[nodeKey])
|
|
295
303
|
const counts = countTree(children)
|
|
304
|
+
const readOnly = Boolean(props.readOnly)
|
|
296
305
|
|
|
297
306
|
const label = isEditing
|
|
298
307
|
? h('input', {
|
|
@@ -324,19 +333,19 @@ window.__ModuleLoader__.load({
|
|
|
324
333
|
: h(
|
|
325
334
|
'span',
|
|
326
335
|
{
|
|
327
|
-
onDoubleClick: () => props.onStartEdit(item),
|
|
328
|
-
title: props.t('editHint'),
|
|
336
|
+
onDoubleClick: readOnly ? undefined : () => props.onStartEdit(item),
|
|
337
|
+
title: readOnly ? undefined : props.t('editHint'),
|
|
329
338
|
style: {
|
|
330
339
|
textDecoration: done ? 'line-through' : 'none',
|
|
331
340
|
wordBreak: 'break-word',
|
|
332
|
-
cursor: 'text',
|
|
341
|
+
cursor: readOnly ? 'default' : 'text',
|
|
333
342
|
},
|
|
334
343
|
},
|
|
335
344
|
item.text,
|
|
336
345
|
)
|
|
337
346
|
|
|
338
347
|
const chevron =
|
|
339
|
-
children.length > 0
|
|
348
|
+
children.length > 0 && !readOnly
|
|
340
349
|
? h(
|
|
341
350
|
'button',
|
|
342
351
|
{
|
|
@@ -480,6 +489,172 @@ window.__ModuleLoader__.load({
|
|
|
480
489
|
return h('section', { style: { marginBottom: 12 } }, header, body)
|
|
481
490
|
}
|
|
482
491
|
|
|
492
|
+
/**
|
|
493
|
+
* The workspace lens, below the session's own lists.
|
|
494
|
+
*
|
|
495
|
+
* Collapsed by default and lazy: nothing is fetched until the section is
|
|
496
|
+
* opened, so a session that never looks costs nothing. A 403 from the host
|
|
497
|
+
* means the profile disabled the lens, and the section disappears.
|
|
498
|
+
*/
|
|
499
|
+
/**
|
|
500
|
+
* One sibling session's tree, read-only.
|
|
501
|
+
*
|
|
502
|
+
* No editing, no collapse state, no handlers: that list belongs to another
|
|
503
|
+
* session, and two writers would give it two truths.
|
|
504
|
+
*/
|
|
505
|
+
function ReadOnlyTree(props) {
|
|
506
|
+
const state = props.state
|
|
507
|
+
|
|
508
|
+
if (!state || state.phase === 'loading') {
|
|
509
|
+
return h('div', { style: { opacity: 0.6, fontSize: 11, paddingLeft: 18 } }, props.t('loadingShort'))
|
|
510
|
+
}
|
|
511
|
+
if (state.phase === 'error') {
|
|
512
|
+
return h('div', { style: { opacity: 0.7, fontSize: 11, paddingLeft: 18 } }, state.message)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const lists = (state.data && state.data.lists) || []
|
|
516
|
+
if (lists.length === 0) {
|
|
517
|
+
return h('div', { style: { opacity: 0.55, fontSize: 11, paddingLeft: 18 } }, props.t('workspaceEmpty'))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const noop = () => {}
|
|
521
|
+
// One list: the entry header already carries its title, so repeating it
|
|
522
|
+
// inside the tree would say the same thing twice.
|
|
523
|
+
const showListTitles = lists.length > 1
|
|
524
|
+
|
|
525
|
+
return h(
|
|
526
|
+
'div',
|
|
527
|
+
{ style: { paddingLeft: 18, paddingBottom: 4 } },
|
|
528
|
+
lists.map((list) =>
|
|
529
|
+
h(
|
|
530
|
+
'div',
|
|
531
|
+
{ key: list.id },
|
|
532
|
+
showListTitles
|
|
533
|
+
? h('div', { style: { fontSize: 11, opacity: 0.6, marginTop: 4 } }, list.title)
|
|
534
|
+
: null,
|
|
535
|
+
h(
|
|
536
|
+
'ul',
|
|
537
|
+
{ style: { margin: 0, padding: 0 } },
|
|
538
|
+
(list.items || []).map((item) =>
|
|
539
|
+
h(Item, {
|
|
540
|
+
key: item.id,
|
|
541
|
+
item,
|
|
542
|
+
depth: 0,
|
|
543
|
+
t: props.t,
|
|
544
|
+
readOnly: true,
|
|
545
|
+
listId: `${props.sessionId}/${list.id}`,
|
|
546
|
+
collapsed: EMPTY_COLLAPSED,
|
|
547
|
+
onToggleNode: noop,
|
|
548
|
+
editing: null,
|
|
549
|
+
onStartEdit: noop,
|
|
550
|
+
onDraft: noop,
|
|
551
|
+
onCommit: noop,
|
|
552
|
+
onCancel: noop,
|
|
553
|
+
}),
|
|
554
|
+
),
|
|
555
|
+
),
|
|
556
|
+
),
|
|
557
|
+
),
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function WorkspaceSection(props) {
|
|
562
|
+
if (!props || props.state.phase === 'disabled') return null
|
|
563
|
+
|
|
564
|
+
const open = props.open
|
|
565
|
+
const state = props.state
|
|
566
|
+
const entries = state.phase === 'ready' ? state.data.checklists || [] : []
|
|
567
|
+
|
|
568
|
+
const header = h(
|
|
569
|
+
'button',
|
|
570
|
+
{
|
|
571
|
+
onClick: props.onToggle,
|
|
572
|
+
title: open ? props.t('collapse') : props.t('expand'),
|
|
573
|
+
style: {
|
|
574
|
+
...BUTTON,
|
|
575
|
+
opacity: 0.75,
|
|
576
|
+
fontSize: 12,
|
|
577
|
+
display: 'flex',
|
|
578
|
+
alignItems: 'baseline',
|
|
579
|
+
gap: 5,
|
|
580
|
+
width: '100%',
|
|
581
|
+
textAlign: 'left',
|
|
582
|
+
padding: '4px 0',
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
h('span', { style: { fontSize: 10, opacity: 0.55 } }, open ? '▾' : '▸'),
|
|
586
|
+
h('span', null, props.t('workspace')),
|
|
587
|
+
state.phase === 'ready'
|
|
588
|
+
? h('span', { style: { opacity: 0.55 } }, String(entries.length))
|
|
589
|
+
: null,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
let body = null
|
|
593
|
+
if (open) {
|
|
594
|
+
if (state.phase === 'loading') {
|
|
595
|
+
body = h('div', { style: { opacity: 0.6, fontSize: 12 } }, props.t('loadingShort'))
|
|
596
|
+
} else if (state.phase === 'error') {
|
|
597
|
+
body = h('div', { style: { opacity: 0.7, fontSize: 12 } }, state.message)
|
|
598
|
+
} else if (state.phase === 'ready') {
|
|
599
|
+
body = entries.length === 0
|
|
600
|
+
? h('div', { style: { opacity: 0.55, fontSize: 12 } }, props.t('workspaceEmpty'))
|
|
601
|
+
: h(
|
|
602
|
+
'ul',
|
|
603
|
+
{ style: { margin: 0, padding: 0 } },
|
|
604
|
+
entries.map((entry) => {
|
|
605
|
+
const treeOpen = Boolean(props.openTrees && props.openTrees[entry.sessionId])
|
|
606
|
+
return h(
|
|
607
|
+
'li',
|
|
608
|
+
{ key: entry.sessionId, style: { listStyle: 'none', margin: 0, padding: 0 } },
|
|
609
|
+
h(
|
|
610
|
+
'button',
|
|
611
|
+
{
|
|
612
|
+
onClick: () => props.onToggleTree(entry.sessionId),
|
|
613
|
+
title: treeOpen ? props.t('collapse') : props.t('expand'),
|
|
614
|
+
style: {
|
|
615
|
+
...BUTTON,
|
|
616
|
+
opacity: 0.8,
|
|
617
|
+
fontSize: 12,
|
|
618
|
+
display: 'flex',
|
|
619
|
+
gap: 6,
|
|
620
|
+
width: '100%',
|
|
621
|
+
textAlign: 'left',
|
|
622
|
+
padding: '1px 0',
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
h('span', { style: { fontSize: 9, opacity: 0.55 } }, treeOpen ? '▾' : '▸'),
|
|
626
|
+
h(
|
|
627
|
+
'span',
|
|
628
|
+
{ style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
629
|
+
entry.title,
|
|
630
|
+
),
|
|
631
|
+
h(
|
|
632
|
+
'span',
|
|
633
|
+
{ style: { opacity: 0.6, fontVariantNumeric: 'tabular-nums' } },
|
|
634
|
+
`${entry.done}/${entry.total}`,
|
|
635
|
+
),
|
|
636
|
+
),
|
|
637
|
+
treeOpen
|
|
638
|
+
? h(ReadOnlyTree, {
|
|
639
|
+
state: props.trees ? props.trees[entry.sessionId] : undefined,
|
|
640
|
+
t: props.t,
|
|
641
|
+
sessionId: entry.sessionId,
|
|
642
|
+
})
|
|
643
|
+
: null,
|
|
644
|
+
)
|
|
645
|
+
}),
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return h(
|
|
651
|
+
'section',
|
|
652
|
+
{ style: { marginTop: 10, borderTop: '1px solid rgba(127,127,127,0.2)', paddingTop: 6 } },
|
|
653
|
+
header,
|
|
654
|
+
body,
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
483
658
|
function ChecklistBody(props) {
|
|
484
659
|
const sessionId = props && props.sessionId
|
|
485
660
|
const snapshot = useChecklistState(sessionId)
|
|
@@ -525,6 +700,49 @@ window.__ModuleLoader__.load({
|
|
|
525
700
|
return () => clearTimeout(timer)
|
|
526
701
|
}, [notice])
|
|
527
702
|
|
|
703
|
+
const [workspaceOpen, setWorkspaceOpen] = React.useState(false)
|
|
704
|
+
const [workspace, setWorkspace] = React.useState({ phase: 'idle' })
|
|
705
|
+
const [openTrees, setOpenTrees] = React.useState({})
|
|
706
|
+
const [trees, setTrees] = React.useState({})
|
|
707
|
+
|
|
708
|
+
// Lazy on purpose: fetched the first time the section is opened, never on
|
|
709
|
+
// load, so the panel costs nothing until someone asks for the lens.
|
|
710
|
+
const toggleWorkspace = () => {
|
|
711
|
+
const next = !workspaceOpen
|
|
712
|
+
setWorkspaceOpen(next)
|
|
713
|
+
if (!next || workspace.phase !== 'idle' || !sessionId) return
|
|
714
|
+
|
|
715
|
+
setWorkspace({ phase: 'loading' })
|
|
716
|
+
fetch(`/dsh-checklist/workspace?session=${encodeURIComponent(sessionId)}`, { cache: 'no-store' })
|
|
717
|
+
.then((response) => {
|
|
718
|
+
if (response.status === 403) return { disabled: true }
|
|
719
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
720
|
+
return response.json()
|
|
721
|
+
})
|
|
722
|
+
.then((data) => setWorkspace(data && data.disabled ? { phase: 'disabled' } : { phase: 'ready', data }))
|
|
723
|
+
.catch((error) => setWorkspace({ phase: 'error', message: String((error && error.message) || error) }))
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Expanding one entry fetches that session's tree once. Nothing to
|
|
727
|
+
// invalidate: the lens never writes, so a cached tree stays true.
|
|
728
|
+
const toggleTree = (targetSessionId) => {
|
|
729
|
+
const next = !openTrees[targetSessionId]
|
|
730
|
+
setOpenTrees((current) => ({ ...current, [targetSessionId]: next }))
|
|
731
|
+
if (!next || trees[targetSessionId]) return
|
|
732
|
+
|
|
733
|
+
setTrees((current) => ({ ...current, [targetSessionId]: { phase: 'loading' } }))
|
|
734
|
+
fetch(`/dsh-checklist/state?session=${encodeURIComponent(targetSessionId)}`, { cache: 'no-store' })
|
|
735
|
+
.then((response) => {
|
|
736
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
737
|
+
return response.json()
|
|
738
|
+
})
|
|
739
|
+
.then((data) => setTrees((current) => ({ ...current, [targetSessionId]: { phase: 'ready', data } })))
|
|
740
|
+
.catch((error) => setTrees((current) => ({
|
|
741
|
+
...current,
|
|
742
|
+
[targetSessionId]: { phase: 'error', message: String((error && error.message) || error) },
|
|
743
|
+
})))
|
|
744
|
+
}
|
|
745
|
+
|
|
528
746
|
const startEdit = (item) => {
|
|
529
747
|
const next = { id: item.id, original: item.text, draft: item.text }
|
|
530
748
|
editingRef.current = next
|
|
@@ -594,6 +812,15 @@ window.__ModuleLoader__.load({
|
|
|
594
812
|
{ style: MUTED },
|
|
595
813
|
h('div', { style: { fontWeight: 600, marginBottom: 4 } }, t('emptyTitle')),
|
|
596
814
|
h('div', null, t('emptyHint')),
|
|
815
|
+
h(WorkspaceSection, {
|
|
816
|
+
t,
|
|
817
|
+
open: workspaceOpen,
|
|
818
|
+
state: workspace,
|
|
819
|
+
onToggle: toggleWorkspace,
|
|
820
|
+
trees,
|
|
821
|
+
openTrees,
|
|
822
|
+
onToggleTree: toggleTree,
|
|
823
|
+
}),
|
|
597
824
|
)
|
|
598
825
|
}
|
|
599
826
|
|
|
@@ -663,6 +890,15 @@ window.__ModuleLoader__.load({
|
|
|
663
890
|
onCancel: cancelEdit,
|
|
664
891
|
}),
|
|
665
892
|
),
|
|
893
|
+
h(WorkspaceSection, {
|
|
894
|
+
t,
|
|
895
|
+
open: workspaceOpen,
|
|
896
|
+
state: workspace,
|
|
897
|
+
onToggle: toggleWorkspace,
|
|
898
|
+
trees,
|
|
899
|
+
openTrees,
|
|
900
|
+
onToggleTree: toggleTree,
|
|
901
|
+
}),
|
|
666
902
|
)
|
|
667
903
|
}
|
|
668
904
|
|
package/lib/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { renderDigest } from './digest.js';
|
|
|
15
15
|
import { decideNudge, isChecklistWrite, MUTATING_TOOLS, NUDGE_ROUTE } from './gate.js';
|
|
16
16
|
import { countState } from './model.js';
|
|
17
17
|
import { serveProbe, PROBE_ROUTE } from './probe.js';
|
|
18
|
-
import { serveItemUpdate, serveState, ITEM_ROUTE, STATE_ROUTE } from './route.js';
|
|
18
|
+
import { serveItemUpdate, serveState, serveWorkspace, ITEM_ROUTE, STATE_ROUTE, WORKSPACE_ROUTE } from './route.js';
|
|
19
19
|
import { ChecklistStore } from './store.js';
|
|
20
20
|
import { registerChecklistTools } from './tools.js';
|
|
21
21
|
export const name = 'dsh-checklist';
|
|
@@ -62,6 +62,8 @@ export function apply(ctx, config = {}) {
|
|
|
62
62
|
registerChecklistTools(ctx, store, {
|
|
63
63
|
maxItemsPerList: config.maxItemsPerList,
|
|
64
64
|
templates: config.templates,
|
|
65
|
+
workspaceView: config.workspaceView,
|
|
66
|
+
workspaceViewLimit: config.workspaceViewLimit,
|
|
65
67
|
});
|
|
66
68
|
// Opportunistic housekeeping: never blocks startup, never fails the plugin.
|
|
67
69
|
if (config.pruneDays !== 0) {
|
|
@@ -191,6 +193,15 @@ export function apply(ctx, config = {}) {
|
|
|
191
193
|
void serveItemUpdate(req, res, store);
|
|
192
194
|
},
|
|
193
195
|
}), 'dsh-checklist: item route');
|
|
196
|
+
// Workspace lens: other sessions' checklists. Read-only, and never part of
|
|
197
|
+
// the prompt unless the agent explicitly asks for it.
|
|
198
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
199
|
+
kind: 'exact',
|
|
200
|
+
path: WORKSPACE_ROUTE,
|
|
201
|
+
handler: (req, res) => {
|
|
202
|
+
void serveWorkspace(req, res, store, config.workspaceViewLimit ?? 10, (config.workspaceView ?? 'on-demand') !== 'off');
|
|
203
|
+
},
|
|
204
|
+
}), 'dsh-checklist: workspace route');
|
|
194
205
|
// Diagnostics channel for the browser half; see src/probe.ts.
|
|
195
206
|
webCtx.effect(() => webCtx.webServer.register({
|
|
196
207
|
kind: 'exact',
|
package/lib/route.js
CHANGED
|
@@ -7,14 +7,58 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { findItemById } from './model.js';
|
|
9
9
|
import { toPublicState } from './state.js';
|
|
10
|
+
import { listWorkspaceChecklists } from './workspace.js';
|
|
10
11
|
export const STATE_ROUTE = '/dsh-checklist/state';
|
|
11
12
|
export const ITEM_ROUTE = '/dsh-checklist/item';
|
|
13
|
+
export const WORKSPACE_ROUTE = '/dsh-checklist/workspace';
|
|
12
14
|
/** Longest session id we will even look at; real ones are UUIDs. */
|
|
13
15
|
const MAX_SESSION_ID = 200;
|
|
14
16
|
/** Request body cap for the one write endpoint. */
|
|
15
17
|
const MAX_BODY_BYTES = 16 * 1024;
|
|
16
18
|
/** Item text cap, matching `checklist_edit`. */
|
|
17
19
|
const MAX_TEXT = 500;
|
|
20
|
+
/**
|
|
21
|
+
* Serve the workspace lens: checklists from other sessions in the same workspace.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately separate from the session state route: this data never enters the
|
|
24
|
+
* agent's context unless a tool explicitly asks for it.
|
|
25
|
+
*
|
|
26
|
+
* @param req - the incoming request; must be GET.
|
|
27
|
+
* @param res - the response to write.
|
|
28
|
+
* @param store - the checklist store.
|
|
29
|
+
* @param limit - maximum entries returned, newest first.
|
|
30
|
+
* @param enabled - false when the profile turned the lens off.
|
|
31
|
+
*/
|
|
32
|
+
export async function serveWorkspace(req, res, store, limit, enabled) {
|
|
33
|
+
if (req.method !== 'GET') {
|
|
34
|
+
res.writeHead(405, { allow: 'GET' });
|
|
35
|
+
res.end();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!enabled) {
|
|
39
|
+
sendJson(res, 403, { error: 'workspace view is disabled in this profile' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let session;
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
45
|
+
session = url.searchParams.get('session');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
sendJson(res, 400, { error: 'malformed request url' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!session || session.length === 0 || session.length > MAX_SESSION_ID) {
|
|
52
|
+
sendJson(res, 400, { error: 'the session query parameter is required' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
sendJson(res, 200, await listWorkspaceChecklists(store, session, limit));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
sendJson(res, 400, { error: 'invalid session id' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
18
62
|
function sendJson(res, status, body) {
|
|
19
63
|
const payload = Buffer.from(`${JSON.stringify(body)}\n`, 'utf8');
|
|
20
64
|
res.writeHead(status, {
|
package/lib/store.js
CHANGED
|
@@ -38,6 +38,22 @@ export class ChecklistStore {
|
|
|
38
38
|
peek(sessionId) {
|
|
39
39
|
return this.cache.get(sessionId);
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Last modification time of one session's checklist file.
|
|
43
|
+
*
|
|
44
|
+
* @param sessionId - the session to inspect.
|
|
45
|
+
* @returns epoch milliseconds, or undefined when that session has no file.
|
|
46
|
+
*/
|
|
47
|
+
async modifiedAt(sessionId) {
|
|
48
|
+
try {
|
|
49
|
+
const info = await stat(this.fileFor(sessionId));
|
|
50
|
+
// Rounded: mtimeMs is a float, and the lens output declares it integer.
|
|
51
|
+
return Math.round(info.mtimeMs);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
41
57
|
/** Load one session (cache first, then disk). */
|
|
42
58
|
async prime(sessionId) {
|
|
43
59
|
const cached = this.cache.get(sessionId);
|
package/lib/tools.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
8
|
import { buildItems, checkRename, countItems, findList, flatten, ITEM_STATES, isItemState, mintId, nextItem, removeItemById, resolveRef, toLines, } from './model.js';
|
|
9
|
+
import { listWorkspaceChecklists } from './workspace.js';
|
|
9
10
|
/** Ships with the plugin so a first run has something to instantiate. */
|
|
10
11
|
const DEFAULT_TEMPLATES = {
|
|
11
12
|
release: [
|
|
@@ -614,6 +615,69 @@ export function registerChecklistTools(ctx, store, config = {}) {
|
|
|
614
615
|
});
|
|
615
616
|
},
|
|
616
617
|
})));
|
|
618
|
+
ctx.effect(() => ctx.tools.register(defineTool({
|
|
619
|
+
name: 'checklist_workspace',
|
|
620
|
+
description: 'List the checklists of OTHER sessions in the same workspace. Read-only; the current session is '
|
|
621
|
+
+ 'excluded, and these lists never enter the prompt on their own. Use it when the user asks what '
|
|
622
|
+
+ 'is pending in this project across sessions.',
|
|
623
|
+
parameters: {
|
|
624
|
+
limit: {
|
|
625
|
+
type: 'integer',
|
|
626
|
+
description: 'Maximum checklists to return, newest first (default 10).',
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
output: {
|
|
630
|
+
schema: {
|
|
631
|
+
type: 'object',
|
|
632
|
+
additionalProperties: false,
|
|
633
|
+
properties: {
|
|
634
|
+
workspaceTitle: { type: 'string' },
|
|
635
|
+
workspacePath: { type: 'string' },
|
|
636
|
+
checklists: {
|
|
637
|
+
type: 'array',
|
|
638
|
+
required: true,
|
|
639
|
+
items: {
|
|
640
|
+
type: 'object',
|
|
641
|
+
additionalProperties: false,
|
|
642
|
+
properties: {
|
|
643
|
+
sessionId: { type: 'string', required: true },
|
|
644
|
+
title: { type: 'string', required: true },
|
|
645
|
+
total: { type: 'integer', required: true },
|
|
646
|
+
done: { type: 'integer', required: true },
|
|
647
|
+
updatedAt: { type: 'integer', required: true },
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
render: (_args, value) => {
|
|
654
|
+
const view = value;
|
|
655
|
+
if (view.checklists.length === 0) {
|
|
656
|
+
return [{ type: 'text', text: 'No other session in this workspace has a checklist.' }];
|
|
657
|
+
}
|
|
658
|
+
const lines = view.checklists.map((entry) => `· ${entry.title} — ${entry.done}/${entry.total}`);
|
|
659
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
timeoutMs: 10_000,
|
|
663
|
+
async execute(args, exec) {
|
|
664
|
+
if ((config.workspaceView ?? 'on-demand') === 'off') {
|
|
665
|
+
throw new Error('checklist_workspace: the workspace lens is disabled in this profile');
|
|
666
|
+
}
|
|
667
|
+
const sessionId = sessionOf(exec);
|
|
668
|
+
const input = args;
|
|
669
|
+
const requested = typeof input.limit === 'number' && Number.isFinite(input.limit)
|
|
670
|
+
? Math.trunc(input.limit)
|
|
671
|
+
: undefined;
|
|
672
|
+
const limit = Math.min(Math.max(1, requested ?? config.workspaceViewLimit ?? 10), 50);
|
|
673
|
+
const view = await listWorkspaceChecklists(store, sessionId, limit);
|
|
674
|
+
return {
|
|
675
|
+
...(view.workspace?.title ? { workspaceTitle: view.workspace.title } : {}),
|
|
676
|
+
...(view.workspace?.path ? { workspacePath: view.workspace.path } : {}),
|
|
677
|
+
checklists: view.checklists,
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
})));
|
|
617
681
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
618
682
|
name: 'checklist_delete',
|
|
619
683
|
description: 'Delete one checklist from the CURRENT session. The list and its items are gone; there is no archive. '
|
package/lib/types/index.d.ts
CHANGED
|
@@ -31,5 +31,14 @@ export interface Config {
|
|
|
31
31
|
* - `off`: do not evaluate at all.
|
|
32
32
|
*/
|
|
33
33
|
nudge?: 'steer' | 'observe' | 'off';
|
|
34
|
+
/**
|
|
35
|
+
* Workspace lens: viewing checklists from other sessions of the same project.
|
|
36
|
+
* - `on-demand` (default): available, but nothing is shown unless asked.
|
|
37
|
+
* - `always`: the sidebar section starts expanded.
|
|
38
|
+
* - `off`: no route, no tool.
|
|
39
|
+
*/
|
|
40
|
+
workspaceView?: 'off' | 'on-demand' | 'always';
|
|
41
|
+
/** Maximum sibling checklists the lens returns (default 10). */
|
|
42
|
+
workspaceViewLimit?: number;
|
|
34
43
|
}
|
|
35
44
|
export declare function apply(ctx: Context, config?: Config): void;
|
package/lib/types/route.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
9
9
|
import type { ChecklistStore } from './store.js';
|
|
10
10
|
export declare const STATE_ROUTE = "/dsh-checklist/state";
|
|
11
11
|
export declare const ITEM_ROUTE = "/dsh-checklist/item";
|
|
12
|
+
export declare const WORKSPACE_ROUTE = "/dsh-checklist/workspace";
|
|
13
|
+
/**
|
|
14
|
+
* Serve the workspace lens: checklists from other sessions in the same workspace.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately separate from the session state route: this data never enters the
|
|
17
|
+
* agent's context unless a tool explicitly asks for it.
|
|
18
|
+
*
|
|
19
|
+
* @param req - the incoming request; must be GET.
|
|
20
|
+
* @param res - the response to write.
|
|
21
|
+
* @param store - the checklist store.
|
|
22
|
+
* @param limit - maximum entries returned, newest first.
|
|
23
|
+
* @param enabled - false when the profile turned the lens off.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serveWorkspace(req: IncomingMessage, res: ServerResponse, store: ChecklistStore, limit: number, enabled: boolean): Promise<void>;
|
|
12
26
|
/**
|
|
13
27
|
* Serve one session's checklist state.
|
|
14
28
|
*
|
package/lib/types/store.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export declare class ChecklistStore {
|
|
|
23
23
|
constructor(root?: string);
|
|
24
24
|
/** The synchronous view used by the prompt digest; `undefined` until primed. */
|
|
25
25
|
peek(sessionId: string): SessionDoc | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Last modification time of one session's checklist file.
|
|
28
|
+
*
|
|
29
|
+
* @param sessionId - the session to inspect.
|
|
30
|
+
* @returns epoch milliseconds, or undefined when that session has no file.
|
|
31
|
+
*/
|
|
32
|
+
modifiedAt(sessionId: string): Promise<number | undefined>;
|
|
26
33
|
/** Load one session (cache first, then disk). */
|
|
27
34
|
prime(sessionId: string): Promise<SessionDoc>;
|
|
28
35
|
/**
|
package/lib/types/tools.d.ts
CHANGED
|
@@ -9,5 +9,9 @@ import type { ChecklistStore } from './store.js';
|
|
|
9
9
|
export interface ToolsConfig {
|
|
10
10
|
maxItemsPerList?: number;
|
|
11
11
|
templates?: Record<string, string[]>;
|
|
12
|
+
/** `off` removes the workspace lens entirely; the other modes expose the tool. */
|
|
13
|
+
workspaceView?: 'off' | 'on-demand' | 'always';
|
|
14
|
+
/** Maximum sibling checklists the lens returns. */
|
|
15
|
+
workspaceViewLimit?: number;
|
|
12
16
|
}
|
|
13
17
|
export declare function registerChecklistTools(ctx: Context, store: ChecklistStore, config?: ToolsConfig): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace lens: a read-only view of checklists from sibling sessions.
|
|
3
|
+
*
|
|
4
|
+
* Two independent facts drive this module, and keeping them apart is what makes
|
|
5
|
+
* it survive host changes:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Membership comes from disk.** A session lives in
|
|
8
|
+
* `$DSH_HOME/sessions/<encoded-workspace>/<sessionId>/`. The host's
|
|
9
|
+
* `storages/workspace.json` was missing a workspace on the machine this was
|
|
10
|
+
* written on, so it is only consulted for a friendly title/path — never to
|
|
11
|
+
* decide which sessions belong together.
|
|
12
|
+
* 2. **Content comes from our own store.** Session logs are never decoded;
|
|
13
|
+
* titles and counts are already in the checklist document.
|
|
14
|
+
*/
|
|
15
|
+
import { type ChecklistStore } from './store.js';
|
|
16
|
+
export interface WorkspaceInfo {
|
|
17
|
+
/** Encoded directory name the host uses under `$DSH_HOME/sessions`. */
|
|
18
|
+
key: string;
|
|
19
|
+
/** Friendly workspace title, when the host metadata knows it. */
|
|
20
|
+
title?: string;
|
|
21
|
+
/** Absolute workspace path, when the host metadata knows it. */
|
|
22
|
+
path?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface WorkspaceListSummary {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
total: number;
|
|
28
|
+
done: number;
|
|
29
|
+
/** Last write to that session's checklist file, in epoch milliseconds. */
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
}
|
|
32
|
+
export interface WorkspaceView {
|
|
33
|
+
workspace?: WorkspaceInfo;
|
|
34
|
+
checklists: WorkspaceListSummary[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find the workspace directory that contains a session.
|
|
38
|
+
*
|
|
39
|
+
* @param sessionId - the session to locate.
|
|
40
|
+
* @returns the encoded workspace key, or undefined when the session has no dir.
|
|
41
|
+
*/
|
|
42
|
+
export declare function workspaceKeyOf(sessionId: string): Promise<string | undefined>;
|
|
43
|
+
/** Session ids that share one workspace directory. */
|
|
44
|
+
export declare function siblingSessionIds(key: string): Promise<string[]>;
|
|
45
|
+
/**
|
|
46
|
+
* List the checklists of every *other* session in the caller's workspace.
|
|
47
|
+
*
|
|
48
|
+
* @param store - the checklist store.
|
|
49
|
+
* @param sessionId - the calling session, excluded from the result.
|
|
50
|
+
* @param limit - maximum entries returned, newest first.
|
|
51
|
+
* @returns the workspace description plus one summary per sibling checklist.
|
|
52
|
+
*/
|
|
53
|
+
export declare function listWorkspaceChecklists(store: ChecklistStore, sessionId: string, limit: number): Promise<WorkspaceView>;
|
package/lib/workspace.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace lens: a read-only view of checklists from sibling sessions.
|
|
3
|
+
*
|
|
4
|
+
* Two independent facts drive this module, and keeping them apart is what makes
|
|
5
|
+
* it survive host changes:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Membership comes from disk.** A session lives in
|
|
8
|
+
* `$DSH_HOME/sessions/<encoded-workspace>/<sessionId>/`. The host's
|
|
9
|
+
* `storages/workspace.json` was missing a workspace on the machine this was
|
|
10
|
+
* written on, so it is only consulted for a friendly title/path — never to
|
|
11
|
+
* decide which sessions belong together.
|
|
12
|
+
* 2. **Content comes from our own store.** Session logs are never decoded;
|
|
13
|
+
* titles and counts are already in the checklist document.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { countItems } from './model.js';
|
|
18
|
+
import { resolveDshHome } from './store.js';
|
|
19
|
+
const SESSION_DIR = /^session-/;
|
|
20
|
+
function sessionsRoot() {
|
|
21
|
+
return join(resolveDshHome(), 'sessions');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the workspace directory that contains a session.
|
|
25
|
+
*
|
|
26
|
+
* @param sessionId - the session to locate.
|
|
27
|
+
* @returns the encoded workspace key, or undefined when the session has no dir.
|
|
28
|
+
*/
|
|
29
|
+
export async function workspaceKeyOf(sessionId) {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = await readdir(sessionsRoot());
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.startsWith('--'))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const info = await stat(join(sessionsRoot(), entry, sessionId));
|
|
42
|
+
if (info.isDirectory())
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not this workspace; keep looking.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Session ids that share one workspace directory. */
|
|
52
|
+
export async function siblingSessionIds(key) {
|
|
53
|
+
try {
|
|
54
|
+
const entries = await readdir(join(sessionsRoot(), key), { withFileTypes: true });
|
|
55
|
+
return entries
|
|
56
|
+
.filter((entry) => entry.isDirectory() && SESSION_DIR.test(entry.name))
|
|
57
|
+
.map((entry) => entry.name);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read the host's workspace table.
|
|
65
|
+
*
|
|
66
|
+
* Best effort: any parse problem yields no records, and the lens still works
|
|
67
|
+
* with the encoded directory name.
|
|
68
|
+
*/
|
|
69
|
+
async function hostWorkspaces() {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await readFile(join(resolveDshHome(), 'storages', 'workspace.json'), 'utf8');
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
const table = parsed.tables?.workspaces;
|
|
74
|
+
if (table === null || typeof table !== 'object')
|
|
75
|
+
return [];
|
|
76
|
+
return Object.values(table)
|
|
77
|
+
.filter((value) => value !== null && typeof value === 'object');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Describe a workspace by matching host records on session membership. */
|
|
84
|
+
async function describeWorkspace(key, siblings) {
|
|
85
|
+
const wanted = new Set(siblings);
|
|
86
|
+
const records = await hostWorkspaces();
|
|
87
|
+
const match = records.find((record) => {
|
|
88
|
+
const ids = record.sessionIds;
|
|
89
|
+
return Array.isArray(ids) && ids.some((id) => typeof id === 'string' && wanted.has(id));
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
key,
|
|
93
|
+
...(typeof match?.title === 'string' ? { title: match.title } : {}),
|
|
94
|
+
...(typeof match?.path === 'string' ? { path: match.path } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* List the checklists of every *other* session in the caller's workspace.
|
|
99
|
+
*
|
|
100
|
+
* @param store - the checklist store.
|
|
101
|
+
* @param sessionId - the calling session, excluded from the result.
|
|
102
|
+
* @param limit - maximum entries returned, newest first.
|
|
103
|
+
* @returns the workspace description plus one summary per sibling checklist.
|
|
104
|
+
*/
|
|
105
|
+
export async function listWorkspaceChecklists(store, sessionId, limit) {
|
|
106
|
+
const key = await workspaceKeyOf(sessionId);
|
|
107
|
+
if (!key)
|
|
108
|
+
return { checklists: [] };
|
|
109
|
+
const siblings = (await siblingSessionIds(key)).filter((id) => id !== sessionId);
|
|
110
|
+
const workspace = await describeWorkspace(key, siblings);
|
|
111
|
+
const checklists = [];
|
|
112
|
+
for (const id of siblings) {
|
|
113
|
+
const updatedAt = await store.modifiedAt(id);
|
|
114
|
+
if (updatedAt === undefined)
|
|
115
|
+
continue; // That session never used the plugin.
|
|
116
|
+
const doc = await store.prime(id);
|
|
117
|
+
const first = doc.lists[0];
|
|
118
|
+
if (!first)
|
|
119
|
+
continue;
|
|
120
|
+
const totals = doc.lists.reduce((acc, list) => {
|
|
121
|
+
const counts = countItems(list.items);
|
|
122
|
+
return { total: acc.total + counts.total, done: acc.done + counts.done };
|
|
123
|
+
}, { total: 0, done: 0 });
|
|
124
|
+
checklists.push({
|
|
125
|
+
sessionId: id,
|
|
126
|
+
title: doc.lists.length === 1 ? first.title : `${first.title} (+${doc.lists.length - 1})`,
|
|
127
|
+
total: totals.total,
|
|
128
|
+
done: totals.done,
|
|
129
|
+
updatedAt,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
checklists.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
133
|
+
return { workspace, checklists: checklists.slice(0, Math.max(1, limit)) };
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-checklists",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|