@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
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
// `crtr memory lint` — the permanent valid-YAML gate over the bounded corpus
|
|
2
2
|
// (the CTO green-checkpoint: zero frontmatter parse errors at authoring time,
|
|
3
3
|
// so an invalid doc fails HERE instead of being silently isolated at runtime).
|
|
4
|
-
// Bounded corpus = the substrate memory dirs (project/user/builtin)
|
|
5
|
-
// every installed plugin's skills tree + ~/.claude/skills. Never a
|
|
4
|
+
// Bounded corpus = the substrate memory dirs (project/user/builtin). Never a
|
|
6
5
|
// filesystem-wide scan.
|
|
7
|
-
import {
|
|
8
|
-
import { basename, join, relative, sep } from 'node:path';
|
|
6
|
+
import { basename, relative, sep } from 'node:path';
|
|
9
7
|
import { defineLeaf } from '../../core/command.js';
|
|
10
8
|
import { general } from '../../core/errors.js';
|
|
11
9
|
import { warn } from '../../core/output.js';
|
|
12
10
|
import { pathExists, readText, walkFiles } from '../../core/fs-utils.js';
|
|
13
11
|
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
14
|
-
import {
|
|
15
|
-
import { scopeMemoryDir } from '../../core/scope.js';
|
|
12
|
+
import { listInstalledPlugins } from '../../core/resolver.js';
|
|
13
|
+
import { pluginMemoryDir, scopeMemoryDir } from '../../core/scope.js';
|
|
16
14
|
import { isDocKind, RUNGS } from '../../core/substrate/index.js';
|
|
17
|
-
import { SKILL_ENTRY_FILE, SKILLS_DIR } from '../../types.js';
|
|
18
15
|
/** The parser normalizes the `always` alias to `content`, so it lints valid. */
|
|
19
16
|
const VALID_RUNGS = [...RUNGS, 'always'];
|
|
20
17
|
const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
|
|
@@ -78,14 +75,14 @@ function lintFile(file, substrateStore, findings) {
|
|
|
78
75
|
export const lintLeaf = defineLeaf({
|
|
79
76
|
name: 'lint',
|
|
80
77
|
description: 'validate frontmatter across the whole bounded document corpus',
|
|
81
|
-
whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (memory stores
|
|
78
|
+
whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (the substrate memory stores) and fail loudly on any invalid YAML or substrate schema violation. Run it before shipping doc changes; CI-friendly (non-zero exit on any finding).',
|
|
82
79
|
help: {
|
|
83
80
|
name: 'memory lint',
|
|
84
81
|
summary: 'strict-parse frontmatter across the bounded corpus; non-zero exit on any finding',
|
|
85
82
|
params: [],
|
|
86
83
|
output: [
|
|
87
84
|
{ name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
|
|
88
|
-
{ name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores
|
|
85
|
+
{ name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores}.' },
|
|
89
86
|
{ name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
|
|
90
87
|
],
|
|
91
88
|
outputKind: 'object',
|
|
@@ -94,40 +91,28 @@ export const lintLeaf = defineLeaf({
|
|
|
94
91
|
run: async () => {
|
|
95
92
|
const findings = [];
|
|
96
93
|
let memoryCount = 0;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
94
|
+
// Substrate memory stores (project/user/builtin), schema-aware. A scope's
|
|
95
|
+
// corpus is its NATIVE memory dir plus each enabled plugin's memory dir —
|
|
96
|
+
// plugin docs are substrate docs and lint through the same schema gate.
|
|
100
97
|
// MEMORY.md index files are not substrate docs — YAML-parse only.
|
|
101
|
-
|
|
102
|
-
const dir = scopeMemoryDir(scope);
|
|
98
|
+
const lintDir = (dir) => {
|
|
103
99
|
if (!dir || !pathExists(dir))
|
|
104
|
-
|
|
100
|
+
return;
|
|
105
101
|
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
106
102
|
if (!relative(dir, file).split(sep).join('/'))
|
|
107
103
|
continue;
|
|
108
104
|
memoryCount += 1;
|
|
109
105
|
lintFile(file, basename(file) !== 'MEMORY.md', findings);
|
|
110
106
|
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const file of walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE)) {
|
|
118
|
-
pluginCount += 1;
|
|
119
|
-
lintFile(file, false, findings);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// 3. ~/.claude/skills — read by the runtime, so it lints too.
|
|
123
|
-
const claudeSkills = join(homedir(), '.claude', SKILLS_DIR);
|
|
124
|
-
if (pathExists(claudeSkills)) {
|
|
125
|
-
for (const file of walkFiles(claudeSkills, (n) => n === SKILL_ENTRY_FILE)) {
|
|
126
|
-
claudeCount += 1;
|
|
127
|
-
lintFile(file, false, findings);
|
|
107
|
+
};
|
|
108
|
+
for (const scope of ['project', 'user', 'builtin']) {
|
|
109
|
+
lintDir(scopeMemoryDir(scope));
|
|
110
|
+
for (const plugin of listInstalledPlugins(scope)) {
|
|
111
|
+
if (plugin.enabled)
|
|
112
|
+
lintDir(pluginMemoryDir(plugin));
|
|
128
113
|
}
|
|
129
114
|
}
|
|
130
|
-
const checked = memoryCount
|
|
115
|
+
const checked = memoryCount;
|
|
131
116
|
if (findings.length > 0) {
|
|
132
117
|
// Human/agent path renders only the message — surface every offender as
|
|
133
118
|
// a scoped stderr notice (the --json path carries them in details too).
|
|
@@ -141,7 +126,7 @@ export const lintLeaf = defineLeaf({
|
|
|
141
126
|
}
|
|
142
127
|
return {
|
|
143
128
|
checked,
|
|
144
|
-
corpora: { memory_stores: memoryCount
|
|
129
|
+
corpora: { memory_stores: memoryCount },
|
|
145
130
|
findings: [],
|
|
146
131
|
follow_up: 'Corpus green — zero invalid frontmatter docs.',
|
|
147
132
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
3
|
-
import { parseSubstrateDoc } from '../../core/substrate/index.js';
|
|
4
|
-
import { listAllSkills } from '../../core/resolver.js';
|
|
3
|
+
import { parseSubstrateDoc, isIndexName, indexDirOf } from '../../core/substrate/index.js';
|
|
5
4
|
import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
|
|
6
5
|
export const listLeaf = defineLeaf({
|
|
7
6
|
name: 'list',
|
|
@@ -15,7 +14,7 @@ export const listLeaf = defineLeaf({
|
|
|
15
14
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Filter to a single scope. Default: all resolved scopes.' },
|
|
16
15
|
],
|
|
17
16
|
output: [
|
|
18
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope}. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
17
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope, is_dir}. A directory INDEX.md surfaces as a dir entry (is_dir true, name = the directory path with a trailing slash); is_dir is false for an ordinary doc. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
19
18
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
|
|
20
19
|
],
|
|
21
20
|
outputKind: 'object',
|
|
@@ -40,21 +39,11 @@ export const listLeaf = defineLeaf({
|
|
|
40
39
|
continue;
|
|
41
40
|
if (kindFilter !== undefined && sub.kind !== kindFilter)
|
|
42
41
|
continue;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
for (const skill of listAllSkills(scopeFilter)) {
|
|
50
|
-
const raw = skill.frontmatter.description;
|
|
51
|
-
const desc = typeof raw === 'string' ? raw : '';
|
|
52
|
-
addItem({ name: skill.name, kind: 'skill', scope: skill.scope, shortForm: desc });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
/* skill corpus unavailable — list substrate documents alone */
|
|
57
|
-
}
|
|
42
|
+
// A directory INDEX surfaces as a dir entry: its dir path with a trailing
|
|
43
|
+
// slash, flagged is_dir, so the inventory distinguishes it from a doc.
|
|
44
|
+
const isDir = isIndexName(sub.name);
|
|
45
|
+
const name = isDir ? indexDirOf(sub.name) + '/' : sub.name;
|
|
46
|
+
addItem({ name, kind: sub.kind, scope: sub.scope, shortForm: sub.shortForm, isDir });
|
|
58
47
|
}
|
|
59
48
|
items.sort((a, b) => {
|
|
60
49
|
const sr = scopeRank(a.scope) - scopeRank(b.scope);
|
|
@@ -72,6 +61,7 @@ export const listLeaf = defineLeaf({
|
|
|
72
61
|
short_form: d.shortForm,
|
|
73
62
|
kind: d.kind,
|
|
74
63
|
scope: d.scope,
|
|
64
|
+
is_dir: d.isDir,
|
|
75
65
|
})),
|
|
76
66
|
follow_up: 'Read one in full with `crtr memory read <name>`. Narrow with --kind / --scope, or search a topic with `crtr memory find <query>`.',
|
|
77
67
|
};
|
|
@@ -2,8 +2,6 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
2
2
|
import { CrtrError, notFound } from '../../core/errors.js';
|
|
3
3
|
import { resolveMemoryDoc } from '../../core/memory-resolver.js';
|
|
4
4
|
import { parseSubstrateDoc } from '../../core/substrate/index.js';
|
|
5
|
-
import { resolveSkill } from '../../core/resolver.js';
|
|
6
|
-
import { parseFrontmatter } from '../../core/frontmatter.js';
|
|
7
5
|
import { readText } from '../../core/fs-utils.js';
|
|
8
6
|
import { MEMORY_KINDS } from './shared.js';
|
|
9
7
|
export const readLeaf = defineLeaf({
|
|
@@ -33,9 +31,9 @@ export const readLeaf = defineLeaf({
|
|
|
33
31
|
const nameRaw = input['name'];
|
|
34
32
|
const kindFilter = input['kind'];
|
|
35
33
|
const includeFrontmatter = input['frontmatter'];
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
34
|
+
// Resolve a substrate/memory document across scopes (project>user>builtin,
|
|
35
|
+
// with leaf-name fallback). The substrate corpus now includes plugin docs
|
|
36
|
+
// (mounted under <pluginName>/), so this resolves every name.
|
|
39
37
|
let doc;
|
|
40
38
|
try {
|
|
41
39
|
doc = resolveMemoryDoc(nameRaw);
|
|
@@ -51,8 +49,7 @@ export const readLeaf = defineLeaf({
|
|
|
51
49
|
: typeof doc.frontmatter?.['kind'] === 'string'
|
|
52
50
|
? doc.frontmatter['kind']
|
|
53
51
|
: 'reference';
|
|
54
|
-
// --kind asserts the resolved kind; a mismatch falls through to
|
|
55
|
-
// (which only satisfy --kind=skill).
|
|
52
|
+
// --kind asserts the resolved kind; a mismatch falls through to not-found.
|
|
56
53
|
if (kindFilter === undefined || kind === kindFilter) {
|
|
57
54
|
const content = includeFrontmatter ? readText(doc.path) : doc.body;
|
|
58
55
|
return {
|
|
@@ -65,36 +62,6 @@ export const readLeaf = defineLeaf({
|
|
|
65
62
|
};
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
|
-
// 2) D4 union fallthrough: the name is not a memory document (or its kind
|
|
69
|
-
// did not match --kind) — resolve it as a plugin/marketplace/scope skill
|
|
70
|
-
// so skills are readable through `crtr memory read` too. Skills are kind
|
|
71
|
-
// "skill", so honor --kind by skipping this path for other kinds.
|
|
72
|
-
if (kindFilter === undefined || kindFilter === 'skill') {
|
|
73
|
-
let skill;
|
|
74
|
-
try {
|
|
75
|
-
skill = resolveSkill(nameRaw);
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
// not_found → fall through to the memory-not-found below. A genuine
|
|
79
|
-
// CrtrError (e.g. ambiguous) propagates. Any other throw means the skill
|
|
80
|
-
// corpus is currently unparseable (the yaml-parser regression owned by
|
|
81
|
-
// the frontmatter/skill track) — degrade to not-found rather than 500.
|
|
82
|
-
if (e instanceof CrtrError && e.code !== 'not_found')
|
|
83
|
-
throw e;
|
|
84
|
-
}
|
|
85
|
-
if (skill !== undefined) {
|
|
86
|
-
const raw = readText(skill.path);
|
|
87
|
-
const content = includeFrontmatter ? raw : parseFrontmatter(raw).body;
|
|
88
|
-
return {
|
|
89
|
-
name: skill.name,
|
|
90
|
-
kind: 'skill',
|
|
91
|
-
scope: skill.scope,
|
|
92
|
-
path: skill.path,
|
|
93
|
-
content,
|
|
94
|
-
follow_up: 'Add --frontmatter to include the YAML header. Browse the inventory with `crtr memory list`.',
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
65
|
throw notFound(`memory document not found: ${nameRaw}`, {
|
|
99
66
|
memory: nameRaw,
|
|
100
67
|
next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const bridgeBranch: import("../../core/command.js").BranchDef;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// `crtr pkg bridge` — generate crouter substrate docs from a Claude skills root
|
|
2
|
+
// and file-level reconcile the bridge plugin (design §2). The mapping lives in
|
|
3
|
+
// core/bridge-map.ts (one testable source of truth); the extension shells to
|
|
4
|
+
// this command rather than duplicating the transform.
|
|
5
|
+
import { join, basename, dirname, isAbsolute } from 'node:path';
|
|
6
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
7
|
+
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
8
|
+
import { usage } from '../../core/errors.js';
|
|
9
|
+
import { userScopeRoot } from '../../core/scope.js';
|
|
10
|
+
import { ensureDir, writeJson, writeText, readText, removePath, walkFiles, } from '../../core/fs-utils.js';
|
|
11
|
+
import { parseFrontmatter, parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
12
|
+
import { buildBridgeDoc } from '../../core/bridge-map.js';
|
|
13
|
+
import { PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE } from '../../types.js';
|
|
14
|
+
/** Enumerate `<source>/**\/SKILL.md`, stopping recursion at the first SKILL.md
|
|
15
|
+
* on any branch — a dir that holds SKILL.md is a skill; its subdirs are assets,
|
|
16
|
+
* not nested skills. */
|
|
17
|
+
function enumerateSkillFiles(root) {
|
|
18
|
+
const out = [];
|
|
19
|
+
if (!existsSync(root))
|
|
20
|
+
return out;
|
|
21
|
+
const stack = [root];
|
|
22
|
+
while (stack.length) {
|
|
23
|
+
const dir = stack.pop();
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const hasSkill = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
|
|
32
|
+
if (hasSkill) {
|
|
33
|
+
out.push(join(dir, 'SKILL.md'));
|
|
34
|
+
continue; // stop descent — deeper dirs are assets
|
|
35
|
+
}
|
|
36
|
+
for (const e of entries)
|
|
37
|
+
if (e.isDirectory())
|
|
38
|
+
stack.push(join(dir, e.name));
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
const bridgeSync = defineLeaf({
|
|
43
|
+
name: 'sync',
|
|
44
|
+
description: 'generate substrate docs from a Claude skills root',
|
|
45
|
+
whenToUse: 'mirroring a Claude Code skills directory into a crtr bridge plugin: maps each SKILL.md to a substrate memory doc and file-level reconciles the plugin (regenerate, prune stale, preserve hand-authored leaves + INDEX)',
|
|
46
|
+
help: {
|
|
47
|
+
name: 'pkg bridge sync',
|
|
48
|
+
summary: 'mirror a Claude skills root into a crtr bridge plugin (idempotent reconcile)',
|
|
49
|
+
guide: 'Each <source>/skills/<skillname>/SKILL.md becomes ~/.crouter/plugins/<name>/memory/<skillname>.md, stamped generator:claude-bridge. Stamped leaves whose source skill is gone are pruned; hand-authored leaves and any INDEX.md are never touched.',
|
|
50
|
+
params: [
|
|
51
|
+
{ kind: 'flag', name: 'source', type: 'path', required: true, constraint: 'Absolute path to a Claude skills root (a dir whose <skillname>/SKILL.md leaves are bridged; recursion stops at the first SKILL.md).' },
|
|
52
|
+
{ kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Bridge plugin name. Dest plugin dir is ~/.crouter/plugins/<name>/.' },
|
|
53
|
+
{ kind: 'flag', name: 'source-desc', type: 'string', required: false, constraint: 'Optional plugin description written into the generated plugin.json.' },
|
|
54
|
+
],
|
|
55
|
+
output: [
|
|
56
|
+
{ name: 'plugin', type: 'string', required: true, constraint: 'Bridge plugin name.' },
|
|
57
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the bridge plugin directory.' },
|
|
58
|
+
{ name: 'written', type: 'number', required: true, constraint: 'Count of substrate leaves generated from source SKILL.md files.' },
|
|
59
|
+
{ name: 'pruned', type: 'number', required: true, constraint: 'Count of stale generator:claude-bridge leaves deleted (source skill gone).' },
|
|
60
|
+
{ name: 'preserved', type: 'number', required: true, constraint: 'Count of untouched protected leaves (INDEX.md or unstamped hand-authored docs).' },
|
|
61
|
+
],
|
|
62
|
+
outputKind: 'object',
|
|
63
|
+
effects: ['Writes ~/.crouter/plugins/<name>/.crouter-plugin/plugin.json and memory/*.md. Deletes stale generator:claude-bridge leaves. Never writes or deletes INDEX.md or unstamped docs.'],
|
|
64
|
+
},
|
|
65
|
+
run: async (input) => {
|
|
66
|
+
const source = input['source'];
|
|
67
|
+
const name = input['name'];
|
|
68
|
+
const sourceDesc = input['sourceDesc'];
|
|
69
|
+
if (!isAbsolute(source)) {
|
|
70
|
+
throw usage(`--source must be an absolute path: ${source}`);
|
|
71
|
+
}
|
|
72
|
+
if (!existsSync(source)) {
|
|
73
|
+
throw usage(`--source does not exist: ${source}`);
|
|
74
|
+
}
|
|
75
|
+
const destRoot = join(userScopeRoot(), 'plugins', name);
|
|
76
|
+
const memoryDir = join(destRoot, 'memory');
|
|
77
|
+
// 1. Ensure the bridge plugin manifest.
|
|
78
|
+
const manifestDir = join(destRoot, PLUGIN_MANIFEST_DIR);
|
|
79
|
+
ensureDir(manifestDir);
|
|
80
|
+
const manifest = {
|
|
81
|
+
name,
|
|
82
|
+
version: '0.0.0',
|
|
83
|
+
...(sourceDesc !== undefined ? { description: sourceDesc } : {}),
|
|
84
|
+
source,
|
|
85
|
+
generator: 'claude-bridge',
|
|
86
|
+
};
|
|
87
|
+
writeJson(join(manifestDir, PLUGIN_MANIFEST_FILE), manifest);
|
|
88
|
+
// 2 + 3. Regenerate one stamped leaf per source skill.
|
|
89
|
+
ensureDir(memoryDir);
|
|
90
|
+
const writtenPaths = new Set();
|
|
91
|
+
for (const file of enumerateSkillFiles(source)) {
|
|
92
|
+
const skillName = basename(dirname(file));
|
|
93
|
+
const parsed = parseFrontmatter(readText(file));
|
|
94
|
+
const doc = buildBridgeDoc({
|
|
95
|
+
name: parsed.data?.name ?? skillName,
|
|
96
|
+
description: parsed.data?.description ?? '',
|
|
97
|
+
body: parsed.body,
|
|
98
|
+
});
|
|
99
|
+
const dest = join(memoryDir, `${skillName}.md`);
|
|
100
|
+
writeText(dest, doc);
|
|
101
|
+
writtenPaths.add(dest);
|
|
102
|
+
}
|
|
103
|
+
const written = writtenPaths.size;
|
|
104
|
+
// 4. Reconcile: prune stale stamped leaves; never touch INDEX.md or
|
|
105
|
+
// unstamped (hand-authored) leaves.
|
|
106
|
+
let pruned = 0;
|
|
107
|
+
let preserved = 0;
|
|
108
|
+
for (const f of walkFiles(memoryDir, (n) => n.endsWith('.md'))) {
|
|
109
|
+
if (writtenPaths.has(f))
|
|
110
|
+
continue; // just regenerated → counted in `written`
|
|
111
|
+
if (basename(f) === 'INDEX.md') {
|
|
112
|
+
preserved++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const generator = parseFrontmatterGeneric(readText(f)).data?.['generator'];
|
|
116
|
+
if (generator === 'claude-bridge') {
|
|
117
|
+
removePath(f);
|
|
118
|
+
pruned++;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
preserved++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { plugin: name, path: destRoot, written, pruned, preserved };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
export const bridgeBranch = defineBranch({
|
|
128
|
+
name: 'bridge',
|
|
129
|
+
description: 'mirror Claude skills into crtr bridge plugins',
|
|
130
|
+
whenToUse: 'generating a crtr bridge plugin from a Claude Code skills root — the substrate-doc mapping and file-level reconcile invoked by the pi bridge extension',
|
|
131
|
+
help: {
|
|
132
|
+
name: 'pkg bridge',
|
|
133
|
+
summary: 'mirror Claude skills roots into crtr bridge plugins',
|
|
134
|
+
model: 'A bridge plugin is a generator:claude-bridge plugin whose memory/ docs are generated from a Claude skills root. Hand-authored leaves and INDEX ceilings are preserved across syncs.',
|
|
135
|
+
},
|
|
136
|
+
children: [bridgeSync],
|
|
137
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { notFound } from '../../core/errors.js';
|
|
3
3
|
import { paginate } from '../../core/pagination.js';
|
|
4
|
-
import { listInstalledPlugins, findPluginByName,
|
|
4
|
+
import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
|
|
5
|
+
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
5
6
|
import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// plugin.inspect.list
|
|
@@ -78,8 +79,8 @@ const pluginList = defineLeaf({
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
const pluginShow = defineLeaf({
|
|
80
81
|
name: 'show',
|
|
81
|
-
description: 'read plugin manifest and
|
|
82
|
-
whenToUse: 'reading one installed plugin in detail to decide before you enable, disable, or remove it — returns the full manifest, the
|
|
82
|
+
description: 'read plugin manifest and substrate inventory',
|
|
83
|
+
whenToUse: 'reading one installed plugin in detail to decide before you enable, disable, or remove it — returns the full manifest, the substrate docs it provides (its `<pluginName>/` memory subtree), its scope, and whether it is currently enabled. Use `pkg plugin inspect list` instead to enumerate plugins rather than drill into one',
|
|
83
84
|
help: {
|
|
84
85
|
name: 'pkg plugin inspect show',
|
|
85
86
|
summary: 'read plugin manifest and metadata by name',
|
|
@@ -93,7 +94,7 @@ const pluginShow = defineLeaf({
|
|
|
93
94
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the plugin directory.' },
|
|
94
95
|
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Whether the plugin is active.' },
|
|
95
96
|
{ name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
|
|
96
|
-
{ name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path
|
|
97
|
+
{ name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path}. Substrate docs provided by the plugin (its `<pluginName>/` memory subtree).' },
|
|
97
98
|
],
|
|
98
99
|
outputKind: 'object',
|
|
99
100
|
effects: ['None. Read-only.'],
|
|
@@ -117,7 +118,8 @@ const pluginShow = defineLeaf({
|
|
|
117
118
|
if (found === null) {
|
|
118
119
|
throw notFound(`plugin not found: ${name}`);
|
|
119
120
|
}
|
|
120
|
-
const
|
|
121
|
+
const prefix = `${found.name}/`;
|
|
122
|
+
const skills = listAllMemoryDocs().filter((d) => d.name.startsWith(prefix));
|
|
121
123
|
return {
|
|
122
124
|
name: found.name,
|
|
123
125
|
scope: found.scope,
|
|
@@ -127,7 +129,6 @@ const pluginShow = defineLeaf({
|
|
|
127
129
|
skills: skills.map((s) => ({
|
|
128
130
|
name: s.name,
|
|
129
131
|
path: s.path,
|
|
130
|
-
enabled: s.enabled,
|
|
131
132
|
})),
|
|
132
133
|
};
|
|
133
134
|
},
|
package/dist/commands/pkg.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { defineBranch } from '../core/command.js';
|
|
5
5
|
import { pluginBranch } from './pkg/plugin.js';
|
|
6
6
|
import { marketBranch } from './pkg/market.js';
|
|
7
|
+
import { bridgeBranch } from './pkg/bridge.js';
|
|
7
8
|
export function registerPkg() {
|
|
8
9
|
return defineBranch({
|
|
9
10
|
name: 'pkg',
|
|
@@ -16,6 +17,6 @@ export function registerPkg() {
|
|
|
16
17
|
name: 'pkg',
|
|
17
18
|
summary: 'manage plugins and plugin marketplaces',
|
|
18
19
|
},
|
|
19
|
-
children: [pluginBranch, marketBranch],
|
|
20
|
+
children: [pluginBranch, marketBranch, bridgeBranch],
|
|
20
21
|
});
|
|
21
22
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
2
|
import { writeFileSync } from 'node:fs';
|
|
3
3
|
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
4
4
|
import { usage, general, notFound } from '../../core/errors.js';
|
|
5
|
-
import {
|
|
5
|
+
import { SKILL_TYPES, isSkillType } from '../../types.js';
|
|
6
6
|
import { parseSkillQualifier, findPluginByName, } from '../../core/resolver.js';
|
|
7
|
-
import { resolveScopeArg, requireScopeRoot,
|
|
8
|
-
import { serializeFrontmatter } from '../../core/frontmatter.js';
|
|
7
|
+
import { resolveScopeArg, requireScopeRoot, scopeMemoryDir, pluginMemoryDir, } from '../../core/scope.js';
|
|
9
8
|
import { ensureScopeInitialized } from '../../core/config.js';
|
|
10
9
|
import { ensureDir, pathExists } from '../../core/fs-utils.js';
|
|
11
10
|
import { skillCreatePrompt, skillTemplatePrompt } from '../../prompts/skill.js';
|
|
11
|
+
import { memoryFilePath, serializeMemoryDoc } from '../memory/shared.js';
|
|
12
12
|
import { VALID_TYPES, resolveWriteScope } from './shared.js';
|
|
13
13
|
export const authorGuide = defineLeaf({
|
|
14
14
|
name: 'guide',
|
|
@@ -42,25 +42,25 @@ export const authorGuide = defineLeaf({
|
|
|
42
42
|
});
|
|
43
43
|
export const authorScaffold = defineLeaf({
|
|
44
44
|
name: 'scaffold',
|
|
45
|
-
description: 'create an empty
|
|
46
|
-
whenToUse: 'creating the empty
|
|
45
|
+
description: 'create an empty skill substrate doc stub',
|
|
46
|
+
whenToUse: 'creating the empty `kind: skill` substrate doc at a `<plugin>/<skill>` qualifier (or a bare `<skill>` name for a scope-owned doc) before you fill in content — it writes the frontmatter and file, then points you at the authoring guide.',
|
|
47
47
|
help: {
|
|
48
48
|
name: 'skill author scaffold',
|
|
49
|
-
summary: 'create an empty
|
|
49
|
+
summary: 'create an empty kind:skill substrate doc stub at the given qualifier',
|
|
50
50
|
params: [
|
|
51
|
-
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier
|
|
52
|
-
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
|
|
53
|
-
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: '
|
|
51
|
+
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier: <plugin>/<skill> to scaffold into a plugin\'s memory/, or a bare <skill> for the scope-owned memory/.' },
|
|
52
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform. Drives the follow-up authoring guide only — not stored on the substrate doc.' },
|
|
53
|
+
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Read-routing line written to the doc\'s when-and-why-to-read frontmatter.' },
|
|
54
54
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
55
55
|
],
|
|
56
56
|
output: [
|
|
57
|
-
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded
|
|
57
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded substrate doc.' },
|
|
58
58
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call to load the authoring guide.' },
|
|
59
59
|
],
|
|
60
60
|
outputKind: 'object',
|
|
61
61
|
effects: [
|
|
62
|
-
'Creates
|
|
63
|
-
'Writes frontmatter
|
|
62
|
+
'Creates a `kind: skill` substrate doc stub under the resolved memory/ dir (scope-owned, or the named plugin\'s).',
|
|
63
|
+
'Writes substrate frontmatter: kind:skill, when-and-why-to-read (from --description if given), system-prompt-visibility:name, file-read-visibility:none.',
|
|
64
64
|
],
|
|
65
65
|
},
|
|
66
66
|
run: async (input) => {
|
|
@@ -82,31 +82,20 @@ export const authorScaffold = defineLeaf({
|
|
|
82
82
|
throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
83
83
|
}
|
|
84
84
|
const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
85
|
+
// Resolve the memory/ dir to scaffold into: the named plugin's, or the
|
|
86
|
+
// scope-owned one. One substrate, one author path.
|
|
87
|
+
let memoryDir;
|
|
88
|
+
if (pluginName === undefined) {
|
|
88
89
|
const scope = resolveWriteScope(scopeStr);
|
|
89
90
|
const scopeRootPath = requireScopeRoot(scope);
|
|
90
91
|
ensureScopeInitialized(scope, scopeRootPath);
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
93
|
-
throw general(`no
|
|
92
|
+
const dir = scopeMemoryDir(scope);
|
|
93
|
+
if (!dir) {
|
|
94
|
+
throw general(`no memory dir for scope ${scope}`);
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
-
skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
97
|
-
if (pathExists(skillFile)) {
|
|
98
|
-
throw general(`skill already exists: ${skillFile}`);
|
|
99
|
-
}
|
|
100
|
-
ensureDir(skillDir);
|
|
101
|
-
const fm = serializeFrontmatter({
|
|
102
|
-
name: skillName,
|
|
103
|
-
description,
|
|
104
|
-
type: skillType,
|
|
105
|
-
});
|
|
106
|
-
writeFileSync(skillFile, fm, 'utf8');
|
|
96
|
+
memoryDir = dir;
|
|
107
97
|
}
|
|
108
98
|
else {
|
|
109
|
-
// Plugin-scoped scaffold
|
|
110
99
|
const scopeForLookup = scopeStr !== undefined
|
|
111
100
|
? (() => {
|
|
112
101
|
const r = resolveScopeArg(scopeStr);
|
|
@@ -119,19 +108,21 @@ export const authorScaffold = defineLeaf({
|
|
|
119
108
|
if (!plugin) {
|
|
120
109
|
throw notFound(`plugin not found: ${pluginName}`);
|
|
121
110
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
ensureDir(skillDir);
|
|
128
|
-
const fm = serializeFrontmatter({
|
|
129
|
-
name: skillName,
|
|
130
|
-
description,
|
|
131
|
-
type: skillType,
|
|
132
|
-
});
|
|
133
|
-
writeFileSync(skillFile, fm, 'utf8');
|
|
111
|
+
memoryDir = pluginMemoryDir(plugin);
|
|
112
|
+
}
|
|
113
|
+
const skillFile = memoryFilePath(memoryDir, skillName);
|
|
114
|
+
if (pathExists(skillFile)) {
|
|
115
|
+
throw general(`skill already exists: ${skillFile}`);
|
|
134
116
|
}
|
|
117
|
+
ensureDir(dirname(skillFile));
|
|
118
|
+
// Catalog-default substrate frontmatter (design §1.5): a name-rung,
|
|
119
|
+
// file-read-none skill doc. --description becomes the read-routing line.
|
|
120
|
+
const frontmatter = { kind: 'skill' };
|
|
121
|
+
if (description !== undefined)
|
|
122
|
+
frontmatter['when-and-why-to-read'] = description;
|
|
123
|
+
frontmatter['system-prompt-visibility'] = 'name';
|
|
124
|
+
frontmatter['file-read-visibility'] = 'none';
|
|
125
|
+
writeFileSync(skillFile, serializeMemoryDoc(frontmatter, ''), 'utf8');
|
|
135
126
|
const typeHint = skillType !== undefined ? `--type ${skillType} ` : '';
|
|
136
127
|
const follow_up = `crtr skill author guide ${typeHint}--topic "${skillName}"`;
|
|
137
128
|
return { path: skillFile, follow_up };
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function formatNeighborQualifier(s: Skill): string;
|
|
3
|
-
export declare function formatNeighborKeywords(s: Skill): string;
|
|
4
|
-
export declare function buildNeighborsSection(skill: Skill): string | null;
|
|
5
|
-
export declare function appendNeighbors(skill: Skill, body: string): string;
|
|
1
|
+
import type { Scope } from '../../types.js';
|
|
6
2
|
export declare function resolveWriteScope(scopeStr: string | undefined): Scope;
|
|
7
3
|
export declare const VALID_TYPES: readonly ["playbook", "primer", "reference", "runbook", "freeform"];
|
|
8
4
|
export type CatalogSource = {
|