@crouton-kit/crouter 0.3.8 → 0.3.11
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/cli.js +14 -24
- package/dist/commands/agent.d.ts +4 -0
- package/dist/commands/agent.js +444 -243
- package/dist/commands/debug.d.ts +1 -1
- package/dist/commands/debug.js +20 -7
- package/dist/commands/human.js +51 -19
- package/dist/commands/job.d.ts +9 -0
- package/dist/commands/job.js +50 -10
- package/dist/commands/mode.d.ts +2 -0
- package/dist/commands/mode.js +231 -0
- package/dist/commands/pkg.js +5 -0
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +24 -11
- package/dist/commands/skill.js +20 -4
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.js +24 -11
- package/dist/commands/sys.js +5 -0
- package/dist/core/__tests__/job.test.js +11 -11
- package/dist/core/__tests__/jobs.test.js +33 -1
- package/dist/core/__tests__/resolver.test.js +69 -1
- package/dist/core/__tests__/spawn.test.d.ts +1 -0
- package/dist/core/__tests__/spawn.test.js +138 -0
- package/dist/core/__tests__/subagents.test.d.ts +1 -0
- package/dist/core/__tests__/subagents.test.js +75 -0
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/command.d.ts +58 -2
- package/dist/core/command.js +62 -14
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +64 -32
- package/dist/core/jobs.d.ts +8 -2
- package/dist/core/jobs.js +109 -6
- package/dist/core/resolver.js +51 -1
- package/dist/core/spawn.d.ts +140 -23
- package/dist/core/spawn.js +392 -73
- package/dist/core/subagents.d.ts +18 -0
- package/dist/core/subagents.js +163 -0
- package/dist/prompts/agent.d.ts +10 -1
- package/dist/prompts/agent.js +34 -3
- package/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/package.json +2 -2
package/dist/core/resolver.js
CHANGED
|
@@ -240,12 +240,62 @@ export function resolveSkill(rawName, opts = {}) {
|
|
|
240
240
|
const direct = findSkillMatches(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
|
|
241
241
|
if (direct.length > 0)
|
|
242
242
|
return pickMatch(direct, skillName, pluginQualifier);
|
|
243
|
+
// Leaf-name fallback: the caller supplied only the final path segment
|
|
244
|
+
// (e.g. "cli-design" for "ai/interface/cli-design"). A direct path lookup
|
|
245
|
+
// missed because the skill lives under a nested path. Match by last segment.
|
|
246
|
+
const byLeaf = findSkillsByLeaf(skillName, pluginQualifier, effectiveScope, effectivePluginFilter);
|
|
247
|
+
if (byLeaf.length === 1)
|
|
248
|
+
return byLeaf[0];
|
|
249
|
+
if (byLeaf.length > 1) {
|
|
250
|
+
throw ambiguous(formatLeafAmbiguousMessage(skillName, byLeaf), {
|
|
251
|
+
skill: skillName,
|
|
252
|
+
candidates: byLeaf.map((m) => ({
|
|
253
|
+
id: formatSkillId(m),
|
|
254
|
+
plugin: m.plugin,
|
|
255
|
+
scope: m.scope,
|
|
256
|
+
path: m.path,
|
|
257
|
+
})),
|
|
258
|
+
next: 'Multiple skills share this leaf name. Re-run with one of the full paths in candidates.',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
243
261
|
throw notFound(formatNotFoundMessage(rawName, skillName, pluginQualifier), {
|
|
244
262
|
skill: skillName,
|
|
245
263
|
plugin: pluginQualifier,
|
|
246
264
|
scope: parsed.scope,
|
|
247
265
|
});
|
|
248
266
|
}
|
|
267
|
+
/** Canonical, unambiguous identifier for a skill. Scope-root skills are
|
|
268
|
+
* qualified by scope; plugin skills by plugin name. */
|
|
269
|
+
function formatSkillId(s) {
|
|
270
|
+
return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}/${s.name}` : `${s.plugin}/${s.name}`;
|
|
271
|
+
}
|
|
272
|
+
/** Match skills whose final path segment equals `leaf`. Only meaningful when
|
|
273
|
+
* `leaf` is a bare segment (no slash) — a slashed query can never equal a
|
|
274
|
+
* single segment, so this returns empty and the caller falls through. */
|
|
275
|
+
function findSkillsByLeaf(leaf, pluginQualifier, scope, pluginFilter) {
|
|
276
|
+
if (leaf.includes('/'))
|
|
277
|
+
return [];
|
|
278
|
+
let all;
|
|
279
|
+
try {
|
|
280
|
+
all = scope ? listAllSkills(scope) : listAllSkills();
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
return all.filter((s) => {
|
|
286
|
+
if ((s.name.split('/').pop() ?? s.name) !== leaf)
|
|
287
|
+
return false;
|
|
288
|
+
if (pluginQualifier && s.plugin !== pluginQualifier)
|
|
289
|
+
return false;
|
|
290
|
+
if (pluginFilter && s.plugin !== pluginFilter)
|
|
291
|
+
return false;
|
|
292
|
+
return true;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function formatLeafAmbiguousMessage(leaf, matches) {
|
|
296
|
+
const ids = matches.map(formatSkillId).join(', ');
|
|
297
|
+
return `ambiguous skill: ${leaf} matches multiple skills: ${ids}`;
|
|
298
|
+
}
|
|
249
299
|
function findSkillMatches(name, pluginQualifier, scope, pluginFilter) {
|
|
250
300
|
const plugins = scope ? listInstalledPlugins(scope) : listAllPlugins();
|
|
251
301
|
const enabledPlugins = plugins.filter((p) => p.enabled);
|
|
@@ -332,7 +382,7 @@ function formatNotFoundMessage(rawName, skillName, pluginQualifier) {
|
|
|
332
382
|
lines.push(` did you mean: ${formatted.join(', ')}`);
|
|
333
383
|
}
|
|
334
384
|
else {
|
|
335
|
-
lines.push(' run `crtr skill list` or `crtr skill search <query>` to discover skills');
|
|
385
|
+
lines.push(' run `crtr skill find list` or `crtr skill find search <query>` to discover skills');
|
|
336
386
|
}
|
|
337
387
|
return lines.join('\n');
|
|
338
388
|
}
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface SpawnAgentOptions {
|
|
2
|
-
/** First user message for the new
|
|
2
|
+
/** First user message for the new agent session. */
|
|
3
3
|
prompt: string;
|
|
4
4
|
cwd: string;
|
|
5
5
|
/** crtr job_id injected as CRTR_JOB_ID env var in the pane. */
|
|
@@ -10,8 +10,14 @@ export interface SpawnAgentOptions {
|
|
|
10
10
|
};
|
|
11
11
|
/** Max panes per tmux window before overflowing to a new window. */
|
|
12
12
|
maxPanesPerWindow: number;
|
|
13
|
-
/** Display name passed to
|
|
13
|
+
/** Display name passed to the agent's `-n` flag; surfaces in pane title and resume picker. */
|
|
14
14
|
name?: string;
|
|
15
|
+
/** Persona appended via `--append-system-prompt` (subagent body). */
|
|
16
|
+
systemPrompt?: string;
|
|
17
|
+
/** Model pattern/id passed via `--model`. */
|
|
18
|
+
model?: string;
|
|
19
|
+
/** Tool allow-list passed to pi via `--tools`. */
|
|
20
|
+
tools?: string[];
|
|
15
21
|
}
|
|
16
22
|
export interface SpawnAgentResult {
|
|
17
23
|
status: 'spawned' | 'spawn-failed' | 'not-in-tmux';
|
|
@@ -22,10 +28,11 @@ export interface SpawnAgentResult {
|
|
|
22
28
|
message: string;
|
|
23
29
|
}
|
|
24
30
|
export interface DetachOptions {
|
|
25
|
-
/** Inner command to run in the pane. If omitted, build
|
|
31
|
+
/** Inner command to run in the pane. If omitted, build the detected agent's
|
|
32
|
+
* invocation around `<prompt>`. */
|
|
26
33
|
command?: string;
|
|
27
|
-
/** Full first user message for the new
|
|
28
|
-
*
|
|
34
|
+
/** Full first user message for the new agent session (ignored when `command`
|
|
35
|
+
* is set). No custom system prompt. */
|
|
29
36
|
prompt?: string;
|
|
30
37
|
cwd: string;
|
|
31
38
|
/** crtr job_id injected as CRTR_JOB_ID env var in the pane and used by the
|
|
@@ -42,7 +49,7 @@ export interface DetachOptions {
|
|
|
42
49
|
* uses the attached client's currently-focused pane — which drifts if the
|
|
43
50
|
* user switches windows between kickoff and spawn. */
|
|
44
51
|
targetPane?: string;
|
|
45
|
-
/** Display name passed to
|
|
52
|
+
/** Display name passed to the agent's `-n` flag; ignored when `command` is set
|
|
46
53
|
* (caller controls the full argv in that mode). */
|
|
47
54
|
name?: string;
|
|
48
55
|
}
|
|
@@ -53,18 +60,124 @@ export interface DetachResult {
|
|
|
53
60
|
}
|
|
54
61
|
export declare function isInTmux(): boolean;
|
|
55
62
|
export declare function shellQuote(s: string): string;
|
|
63
|
+
/** Coding-agent CLIs crtr knows how to spawn as a sibling worker. */
|
|
64
|
+
export type AgentKind = 'claude' | 'pi';
|
|
65
|
+
/**
|
|
66
|
+
* Detect which coding-agent CLI is hosting the current crtr process so spawns
|
|
67
|
+
* launch a matching sibling. pi exports `PI_CODING_AGENT=true` into its tool
|
|
68
|
+
* subprocess environment; Claude Code exports `CLAUDECODE` /
|
|
69
|
+
* `CLAUDE_CODE_SESSION_ID`. Defaults to claude when no signal is present
|
|
70
|
+
* (preserves prior behavior).
|
|
71
|
+
*/
|
|
72
|
+
export declare function detectAgentKind(): AgentKind;
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a `--model` value for the target agent CLI.
|
|
75
|
+
*
|
|
76
|
+
* Subagent frontmatter uses Claude Code's bare aliases (`sonnet`, `opus`,
|
|
77
|
+
* `haiku`, optionally with a `:thinking` suffix). The `claude` CLI resolves
|
|
78
|
+
* those natively, but `pi` maps a bare alias to its default provider —
|
|
79
|
+
* `amazon-bedrock` — which most users have not authenticated, so the spawn
|
|
80
|
+
* dies with "No API key found for amazon-bedrock". These aliases name Anthropic
|
|
81
|
+
* models, so under pi we pin them to the `anthropic/` provider (preserving any
|
|
82
|
+
* `:thinking` suffix). Values that already carry a `provider/` prefix or are
|
|
83
|
+
* concrete model ids are passed through untouched.
|
|
84
|
+
*/
|
|
85
|
+
export declare function normalizeModelForKind(model: string, kind: AgentKind): string;
|
|
86
|
+
export interface AgentCommandOptions {
|
|
87
|
+
/** First user message delivered to the new agent session. */
|
|
88
|
+
prompt: string;
|
|
89
|
+
/** Display name (`-n`); surfaces in the pane title and resume picker. */
|
|
90
|
+
name?: string;
|
|
91
|
+
/** Fork an existing session into a fresh one rather than starting clean. */
|
|
92
|
+
fork?: {
|
|
93
|
+
sessionId: string;
|
|
94
|
+
};
|
|
95
|
+
/** Persona/system prompt appended to the agent's default (`--append-system-prompt`).
|
|
96
|
+
* Used to apply a subagent definition's body. */
|
|
97
|
+
systemPrompt?: string;
|
|
98
|
+
/** Model pattern/id passed via `--model` (both claude and pi). */
|
|
99
|
+
model?: string;
|
|
100
|
+
/** Tool allow-list. Passed to pi via `--tools`; ignored for claude, whose
|
|
101
|
+
* tool names and gating flag differ. */
|
|
102
|
+
tools?: string[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the agent-CLI invocation (no job wrapper) for the given kind.
|
|
106
|
+
*
|
|
107
|
+
* claude: `claude [-n <name>] [--resume <id> --fork-session] \
|
|
108
|
+
* --dangerously-skip-permissions <prompt>`
|
|
109
|
+
* pi: `pi [-n <name>] [--fork <id>] <prompt>`
|
|
110
|
+
*
|
|
111
|
+
* pi has no permission popups, so it needs no skip-permissions flag.
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildAgentCommand(opts: AgentCommandOptions, kind?: AgentKind): string;
|
|
114
|
+
export interface AgentPrintArgv {
|
|
115
|
+
cmd: string;
|
|
116
|
+
args: string[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Argv for a non-interactive print-mode run.
|
|
120
|
+
*
|
|
121
|
+
* claude: `claude [-n <name>] [--resume <id> --fork-session] -p \
|
|
122
|
+
* --dangerously-skip-permissions <prompt>`
|
|
123
|
+
* pi: `pi [-n <name>] [--fork <id>] -p <prompt>`
|
|
124
|
+
*
|
|
125
|
+
* Returned as a cmd + args array so callers can spawn without a shell.
|
|
126
|
+
*/
|
|
127
|
+
export declare function buildAgentPrintArgv(opts: AgentCommandOptions, kind?: AgentKind): AgentPrintArgv;
|
|
128
|
+
/** Same as buildAgentPrintArgv but rendered as a single shell-quoted string. */
|
|
129
|
+
export declare function buildAgentPrintCommand(opts: AgentCommandOptions, kind?: AgentKind): string;
|
|
130
|
+
export interface HeadlessRunResult {
|
|
131
|
+
status: 'done' | 'failed';
|
|
132
|
+
/** Captured stdout on success; stdout+stderr (or an error message) on failure. */
|
|
133
|
+
output: string;
|
|
134
|
+
exitCode: number | null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run the agent headlessly and resolve once it exits. A blocking caller awaits
|
|
138
|
+
* this. stdout is captured as the result; a non-zero exit yields status
|
|
139
|
+
* 'failed' with the combined output.
|
|
140
|
+
*/
|
|
141
|
+
export declare function runAgentHeadless(opts: {
|
|
142
|
+
prompt: string;
|
|
143
|
+
name?: string;
|
|
144
|
+
cwd: string;
|
|
145
|
+
systemPrompt?: string;
|
|
146
|
+
model?: string;
|
|
147
|
+
tools?: string[];
|
|
148
|
+
}): Promise<HeadlessRunResult>;
|
|
149
|
+
export interface HeadlessDetachResult {
|
|
150
|
+
status: 'spawned' | 'spawn-failed';
|
|
151
|
+
pid?: number;
|
|
152
|
+
message: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Launch a headless agent detached (background). Its print output is captured
|
|
156
|
+
* and delivered to the job via `crtr job submit`; a non-zero exit marks the job
|
|
157
|
+
* failed. Returns immediately with the wrapper pid (recorded for crash
|
|
158
|
+
* detection). No tmux required.
|
|
159
|
+
*/
|
|
160
|
+
export declare function spawnHeadlessDetached(opts: {
|
|
161
|
+
prompt: string;
|
|
162
|
+
name?: string;
|
|
163
|
+
cwd: string;
|
|
164
|
+
jobId: string;
|
|
165
|
+
systemPrompt?: string;
|
|
166
|
+
model?: string;
|
|
167
|
+
tools?: string[];
|
|
168
|
+
}): HeadlessDetachResult;
|
|
56
169
|
export declare function countPanesInCurrentWindow(): number;
|
|
57
170
|
/**
|
|
58
171
|
* Find a window in the current tmux session with fewer than `maxPanesPerWindow`
|
|
59
|
-
* panes AND where every existing pane
|
|
60
|
-
* Prefers the active window so the spawned pane is visible
|
|
61
|
-
* otherwise falls back to the first other eligible window. Returns
|
|
62
|
-
* window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
|
|
172
|
+
* panes AND where every existing pane hosts an agent (claude or pi) as its
|
|
173
|
+
* foreground process. Prefers the active window so the spawned pane is visible
|
|
174
|
+
* to the user; otherwise falls back to the first other eligible window. Returns
|
|
175
|
+
* the tmux window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
|
|
63
176
|
*
|
|
64
177
|
* Windows holding non-agent panes (dashboards, log tails, idle shells, editors,
|
|
65
178
|
* REPLs, etc.) are skipped so spawning never disrupts those workflows. A pane
|
|
66
|
-
* qualifies as long as
|
|
67
|
-
* helpers like `caffeinate` don't disqualify it.
|
|
179
|
+
* qualifies as long as an agent comm is among its foreground commands —
|
|
180
|
+
* co-resident helpers like `caffeinate` don't disqualify it.
|
|
68
181
|
*/
|
|
69
182
|
export declare function findWindowWithSpace(maxPanesPerWindow: number): string | null;
|
|
70
183
|
/**
|
|
@@ -78,23 +191,27 @@ export declare function findWindowWithSpace(maxPanesPerWindow: number): string |
|
|
|
78
191
|
*/
|
|
79
192
|
export declare function scheduleKillCurrentPane(delaySeconds: number): boolean;
|
|
80
193
|
/**
|
|
81
|
-
* Fire-and-forget: launch an interactive
|
|
194
|
+
* Fire-and-forget: launch an interactive agent in a new pane (or window),
|
|
82
195
|
* then schedule the originating pane to be killed after `killAfterSeconds`.
|
|
83
196
|
*
|
|
84
197
|
* No custom system prompt — the task is delivered as the first user message.
|
|
85
|
-
* Returns as soon as the new pane is up; does NOT wait for
|
|
198
|
+
* Returns as soon as the new pane is up; does NOT wait for the agent to finish.
|
|
86
199
|
*/
|
|
87
200
|
export declare function spawnAndDetach(opts: DetachOptions): DetachResult;
|
|
201
|
+
/** Originating pane id + session id of the host (pi/claude) crtr runs under. */
|
|
202
|
+
export declare function originContext(): {
|
|
203
|
+
pane: string;
|
|
204
|
+
sessionId: string;
|
|
205
|
+
} | null;
|
|
206
|
+
/** Deterministic subagent session name for an originating pane id (e.g. `%5`). */
|
|
207
|
+
export declare function subagentSessionName(pane: string): string;
|
|
88
208
|
/**
|
|
89
|
-
* Async sibling spawn. Launches
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* 1. Current window, if it has space.
|
|
95
|
-
* 2. Any other window in the session with space.
|
|
96
|
-
* 3. New window (every existing window at capacity).
|
|
209
|
+
* Async sibling spawn. Launches an interactive agent (claude or pi, per
|
|
210
|
+
* detectAgentKind) in the dedicated subagent session, progressively filling
|
|
211
|
+
* windows up to `maxPanesPerWindow` before creating a new window. Returns
|
|
212
|
+
* immediately with the pane id; the parent stays alive. Focus is never
|
|
213
|
+
* switched — the user jumps to the subagent session with Alt-o.
|
|
97
214
|
*
|
|
98
|
-
* If `fork` is set,
|
|
215
|
+
* If `fork` is set, forks the host session into a fresh one.
|
|
99
216
|
*/
|
|
100
217
|
export declare function spawnAgent(opts: SpawnAgentOptions): SpawnAgentResult;
|