@crouton-kit/crouter 0.3.21 → 0.3.22
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 @@
|
|
|
1
|
+
export declare const readLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineLeaf } from '../../core/command.js';
|
|
2
|
+
import { notFound, usage } from '../../core/errors.js';
|
|
3
|
+
import { resolveRef } from '../../core/canvas/index.js';
|
|
4
|
+
export const readLeaf = defineLeaf({
|
|
5
|
+
name: 'read',
|
|
6
|
+
description: 'read one search hit\'s full body by its ref',
|
|
7
|
+
whenToUse: 'you picked a hit from `canvas history search` (or `canvas history show`) and want its full content — pass the `<node-id>:<relpath>` ref verbatim. Strips frontmatter by default; add --frontmatter to keep the YAML header.',
|
|
8
|
+
help: {
|
|
9
|
+
name: 'canvas history read',
|
|
10
|
+
summary: 'resolve a <node-id>:<relpath> ref to its full artifact body',
|
|
11
|
+
params: [
|
|
12
|
+
{ kind: 'positional', name: 'ref', required: true, constraint: 'The <node-id>:<relpath> handle from `canvas history search` (e.g. mq186ky0-c754531c:reports/20260607T075536-final.md, or <node-id>:meta for a node\'s identity).' },
|
|
13
|
+
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'Include the artifact\'s YAML frontmatter. Stripped by default.' },
|
|
14
|
+
],
|
|
15
|
+
output: [
|
|
16
|
+
{ name: 'ref', type: 'string', required: true, constraint: 'Echo of the resolved ref.' },
|
|
17
|
+
{ name: 'node', type: 'string', required: true, constraint: 'Node name + id.' },
|
|
18
|
+
{ name: 'source', type: 'string', required: true, constraint: 'report | doc | roadmap | meta (+ report-kind for reports).' },
|
|
19
|
+
{ name: 'ts', type: 'string', required: true, constraint: 'Artifact timestamp.' },
|
|
20
|
+
{ name: 'content', type: 'string', required: true, constraint: 'Full body. Frontmatter stripped unless --frontmatter.' },
|
|
21
|
+
],
|
|
22
|
+
outputKind: 'object',
|
|
23
|
+
effects: ['None. Read-only.'],
|
|
24
|
+
},
|
|
25
|
+
run: async (input) => {
|
|
26
|
+
const ref = input['ref'].trim();
|
|
27
|
+
const includeFrontmatter = input['frontmatter'] === true;
|
|
28
|
+
if (!ref.includes(':')) {
|
|
29
|
+
throw usage(`ref must be <node-id>:<relpath>; received: ${ref}`, {
|
|
30
|
+
received: ref,
|
|
31
|
+
next: 'Get a valid ref from `canvas history search` or `canvas history show <node-id>`.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
let resolved;
|
|
35
|
+
try {
|
|
36
|
+
resolved = resolveRef(ref);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
throw usage(`ref rejected: ${e.message}`, { received: ref, next: 'Refs come verbatim from `canvas history search`; do not hand-construct relpaths.' });
|
|
40
|
+
}
|
|
41
|
+
if (resolved === null) {
|
|
42
|
+
throw notFound(`no artifact for ref: ${ref}`, {
|
|
43
|
+
ref,
|
|
44
|
+
next: 'The node or file may not exist. Re-run `canvas history search` for a current ref, or `canvas history show <node-id>` to list a node\'s artifacts.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const source = resolved.source === 'report' && resolved.reportKind ? `report:${resolved.reportKind}` : resolved.source;
|
|
48
|
+
const content = includeFrontmatter && resolved.raw !== null ? resolved.raw : resolved.body;
|
|
49
|
+
return {
|
|
50
|
+
ref,
|
|
51
|
+
node: `${resolved.nodeName} (${resolved.nodeId})`,
|
|
52
|
+
source,
|
|
53
|
+
ts: resolved.ts,
|
|
54
|
+
content,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
render: (r) => {
|
|
58
|
+
const head = `${r['node']} | source: ${r['source']} | ts: ${r['ts']} | ref: ${r['ref']}`;
|
|
59
|
+
return `${head}\n\n${String(r['content']).trim()}`;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const searchLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { defineLeaf } from '../../core/command.js';
|
|
2
|
+
import { usage } from '../../core/errors.js';
|
|
3
|
+
import { buildCorpus, } from '../../core/canvas/index.js';
|
|
4
|
+
const TYPES = ['report', 'doc', 'roadmap', 'meta'];
|
|
5
|
+
const NODE_KINDS = ['developer', 'spec', 'design', 'review', 'explore', 'plan', 'general'];
|
|
6
|
+
const STATUSES = ['active', 'idle', 'done', 'dead', 'canceled'];
|
|
7
|
+
const SORTS = ['relevance', 'recency', 'oldest'];
|
|
8
|
+
const REL_UNITS = {
|
|
9
|
+
s: 1000,
|
|
10
|
+
m: 60_000,
|
|
11
|
+
h: 3_600_000,
|
|
12
|
+
d: 86_400_000,
|
|
13
|
+
w: 604_800_000,
|
|
14
|
+
mo: 2_592_000_000,
|
|
15
|
+
y: 31_536_000_000,
|
|
16
|
+
};
|
|
17
|
+
/** ISO 8601 (`2026-06-01`) → epoch ms (absolute); relative (`7d`, `2w`, `1mo`)
|
|
18
|
+
* → now minus the duration. Throws usage on an unparseable value. */
|
|
19
|
+
function parseWhen(field, raw) {
|
|
20
|
+
const rel = raw.match(/^(\d+)(mo|[smhdwy])$/);
|
|
21
|
+
if (rel !== null)
|
|
22
|
+
return Date.now() - Number(rel[1]) * REL_UNITS[rel[2]];
|
|
23
|
+
const abs = Date.parse(raw);
|
|
24
|
+
if (!Number.isNaN(abs))
|
|
25
|
+
return abs;
|
|
26
|
+
throw usage(`--${field} must be ISO 8601 (2026-06-01) or relative (7d, 2w, 1mo); received: ${raw}`);
|
|
27
|
+
}
|
|
28
|
+
function splitCsv(v) {
|
|
29
|
+
if (v === undefined || v === '')
|
|
30
|
+
return undefined;
|
|
31
|
+
return v.split(',').map((s) => s.trim()).filter((s) => s !== '');
|
|
32
|
+
}
|
|
33
|
+
function encodeCursor(offset) {
|
|
34
|
+
return Buffer.from(String(offset), 'utf8').toString('base64');
|
|
35
|
+
}
|
|
36
|
+
function decodeCursor(c) {
|
|
37
|
+
const n = Number(Buffer.from(c, 'base64').toString('utf8'));
|
|
38
|
+
if (!Number.isInteger(n) || n < 0)
|
|
39
|
+
throw usage(`invalid --cursor: ${c}`);
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
42
|
+
/** One snippet block: the matching line(s), `lines` deep. Falls back to the
|
|
43
|
+
* title or the first body lines when nothing matched on the body. */
|
|
44
|
+
function snippet(a, terms, lines) {
|
|
45
|
+
const body = a.loadBody();
|
|
46
|
+
const rows = body.split('\n');
|
|
47
|
+
if (terms.length > 0) {
|
|
48
|
+
const idx = rows.findIndex((r) => {
|
|
49
|
+
const lc = r.toLowerCase();
|
|
50
|
+
return terms.some((t) => lc.includes(t));
|
|
51
|
+
});
|
|
52
|
+
if (idx !== -1) {
|
|
53
|
+
return rows
|
|
54
|
+
.slice(idx, idx + lines)
|
|
55
|
+
.map((r) => r.trim())
|
|
56
|
+
.filter((r) => r !== '')
|
|
57
|
+
.join(' ⏎ ');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const lead = rows.map((r) => r.trim()).filter((r) => r !== '').slice(0, lines).join(' ⏎ ');
|
|
61
|
+
return lead !== '' ? lead : a.title;
|
|
62
|
+
}
|
|
63
|
+
function sortArtifacts(arts, mode, scored) {
|
|
64
|
+
const out = [...arts];
|
|
65
|
+
if (mode === 'relevance' && scored !== undefined) {
|
|
66
|
+
out.sort((x, y) => (scored.get(y.ref) ?? 0) - (scored.get(x.ref) ?? 0) || y.tsMs - x.tsMs || x.ref.localeCompare(y.ref));
|
|
67
|
+
}
|
|
68
|
+
else if (mode === 'oldest') {
|
|
69
|
+
out.sort((x, y) => x.tsMs - y.tsMs || x.ref.localeCompare(y.ref));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
out.sort((x, y) => y.tsMs - x.tsMs || x.ref.localeCompare(y.ref));
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
export const searchLeaf = defineLeaf({
|
|
77
|
+
name: 'search',
|
|
78
|
+
description: 'ranked content search across the cwd\'s node history',
|
|
79
|
+
whenToUse: 'you want to find past work by what it SAYS — a design, a final report, a roadmap, a finding — across every node that ran in this cwd, ranked by relevance. Omit the query to browse the record by recency ("what happened here lately"). Narrow with --type/--kind/--status/--since, scope elsewhere with --cwd/--all-cwds/--under/--node, or switch to --grep for an exact regex over bodies. Use `canvas history read <ref>` to read a hit in full, `canvas history show <node-id>` to list one node\'s artifacts, and `canvas revive <id>` to reopen a node you found.',
|
|
80
|
+
help: {
|
|
81
|
+
name: 'canvas history search',
|
|
82
|
+
summary: 'ranked/filtered/sorted content search over the per-cwd episodic record (reports + context docs + meta)',
|
|
83
|
+
params: [
|
|
84
|
+
{ kind: 'positional', name: 'query', required: false, constraint: 'Whitespace-separated terms, matched case-insensitively and weighted across node name/description, report titles, and doc headings (and full bodies with --body); artifacts matching more/stronger fields rank higher. Omit to browse by recency (no relevance ranking; --sort still applies). Under --grep: an ECMAScript regex matched against bodies (line hits).' },
|
|
85
|
+
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Search another project\'s record. Default: the calling node\'s cwd (process cwd when not a node). Mutually exclusive with --all-cwds.' },
|
|
86
|
+
{ kind: 'flag', name: 'all-cwds', type: 'bool', required: false, constraint: 'Search every cwd on the canvas. Mutually exclusive with --cwd.' },
|
|
87
|
+
{ kind: 'flag', name: 'under', type: 'string', required: false, constraint: 'Restrict to a node and its subscription descendants (one initiative / sub-DAG).' },
|
|
88
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Restrict to specific node ids. Comma-separated for several.' },
|
|
89
|
+
{ kind: 'flag', name: 'type', type: 'string', required: false, constraint: `Corpus to search; comma-separated for several. One of: ${TYPES.join(', ')}. report = push history; doc = context artifacts; roadmap = roadmap.md specifically; meta = node name/description/kind. Default: all.` },
|
|
90
|
+
{ kind: 'flag', name: 'report-kind', type: 'enum', choices: ['final', 'update'], required: false, constraint: 'Narrow reports to outcome summaries (final) or progress (update). Implies --type report.' },
|
|
91
|
+
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: `Node kind; comma-separated for several. One of: ${NODE_KINDS.join(', ')}.` },
|
|
92
|
+
{ kind: 'flag', name: 'status', type: 'string', required: false, constraint: `Node lifecycle status; comma-separated for several. One of: ${STATUSES.join(', ')} (e.g. done = completed work only).` },
|
|
93
|
+
{ kind: 'flag', name: 'since', type: 'string', required: false, constraint: 'Lower bound on artifact timestamp. ISO 8601 (2026-06-01) or relative (7d, 2w, 1mo).' },
|
|
94
|
+
{ kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'Upper bound on artifact timestamp. ISO 8601 or relative.' },
|
|
95
|
+
{ kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex matched against bodies (one hit per matching line) instead of weighted ranking. Requires a query. Mutually exclusive with --body.' },
|
|
96
|
+
{ kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh full body text in the ranking (default weighs name/title/heading only). Ignored under --grep.' },
|
|
97
|
+
{ kind: 'flag', name: 'sort', type: 'enum', choices: SORTS, required: false, constraint: 'relevance (default with a query) ranks by score; recency (default without a query) / oldest order by artifact timestamp.' },
|
|
98
|
+
{ kind: 'flag', name: 'snippet-lines', type: 'int', required: false, default: 1, constraint: 'Context lines per hit. Default 1.' },
|
|
99
|
+
{ kind: 'flag', name: 'full', type: 'bool', required: false, constraint: 'Inline each hit\'s full body. Off by default; pair with a small --limit. Ignored under --grep.' },
|
|
100
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: 'Page size. Default 20, hard max 100.' },
|
|
101
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from a prior next_cursor; omit on the first call.' },
|
|
102
|
+
],
|
|
103
|
+
output: [
|
|
104
|
+
{ name: 'hits', type: 'object[]', required: true, constraint: 'Ranked/sorted hits, each: {ref, node, kind, source, ts, score?, snippet}. Under --grep: {ref, node, ts, line, text} per matching body line, sorted by ref then line. ref is the <node-id>:<relpath> handle passed verbatim to `canvas history read`.' },
|
|
105
|
+
{ name: 'next_cursor', type: 'string|null', required: true, constraint: 'Token for the next page; null is the only end-of-list signal.' },
|
|
106
|
+
{ name: 'total', type: 'int', required: true, constraint: 'Total matching artifacts (exact).' },
|
|
107
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
108
|
+
],
|
|
109
|
+
outputKind: 'object',
|
|
110
|
+
effects: ['None. Read-only.'],
|
|
111
|
+
},
|
|
112
|
+
run: async (input) => {
|
|
113
|
+
const query = (input['query'] ?? '').trim();
|
|
114
|
+
const cwd = input['cwd'];
|
|
115
|
+
const allCwds = input['allCwds'] === true;
|
|
116
|
+
const under = input['under'];
|
|
117
|
+
const nodes = splitCsv(input['node']);
|
|
118
|
+
const grep = input['grep'] === true;
|
|
119
|
+
const weighBody = input['body'] === true;
|
|
120
|
+
const full = input['full'] === true;
|
|
121
|
+
const snippetLines = Math.max(1, input['snippetLines'] ?? 1);
|
|
122
|
+
const limit = Math.min(100, Math.max(1, input['limit'] ?? 20));
|
|
123
|
+
const cursor = input['cursor'];
|
|
124
|
+
if (cwd !== undefined && allCwds)
|
|
125
|
+
throw usage('--cwd and --all-cwds are mutually exclusive.');
|
|
126
|
+
if (grep && weighBody)
|
|
127
|
+
throw usage('--grep and --body are mutually exclusive (--grep always scans bodies).');
|
|
128
|
+
if (grep && query === '')
|
|
129
|
+
throw usage('--grep requires a query (the regex). Omit --grep to browse by recency.');
|
|
130
|
+
const reportKind = input['reportKind'];
|
|
131
|
+
const types = splitCsv(input['type']);
|
|
132
|
+
if (types !== undefined) {
|
|
133
|
+
const bad = types.find((t) => !TYPES.includes(t));
|
|
134
|
+
if (bad !== undefined)
|
|
135
|
+
throw usage(`--type must be one of: ${TYPES.join(', ')}; received: ${bad}`);
|
|
136
|
+
}
|
|
137
|
+
const scope = { cwd, allCwds, under, nodes };
|
|
138
|
+
const corpus = {
|
|
139
|
+
types,
|
|
140
|
+
reportKind,
|
|
141
|
+
kinds: splitCsv(input['kind']),
|
|
142
|
+
statuses: splitCsv(input['status']),
|
|
143
|
+
sinceMs: input['since'] !== undefined ? parseWhen('since', input['since']) : undefined,
|
|
144
|
+
untilMs: input['until'] !== undefined ? parseWhen('until', input['until']) : undefined,
|
|
145
|
+
};
|
|
146
|
+
const arts = buildCorpus(scope, corpus);
|
|
147
|
+
const offset = cursor !== undefined ? decodeCursor(cursor) : 0;
|
|
148
|
+
// --- grep mode: regex over bodies, one row per matching line ---
|
|
149
|
+
if (grep) {
|
|
150
|
+
let re;
|
|
151
|
+
try {
|
|
152
|
+
re = new RegExp(query, 'i');
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw usage(`invalid regex pattern: ${query}`);
|
|
156
|
+
}
|
|
157
|
+
const lineHits = [];
|
|
158
|
+
for (const a of arts) {
|
|
159
|
+
const rows = a.loadBody().split('\n');
|
|
160
|
+
rows.forEach((text, i) => {
|
|
161
|
+
if (re.test(text))
|
|
162
|
+
lineHits.push({ ref: a.ref, node: `${a.nodeName} (${a.nodeId})`, ts: a.ts, line: i + 1, text: text.trim() });
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
lineHits.sort((x, y) => x.ref.localeCompare(y.ref) || x.line - y.line);
|
|
166
|
+
const page = lineHits.slice(offset, offset + limit);
|
|
167
|
+
const nextCursor = offset + limit < lineHits.length ? encodeCursor(offset + limit) : null;
|
|
168
|
+
return {
|
|
169
|
+
hits: page,
|
|
170
|
+
next_cursor: nextCursor,
|
|
171
|
+
total: lineHits.length,
|
|
172
|
+
follow_up: 'Read a hit with `canvas history read <ref>`. Drop --grep for a ranked topic search; narrow with `canvas history search -h`.',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// --- ranked / browse mode ---
|
|
176
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
177
|
+
let pool = arts;
|
|
178
|
+
let scored;
|
|
179
|
+
const sortMode = input['sort'] ?? (terms.length > 0 ? 'relevance' : 'recency');
|
|
180
|
+
if (terms.length > 0 && sortMode === 'relevance') {
|
|
181
|
+
scored = new Map();
|
|
182
|
+
const matched = [];
|
|
183
|
+
for (const a of arts) {
|
|
184
|
+
const titleLc = a.title.toLowerCase();
|
|
185
|
+
const nameLc = a.nodeName.toLowerCase();
|
|
186
|
+
const descLc = a.nodeDesc.toLowerCase();
|
|
187
|
+
const bodyLc = weighBody ? a.loadBody().toLowerCase() : null;
|
|
188
|
+
let score = 0;
|
|
189
|
+
for (const t of terms) {
|
|
190
|
+
if (titleLc.includes(t))
|
|
191
|
+
score += 10;
|
|
192
|
+
if (nameLc.includes(t))
|
|
193
|
+
score += 5;
|
|
194
|
+
if (descLc.includes(t))
|
|
195
|
+
score += 3;
|
|
196
|
+
if (bodyLc !== null && bodyLc.includes(t))
|
|
197
|
+
score += 1;
|
|
198
|
+
}
|
|
199
|
+
if (score > 0) {
|
|
200
|
+
scored.set(a.ref, score);
|
|
201
|
+
matched.push(a);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
pool = matched;
|
|
205
|
+
}
|
|
206
|
+
else if (terms.length > 0) {
|
|
207
|
+
// Query present but a timeline sort requested: filter by match, order by ts.
|
|
208
|
+
pool = arts.filter((a) => {
|
|
209
|
+
const hay = `${a.title}\n${a.nodeName}\n${a.nodeDesc}${weighBody ? '\n' + a.loadBody() : ''}`.toLowerCase();
|
|
210
|
+
return terms.some((t) => hay.includes(t));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const sorted = sortArtifacts(pool, sortMode, scored);
|
|
214
|
+
const page = sorted.slice(offset, offset + limit);
|
|
215
|
+
const nextCursor = offset + limit < sorted.length ? encodeCursor(offset + limit) : null;
|
|
216
|
+
const hits = page.map((a) => {
|
|
217
|
+
const base = {
|
|
218
|
+
ref: a.ref,
|
|
219
|
+
node: `${a.nodeName} (${a.nodeId})`,
|
|
220
|
+
kind: a.nodeKind,
|
|
221
|
+
source: a.source === 'report' && a.reportKind ? `report:${a.reportKind}` : a.source,
|
|
222
|
+
ts: a.ts,
|
|
223
|
+
};
|
|
224
|
+
if (sortMode === 'relevance' && scored !== undefined)
|
|
225
|
+
base['score'] = scored.get(a.ref) ?? 0;
|
|
226
|
+
if (full)
|
|
227
|
+
base['body'] = a.loadBody();
|
|
228
|
+
else
|
|
229
|
+
base['snippet'] = snippet(a, terms, snippetLines);
|
|
230
|
+
return base;
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
hits,
|
|
234
|
+
next_cursor: nextCursor,
|
|
235
|
+
total: sorted.length,
|
|
236
|
+
follow_up: 'Read a hit with `canvas history read <ref>`; narrow with `canvas history search -h`.',
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
render: (r) => renderSearch(r),
|
|
240
|
+
});
|
|
241
|
+
function renderSearch(r) {
|
|
242
|
+
const hits = r['hits'];
|
|
243
|
+
const total = r['total'];
|
|
244
|
+
const nextCursor = r['next_cursor'];
|
|
245
|
+
const full = hits.length > 0 && Object.prototype.hasOwnProperty.call(hits[0], 'body');
|
|
246
|
+
const parts = [];
|
|
247
|
+
if (hits.length === 0) {
|
|
248
|
+
parts.push('0 hits.');
|
|
249
|
+
}
|
|
250
|
+
else if (full) {
|
|
251
|
+
parts.push(`${hits.length} of ${total} hits:`);
|
|
252
|
+
for (const h of hits) {
|
|
253
|
+
const head = `### ${h['ref']}\n- node: ${h['node']} | kind: ${h['kind']} | source: ${h['source']} | ts: ${h['ts']}`;
|
|
254
|
+
parts.push(`${head}\n\n${String(h['body'] ?? '').trim()}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
parts.push(`${hits.length} of ${total} hits:`);
|
|
259
|
+
const cols = Object.keys(hits[0]).filter((c) => c !== 'body');
|
|
260
|
+
const head = `| ${cols.join(' | ')} |`;
|
|
261
|
+
const sep = `| ${cols.map(() => '---').join(' | ')} |`;
|
|
262
|
+
const body = hits
|
|
263
|
+
.map((h) => `| ${cols.map((c) => String(h[c] ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |`)
|
|
264
|
+
.join('\n');
|
|
265
|
+
parts.push(`${head}\n${sep}\n${body}`);
|
|
266
|
+
}
|
|
267
|
+
parts.push(`- next_cursor: ${nextCursor ?? 'null'}\n- total: ${total}`);
|
|
268
|
+
parts.push(String(r['follow_up']));
|
|
269
|
+
return parts.join('\n\n');
|
|
270
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const showLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineLeaf } from '../../core/command.js';
|
|
2
|
+
import { notFound } from '../../core/errors.js';
|
|
3
|
+
import { getNode, nodeArtifacts } from '../../core/canvas/index.js';
|
|
4
|
+
import { statSync } from 'node:fs';
|
|
5
|
+
const TYPES = ['report', 'doc', 'roadmap'];
|
|
6
|
+
export const showLeaf = defineLeaf({
|
|
7
|
+
name: 'show',
|
|
8
|
+
description: 'enumerate one node\'s artifacts as refs',
|
|
9
|
+
whenToUse: 'you have a node id and want to see everything it left behind — its reports and context docs as refs ready for `canvas history read`. The bridge from "found the node" to "read its artifacts". Use `node inspect show` instead for the node\'s topology/neighbors, not its content.',
|
|
10
|
+
help: {
|
|
11
|
+
name: 'canvas history show',
|
|
12
|
+
summary: 'list one node\'s artifacts (reports + context docs) as refs ready for `canvas history read`',
|
|
13
|
+
params: [
|
|
14
|
+
{ kind: 'positional', name: 'node-id', required: true, constraint: 'The node whose artifacts to list.' },
|
|
15
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: [...TYPES], required: false, constraint: 'Narrow to one corpus: report | doc | roadmap. Default: all.' },
|
|
16
|
+
{ kind: 'flag', name: 'sort', type: 'enum', choices: ['recency', 'oldest'], required: false, constraint: 'Order by artifact timestamp. Default recency.' },
|
|
17
|
+
],
|
|
18
|
+
output: [
|
|
19
|
+
{ name: 'node', type: 'string', required: true, constraint: 'Node name + id.' },
|
|
20
|
+
{ name: 'artifacts', type: 'object[]', required: true, constraint: 'Each: {ref, source, ts, detail}. source = report:<kind> | doc | roadmap. detail = report title / doc heading + size. ref passes verbatim to `canvas history read`.' },
|
|
21
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
22
|
+
],
|
|
23
|
+
outputKind: 'object',
|
|
24
|
+
effects: ['None. Read-only.'],
|
|
25
|
+
},
|
|
26
|
+
run: async (input) => {
|
|
27
|
+
const nodeId = input['nodeId'].trim();
|
|
28
|
+
const type = input['type'];
|
|
29
|
+
const sort = input['sort'] ?? 'recency';
|
|
30
|
+
if (getNode(nodeId) === null) {
|
|
31
|
+
throw notFound(`unknown node: ${nodeId}`, {
|
|
32
|
+
received: nodeId,
|
|
33
|
+
next: 'List nodes with `crtr node inspect list`, or find one with `canvas history search`.',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const arts = nodeArtifacts(nodeId, type !== undefined ? [type] : undefined);
|
|
37
|
+
arts.sort((a, b) => (sort === 'oldest' ? a.tsMs - b.tsMs : b.tsMs - a.tsMs) || a.ref.localeCompare(b.ref));
|
|
38
|
+
const artifacts = arts.map((a) => {
|
|
39
|
+
let size = 0;
|
|
40
|
+
try {
|
|
41
|
+
if (a.path !== null)
|
|
42
|
+
size = statSync(a.path).size;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* ignore */
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ref: a.ref,
|
|
49
|
+
source: a.source === 'report' && a.reportKind ? `report:${a.reportKind}` : a.source,
|
|
50
|
+
ts: a.ts,
|
|
51
|
+
detail: a.title + (size > 0 ? ` (${size}B)` : ''),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const node = getNode(nodeId);
|
|
55
|
+
return {
|
|
56
|
+
node: `${node?.name} (${nodeId})`,
|
|
57
|
+
artifacts,
|
|
58
|
+
follow_up: 'Read one with `canvas history read <ref>`; reopen the node with `canvas revive ' + nodeId + '`.',
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
render: (r) => {
|
|
62
|
+
const arts = r['artifacts'];
|
|
63
|
+
const parts = [`node: ${r['node']}`];
|
|
64
|
+
if (arts.length === 0) {
|
|
65
|
+
parts.push('0 artifacts.');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const cols = Object.keys(arts[0]);
|
|
69
|
+
const head = `| ${cols.join(' | ')} |`;
|
|
70
|
+
const sep = `| ${cols.map(() => '---').join(' | ')} |`;
|
|
71
|
+
const body = arts
|
|
72
|
+
.map((a) => `| ${cols.map((c) => String(a[c] ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |`)
|
|
73
|
+
.join('\n');
|
|
74
|
+
parts.push(`${arts.length} artifacts:\n\n${head}\n${sep}\n${body}`);
|
|
75
|
+
}
|
|
76
|
+
parts.push(String(r['follow_up']));
|
|
77
|
+
return parts.join('\n\n');
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `canvas history` — search and recall the canvas's record of past work in a
|
|
2
|
+
// cwd. The accumulated episodic record of every node that ran here and the
|
|
3
|
+
// artifacts it left: its reports (final/update/urgent outcome summaries) and
|
|
4
|
+
// context docs (specs, designs, roadmaps, findings). Distinct from `crtr
|
|
5
|
+
// memory`, which is curated semantic knowledge held independent of any episode.
|
|
6
|
+
// This branch owns CONTENT; it never duplicates the graph (use `canvas revive`
|
|
7
|
+
// to reopen a found node, `node inspect show` for its topology).
|
|
8
|
+
import { defineBranch } from '../core/command.js';
|
|
9
|
+
import { searchLeaf } from './canvas-history/search.js';
|
|
10
|
+
import { readLeaf } from './canvas-history/read.js';
|
|
11
|
+
import { showLeaf } from './canvas-history/show.js';
|
|
12
|
+
import { callerCwd, corpusStats } from '../core/canvas/index.js';
|
|
13
|
+
/** The bounded per-cwd `<corpus>` aggregate for the branch -h — pre-empts a
|
|
14
|
+
* `search` call just to gauge the corpus (cli-design §15: aggregate, never
|
|
15
|
+
* enumerate). Soft-fails to omission if the db is unreachable. */
|
|
16
|
+
function corpusBlock() {
|
|
17
|
+
const cwd = callerCwd();
|
|
18
|
+
const s = corpusStats(cwd);
|
|
19
|
+
const span = s.span !== null ? `${s.span.from} → ${s.span.to}` : 'n/a';
|
|
20
|
+
return `<corpus cwd="${cwd}" nodes="${s.nodes}" reports="${s.reports}" docs="${s.docs}" span="${span}"/>`;
|
|
21
|
+
}
|
|
22
|
+
export const historyBranch = defineBranch({
|
|
23
|
+
name: 'history',
|
|
24
|
+
description: 'search and recall the canvas\'s record of past work in this cwd',
|
|
25
|
+
whenToUse: 'you want to find or re-read what was DONE in a cwd — a past design, a final report, a roadmap, a finding — across every node that ran there. Use it when picking up prior work ("that caching work from last week"), recovering an artifact, or surveying a project\'s history. Distinct from `crtr memory` (curated semantic knowledge, not episodes) and from `canvas dashboard`/`node inspect` (graph topology, not content).',
|
|
26
|
+
help: {
|
|
27
|
+
name: 'canvas history',
|
|
28
|
+
summary: 'search and recall the canvas\'s record of past work in this cwd',
|
|
29
|
+
model: 'The accumulated record of every node that ran in a cwd and the artifacts it left — its reports (final/update outcome summaries) and context docs (specs, designs, roadmaps, findings). `search` finds by content (ranked, filtered, sorted; omit the query to browse by recency); `read` pulls one hit\'s full body by its <node-id>:<relpath> ref; `show` lists everything one node left. This is the episodic record of what was DONE — distinct from `crtr memory`, which is curated semantic knowledge. To reopen a node you found, use `canvas revive`; for its place in the graph, `node inspect show`.',
|
|
30
|
+
dynamicState: corpusBlock,
|
|
31
|
+
},
|
|
32
|
+
children: [searchLeaf, readLeaf, showLeaf],
|
|
33
|
+
});
|
package/dist/commands/canvas.js
CHANGED
|
@@ -15,6 +15,7 @@ import { daemonBranch } from './daemon.js';
|
|
|
15
15
|
import { tmuxSpreadLeaf } from './tmux-spread.js';
|
|
16
16
|
import { chordLeaf } from './chord.js';
|
|
17
17
|
import { canvasPruneLeaf } from './canvas-prune.js';
|
|
18
|
+
import { historyBranch } from './canvas-history.js';
|
|
18
19
|
export function registerCanvas() {
|
|
19
20
|
return defineBranch({
|
|
20
21
|
name: 'canvas',
|
|
@@ -26,8 +27,8 @@ export function registerCanvas() {
|
|
|
26
27
|
help: {
|
|
27
28
|
name: 'canvas',
|
|
28
29
|
summary: 'observe and supervise the whole agent graph',
|
|
29
|
-
model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
|
|
30
|
+
model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `history` searches and recalls the content record (reports + context docs) of past work in a cwd; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
|
|
30
31
|
},
|
|
31
|
-
children: [dashboardLeaf, browseLeaf, attentionBranch, reviveLeaf, tmuxSpreadLeaf, daemonBranch, chordLeaf, canvasPruneLeaf],
|
|
32
|
+
children: [dashboardLeaf, browseLeaf, attentionBranch, reviveLeaf, historyBranch, tmuxSpreadLeaf, daemonBranch, chordLeaf, canvasPruneLeaf],
|
|
32
33
|
});
|
|
33
34
|
}
|
|
@@ -2,9 +2,6 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
2
2
|
import { usage } from '../../core/errors.js';
|
|
3
3
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
4
4
|
import { parseSubstrateDoc } from '../../core/substrate/index.js';
|
|
5
|
-
import { listAllSkills } from '../../core/resolver.js';
|
|
6
|
-
import { parseFrontmatter } from '../../core/frontmatter.js';
|
|
7
|
-
import { readText } from '../../core/fs-utils.js';
|
|
8
5
|
import { MEMORY_KINDS } from './shared.js';
|
|
9
6
|
function substrateUnit(d) {
|
|
10
7
|
return {
|
|
@@ -17,35 +14,14 @@ function substrateUnit(d) {
|
|
|
17
14
|
loadBody: () => d.body,
|
|
18
15
|
};
|
|
19
16
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
name: s.name,
|
|
25
|
-
kind: 'skill',
|
|
26
|
-
scope: s.scope,
|
|
27
|
-
path: s.path,
|
|
28
|
-
// A skill's description plays the read-routing role; keywords ride along so
|
|
29
|
-
// the ranker weighs them too. short_form surfaces the description as the hook.
|
|
30
|
-
routing: [desc, keywords].filter((x) => x).join(' '),
|
|
31
|
-
shortForm: desc,
|
|
32
|
-
loadBody: () => parseFrontmatter(readText(s.path)).body,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/** The unioned candidate set: every substrate memory document + every skill,
|
|
36
|
-
* optionally narrowed to one kind. find searches EVERYTHING — it never applies
|
|
37
|
-
* gate or visibility-rung filtering (design §11#3). The skill half is wrapped
|
|
38
|
-
* defensively: the skill corpus is parsed by the resolver, which currently
|
|
39
|
-
* hard-throws on at least one shipped skill's frontmatter (a pre-existing
|
|
40
|
-
* regression in the yaml-parser swap, owned by the frontmatter/skill track). A
|
|
41
|
-
* broken skill corpus must degrade the union to substrate-docs-only, never
|
|
42
|
-
* crash `memory find`.
|
|
17
|
+
/** The candidate set: every substrate memory document (native + plugin, supplied
|
|
18
|
+
* by listAllMemoryDocs), optionally narrowed to one kind. find searches
|
|
19
|
+
* EVERYTHING — it never applies gate or visibility-rung filtering (design
|
|
20
|
+
* §11#3).
|
|
43
21
|
*
|
|
44
|
-
* Dedup by (scope, name):
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* unit encountered for a given identity wins — substrate docs before skill-plugin
|
|
48
|
-
* docs, which mirrors the memory resolver's own precedence. */
|
|
22
|
+
* Dedup by (scope, name): native and plugin docs of the same scope can collide
|
|
23
|
+
* on identity. The FIRST unit encountered for a given identity wins — native
|
|
24
|
+
* before plugin, which mirrors the memory resolver's own precedence. */
|
|
49
25
|
function candidates(kindFilter) {
|
|
50
26
|
const seen = new Set(); // "scope/name" → first wins
|
|
51
27
|
const add = (u) => {
|
|
@@ -64,15 +40,6 @@ function candidates(kindFilter) {
|
|
|
64
40
|
continue;
|
|
65
41
|
add(substrateUnit(sub));
|
|
66
42
|
}
|
|
67
|
-
if (kindFilter === undefined || kindFilter === 'skill') {
|
|
68
|
-
try {
|
|
69
|
-
for (const skill of listAllSkills())
|
|
70
|
-
add(skillUnit(skill));
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* skill corpus unavailable — search substrate documents alone */
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
43
|
return units;
|
|
77
44
|
}
|
|
78
45
|
export const findLeaf = defineLeaf({
|