@crouton-kit/crouter 0.3.21 → 0.3.23
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/dist/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +61 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +270 -0
- package/dist/commands/canvas-history/show.d.ts +1 -0
- package/dist/commands/canvas-history/show.js +79 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +33 -0
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/memory/find.js +7 -40
- package/dist/commands/memory/lint.js +19 -34
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/pkg/bridge.d.ts +1 -0
- package/dist/commands/pkg/bridge.js +137 -0
- package/dist/commands/pkg/plugin-inspect.js +7 -6
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/skill/author.js +35 -44
- package/dist/commands/skill/shared.d.ts +1 -5
- package/dist/commands/skill/shared.js +9 -63
- package/dist/commands/skill.js +5 -6
- package/dist/commands/sys/doctor.js +6 -144
- package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
- package/dist/core/__tests__/child-death-wake.test.js +133 -185
- package/dist/core/__tests__/daemon-liveness.test.js +28 -258
- package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
- package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
- package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
- package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
- package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
- package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +1 -1
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
- package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +5 -5
- package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
- package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
- package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
- package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
- package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
- package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/full/spike-harness.test.js +117 -0
- package/dist/core/__tests__/grace-clock.test.js +72 -75
- package/dist/core/__tests__/helpers/harness.d.ts +35 -1
- package/dist/core/__tests__/helpers/harness.js +70 -12
- package/dist/core/__tests__/live-mutation-verbs.test.js +99 -104
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/revive.test.js +96 -86
- package/dist/core/__tests__/subscription-delivery.test.js +116 -138
- package/dist/core/__tests__/wake-origin.test.js +54 -50
- package/dist/core/bridge-map.d.ts +19 -0
- package/dist/core/bridge-map.js +73 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +389 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/config.js +3 -22
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/persona.js +5 -7
- package/dist/core/scope.d.ts +7 -5
- package/dist/core/scope.js +16 -17
- package/dist/core/substrate/ceiling.d.ts +28 -0
- package/dist/core/substrate/ceiling.js +87 -0
- package/dist/core/substrate/index.d.ts +2 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/core/substrate/on-read.js +18 -2
- package/dist/core/substrate/render.d.ts +2 -3
- package/dist/core/substrate/render.js +63 -51
- package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
- package/dist/types.d.ts +3 -18
- package/dist/types.js +0 -8
- package/package.json +4 -3
- package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
- package/dist/builtin-skills/skills/design/SKILL.md +0 -51
- package/dist/builtin-skills/skills/development/SKILL.md +0 -109
- package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
- package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
- package/dist/commands/skill/state.d.ts +0 -3
- package/dist/commands/skill/state.js +0 -71
- package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
- package/dist/core/__tests__/broker-dialogs.test.js +0 -126
- package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
- package/dist/core/__tests__/resolver.test.js +0 -181
- package/dist/core/__tests__/spike-harness.test.js +0 -242
- /package/dist/core/__tests__/{broker-attach-limits.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spike-harness.test.d.ts → full/spike-harness.test.d.ts} +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// canvas history — the scan-first engine over the per-cwd episodic record.
|
|
2
|
+
//
|
|
3
|
+
// The canvas accumulates a high-signal corpus per cwd: every node that ran
|
|
4
|
+
// there plus the artifacts it left — its `reports/` (append-only push history:
|
|
5
|
+
// final/update/urgent outcome summaries) and `context/` docs (specs, designs,
|
|
6
|
+
// roadmaps, findings). This module is the ONE place that scans that corpus; it
|
|
7
|
+
// lives in the canvas data-access layer, so it resolves every node + path
|
|
8
|
+
// through canvas.ts + paths.ts and never hand-joins. No persistent index —
|
|
9
|
+
// node facets (cwd/kind/status) are filtered cheaply against the db FIRST, then
|
|
10
|
+
// only the surviving nodes' artifact files are read. FTS5/bm25 is the future
|
|
11
|
+
// upgrade; the surface above is backend-independent.
|
|
12
|
+
import { statSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { join, relative, sep } from 'node:path';
|
|
14
|
+
import { getNode, listNodes, view } from './canvas.js';
|
|
15
|
+
import { contextDir, reportsDir, nodeDir } from './paths.js';
|
|
16
|
+
import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
|
|
17
|
+
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
18
|
+
/** The cwd the caller means by default: the calling node's cwd (resolved from
|
|
19
|
+
* CRTR_NODE_ID via the canvas data layer), falling back to the process cwd
|
|
20
|
+
* when not run as a node. */
|
|
21
|
+
export function callerCwd() {
|
|
22
|
+
const id = process.env['CRTR_NODE_ID'];
|
|
23
|
+
if (id !== undefined && id !== '') {
|
|
24
|
+
const node = getNode(id);
|
|
25
|
+
if (node !== null)
|
|
26
|
+
return node.cwd;
|
|
27
|
+
}
|
|
28
|
+
return process.cwd();
|
|
29
|
+
}
|
|
30
|
+
/** Compact report stamp `20260607T075536` → `2026-06-07T07:55:36`. */
|
|
31
|
+
function compactToIso(stamp) {
|
|
32
|
+
const m = stamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/);
|
|
33
|
+
if (m === null)
|
|
34
|
+
return stamp;
|
|
35
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`;
|
|
36
|
+
}
|
|
37
|
+
function toMs(ts) {
|
|
38
|
+
const n = Date.parse(ts);
|
|
39
|
+
return Number.isNaN(n) ? 0 : n;
|
|
40
|
+
}
|
|
41
|
+
/** First markdown heading (`# …`) or first non-empty line, trimmed. */
|
|
42
|
+
function firstHeading(body) {
|
|
43
|
+
const lines = body.split('\n');
|
|
44
|
+
for (const l of lines) {
|
|
45
|
+
const t = l.trim();
|
|
46
|
+
if (t.startsWith('#'))
|
|
47
|
+
return t.replace(/^#+\s*/, '').trim();
|
|
48
|
+
}
|
|
49
|
+
for (const l of lines) {
|
|
50
|
+
const t = l.trim();
|
|
51
|
+
if (t !== '')
|
|
52
|
+
return t.length > 120 ? t.slice(0, 120) : t;
|
|
53
|
+
}
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
/** A memoized body loader for a file artifact — reads + strips frontmatter once. */
|
|
57
|
+
function fileBodyLoader(absPath) {
|
|
58
|
+
let cached = null;
|
|
59
|
+
const load = () => {
|
|
60
|
+
if (cached !== null)
|
|
61
|
+
return cached;
|
|
62
|
+
const raw = readTextIfExists(absPath);
|
|
63
|
+
cached = raw === null ? '' : parseFrontmatterGeneric(raw).body;
|
|
64
|
+
return cached;
|
|
65
|
+
};
|
|
66
|
+
// Read the head eagerly for ts/title derivation (the file is small markdown).
|
|
67
|
+
const raw = readTextIfExists(absPath);
|
|
68
|
+
const head = raw === null ? '' : raw;
|
|
69
|
+
if (raw !== null)
|
|
70
|
+
cached = parseFrontmatterGeneric(raw).body;
|
|
71
|
+
return { load, head };
|
|
72
|
+
}
|
|
73
|
+
function reportArtifact(row, desc, filename) {
|
|
74
|
+
const abs = join(reportsDir(row.node_id), filename);
|
|
75
|
+
const m = filename.match(/^([0-9]{8}T[0-9]{6})-(\w+)\.md$/);
|
|
76
|
+
const { load, head } = fileBodyLoader(abs);
|
|
77
|
+
const fm = parseFrontmatterGeneric(head).data;
|
|
78
|
+
const reportKind = (typeof fm?.['kind'] === 'string' ? fm['kind'] : undefined) ?? (m ? m[2] : 'report');
|
|
79
|
+
const ts = (typeof fm?.['ts'] === 'string' ? fm['ts'] : undefined) ??
|
|
80
|
+
(m ? compactToIso(m[1]) : new Date(statSync(abs).mtimeMs).toISOString());
|
|
81
|
+
const body = load();
|
|
82
|
+
return {
|
|
83
|
+
ref: `${row.node_id}:reports/${filename}`,
|
|
84
|
+
nodeId: row.node_id,
|
|
85
|
+
relpath: `reports/${filename}`,
|
|
86
|
+
source: 'report',
|
|
87
|
+
reportKind,
|
|
88
|
+
ts,
|
|
89
|
+
tsMs: toMs(ts),
|
|
90
|
+
title: firstHeading(body),
|
|
91
|
+
nodeName: row.name,
|
|
92
|
+
nodeKind: row.kind,
|
|
93
|
+
nodeStatus: row.status,
|
|
94
|
+
nodeCwd: row.cwd,
|
|
95
|
+
nodeDesc: desc,
|
|
96
|
+
path: abs,
|
|
97
|
+
loadBody: load,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function docArtifact(row, desc, abs) {
|
|
101
|
+
const rel = relative(nodeDir(row.node_id), abs).split(sep).join('/');
|
|
102
|
+
const isRoadmap = rel === 'context/roadmap.md';
|
|
103
|
+
const { load } = fileBodyLoader(abs);
|
|
104
|
+
let ts;
|
|
105
|
+
try {
|
|
106
|
+
ts = new Date(statSync(abs).mtimeMs).toISOString();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
ts = row.created;
|
|
110
|
+
}
|
|
111
|
+
const body = load();
|
|
112
|
+
return {
|
|
113
|
+
ref: `${row.node_id}:${rel}`,
|
|
114
|
+
nodeId: row.node_id,
|
|
115
|
+
relpath: rel,
|
|
116
|
+
source: isRoadmap ? 'roadmap' : 'doc',
|
|
117
|
+
ts,
|
|
118
|
+
tsMs: toMs(ts),
|
|
119
|
+
title: firstHeading(body) || rel.replace(/^context\//, ''),
|
|
120
|
+
nodeName: row.name,
|
|
121
|
+
nodeKind: row.kind,
|
|
122
|
+
nodeStatus: row.status,
|
|
123
|
+
nodeCwd: row.cwd,
|
|
124
|
+
nodeDesc: desc,
|
|
125
|
+
path: abs,
|
|
126
|
+
loadBody: load,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function metaArtifact(row, desc) {
|
|
130
|
+
const body = `${row.name}\n${desc}\n${row.kind}`;
|
|
131
|
+
return {
|
|
132
|
+
ref: `${row.node_id}:meta`,
|
|
133
|
+
nodeId: row.node_id,
|
|
134
|
+
relpath: 'meta',
|
|
135
|
+
source: 'meta',
|
|
136
|
+
ts: row.created,
|
|
137
|
+
tsMs: toMs(row.created),
|
|
138
|
+
title: row.name,
|
|
139
|
+
nodeName: row.name,
|
|
140
|
+
nodeKind: row.kind,
|
|
141
|
+
nodeStatus: row.status,
|
|
142
|
+
nodeCwd: row.cwd,
|
|
143
|
+
nodeDesc: desc,
|
|
144
|
+
path: null,
|
|
145
|
+
loadBody: () => body,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** List the markdown reports a node left, newest filename first. */
|
|
149
|
+
function listReportFiles(nodeId) {
|
|
150
|
+
const dir = reportsDir(nodeId);
|
|
151
|
+
if (!pathExists(dir))
|
|
152
|
+
return [];
|
|
153
|
+
try {
|
|
154
|
+
return readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Enumerate every context markdown doc a node left (recursive). */
|
|
161
|
+
function listDocFiles(nodeId) {
|
|
162
|
+
const dir = contextDir(nodeId);
|
|
163
|
+
if (!pathExists(dir))
|
|
164
|
+
return [];
|
|
165
|
+
try {
|
|
166
|
+
return walkFiles(dir, (name) => name.endsWith('.md'));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** The node desc lives in meta.json, not the row — read it once per survivor. */
|
|
173
|
+
function nodeDescOf(nodeId) {
|
|
174
|
+
const n = getNode(nodeId);
|
|
175
|
+
return n?.description ?? '';
|
|
176
|
+
}
|
|
177
|
+
/** Build the searchable artifact set for a scope + corpus filter. Node facets
|
|
178
|
+
* are applied against the db rows first (cheap); only surviving nodes' files
|
|
179
|
+
* are read. */
|
|
180
|
+
export function buildCorpus(scope, corpus) {
|
|
181
|
+
const rows = scopedRows(scope);
|
|
182
|
+
// Node-facet narrowing (cheap, on the row).
|
|
183
|
+
const kinds = corpus.kinds;
|
|
184
|
+
const statuses = corpus.statuses;
|
|
185
|
+
const survivors = rows.filter((r) => {
|
|
186
|
+
if (kinds !== undefined && kinds.length > 0 && !kinds.includes(r.kind))
|
|
187
|
+
return false;
|
|
188
|
+
if (statuses !== undefined && statuses.length > 0 && !statuses.includes(r.status))
|
|
189
|
+
return false;
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
// --report-kind implies type=report.
|
|
193
|
+
const wantReportKind = corpus.reportKind;
|
|
194
|
+
const types = corpus.types !== undefined && corpus.types.length > 0
|
|
195
|
+
? corpus.types
|
|
196
|
+
: wantReportKind !== undefined
|
|
197
|
+
? ['report']
|
|
198
|
+
: ['report', 'doc', 'roadmap', 'meta'];
|
|
199
|
+
const want = new Set(types);
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const row of survivors) {
|
|
202
|
+
const desc = want.has('meta') || want.has('report') || want.has('doc') || want.has('roadmap')
|
|
203
|
+
? nodeDescOf(row.node_id)
|
|
204
|
+
: '';
|
|
205
|
+
if (want.has('meta'))
|
|
206
|
+
out.push(metaArtifact(row, desc));
|
|
207
|
+
if (want.has('report')) {
|
|
208
|
+
for (const f of listReportFiles(row.node_id)) {
|
|
209
|
+
const a = reportArtifact(row, desc, f);
|
|
210
|
+
if (a === null)
|
|
211
|
+
continue;
|
|
212
|
+
if (wantReportKind !== undefined && a.reportKind !== wantReportKind)
|
|
213
|
+
continue;
|
|
214
|
+
out.push(a);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (want.has('doc') || want.has('roadmap')) {
|
|
218
|
+
for (const abs of listDocFiles(row.node_id)) {
|
|
219
|
+
const a = docArtifact(row, desc, abs);
|
|
220
|
+
if (a.source === 'roadmap' && !want.has('roadmap'))
|
|
221
|
+
continue;
|
|
222
|
+
if (a.source === 'doc' && !want.has('doc'))
|
|
223
|
+
continue;
|
|
224
|
+
out.push(a);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Time window on artifact ts.
|
|
229
|
+
return out.filter((a) => {
|
|
230
|
+
if (corpus.sinceMs !== undefined && a.tsMs < corpus.sinceMs)
|
|
231
|
+
return false;
|
|
232
|
+
if (corpus.untilMs !== undefined && a.tsMs > corpus.untilMs)
|
|
233
|
+
return false;
|
|
234
|
+
return true;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** Resolve the scope to the node rows in play. */
|
|
238
|
+
function scopedRows(scope) {
|
|
239
|
+
const all = listNodes();
|
|
240
|
+
const byId = new Map(all.map((r) => [r.node_id, r]));
|
|
241
|
+
if (scope.nodes !== undefined && scope.nodes.length > 0) {
|
|
242
|
+
return scope.nodes.map((id) => byId.get(id)).filter((r) => r !== undefined);
|
|
243
|
+
}
|
|
244
|
+
if (scope.under !== undefined && scope.under !== '') {
|
|
245
|
+
const ids = [scope.under, ...view(scope.under)];
|
|
246
|
+
return ids.map((id) => byId.get(id)).filter((r) => r !== undefined);
|
|
247
|
+
}
|
|
248
|
+
if (scope.allCwds === true)
|
|
249
|
+
return all;
|
|
250
|
+
const cwd = scope.cwd !== undefined && scope.cwd !== '' ? scope.cwd : callerCwd();
|
|
251
|
+
return all.filter((r) => r.cwd === cwd);
|
|
252
|
+
}
|
|
253
|
+
/** Resolve a `<node-id>:<relpath>` handle to its content. Returns null when the
|
|
254
|
+
* node is unknown or the artifact does not exist. Throws on path traversal. */
|
|
255
|
+
export function resolveRef(ref) {
|
|
256
|
+
const idx = ref.indexOf(':');
|
|
257
|
+
if (idx === -1)
|
|
258
|
+
return null;
|
|
259
|
+
const nodeId = ref.slice(0, idx);
|
|
260
|
+
const relpath = ref.slice(idx + 1);
|
|
261
|
+
const node = getNode(nodeId);
|
|
262
|
+
if (node === null)
|
|
263
|
+
return null;
|
|
264
|
+
if (relpath === 'meta') {
|
|
265
|
+
const body = `# ${node.name}\n\n- kind: ${node.kind}\n- status: ${node.status}\n- cwd: ${node.cwd}\n- created: ${node.created}\n\n${node.description ?? ''}`;
|
|
266
|
+
return {
|
|
267
|
+
nodeId,
|
|
268
|
+
relpath,
|
|
269
|
+
source: 'meta',
|
|
270
|
+
ts: node.created,
|
|
271
|
+
nodeName: node.name,
|
|
272
|
+
body,
|
|
273
|
+
raw: null,
|
|
274
|
+
path: null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (relpath.includes('..') || relpath.startsWith('/')) {
|
|
278
|
+
throw new Error(`illegal relpath in ref: ${relpath}`);
|
|
279
|
+
}
|
|
280
|
+
const abs = join(nodeDir(nodeId), relpath);
|
|
281
|
+
const raw = readTextIfExists(abs);
|
|
282
|
+
if (raw === null)
|
|
283
|
+
return null;
|
|
284
|
+
const { body, data: fm } = parseFrontmatterGeneric(raw);
|
|
285
|
+
const isReport = relpath.startsWith('reports/');
|
|
286
|
+
const isRoadmap = relpath === 'context/roadmap.md';
|
|
287
|
+
let ts;
|
|
288
|
+
if (typeof fm?.['ts'] === 'string')
|
|
289
|
+
ts = fm['ts'];
|
|
290
|
+
else {
|
|
291
|
+
try {
|
|
292
|
+
ts = new Date(statSync(abs).mtimeMs).toISOString();
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
ts = node.created;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
nodeId,
|
|
300
|
+
relpath,
|
|
301
|
+
source: isReport ? 'report' : isRoadmap ? 'roadmap' : 'doc',
|
|
302
|
+
reportKind: isReport && typeof fm?.['kind'] === 'string' ? fm['kind'] : undefined,
|
|
303
|
+
ts,
|
|
304
|
+
nodeName: node.name,
|
|
305
|
+
body,
|
|
306
|
+
raw,
|
|
307
|
+
path: abs,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/** Enumerate one node's artifacts (for `history show`). */
|
|
311
|
+
export function nodeArtifacts(nodeId, types) {
|
|
312
|
+
const node = getNode(nodeId);
|
|
313
|
+
if (node === null)
|
|
314
|
+
return [];
|
|
315
|
+
const row = {
|
|
316
|
+
node_id: node.node_id,
|
|
317
|
+
name: node.name,
|
|
318
|
+
kind: node.kind,
|
|
319
|
+
mode: node.mode,
|
|
320
|
+
lifecycle: node.lifecycle,
|
|
321
|
+
status: node.status,
|
|
322
|
+
cwd: node.cwd,
|
|
323
|
+
host_kind: node.host_kind ?? null,
|
|
324
|
+
parent: node.parent ?? null,
|
|
325
|
+
created: node.created,
|
|
326
|
+
intent: node.intent ?? null,
|
|
327
|
+
pi_pid: node.pi_pid ?? null,
|
|
328
|
+
window: node.window ?? null,
|
|
329
|
+
tmux_session: node.tmux_session ?? null,
|
|
330
|
+
pane: node.pane ?? null,
|
|
331
|
+
};
|
|
332
|
+
const want = new Set(types !== undefined && types.length > 0 ? types : ['report', 'doc', 'roadmap']);
|
|
333
|
+
const desc = node.description ?? '';
|
|
334
|
+
const out = [];
|
|
335
|
+
if (want.has('report')) {
|
|
336
|
+
for (const f of listReportFiles(nodeId)) {
|
|
337
|
+
const a = reportArtifact(row, desc, f);
|
|
338
|
+
if (a !== null)
|
|
339
|
+
out.push(a);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (want.has('doc') || want.has('roadmap')) {
|
|
343
|
+
for (const abs of listDocFiles(nodeId)) {
|
|
344
|
+
const a = docArtifact(row, desc, abs);
|
|
345
|
+
if (a.source === 'roadmap' && !want.has('roadmap'))
|
|
346
|
+
continue;
|
|
347
|
+
if (a.source === 'doc' && !want.has('doc'))
|
|
348
|
+
continue;
|
|
349
|
+
out.push(a);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
/** Bounded per-cwd aggregate for the branch `-h` `<corpus>` block. Counts files
|
|
355
|
+
* on disk without reading bodies. */
|
|
356
|
+
export function corpusStats(cwd) {
|
|
357
|
+
const rows = listNodes().filter((r) => r.cwd === cwd);
|
|
358
|
+
let reports = 0;
|
|
359
|
+
let docs = 0;
|
|
360
|
+
let min = Infinity;
|
|
361
|
+
let max = -Infinity;
|
|
362
|
+
for (const row of rows) {
|
|
363
|
+
const cMs = toMs(row.created);
|
|
364
|
+
if (cMs > 0) {
|
|
365
|
+
if (cMs < min)
|
|
366
|
+
min = cMs;
|
|
367
|
+
if (cMs > max)
|
|
368
|
+
max = cMs;
|
|
369
|
+
}
|
|
370
|
+
for (const f of listReportFiles(row.node_id)) {
|
|
371
|
+
reports++;
|
|
372
|
+
const m = f.match(/^([0-9]{8}T[0-9]{6})-/);
|
|
373
|
+
if (m !== null) {
|
|
374
|
+
const t = toMs(compactToIso(m[1]));
|
|
375
|
+
if (t > 0) {
|
|
376
|
+
if (t < min)
|
|
377
|
+
min = t;
|
|
378
|
+
if (t > max)
|
|
379
|
+
max = t;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
docs += listDocFiles(row.node_id).length;
|
|
384
|
+
}
|
|
385
|
+
const span = min !== Infinity && max !== -Infinity
|
|
386
|
+
? { from: new Date(min).toISOString().slice(0, 10), to: new Date(max).toISOString().slice(0, 10) }
|
|
387
|
+
: null;
|
|
388
|
+
return { cwd, nodes: rows.length, reports, docs, span };
|
|
389
|
+
}
|
package/dist/core/config.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { CONFIG_FILE, STATE_FILE,
|
|
2
|
+
import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig } from '../types.js';
|
|
3
3
|
import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
4
4
|
import { scopeRoot, requireScopeRoot } from './scope.js';
|
|
5
|
-
import { diag } from './io.js';
|
|
6
5
|
function configPathFor(root) {
|
|
7
6
|
return join(root, CONFIG_FILE);
|
|
8
7
|
}
|
|
@@ -24,24 +23,7 @@ export function readConfig(scope) {
|
|
|
24
23
|
const existing = readJsonIfExists(configPathFor(root));
|
|
25
24
|
if (!existing)
|
|
26
25
|
return defaultScopeConfig();
|
|
27
|
-
|
|
28
|
-
if ((existing.schema_version ?? 0) < SCHEMA_VERSION) {
|
|
29
|
-
migrateSkillConfigKeys(cfg, scope, root);
|
|
30
|
-
}
|
|
31
|
-
return cfg;
|
|
32
|
-
}
|
|
33
|
-
function migrateSkillConfigKeys(cfg, scope, root) {
|
|
34
|
-
const colonKeys = Object.keys(cfg.skills).filter((k) => k.includes(':'));
|
|
35
|
-
if (colonKeys.length > 0) {
|
|
36
|
-
for (const key of colonKeys) {
|
|
37
|
-
const newKey = key.replace(/:/g, '/');
|
|
38
|
-
cfg.skills[newKey] = cfg.skills[key];
|
|
39
|
-
delete cfg.skills[key];
|
|
40
|
-
}
|
|
41
|
-
diag(`crtr: migrated ${colonKeys.length} skill config keys to slash form in ${scope}`);
|
|
42
|
-
}
|
|
43
|
-
cfg.schema_version = SCHEMA_VERSION;
|
|
44
|
-
writeJson(configPathFor(root), cfg);
|
|
26
|
+
return mergeConfig(existing);
|
|
45
27
|
}
|
|
46
28
|
export function readState(scope) {
|
|
47
29
|
const root = scopeRoot(scope);
|
|
@@ -121,7 +103,6 @@ function mergeConfig(partial) {
|
|
|
121
103
|
const schema_version = partial.schema_version === undefined ? defaults.schema_version : partial.schema_version;
|
|
122
104
|
const marketplaces = partial.marketplaces === undefined ? {} : partial.marketplaces;
|
|
123
105
|
const plugins = partial.plugins === undefined ? {} : partial.plugins;
|
|
124
|
-
const skills = partial.skills === undefined ? {} : partial.skills;
|
|
125
106
|
const au = partial.auto_update;
|
|
126
107
|
const rawInterval = au && typeof au.interval_hours === 'number' ? au.interval_hours : undefined;
|
|
127
108
|
const interval_hours = rawInterval !== undefined && Number.isFinite(rawInterval) && rawInterval >= 0
|
|
@@ -140,7 +121,7 @@ function mergeConfig(partial) {
|
|
|
140
121
|
// The merge drops unknown keys, so `headless` must be carried explicitly or it
|
|
141
122
|
// is lost on every read-modify-write of config.json.
|
|
142
123
|
const headless = typeof partial.headless === 'boolean' ? partial.headless : defaults.headless;
|
|
143
|
-
return { schema_version, marketplaces, plugins,
|
|
124
|
+
return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, canvasNav, headless };
|
|
144
125
|
}
|
|
145
126
|
export function updateConfig(scope, mutate) {
|
|
146
127
|
const cfg = readConfig(scope);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Scope } from '../types.js';
|
|
1
|
+
import type { InstalledPlugin, Scope } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Thin memory-document resolver for the document substrate. Mirrors the SHAPE
|
|
4
4
|
* of the skill resolver (qualifier parse → scope precedence → direct path
|
|
@@ -31,8 +31,15 @@ export declare function memoryDocId(doc: MemoryDoc): string;
|
|
|
31
31
|
/** All memory docs in one scope's memory/ dir, scanned recursively for *.md
|
|
32
32
|
* (topical subdirs supported), sorted by path-derived name. */
|
|
33
33
|
export declare function listMemoryDocs(scope: Scope): MemoryDoc[];
|
|
34
|
+
/** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
|
|
35
|
+
* namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
|
|
36
|
+
* doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
|
|
37
|
+
* slash-separated) then prefixing the plugin name. Builtin has no plugins. */
|
|
38
|
+
export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: Scope): MemoryDoc[];
|
|
34
39
|
/** All memory docs across the resolved scopes, in precedence order
|
|
35
|
-
* (project, then user, then builtin)
|
|
40
|
+
* (project, then user, then builtin). Within each scope, native docs are
|
|
41
|
+
* emitted FIRST, then that scope's enabled-plugin docs — so native wins on the
|
|
42
|
+
* caller's first-wins (scope,name) dedup. Each group is name-sorted within. */
|
|
36
43
|
export declare function listAllMemoryDocs(scope?: Scope): MemoryDoc[];
|
|
37
44
|
/**
|
|
38
45
|
* Resolve a path-derived name to a single memory document.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { join, relative, sep } from 'node:path';
|
|
2
2
|
import { pathExists, readText, walkFiles } from './fs-utils.js';
|
|
3
3
|
import { parseFrontmatterGeneric } from './frontmatter.js';
|
|
4
|
-
import { parseSkillQualifier } from './resolver.js';
|
|
4
|
+
import { listInstalledPlugins, parseSkillQualifier } from './resolver.js';
|
|
5
5
|
import { ambiguous, notFound, usage } from './errors.js';
|
|
6
6
|
import { warn } from './output.js';
|
|
7
|
-
import { projectScopeRoot, scopeMemoryDir } from './scope.js';
|
|
7
|
+
import { pluginMemoryDir, projectScopeRoot, scopeMemoryDir } from './scope.js';
|
|
8
8
|
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
9
9
|
export function memoryDocId(doc) {
|
|
10
10
|
return `${doc.scope}/${doc.name}`;
|
|
@@ -50,14 +50,50 @@ export function listMemoryDocs(scope) {
|
|
|
50
50
|
}
|
|
51
51
|
return docs.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
52
|
}
|
|
53
|
+
/** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
|
|
54
|
+
* namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
|
|
55
|
+
* doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
|
|
56
|
+
* slash-separated) then prefixing the plugin name. Builtin has no plugins. */
|
|
57
|
+
export function listPluginMemoryDocs(plugin, scope) {
|
|
58
|
+
const dir = pluginMemoryDir(plugin);
|
|
59
|
+
if (!pathExists(dir))
|
|
60
|
+
return [];
|
|
61
|
+
const docs = [];
|
|
62
|
+
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
63
|
+
const derived = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
|
|
64
|
+
if (!derived)
|
|
65
|
+
continue;
|
|
66
|
+
const name = `${plugin.name}/${derived}`;
|
|
67
|
+
try {
|
|
68
|
+
docs.push(loadMemoryDoc(name, scope, file));
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
72
|
+
warn(`invalid frontmatter in ${file}: ${msg}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return docs.sort((a, b) => a.name.localeCompare(b.name));
|
|
76
|
+
}
|
|
53
77
|
/** All memory docs across the resolved scopes, in precedence order
|
|
54
|
-
* (project, then user, then builtin)
|
|
78
|
+
* (project, then user, then builtin). Within each scope, native docs are
|
|
79
|
+
* emitted FIRST, then that scope's enabled-plugin docs — so native wins on the
|
|
80
|
+
* caller's first-wins (scope,name) dedup. Each group is name-sorted within. */
|
|
55
81
|
export function listAllMemoryDocs(scope) {
|
|
56
|
-
return scopesInPrecedence(scope).flatMap(
|
|
82
|
+
return scopesInPrecedence(scope).flatMap((s) => [
|
|
83
|
+
...listMemoryDocs(s),
|
|
84
|
+
...listInstalledPlugins(s)
|
|
85
|
+
.filter((p) => p.enabled)
|
|
86
|
+
.flatMap((p) => listPluginMemoryDocs(p, s)),
|
|
87
|
+
]);
|
|
57
88
|
}
|
|
58
89
|
/** Direct full-path lookup of memory/<name>.md across scopes, precedence-first.
|
|
59
90
|
* Returns every scope's hit in precedence order; the resolver takes the first
|
|
60
|
-
* (highest-precedence) one — a fully-specified name is never ambiguous.
|
|
91
|
+
* (highest-precedence) one — a fully-specified name is never ambiguous.
|
|
92
|
+
*
|
|
93
|
+
* A directory INDEX is the cleaner contract: when `<name>.md` is absent but
|
|
94
|
+
* `<name>/INDEX.md` exists, the bare dir name (`taste`) resolves to the dir's
|
|
95
|
+
* INDEX doc — carrying the dir name as its identity. (`taste/INDEX` still
|
|
96
|
+
* resolves directly as the file path.) */
|
|
61
97
|
function findMemoryMatches(name, scope) {
|
|
62
98
|
const matches = [];
|
|
63
99
|
for (const s of scopesInPrecedence(scope)) {
|
|
@@ -65,9 +101,13 @@ function findMemoryMatches(name, scope) {
|
|
|
65
101
|
if (!dir)
|
|
66
102
|
continue;
|
|
67
103
|
const path = join(dir, ...name.split('/')) + '.md';
|
|
68
|
-
if (
|
|
104
|
+
if (pathExists(path)) {
|
|
105
|
+
matches.push(loadMemoryDoc(name, s, path));
|
|
69
106
|
continue;
|
|
70
|
-
|
|
107
|
+
}
|
|
108
|
+
const indexPath = join(dir, ...name.split('/'), 'INDEX.md');
|
|
109
|
+
if (pathExists(indexPath))
|
|
110
|
+
matches.push(loadMemoryDoc(name, s, indexPath));
|
|
71
111
|
}
|
|
72
112
|
return matches;
|
|
73
113
|
}
|
package/dist/core/resolver.d.ts
CHANGED
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import type { InstalledMarketplace, InstalledPlugin, Scope
|
|
1
|
+
import type { InstalledMarketplace, InstalledPlugin, Scope } from '../types.js';
|
|
2
2
|
export declare function listInstalledPlugins(scope: Scope): InstalledPlugin[];
|
|
3
3
|
export declare function listAllPlugins(): InstalledPlugin[];
|
|
4
4
|
export declare function findPluginByName(name: string, scope?: Scope): InstalledPlugin | null;
|
|
5
|
-
interface ScopeConfigs {
|
|
6
|
-
project?: ScopeConfig;
|
|
7
|
-
user: ScopeConfig;
|
|
8
|
-
}
|
|
9
|
-
export declare function effectiveSkillEnabled(pluginName: string, skillName: string, cfgs: ScopeConfigs): {
|
|
10
|
-
enabled: boolean;
|
|
11
|
-
disabledIn?: Scope;
|
|
12
|
-
};
|
|
13
|
-
export declare function listSkillsInPlugin(plugin: InstalledPlugin, cfgs?: ScopeConfigs): Skill[];
|
|
14
|
-
export declare function listScopeRootSkills(scope: Scope, cfgs?: ScopeConfigs): Skill[];
|
|
15
|
-
export declare function listAllSkills(scopeFilter?: Scope): Skill[];
|
|
16
|
-
export declare function listSkillSiblings(skill: Skill): Skill[];
|
|
17
|
-
export declare function listSkillChildren(skill: Skill): Skill[];
|
|
18
|
-
export interface SkillResolutionOpts {
|
|
19
|
-
scope?: Scope;
|
|
20
|
-
pluginFilter?: string;
|
|
21
|
-
}
|
|
22
|
-
export declare function resolveSkill(rawName: string, opts?: SkillResolutionOpts): Skill;
|
|
23
5
|
export interface ParsedSkillQualifier {
|
|
24
6
|
scope?: Scope;
|
|
25
7
|
segments: string[];
|
|
@@ -28,15 +10,4 @@ export declare function parseSkillQualifier(raw: string): ParsedSkillQualifier;
|
|
|
28
10
|
export declare function listInstalledMarketplaces(scope: Scope): InstalledMarketplace[];
|
|
29
11
|
export declare function listAllMarketplaces(): InstalledMarketplace[];
|
|
30
12
|
export declare function findMarketplaceByName(name: string, scope?: Scope): InstalledMarketplace | null;
|
|
31
|
-
export interface CategoryResolution {
|
|
32
|
-
id: string;
|
|
33
|
-
plugin: string | undefined;
|
|
34
|
-
scope: Scope | undefined;
|
|
35
|
-
dir: string;
|
|
36
|
-
indexPath: string | undefined;
|
|
37
|
-
skills: Skill[];
|
|
38
|
-
}
|
|
39
|
-
export declare function resolveCategory(name: string, opts?: SkillResolutionOpts): CategoryResolution | null;
|
|
40
|
-
export declare function buildCategoryIndex(cat: CategoryResolution): string;
|
|
41
13
|
export declare function scopeRootsLabel(): string;
|
|
42
|
-
export {};
|