@lucascouts/claude-agent-tui 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-agent.d.ts +187 -13
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +444 -59
- package/dist/agent-catalog.d.ts +96 -0
- package/dist/agent-catalog.d.ts.map +1 -0
- package/dist/agent-catalog.js +287 -0
- package/dist/claude-path.d.ts.map +1 -1
- package/dist/claude-path.js +6 -0
- package/dist/end-of-turn.d.ts +6 -0
- package/dist/end-of-turn.d.ts.map +1 -1
- package/dist/end-of-turn.js +8 -1
- package/dist/engine-lifecycle.d.ts +66 -1
- package/dist/engine-lifecycle.d.ts.map +1 -1
- package/dist/engine-lifecycle.js +43 -4
- package/dist/engine-pty.d.ts +70 -2
- package/dist/engine-pty.d.ts.map +1 -1
- package/dist/engine-pty.js +80 -6
- package/dist/gate/settings-writer.d.ts +14 -0
- package/dist/gate/settings-writer.d.ts.map +1 -1
- package/dist/gate/settings-writer.js +49 -0
- package/dist/image-input.d.ts +31 -0
- package/dist/image-input.d.ts.map +1 -0
- package/dist/image-input.js +79 -0
- package/dist/image-vision-smoke.d.ts +52 -0
- package/dist/image-vision-smoke.d.ts.map +1 -0
- package/dist/image-vision-smoke.js +111 -0
- package/dist/index.js +6 -0
- package/dist/mcp-config-writer.d.ts +61 -0
- package/dist/mcp-config-writer.d.ts.map +1 -0
- package/dist/mcp-config-writer.js +172 -0
- package/dist/model-catalog.d.ts +29 -2
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +50 -10
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +9 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -1
- package/dist/usage.d.ts +3 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +3 -0
- package/package.json +8 -8
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** A single discoverable main-thread agent persona, as advertised to the Zed `agent` selector. */
|
|
2
|
+
export interface AgentCatalogEntry {
|
|
3
|
+
/**
|
|
4
|
+
* The persona reference passed to `claude --agent <value>`. GUARANTEED to match
|
|
5
|
+
* {@link SAFE_AGENT_REF} (a single safe segment, or a namespaced `plugin:name`) — entries whose
|
|
6
|
+
* resolved name fails this are dropped at discovery, so this string is always safe to double-quote
|
|
7
|
+
* into a `-lc` shell command.
|
|
8
|
+
*/
|
|
9
|
+
value: string;
|
|
10
|
+
/**
|
|
11
|
+
* Human-facing label. From the probe path it is the raw reference (e.g. `epic:analyst`); from the
|
|
12
|
+
* glob path it is the frontmatter `name`, else a humanized form of the filename.
|
|
13
|
+
*/
|
|
14
|
+
displayName: string;
|
|
15
|
+
/** The frontmatter `description`, when present (glob path only — the probe lists names, not docs). */
|
|
16
|
+
description?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Injectable seams for {@link discoverAgents} — defaults wire the `node:` stdlib. Tests pass fakes so
|
|
20
|
+
* discovery is exercised against an in-memory fs / canned probe output, never the real disk, the real
|
|
21
|
+
* `~/.claude`, or the real `claude` binary.
|
|
22
|
+
*/
|
|
23
|
+
export interface DiscoverAgentsDeps {
|
|
24
|
+
/**
|
|
25
|
+
* Probe `claude` for its canonical agent list (the PRIMARY source). Returns the raw combined
|
|
26
|
+
* stdout+stderr of `claude --agent <invalid>` (which contains the `Available agents: …` line), or
|
|
27
|
+
* `null` when the probe cannot run (missing binary / timeout) so discovery falls back to the glob.
|
|
28
|
+
* Default: {@link defaultProbeClaudeAgents}. Tests inject a canned string (or `null`) to stay
|
|
29
|
+
* hermetic — a glob-path test MUST pass `() => null` to force the fallback.
|
|
30
|
+
*/
|
|
31
|
+
probeClaudeAgents?: () => string | null;
|
|
32
|
+
/** Resolve the user's home directory (default: `os.homedir`). Glob path only. */
|
|
33
|
+
homedir?: () => string;
|
|
34
|
+
/** List the `*.md` filenames in `dir`; MUST return `[]` when `dir` is missing/unreadable. */
|
|
35
|
+
readdirMd?: (dir: string) => string[];
|
|
36
|
+
/** Read a file's UTF-8 contents (default: `fs.readFileSync(p, "utf8")`). */
|
|
37
|
+
readFile?: (path: string) => string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The persona-SEGMENT allowlist (R3.3). A single un-namespaced segment: letters/digits/underscore/
|
|
41
|
+
* hyphen only (no spaces, no shell metacharacters, no path separators, no `:`). Guards the glob path
|
|
42
|
+
* (on-disk `.md` names) and is the per-segment building block of {@link SAFE_AGENT_REF}.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SAFE_AGENT_NAME: RegExp;
|
|
45
|
+
/** True iff `name` is a non-empty string matching {@link SAFE_AGENT_NAME} (R3.3). */
|
|
46
|
+
export declare function isSafeAgentName(name: unknown): name is string;
|
|
47
|
+
/**
|
|
48
|
+
* The persona-REFERENCE allowlist (R3.6). A `--agent "<value>"` reference is either a single safe
|
|
49
|
+
* segment (a built-in like `Explore`, or a user/project `.md` persona) OR a namespaced plugin persona
|
|
50
|
+
* `plugin:name` — exactly ONE optional `:`-separated second segment, each segment a
|
|
51
|
+
* {@link SAFE_AGENT_NAME}. The `:` is inert inside the double-quoted shell argument.
|
|
52
|
+
*/
|
|
53
|
+
export declare const SAFE_AGENT_REF: RegExp;
|
|
54
|
+
/** True iff `name` is a non-empty string matching {@link SAFE_AGENT_REF} (R3.6). */
|
|
55
|
+
export declare function isSafeAgentRef(name: unknown): name is string;
|
|
56
|
+
/**
|
|
57
|
+
* Extract the agent names from a `claude --agent <invalid>` probe output. Matches the
|
|
58
|
+
* `Available agents: a, b, c` line (tolerant of leading text and a trailing newline — `.` stops at the
|
|
59
|
+
* newline so only that line is captured), splits on commas, trims, and drops empties. Returns `[]` when
|
|
60
|
+
* the line is absent (format changed → the caller falls back to the glob).
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseAvailableAgents(probeOutput: string): string[];
|
|
63
|
+
/** Frontmatter fields we extract — only `name`/`description` matter for the catalog. */
|
|
64
|
+
interface Frontmatter {
|
|
65
|
+
name?: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Minimal `---`-fenced frontmatter line-parser (NOT a full YAML dep). Reads the leading
|
|
70
|
+
* `---\n … \n---` block and pulls `key: value` lines for the keys we care about; surrounding quotes on
|
|
71
|
+
* a value are stripped. A file without an opening `---` fence yields `{}` (filename-fallback applies).
|
|
72
|
+
* Tiny + pure on purpose — the persona frontmatter we consume is flat `key: value`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseFrontmatter(content: string): Frontmatter;
|
|
75
|
+
/**
|
|
76
|
+
* Discover the main-thread agent personas selectable via `claude --agent <name>`.
|
|
77
|
+
*
|
|
78
|
+
* PRIMARY (Task 7, R3.5): the {@link DiscoverAgentsDeps.probeClaudeAgents} probe asks `claude` for its
|
|
79
|
+
* canonical list (enabled plugin personas + built-ins, exactly as `claude --agent` resolves them) and
|
|
80
|
+
* we parse the `Available agents:` line; the bare `claude` default and any name failing the R3.6
|
|
81
|
+
* reference allowlist are dropped, and the rest are deduped + sorted by `value`. When the probe yields
|
|
82
|
+
* at least one persona, that is the result.
|
|
83
|
+
*
|
|
84
|
+
* FALLBACK (R3.1): when the probe returns `null` (binary missing / timeout) OR its output has no
|
|
85
|
+
* `Available agents:` line OR every listed name was dropped, discovery degrades to the on-disk glob
|
|
86
|
+
* ({@link globDiscoverAgents}).
|
|
87
|
+
*
|
|
88
|
+
* Never throws: a failed probe, a missing/unreadable dir or file, or an empty/all-unsafe result yields
|
|
89
|
+
* `[]` (the empty-state that hides the picker in a later sub-task).
|
|
90
|
+
*
|
|
91
|
+
* @param cwd the SESSION cwd (project dir) whose `.claude/agents` takes precedence in the fallback.
|
|
92
|
+
* @param deps injectable probe/fs/home seams (default: `node:` stdlib + the real `claude` probe).
|
|
93
|
+
*/
|
|
94
|
+
export declare function discoverAgents(cwd: string, deps?: DiscoverAgentsDeps): AgentCatalogEntry[];
|
|
95
|
+
export {};
|
|
96
|
+
//# sourceMappingURL=agent-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-catalog.d.ts","sourceRoot":"","sources":["../src/agent-catalog.ts"],"names":[],"mappings":"AA4CA,kGAAkG;AAClG,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sGAAsG;IACtG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACxC,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,6FAA6F;IAC7F,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAqB,CAAC;AAElD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAE7D;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAyC,CAAC;AAErE,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAE5D;AA0CD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAOlE;AAkDD,wFAAwF;AACxF,UAAU,WAAW;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CA0B7D;AAiED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,iBAAiB,EAAE,CAe9F"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Story 056 / Task 3.1 + Task 7 (R3.1, R3.3, R3.5, R3.6) — main-thread agent-persona discovery for
|
|
2
|
+
// the Zed Agent Panel's `agent` selector.
|
|
3
|
+
//
|
|
4
|
+
// HYBRID DISCOVERY (refined in Task 7 — was glob-only):
|
|
5
|
+
// PRIMARY (R3.5): ask `claude` itself. We spawn `claude --agent <invalid-sentinel>` through a SAFE
|
|
6
|
+
// args-array child process (NO shell, 8s timeout, EMPTY stdin ⇒ no prompt ⇒ NO tokens/billing).
|
|
7
|
+
// `claude` rejects the sentinel and prints `Available agents: a, b, c` — listing EXACTLY what
|
|
8
|
+
// `claude --agent` accepts: enabled plugin personas (namespaced `plugin:name`) AND the built-ins
|
|
9
|
+
// (Explore, Plan, general-purpose, …). This is the same source the upstream SDK engine uses, so
|
|
10
|
+
// the panel reaches real parity with the original. LIVE-VERIFIED against 2.1.195 (deterministic,
|
|
11
|
+
// cwd-independent). The bare `claude` built-in = the no-persona default and is dropped (it maps to
|
|
12
|
+
// the option's existing "default" sentinel). The spawn mirrors claude-path.ts (execFileSync,
|
|
13
|
+
// args-list, NEVER a shell — C5); the only argument is a CONSTANT sentinel, never user input.
|
|
14
|
+
// FALLBACK (R3.1): when the probe cannot run (binary missing / timeout) OR the error format changes
|
|
15
|
+
// (no `Available agents:` line on a future version), discovery degrades to a glob of the on-disk
|
|
16
|
+
// persona files: `<cwd>/.claude/agents/*.md` (project — PRECEDENCE) + `~/.claude/agents/*.md`
|
|
17
|
+
// (user). Both dirs empty (the common case) yields `[]` and a later sub-task's gate hides the
|
|
18
|
+
// picker entirely.
|
|
19
|
+
//
|
|
20
|
+
// NOTE: `claude agents` (the subcommand) is the *background-agents* manager — it lists dispatched
|
|
21
|
+
// cloud SESSIONS, NOT persona definitions — so we never call it; the persona list comes from the
|
|
22
|
+
// `--agent` rejection message above.
|
|
23
|
+
//
|
|
24
|
+
// SECURITY (R3.3 / R3.6 — the highest-severity item). The `value` we return is interpolated into a
|
|
25
|
+
// `-lc` shell string via `--agent "<name>"` (Task 3.3). A plugin persona is NAMESPACED (`plugin:name`),
|
|
26
|
+
// so the boundary allowlist is {@link SAFE_AGENT_REF} = `/^[A-Za-z0-9_-]+(?::[A-Za-z0-9_-]+)?$/`: one
|
|
27
|
+
// optional `:`-separated segment, each segment letters/digits/`_`/`-` only (the `:` is inert inside the
|
|
28
|
+
// double-quoted argument). Every resolved name is checked and DROPPED if it fails — a name with spaces,
|
|
29
|
+
// quotes, path separators, command substitution, chaining, … is NEVER returned. The second layer is
|
|
30
|
+
// double-quoting at spawn time. {@link SAFE_AGENT_NAME} (single segment) still guards the glob path,
|
|
31
|
+
// where on-disk `.md` persona names are always un-namespaced.
|
|
32
|
+
//
|
|
33
|
+
// PURE + dependency-injectable (cf. claude-path.ts / usage-env.ts): all fs/home/probe access goes
|
|
34
|
+
// through the {@link DiscoverAgentsDeps} seams so the unit tests inject fakes and never touch the real
|
|
35
|
+
// disk or spawn the real `claude` binary. The only `node:` builtins are os/fs/path/child_process; no
|
|
36
|
+
// shell is ever invoked. Dependency-free: a minimal inline frontmatter line-parser stands in for a YAML
|
|
37
|
+
// dependency (FORK.md pins node-pty + the SDK as the only runtime deps; this adds none).
|
|
38
|
+
//
|
|
39
|
+
// node:test runner: `node --experimental-strip-types --test test/agent-catalog.test.ts`
|
|
40
|
+
import { homedir as osHomedir } from "node:os";
|
|
41
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
42
|
+
import { execFileSync } from "node:child_process";
|
|
43
|
+
import { join } from "node:path";
|
|
44
|
+
/**
|
|
45
|
+
* The persona-SEGMENT allowlist (R3.3). A single un-namespaced segment: letters/digits/underscore/
|
|
46
|
+
* hyphen only (no spaces, no shell metacharacters, no path separators, no `:`). Guards the glob path
|
|
47
|
+
* (on-disk `.md` names) and is the per-segment building block of {@link SAFE_AGENT_REF}.
|
|
48
|
+
*/
|
|
49
|
+
export const SAFE_AGENT_NAME = /^[A-Za-z0-9_-]+$/;
|
|
50
|
+
/** True iff `name` is a non-empty string matching {@link SAFE_AGENT_NAME} (R3.3). */
|
|
51
|
+
export function isSafeAgentName(name) {
|
|
52
|
+
return typeof name === "string" && SAFE_AGENT_NAME.test(name);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The persona-REFERENCE allowlist (R3.6). A `--agent "<value>"` reference is either a single safe
|
|
56
|
+
* segment (a built-in like `Explore`, or a user/project `.md` persona) OR a namespaced plugin persona
|
|
57
|
+
* `plugin:name` — exactly ONE optional `:`-separated second segment, each segment a
|
|
58
|
+
* {@link SAFE_AGENT_NAME}. The `:` is inert inside the double-quoted shell argument.
|
|
59
|
+
*/
|
|
60
|
+
export const SAFE_AGENT_REF = /^[A-Za-z0-9_-]+(?::[A-Za-z0-9_-]+)?$/;
|
|
61
|
+
/** True iff `name` is a non-empty string matching {@link SAFE_AGENT_REF} (R3.6). */
|
|
62
|
+
export function isSafeAgentRef(name) {
|
|
63
|
+
return typeof name === "string" && SAFE_AGENT_REF.test(name);
|
|
64
|
+
}
|
|
65
|
+
/** A sentinel `--agent` value that can never be a real persona — forces the "not found" + list. */
|
|
66
|
+
const PROBE_SENTINEL = "__acp_agent_probe_sentinel__";
|
|
67
|
+
/** The bare built-in `claude` = the no-persona default (the option's existing "default" sentinel). */
|
|
68
|
+
const DEFAULT_PERSONA = "claude";
|
|
69
|
+
/**
|
|
70
|
+
* Default {@link DiscoverAgentsDeps.probeClaudeAgents}: spawn `claude --agent <sentinel>` and return
|
|
71
|
+
* its combined output. `execFileSync` (args-array — NO shell, mirroring claude-path.ts / C5) throws on
|
|
72
|
+
* the expected non-zero exit; the `Available agents: …` line is carried on the error's stdout/stderr.
|
|
73
|
+
* Empty stdin + an 8s timeout keep it cheap and tokenless. Any failure (missing binary, timeout,
|
|
74
|
+
* unexpected shape) → `null`, which routes the caller to the glob fallback. The sole argument is the
|
|
75
|
+
* CONSTANT {@link PROBE_SENTINEL} — never user input — so there is no injection surface.
|
|
76
|
+
*
|
|
77
|
+
* OPT-IN (`FORK_AGENT_PROBE=1`): the spawn runs ONLY when production enables it (the fork entrypoint
|
|
78
|
+
* `index.ts` sets the flag). Unit tests import this module directly WITHOUT the flag, so the default
|
|
79
|
+
* probe is a no-op (`null` → glob fallback) — they never spawn the real `claude` and stay hermetic and
|
|
80
|
+
* fast. Tests that DO exercise the probe path inject {@link DiscoverAgentsDeps.probeClaudeAgents}
|
|
81
|
+
* directly, bypassing this gate.
|
|
82
|
+
*/
|
|
83
|
+
function defaultProbeClaudeAgents() {
|
|
84
|
+
if (process.env.FORK_AGENT_PROBE !== "1")
|
|
85
|
+
return null; // opt-in — production (index.ts) enables it
|
|
86
|
+
try {
|
|
87
|
+
// stdio: stdin IGNORED (claude reads EOF → no hang, no prompt → no tokens); stdout+stderr PIPED so
|
|
88
|
+
// the `Available agents:` line (claude writes it to stderr) is captured, NEVER leaked to the fork's
|
|
89
|
+
// own stdout (the ACP wire) or stderr (the log). The list lands on the throw's stderr below.
|
|
90
|
+
const out = execFileSync("claude", ["--agent", PROBE_SENTINEL], {
|
|
91
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
92
|
+
timeout: 8000,
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
maxBuffer: 1024 * 1024,
|
|
95
|
+
});
|
|
96
|
+
return out.length > 0 ? out : null;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const e = err;
|
|
100
|
+
const out = `${e?.stdout ?? ""}${e?.stderr ?? ""}`;
|
|
101
|
+
return out.length > 0 ? out : null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Extract the agent names from a `claude --agent <invalid>` probe output. Matches the
|
|
106
|
+
* `Available agents: a, b, c` line (tolerant of leading text and a trailing newline — `.` stops at the
|
|
107
|
+
* newline so only that line is captured), splits on commas, trims, and drops empties. Returns `[]` when
|
|
108
|
+
* the line is absent (format changed → the caller falls back to the glob).
|
|
109
|
+
*/
|
|
110
|
+
export function parseAvailableAgents(probeOutput) {
|
|
111
|
+
const m = /Available agents:\s*(.+)/.exec(probeOutput);
|
|
112
|
+
if (!m)
|
|
113
|
+
return [];
|
|
114
|
+
return m[1]
|
|
115
|
+
.split(",")
|
|
116
|
+
.map((s) => s.trim())
|
|
117
|
+
.filter((s) => s.length > 0);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve a probe-listed agent name into an entry, or `null` to drop it. The bare `claude` built-in is
|
|
121
|
+
* the no-persona default (mapped to the option's existing "default" sentinel) so it is dropped; any
|
|
122
|
+
* name failing the R3.6 reference allowlist is dropped. `displayName` is the raw reference (e.g.
|
|
123
|
+
* `epic:analyst`), which is what the panel shows.
|
|
124
|
+
*/
|
|
125
|
+
function entryFromProbeName(name) {
|
|
126
|
+
if (name === DEFAULT_PERSONA)
|
|
127
|
+
return null;
|
|
128
|
+
if (!isSafeAgentRef(name))
|
|
129
|
+
return null;
|
|
130
|
+
return { value: name, displayName: name };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Default `readdirMd`: list `*.md` filenames in `dir`, returning `[]` when the directory is missing or
|
|
134
|
+
* unreadable (the empty-state — never throws). Sorted for a stable first-wins ordering within a dir.
|
|
135
|
+
*/
|
|
136
|
+
function defaultReaddirMd(dir) {
|
|
137
|
+
try {
|
|
138
|
+
return readdirSync(dir)
|
|
139
|
+
.filter((f) => f.toLowerCase().endsWith(".md"))
|
|
140
|
+
.sort();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Default `readFile`: UTF-8 file read via the `node:fs` stdlib. */
|
|
147
|
+
function defaultReadFile(path) {
|
|
148
|
+
return readFileSync(path, "utf8");
|
|
149
|
+
}
|
|
150
|
+
/** The leading `name` (sans `.md`) of a persona file, used as the value/displayName fallback. */
|
|
151
|
+
function baseName(filename) {
|
|
152
|
+
return filename.replace(/\.md$/i, "");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Humanize a filename stem into a display label when the frontmatter has no `name`:
|
|
156
|
+
* `code-reviewer` → `Code Reviewer`, `db_admin` → `Db Admin`. Pure string cosmetics.
|
|
157
|
+
*/
|
|
158
|
+
function humanize(stem) {
|
|
159
|
+
return stem
|
|
160
|
+
.split(/[-_]+/)
|
|
161
|
+
.filter((w) => w.length > 0)
|
|
162
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
163
|
+
.join(" ");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Minimal `---`-fenced frontmatter line-parser (NOT a full YAML dep). Reads the leading
|
|
167
|
+
* `---\n … \n---` block and pulls `key: value` lines for the keys we care about; surrounding quotes on
|
|
168
|
+
* a value are stripped. A file without an opening `---` fence yields `{}` (filename-fallback applies).
|
|
169
|
+
* Tiny + pure on purpose — the persona frontmatter we consume is flat `key: value`.
|
|
170
|
+
*/
|
|
171
|
+
export function parseFrontmatter(content) {
|
|
172
|
+
// The opening fence must be the very first line (allowing a leading BOM / blank lines is overkill
|
|
173
|
+
// for this format). Bail to `{}` when there is no fenced block.
|
|
174
|
+
const lines = content.split(/\r?\n/);
|
|
175
|
+
if (lines[0]?.trim() !== "---")
|
|
176
|
+
return {};
|
|
177
|
+
const out = {};
|
|
178
|
+
for (let i = 1; i < lines.length; i++) {
|
|
179
|
+
const line = lines[i];
|
|
180
|
+
if (line.trim() === "---")
|
|
181
|
+
break; // closing fence — stop
|
|
182
|
+
const m = /^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(line);
|
|
183
|
+
if (!m)
|
|
184
|
+
continue; // not a `key: value` line — skip (lists/comments/blanks)
|
|
185
|
+
const key = m[1].toLowerCase();
|
|
186
|
+
if (key !== "name" && key !== "description")
|
|
187
|
+
continue;
|
|
188
|
+
let value = m[2].trim();
|
|
189
|
+
// Strip a single pair of matching surrounding quotes.
|
|
190
|
+
if (value.length >= 2 &&
|
|
191
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
192
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
193
|
+
value = value.slice(1, -1);
|
|
194
|
+
}
|
|
195
|
+
if (value.length > 0)
|
|
196
|
+
out[key] = value;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolve a single persona `*.md` file into an {@link AgentCatalogEntry}, or `null` when it cannot
|
|
202
|
+
* contribute a SAFE entry. The resolved name is the frontmatter `name` if present, else the filename
|
|
203
|
+
* stem; it is then run through the R3.3 allowlist and the file is DROPPED (→ `null`) on any failure —
|
|
204
|
+
* including an unreadable file. `displayName` falls back to a humanized stem.
|
|
205
|
+
*/
|
|
206
|
+
function resolveEntry(dir, filename, readFile) {
|
|
207
|
+
let fm;
|
|
208
|
+
try {
|
|
209
|
+
fm = parseFrontmatter(readFile(join(dir, filename)));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null; // unreadable file — skip it, never throw (R3.1 graceful)
|
|
213
|
+
}
|
|
214
|
+
const stem = baseName(filename);
|
|
215
|
+
const resolvedName = fm.name ?? stem;
|
|
216
|
+
// SECURITY (R3.3): drop any entry whose name escapes the single-segment allowlist — never returned.
|
|
217
|
+
if (!isSafeAgentName(resolvedName))
|
|
218
|
+
return null;
|
|
219
|
+
return {
|
|
220
|
+
value: resolvedName,
|
|
221
|
+
displayName: fm.name ?? humanize(stem),
|
|
222
|
+
...(fm.description ? { description: fm.description } : {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** Sort entries by `value` for a stable, scope-independent ordering. */
|
|
226
|
+
function sortByValue(entries) {
|
|
227
|
+
return entries.sort((a, b) => (a.value < b.value ? -1 : a.value > b.value ? 1 : 0));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* FALLBACK glob discovery (R3.1): scan `<cwd>/.claude/agents/*.md` (project — PRECEDENCE) and
|
|
231
|
+
* `~/.claude/agents/*.md` (user), parsing frontmatter `name`/`description` (filename stem as the
|
|
232
|
+
* fallback name), sanitizing against the R3.3 allowlist, deduping by `value` (project beats user;
|
|
233
|
+
* first-wins within a dir), and sorting by `value`. Never throws.
|
|
234
|
+
*/
|
|
235
|
+
function globDiscoverAgents(cwd, deps) {
|
|
236
|
+
const homedir = deps.homedir ?? osHomedir;
|
|
237
|
+
const readdirMd = deps.readdirMd ?? defaultReaddirMd;
|
|
238
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
239
|
+
const projectDir = join(cwd, ".claude", "agents");
|
|
240
|
+
const userDir = join(homedir(), ".claude", "agents");
|
|
241
|
+
const byValue = new Map();
|
|
242
|
+
for (const dir of [projectDir, userDir]) {
|
|
243
|
+
for (const filename of readdirMd(dir)) {
|
|
244
|
+
const entry = resolveEntry(dir, filename, readFile);
|
|
245
|
+
if (entry && !byValue.has(entry.value)) {
|
|
246
|
+
byValue.set(entry.value, entry);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return sortByValue([...byValue.values()]);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Discover the main-thread agent personas selectable via `claude --agent <name>`.
|
|
254
|
+
*
|
|
255
|
+
* PRIMARY (Task 7, R3.5): the {@link DiscoverAgentsDeps.probeClaudeAgents} probe asks `claude` for its
|
|
256
|
+
* canonical list (enabled plugin personas + built-ins, exactly as `claude --agent` resolves them) and
|
|
257
|
+
* we parse the `Available agents:` line; the bare `claude` default and any name failing the R3.6
|
|
258
|
+
* reference allowlist are dropped, and the rest are deduped + sorted by `value`. When the probe yields
|
|
259
|
+
* at least one persona, that is the result.
|
|
260
|
+
*
|
|
261
|
+
* FALLBACK (R3.1): when the probe returns `null` (binary missing / timeout) OR its output has no
|
|
262
|
+
* `Available agents:` line OR every listed name was dropped, discovery degrades to the on-disk glob
|
|
263
|
+
* ({@link globDiscoverAgents}).
|
|
264
|
+
*
|
|
265
|
+
* Never throws: a failed probe, a missing/unreadable dir or file, or an empty/all-unsafe result yields
|
|
266
|
+
* `[]` (the empty-state that hides the picker in a later sub-task).
|
|
267
|
+
*
|
|
268
|
+
* @param cwd the SESSION cwd (project dir) whose `.claude/agents` takes precedence in the fallback.
|
|
269
|
+
* @param deps injectable probe/fs/home seams (default: `node:` stdlib + the real `claude` probe).
|
|
270
|
+
*/
|
|
271
|
+
export function discoverAgents(cwd, deps = {}) {
|
|
272
|
+
// PRIMARY: ask `claude` — the source of truth for what `--agent` accepts.
|
|
273
|
+
const probe = deps.probeClaudeAgents ?? defaultProbeClaudeAgents;
|
|
274
|
+
const probeOut = probe();
|
|
275
|
+
if (probeOut) {
|
|
276
|
+
const byValue = new Map();
|
|
277
|
+
for (const name of parseAvailableAgents(probeOut)) {
|
|
278
|
+
const entry = entryFromProbeName(name);
|
|
279
|
+
if (entry && !byValue.has(entry.value))
|
|
280
|
+
byValue.set(entry.value, entry);
|
|
281
|
+
}
|
|
282
|
+
if (byValue.size > 0)
|
|
283
|
+
return sortByValue([...byValue.values()]);
|
|
284
|
+
}
|
|
285
|
+
// FALLBACK: the probe is unavailable or its format changed — glob the on-disk personas.
|
|
286
|
+
return globDiscoverAgents(cwd, deps);
|
|
287
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-path.d.ts","sourceRoot":"","sources":["../src/claude-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"claude-path.d.ts","sourceRoot":"","sources":["../src/claude-path.ts"],"names":[],"mappings":"AA+BA;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,YAAY,CAAC;AAenD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAc/E;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,WAAgC,GACrC,MAAM,GAAG,IAAI,CAMf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAChC,IAAI,CAON;AAED,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACnD,qEAAqE;IACrE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,cAAmB,GAAG,MAAM,CAiCnE"}
|
package/dist/claude-path.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
27
|
import { accessSync, constants } from "node:fs";
|
|
28
28
|
import { delimiter, join } from "node:path";
|
|
29
|
+
// Story 058 R3.2 — version-aware image-vision smoke (observability only; NEVER gates image:true, R3.1).
|
|
30
|
+
import { reportImageVisionSmoke } from "./image-vision-smoke.js";
|
|
29
31
|
/**
|
|
30
32
|
* Provenance pin the live `claude` version is measured against — kept in lockstep
|
|
31
33
|
* with `fork/.fork-provenance.json`'s externalRuntimeDeps `claude` entry. A live
|
|
@@ -121,6 +123,10 @@ export function resolveClaudePath(opts = {}) {
|
|
|
121
123
|
log(`[claude-path] resolved claude ${candidate} (version ${detected})`);
|
|
122
124
|
}
|
|
123
125
|
reportVersionDrift(detected, PROVENANCE_CLAUDE_VERSION, log);
|
|
126
|
+
// Story 058 R3.2 — version-aware @image vision smoke: warn ONCE (one-liner) when this claude
|
|
127
|
+
// is NOT a version confirmed to vision-encode @image via Read. Observability only — it never
|
|
128
|
+
// blocks and never touches promptCapabilities.image (stays `image: true`, R3.1).
|
|
129
|
+
reportImageVisionSmoke(detected, log);
|
|
124
130
|
}
|
|
125
131
|
catch {
|
|
126
132
|
// Observability is best-effort; never let it break resolution.
|
package/dist/end-of-turn.d.ts
CHANGED
|
@@ -144,6 +144,12 @@ export interface TurnResolverOptions {
|
|
|
144
144
|
logger?: StopReasonLogger;
|
|
145
145
|
deltaTMs?: number;
|
|
146
146
|
watchdogMs?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Story 056 (#812) — fired EXACTLY ONCE, ONLY on a real end-of-turn boundary (NOT on cancel, NOT
|
|
149
|
+
* on watchdog timeout), AFTER the prompt resolves. Fire-and-forget; the callback MUST NOT
|
|
150
|
+
* throw/block.
|
|
151
|
+
*/
|
|
152
|
+
onTurnResolved?: () => void;
|
|
147
153
|
}
|
|
148
154
|
/** A detector paired with the awaitable {@link PromptResponse} the prompt() loop returns. */
|
|
149
155
|
export interface TurnResolver {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"end-of-turn.d.ts","sourceRoot":"","sources":["../src/end-of-turn.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"end-of-turn.d.ts","sourceRoot":"","sources":["../src/end-of-turn.ts"],"names":[],"mappings":"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"}
|
package/dist/end-of-turn.js
CHANGED
|
@@ -394,7 +394,14 @@ export function createTurnResolver(opts = {}) {
|
|
|
394
394
|
action();
|
|
395
395
|
}
|
|
396
396
|
const detector = createEndOfTurnDetector({
|
|
397
|
-
onEndOfTurn: (boundary) =>
|
|
397
|
+
onEndOfTurn: (boundary) =>
|
|
398
|
+
// Story 056 (#812): fire onTurnResolved inside the SAME settle, AFTER the prompt resolves, so it
|
|
399
|
+
// runs EXACTLY ONCE per real end-of-turn. cancel() and onTurnTimeout intentionally do NOT call
|
|
400
|
+
// it — the settle latch + the detector's firedUuids guarantee a single call per turn.
|
|
401
|
+
settle(() => {
|
|
402
|
+
resolveFn({ stopReason: mapStopReason(readStopReason(boundary), opts.logger) });
|
|
403
|
+
opts.onTurnResolved?.();
|
|
404
|
+
}),
|
|
398
405
|
onTurnTimeout: (error) => settle(() => rejectFn(error)),
|
|
399
406
|
schedule: opts.schedule,
|
|
400
407
|
sessionId: opts.sessionId,
|
|
@@ -147,8 +147,27 @@ export declare class SessionEngine {
|
|
|
147
147
|
* launch AND the `|| claude` fresh fallback as `--permission-mode <mode>`, so an in-place re-spawn
|
|
148
148
|
* (a dontAsk/bypass switch) reattaches the SAME sessionId/transcript under the new mode. Still no
|
|
149
149
|
* `-p`/`--print`/`stream-json` — billing stays subscription `cli`.
|
|
150
|
+
*
|
|
151
|
+
* Story 056 (R3.3): an optional `agent` persona name is likewise carried via `flags` as the
|
|
152
|
+
* DOUBLE-QUOTED `--agent "<name>"`. Because `flags` is interpolated into BOTH the `--resume "<id>"`
|
|
153
|
+
* launch AND the `|| claude` fresh fallback, this single addition reaches both branches (R3.3). It is
|
|
154
|
+
* the SECOND layer of the command-injection defense — re-asserted via {@link isSafeAgentRef} (which
|
|
155
|
+
* accepts a namespaced `plugin:name`) so an unsafe name is DROPPED (no flag), never interpolated.
|
|
156
|
+
* Still no `-p`/`--print`/`stream-json`.
|
|
157
|
+
*
|
|
158
|
+
* Story 057 (R1.1/R1.2): an optional `additionalDirectories` list is folded into the SAME `flags`
|
|
159
|
+
* string as ONE double-quoted `--add-dir "<dir>"` per dir — so, like the agent flag, a single addition
|
|
160
|
+
* reaches BOTH the `--resume "<id>"` half AND the `|| claude` fallback half. Each dir is re-asserted via
|
|
161
|
+
* {@link isSafeDir} (absolute + existing + no shell metacharacter); an unsafe dir is DROPPED (logged),
|
|
162
|
+
* never interpolated. Still no `-p`/`--print`/`stream-json`.
|
|
163
|
+
*
|
|
164
|
+
* Story 057 (R2.2): an optional `mcpConfigFile` PATH is likewise folded into the SAME `flags` string as
|
|
165
|
+
* the DOUBLE-QUOTED `--mcp-config "<file>"` (a file path — the secret-bearing JSON stays off the command
|
|
166
|
+
* line), so this single addition reaches BOTH the `--resume "<id>"` launch AND the `|| claude` fresh
|
|
167
|
+
* fallback. R2.2 HARD rule: `--strict-mcp-config` is NEVER emitted, so a resumed turn keeps MERGING the
|
|
168
|
+
* scratch with the user's own `.mcp.json`/settings MCP servers. Still no `-p`/`--print`/`stream-json`.
|
|
150
169
|
*/
|
|
151
|
-
export declare function buildResumeArgv(sessionId: string, permissionMode?: string, effortLevel?: string): [string, string];
|
|
170
|
+
export declare function buildResumeArgv(sessionId: string, permissionMode?: string, effortLevel?: string, agent?: string, additionalDirectories?: string[], mcpConfigFile?: string): [string, string];
|
|
152
171
|
/** Options for {@link spawnResumePty}. */
|
|
153
172
|
export interface SpawnResumeOptions {
|
|
154
173
|
/** The prior session id to reattach to (== the JSONL transcript basename). */
|
|
@@ -166,6 +185,27 @@ export interface SpawnResumeOptions {
|
|
|
166
185
|
permissionMode?: string;
|
|
167
186
|
/** Story 046 (R2.2): carry `--effort <level>` into the resume argv for an effort re-spawn. */
|
|
168
187
|
effortLevel?: string;
|
|
188
|
+
/**
|
|
189
|
+
* Story 056 (R3.3): carry the DOUBLE-QUOTED `--agent "<name>"` into the resume argv (both the
|
|
190
|
+
* `--resume` and the `|| claude` fallback branches). Injection-safe — re-asserted against the R3.3
|
|
191
|
+
* allowlist so an unsafe name is dropped. Absent → no flag.
|
|
192
|
+
*/
|
|
193
|
+
agent?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Story 057 (R1.1/R1.2): carry ONE double-quoted `--add-dir "<dir>"` per dir into the resume argv —
|
|
196
|
+
* folded into the shared `flags`, so the dirs reach BOTH the `--resume` and the `|| claude` fallback
|
|
197
|
+
* branches. Injection-safe — each dir re-asserted via {@link isSafeDir} so an unsafe dir is dropped.
|
|
198
|
+
* INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent/empty → no flag.
|
|
199
|
+
*/
|
|
200
|
+
additionalDirectories?: string[];
|
|
201
|
+
/**
|
|
202
|
+
* Story 057 (R2.2): carry the DOUBLE-QUOTED `--mcp-config "<file>"` into the resume argv — folded into
|
|
203
|
+
* the shared `flags`, so the scratch-config path reaches BOTH the `--resume` and the `|| claude`
|
|
204
|
+
* fallback branches. NEVER paired with `--strict-mcp-config`, so the resumed turn keeps MERGING the
|
|
205
|
+
* scratch with the user's own `.mcp.json`/settings MCP servers (R2.2). A file path keeps the JSON off
|
|
206
|
+
* the command line. INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent → no flag.
|
|
207
|
+
*/
|
|
208
|
+
mcpConfigFile?: string;
|
|
169
209
|
}
|
|
170
210
|
/**
|
|
171
211
|
* Spawn the resume PTY, reusing story 013's sanitized env (R4.2) so the resumed turn keeps the
|
|
@@ -180,6 +220,12 @@ export declare function spawnResumePty(opts: SpawnResumeOptions): PtyEngineHandl
|
|
|
180
220
|
export interface CreateSessionEngineOptions {
|
|
181
221
|
/** Host working directory the spawned TUI runs in (story 013 spawn). */
|
|
182
222
|
cwd: string;
|
|
223
|
+
/**
|
|
224
|
+
* Story 056 v4 — OPTIONAL pre-chosen session id for an in-place FRESH re-spawn (a pre-interaction
|
|
225
|
+
* selector change reuses the session's id). Forwarded to {@link spawnClaudePty}; absent → a fresh
|
|
226
|
+
* `randomUUID()` (the normal createSession path).
|
|
227
|
+
*/
|
|
228
|
+
sessionId?: string;
|
|
183
229
|
/** Base environment to sanitize; defaults to the parent process env. */
|
|
184
230
|
baseEnv?: Record<string, string | undefined>;
|
|
185
231
|
/** Injectable spawn function (defaults to node-pty's `spawn`); tests pass a fake. */
|
|
@@ -191,6 +237,25 @@ export interface CreateSessionEngineOptions {
|
|
|
191
237
|
permissionMode?: string;
|
|
192
238
|
/** Story 046 (R2.2): forwarded to {@link spawnClaudePty} as `--effort <level>` for a non-"default" seed. */
|
|
193
239
|
effortLevel?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Story 056 (R3.3): forwarded to {@link spawnClaudePty} as the DOUBLE-QUOTED `--agent "<name>"` for a
|
|
242
|
+
* fresh spawn seeded to a main-thread agent persona. Injection-safe (R3.3 allowlist re-assert).
|
|
243
|
+
* Absent → no flag.
|
|
244
|
+
*/
|
|
245
|
+
agent?: string;
|
|
246
|
+
/**
|
|
247
|
+
* Story 057 (R1.1/R1.2): forwarded to {@link spawnClaudePty} as ONE double-quoted `--add-dir "<dir>"`
|
|
248
|
+
* per safe dir for a fresh spawn. Injection-safe (each dir re-asserted via {@link isSafeDir}).
|
|
249
|
+
* INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent/empty → no flag.
|
|
250
|
+
*/
|
|
251
|
+
additionalDirectories?: string[];
|
|
252
|
+
/**
|
|
253
|
+
* Story 057 (R2.2): forwarded to {@link spawnClaudePty} as the DOUBLE-QUOTED `--mcp-config "<file>"`
|
|
254
|
+
* for a fresh spawn (the fork-controlled scratch path from `mcp-config-writer.ts`). NEVER paired with
|
|
255
|
+
* `--strict-mcp-config`, so claude MERGES the scratch with the user's own `.mcp.json`/settings MCP
|
|
256
|
+
* servers (R2.2). INTERACTIVE-ONLY: adds no `-p`/`--print`/`stream-json`. Absent → no flag.
|
|
257
|
+
*/
|
|
258
|
+
mcpConfigFile?: string;
|
|
194
259
|
/**
|
|
195
260
|
* Story 034 (§9 hybrid gate): per-session SCRATCH settings file carrying the fork's
|
|
196
261
|
* `PreToolUse` hook, forwarded to {@link spawnClaudePty} as `--settings "<file>"`. The
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-lifecycle.d.ts","sourceRoot":"","sources":["../src/engine-lifecycle.ts"],"names":[],"mappings":"AAuBA,OAAO,GAAG,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"engine-lifecycle.d.ts","sourceRoot":"","sources":["../src/engine-lifecycle.ts"],"names":[],"mappings":"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"}
|