zidane 5.14.4 → 6.0.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-24uU2WDs.js → acp-Irby04Us.js} +7 -8
- package/dist/{acp-24uU2WDs.js.map → acp-Irby04Us.js.map} +1 -1
- package/dist/acp-cli.js +7 -7
- package/dist/acp.d.ts +2 -3
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +1 -1
- package/dist/{agent-Cch2ayTt.js → agent-DkSmGkDM.js} +10871 -5411
- package/dist/agent-DkSmGkDM.js.map +1 -0
- package/dist/agent.d.ts +1 -2
- package/dist/agent.js +2 -2
- package/dist/{anthropic-RGqsJlC8.js → anthropic-CTHsdFG9.js} +46 -6
- package/dist/anthropic-CTHsdFG9.js.map +1 -0
- package/dist/{auth-C-ms5N2x.js → auth-BDSu_0t3.js} +13 -86
- package/dist/auth-BDSu_0t3.js.map +1 -0
- package/dist/chat/pure.d.ts +4 -4
- package/dist/chat/pure.js +4 -3
- package/dist/chat.d.ts +45 -8
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +9 -6
- package/dist/chat.js.map +1 -1
- package/dist/completion-core-CXYSVhZo.js +147 -0
- package/dist/completion-core-CXYSVhZo.js.map +1 -0
- package/dist/{context-breakdown-kO-pDsay.js → context-breakdown-Dzo25N45.js} +2 -23
- package/dist/context-breakdown-Dzo25N45.js.map +1 -0
- package/dist/contexts/daytona.d.ts +3 -3
- package/dist/contexts/daytona.js +1 -1
- package/dist/contexts/docker.d.ts +10 -2
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +142 -103
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +2 -2
- package/dist/contexts/e2b.js +1 -1
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +4 -1
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-Lzf5huID.js → contexts-Vone3qQQ.js} +26 -37
- package/dist/contexts-Vone3qQQ.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/errors.d.ts +1 -2
- package/dist/errors.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +3 -3
- package/dist/exec-file-size-PXkr-MGA.js +63 -0
- package/dist/exec-file-size-PXkr-MGA.js.map +1 -0
- package/dist/extensions.d.ts +2 -0
- package/dist/extensions.js +2 -0
- package/dist/generate-data-WL_yGVL6.js +324 -0
- package/dist/generate-data-WL_yGVL6.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +15 -10
- package/dist/headless.js.map +1 -1
- package/dist/index-B8UtiZy7.d.ts +14967 -0
- package/dist/index-B8UtiZy7.d.ts.map +1 -0
- package/dist/{index-B3ciFUZx.d.ts → index-DsPQbyUn.d.ts} +2 -2
- package/dist/index-DsPQbyUn.d.ts.map +1 -0
- package/dist/index.d.ts +6 -12
- package/dist/index.js +20 -43
- package/dist/index.js.map +1 -1
- package/dist/{logger-Ktm-lj1s.js → logger-DBX9uYOw.js} +108 -5
- package/dist/logger-DBX9uYOw.js.map +1 -0
- package/dist/login-DOF8pMVM.js +539 -0
- package/dist/login-DOF8pMVM.js.map +1 -0
- package/dist/{mcp-Dn5W65Lv.js → mcp-CcvuVXB9.js} +2 -2
- package/dist/{mcp-Dn5W65Lv.js.map → mcp-CcvuVXB9.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-YZQLKaz2.js → messages-CwujiS74.js} +4 -4
- package/dist/messages-CwujiS74.js.map +1 -0
- package/dist/{openai-compat-0Y7jcncn.js → openai-compat-BvgYGSR1.js} +7 -6
- package/dist/{openai-compat-0Y7jcncn.js.map → openai-compat-BvgYGSR1.js.map} +1 -1
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/{glob-DluQFSw1.js → output-cap-kBUHBlnq.js} +50 -2
- package/dist/output-cap-kBUHBlnq.js.map +1 -0
- package/dist/presets.d.ts +1 -2
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +4 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-DgmdYGKm.js → providers-z6LDMAr3.js} +3 -3
- package/dist/{providers-DgmdYGKm.js.map → providers-z6LDMAr3.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/{read-state-BymF41Jb.js → read-state-Dq_TXUX1.js} +2 -2
- package/dist/{read-state-BymF41Jb.js.map → read-state-Dq_TXUX1.js.map} +1 -1
- package/dist/{interpolate-BtIgcCuz.js → resolve-Cr8JSL_O.js} +562 -136
- package/dist/resolve-Cr8JSL_O.js.map +1 -0
- package/dist/restate.d.ts +2 -2
- package/dist/restate.js +2 -2
- package/dist/{sandbox-Dgy-m6UF.d.ts → sandbox-CgjG2VvQ.d.ts} +2 -2
- package/dist/{sandbox-Dgy-m6UF.d.ts.map → sandbox-CgjG2VvQ.d.ts.map} +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-CqrAFAKJ.js → session-CsdDIPY8.js} +108 -3
- package/dist/session-CsdDIPY8.js.map +1 -0
- package/dist/session.d.ts +2 -2
- package/dist/session.js +3 -3
- package/dist/skills-Bsb0rvWI.js +295 -0
- package/dist/skills-Bsb0rvWI.js.map +1 -0
- package/dist/skills.d.ts +2 -3
- package/dist/skills.js +3 -24
- package/dist/{errors-CkTvg97v.d.ts → timeout-8vSIRjzl.d.ts} +10 -2
- package/dist/timeout-8vSIRjzl.d.ts.map +1 -0
- package/dist/{timeout-kGGSXagK.js → timeout-BDetbuN6.js} +59 -2
- package/dist/timeout-BDetbuN6.js.map +1 -0
- package/dist/tool-formatters-DY0TDb1r.d.ts +378 -0
- package/dist/tool-formatters-DY0TDb1r.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +2 -3
- package/dist/tools.js +3 -4
- package/dist/{transcript-anchors-BHIgMlPq.d.ts → transcript-anchors-Cb-bDbNH.d.ts} +233 -640
- package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +1 -0
- package/dist/{transcript-anchors-DsoBHg43.js → transcript-anchors-DSRPWjiH.js} +391 -961
- package/dist/transcript-anchors-DSRPWjiH.js.map +1 -0
- package/dist/tui.d.ts +335 -11
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +6375 -1892
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-Dw02ZoSQ.d.ts → turn-operations-CgMROLsr.d.ts} +29 -4
- package/dist/turn-operations-CgMROLsr.d.ts.map +1 -0
- package/dist/{turn-operations-tG0vuBNc.js → turn-operations-Dq9Kx7m2.js} +321 -81
- package/dist/turn-operations-Dq9Kx7m2.js.map +1 -0
- package/dist/{types-BqdTeBaC.d.ts → types-BDccavwj.d.ts} +42 -1
- package/dist/types-BDccavwj.d.ts.map +1 -0
- package/dist/{types-CyVGdbia.js → types-BIarq1qE.js} +90 -2
- package/dist/types-BIarq1qE.js.map +1 -0
- package/dist/types.d.ts +5 -6
- package/dist/types.js +1 -1
- package/dist/{xai-tPdbAGkq.js → xai-B_e6TOA8.js} +2 -2
- package/dist/{xai-tPdbAGkq.js.map → xai-B_e6TOA8.js.map} +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHAT.md +7 -6
- package/docs/EXECUTION_CONTEXT.md +16 -0
- package/docs/EXTENSIONS.md +1063 -0
- package/docs/SKILL.md +16 -0
- package/docs/TUI.md +10 -3
- package/package.json +12 -3
- package/dist/agent-Cch2ayTt.js.map +0 -1
- package/dist/agent-EFWt6uQ_.d.ts +0 -6914
- package/dist/agent-EFWt6uQ_.d.ts.map +0 -1
- package/dist/anthropic-RGqsJlC8.js.map +0 -1
- package/dist/auth-C-ms5N2x.js.map +0 -1
- package/dist/context-breakdown-kO-pDsay.js.map +0 -1
- package/dist/contexts-Lzf5huID.js.map +0 -1
- package/dist/errors-CkTvg97v.d.ts.map +0 -1
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/glob-shell-rJMoCIGb.js +0 -21
- package/dist/glob-shell-rJMoCIGb.js.map +0 -1
- package/dist/index-B3ciFUZx.d.ts.map +0 -1
- package/dist/index-BtzE3Uaq.d.ts +0 -2738
- package/dist/index-BtzE3Uaq.d.ts.map +0 -1
- package/dist/index-tSzOaWNt.d.ts +0 -316
- package/dist/index-tSzOaWNt.d.ts.map +0 -1
- package/dist/interpolate-BtIgcCuz.js.map +0 -1
- package/dist/logger-DGiGf7DW.d.ts +0 -102
- package/dist/logger-DGiGf7DW.d.ts.map +0 -1
- package/dist/logger-Ktm-lj1s.js.map +0 -1
- package/dist/login-CCA-1lgK.js +0 -267
- package/dist/login-CCA-1lgK.js.map +0 -1
- package/dist/messages-YZQLKaz2.js.map +0 -1
- package/dist/policy-DcGlpaNs.d.ts +0 -129
- package/dist/policy-DcGlpaNs.d.ts.map +0 -1
- package/dist/presets-BQWYMHdd.js +0 -114
- package/dist/presets-BQWYMHdd.js.map +0 -1
- package/dist/run-summary-DkN8KsHM.d.ts +0 -249
- package/dist/run-summary-DkN8KsHM.d.ts.map +0 -1
- package/dist/session-CqrAFAKJ.js.map +0 -1
- package/dist/shell-quote-BmnhZmdM.js +0 -33
- package/dist/shell-quote-BmnhZmdM.js.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/timeout-CpFm0jJ6.d.ts +0 -10
- package/dist/timeout-CpFm0jJ6.d.ts.map +0 -1
- package/dist/timeout-kGGSXagK.js.map +0 -1
- package/dist/tool-formatters-C6iLgc9f.d.ts +0 -1454
- package/dist/tool-formatters-C6iLgc9f.d.ts.map +0 -1
- package/dist/tools-CCd8e-HT.js +0 -1739
- package/dist/tools-CCd8e-HT.js.map +0 -1
- package/dist/transcript-anchors-BHIgMlPq.d.ts.map +0 -1
- package/dist/transcript-anchors-DsoBHg43.js.map +0 -1
- package/dist/turn-operations-Dw02ZoSQ.d.ts.map +0 -1
- package/dist/turn-operations-tG0vuBNc.js.map +0 -1
- package/dist/types-BqdTeBaC.d.ts.map +0 -1
- package/dist/types-CyVGdbia.js.map +0 -1
|
@@ -1,63 +1,115 @@
|
|
|
1
1
|
import { a as AgentToolNotAllowedError, l as errorMessage } from "./errors-DJUxZg9b.js";
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
5
6
|
import { homedir, tmpdir } from "node:os";
|
|
6
|
-
//#region src/
|
|
7
|
+
//#region src/chat/project-root.ts
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Git root detection — walks parents from `cwd` looking for a `.git`
|
|
10
|
+
* entry, returning the absolute path of the repo root or `null` when
|
|
11
|
+
* the search reaches the filesystem root.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Why parent-walk over `git rev-parse --show-toplevel`:
|
|
14
|
+
*
|
|
15
|
+
* - Zero dependencies (no shell-out, no git binary requirement,
|
|
16
|
+
* works even when `git` is missing from `PATH`).
|
|
17
|
+
* - Pure-sync, deterministic, easy to test against tmp dirs.
|
|
18
|
+
* - Equally fast in practice — most lookups are a single `existsSync`.
|
|
19
|
+
*
|
|
20
|
+
* Recognizes `.git` as either:
|
|
21
|
+
*
|
|
22
|
+
* - A directory (standard repo layout).
|
|
23
|
+
* - A file containing `gitdir: …` (worktrees + submodules).
|
|
24
|
+
*
|
|
25
|
+
* Bare repos (no working tree) are intentionally NOT detected here —
|
|
26
|
+
* the TUI's data dir contract is "project working tree", and a bare
|
|
27
|
+
* repo has no working files to scope sessions against.
|
|
16
28
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Walk parents of `cwd` looking for a `.git` entry. Returns the
|
|
31
|
+
* absolute path of the directory containing `.git`, or `null` when no
|
|
32
|
+
* git repo is found above `cwd`.
|
|
33
|
+
*
|
|
34
|
+
* Stops at the filesystem root (`/`) — no infinite loop on `dirname`'s
|
|
35
|
+
* idempotent root case (`dirname('/')` returns `/`).
|
|
36
|
+
*/
|
|
37
|
+
function findGitRoot(cwd = process.cwd()) {
|
|
38
|
+
let dir = resolve(cwd);
|
|
39
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
40
|
+
if (hasGitMarker(dir)) return dir;
|
|
41
|
+
const parent = dirname(dir);
|
|
42
|
+
if (parent === dir) return null;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `.git` exists as a directory (standard repo) OR as a file containing
|
|
49
|
+
* a `gitdir:` pointer (worktree / submodule). Tolerant on stat failures
|
|
50
|
+
* — a permission error on a parent directory shouldn't crash the walk.
|
|
51
|
+
*/
|
|
52
|
+
function hasGitMarker(dir) {
|
|
53
|
+
const candidate = resolve(dir, ".git");
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(candidate)) return false;
|
|
56
|
+
const stat = statSync(candidate);
|
|
57
|
+
return stat.isDirectory() || stat.isFile();
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
23
61
|
}
|
|
24
62
|
//#endregion
|
|
25
|
-
//#region src/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
//#region src/chat/project-user-paths.ts
|
|
64
|
+
/**
|
|
65
|
+
* Shared search-path builder for project + user config discovery.
|
|
66
|
+
*
|
|
67
|
+
* Both Skills (`skills/<name>/SKILL.md`) and MCP servers (`mcps.json`)
|
|
68
|
+
* follow the same first-found-wins order:
|
|
69
|
+
*
|
|
70
|
+
* 1. `{project-root}/.agents/<sub>` — project, agnostic convention
|
|
71
|
+
* 2. `{project-root}/.{prefix}/<sub>` — project, zidane (`.zidane` default)
|
|
72
|
+
* 3. `~/.agents/<sub>` — user, agnostic
|
|
73
|
+
* 4. `~/.{prefix}/<sub>` — user, zidane
|
|
74
|
+
*
|
|
75
|
+
* `{project-root}` is the git root walked up from `cwd` when present,
|
|
76
|
+
* else `cwd` itself. This pairs with the project-scoped session
|
|
77
|
+
* storage (`resolveStoragePaths`) so all three project-aware surfaces
|
|
78
|
+
* — sessions, mcps, skills — anchor at the same `.{prefix}/`.
|
|
79
|
+
*
|
|
80
|
+
* Centralizing the pattern keeps the four paths in one place — a third
|
|
81
|
+
* discovery surface (hooks, rules, …) gets the same convention for free
|
|
82
|
+
* and stays in lock-step on a prefix bump.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the four search paths for a given subdirectory (`skills`,
|
|
86
|
+
* `mcps.json`, …). Returns absolute paths regardless of whether they
|
|
87
|
+
* exist — the caller chooses to `existsSync`-filter or feed straight
|
|
88
|
+
* into a tolerant discovery routine.
|
|
89
|
+
*/
|
|
90
|
+
function projectUserPaths(opts) {
|
|
91
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
92
|
+
const home = opts.home ?? homedir();
|
|
93
|
+
const projectRoot = findGitRoot(cwd) ?? cwd;
|
|
94
|
+
const prefix = (opts.prefix ?? ".zidane").replace(/^\./, "");
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
path: resolve(projectRoot, `.agents/${opts.subPath}`),
|
|
98
|
+
source: "project"
|
|
38
99
|
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
byName.set(skill.name, {
|
|
43
|
-
skill,
|
|
44
|
-
activatedAt: Date.now(),
|
|
45
|
-
activatedVia: via
|
|
46
|
-
});
|
|
47
|
-
return "ok";
|
|
100
|
+
{
|
|
101
|
+
path: resolve(projectRoot, `.${prefix}/${opts.subPath}`),
|
|
102
|
+
source: "project"
|
|
48
103
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
byName.delete(name);
|
|
53
|
-
return existing;
|
|
104
|
+
{
|
|
105
|
+
path: resolve(home, `.agents/${opts.subPath}`),
|
|
106
|
+
source: "user"
|
|
54
107
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return snapshot;
|
|
108
|
+
{
|
|
109
|
+
path: resolve(home, `.${prefix}/${opts.subPath}`),
|
|
110
|
+
source: "user"
|
|
59
111
|
}
|
|
60
|
-
|
|
112
|
+
];
|
|
61
113
|
}
|
|
62
114
|
//#endregion
|
|
63
115
|
//#region src/skills/validate.ts
|
|
@@ -274,6 +326,466 @@ function isToolAllowedByUnion(displayName, input, union) {
|
|
|
274
326
|
return union.some((pattern) => matchesAllowedTool(displayName, input, pattern));
|
|
275
327
|
}
|
|
276
328
|
//#endregion
|
|
329
|
+
//#region src/skills/interpolate.ts
|
|
330
|
+
/** Regex to match !`command` patterns in skill instructions */
|
|
331
|
+
const SHELL_INTERPOLATION_RE = /!`([^`]+)`/g;
|
|
332
|
+
/**
|
|
333
|
+
* Replace every !\`command\` pattern with a static placeholder, without
|
|
334
|
+
* executing anything. Used when shell interpolation is disabled
|
|
335
|
+
* (`SkillsConfig.allowShellInterpolation: false`): skill content can be
|
|
336
|
+
* third-party input, and running its embedded commands on activation
|
|
337
|
+
* bypasses the allowed-tools gate.
|
|
338
|
+
*/
|
|
339
|
+
function stripShellInterpolations(instructions) {
|
|
340
|
+
return instructions.replace(SHELL_INTERPOLATION_RE, "[shell interpolation disabled]");
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Interpolate shell commands in skill instructions.
|
|
344
|
+
*
|
|
345
|
+
* Runs each !\`command\` through the execution context and replaces
|
|
346
|
+
* the placeholder with the command's stdout. If a command fails,
|
|
347
|
+
* the placeholder is replaced with an error message (and
|
|
348
|
+
* `options.onFailure` is notified when provided).
|
|
349
|
+
*
|
|
350
|
+
* @param instructions - Raw skill instructions with potential !\`command\` patterns
|
|
351
|
+
* @param execution - The execution context to run commands in
|
|
352
|
+
* @param handle - The active execution handle
|
|
353
|
+
* @returns Instructions with all !\`command\` patterns replaced by output
|
|
354
|
+
*/
|
|
355
|
+
async function interpolateShellCommands(instructions, execution, handle, options = {}) {
|
|
356
|
+
const matches = [...instructions.matchAll(SHELL_INTERPOLATION_RE)];
|
|
357
|
+
if (matches.length === 0) return instructions;
|
|
358
|
+
const replacements = [];
|
|
359
|
+
for (const match of matches) {
|
|
360
|
+
const command = match[1];
|
|
361
|
+
const index = match.index;
|
|
362
|
+
const length = match[0].length;
|
|
363
|
+
if (options.approve) {
|
|
364
|
+
let approved = false;
|
|
365
|
+
try {
|
|
366
|
+
approved = await options.approve(command);
|
|
367
|
+
} catch {
|
|
368
|
+
approved = false;
|
|
369
|
+
}
|
|
370
|
+
if (!approved) {
|
|
371
|
+
const placeholder = "[shell interpolation not approved]";
|
|
372
|
+
options.onFailure?.(command, placeholder);
|
|
373
|
+
replacements.push({
|
|
374
|
+
index,
|
|
375
|
+
length,
|
|
376
|
+
output: placeholder
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const result = await execution.exec(handle, command, { timeout: 30 });
|
|
383
|
+
if (result.exitCode === 0) replacements.push({
|
|
384
|
+
index,
|
|
385
|
+
length,
|
|
386
|
+
output: result.stdout.trim()
|
|
387
|
+
});
|
|
388
|
+
else {
|
|
389
|
+
const placeholder = `[command failed (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}]`;
|
|
390
|
+
options.onFailure?.(command, placeholder);
|
|
391
|
+
replacements.push({
|
|
392
|
+
index,
|
|
393
|
+
length,
|
|
394
|
+
output: placeholder
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const placeholder = `[command error: ${errorMessage(err)}]`;
|
|
399
|
+
options.onFailure?.(command, placeholder);
|
|
400
|
+
replacements.push({
|
|
401
|
+
index,
|
|
402
|
+
length,
|
|
403
|
+
output: placeholder
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let result = instructions;
|
|
408
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
409
|
+
const { index, length, output } = replacements[i];
|
|
410
|
+
result = result.slice(0, index) + output + result.slice(index + length);
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/xml.ts
|
|
416
|
+
/**
|
|
417
|
+
* Minimal XML helpers used by catalog/result builders that emit
|
|
418
|
+
* model-facing pseudo-XML (skills catalog, searchable_tools, tool_search
|
|
419
|
+
* results, skill_content wrappers).
|
|
420
|
+
*
|
|
421
|
+
* Scope: attribute / text-node escaping only — we do NOT emit a full XML
|
|
422
|
+
* document, the model sees these strings as free-form text. Centralised
|
|
423
|
+
* here so the four near-identical copies in `agent.ts`, `skills/catalog.ts`,
|
|
424
|
+
* `tools/skills-use.ts`, and `tools/tool-search.ts` don't drift.
|
|
425
|
+
*/
|
|
426
|
+
const RE_AMP = /&/g;
|
|
427
|
+
const RE_LT = /</g;
|
|
428
|
+
const RE_GT = />/g;
|
|
429
|
+
const RE_QUOT = /"/g;
|
|
430
|
+
function escapeXml(str) {
|
|
431
|
+
return str.replace(RE_AMP, "&").replace(RE_LT, "<").replace(RE_GT, ">").replace(RE_QUOT, """);
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/install/source.ts
|
|
435
|
+
/**
|
|
436
|
+
* Shared install-source layer — the source-agnostic half of "install a
|
|
437
|
+
* thing from github/npm". Extracted from the extensions installer so
|
|
438
|
+
* SKILL installs (and any future installable artifact) reuse the exact
|
|
439
|
+
* same spec grammar, fetch pipeline, and hardening instead of forking
|
|
440
|
+
* them:
|
|
441
|
+
*
|
|
442
|
+
* - Spec grammar: `owner/repo[#ref][:subdir]`, `github:owner/repo`,
|
|
443
|
+
* pasted GitHub URLs (including `/tree/<ref>/<path>`),
|
|
444
|
+
* `pkg[@version]`, `@scope/pkg[@version]`.
|
|
445
|
+
* - Fetch: `git clone --depth 1` (records the exact commit) with a
|
|
446
|
+
* codeload-tarball fallback, npm registry resolve (exact versions +
|
|
447
|
+
* dist-tags) + tarball download. Downloads are double-capped at
|
|
448
|
+
* {@link MAX_DOWNLOAD_BYTES} (content-length header AND post-hoc for
|
|
449
|
+
* chunked responses). Extraction requires `tar` on PATH.
|
|
450
|
+
* - All side effects go through the injectable {@link InstallIo} seams
|
|
451
|
+
* so tests stay offline and hosts can instrument.
|
|
452
|
+
*
|
|
453
|
+
* Flavor-specific concerns stay OUT of this module: candidate
|
|
454
|
+
* enumeration, validation, target roots, provenance files, dependency
|
|
455
|
+
* installs. Flavors subclass {@link InstallSourceError} to widen the
|
|
456
|
+
* error-code union while keeping `.code`-based handling uniform.
|
|
457
|
+
*/
|
|
458
|
+
/**
|
|
459
|
+
* Base error for install pipelines. `code` is a plain string here so
|
|
460
|
+
* flavors (extensions, skills) can widen it with their own codes while
|
|
461
|
+
* callers keep a single `err.code` switch. The base layer itself only
|
|
462
|
+
* throws `'invalid-source'` and `'fetch-failed'`.
|
|
463
|
+
*/
|
|
464
|
+
var InstallSourceError = class extends Error {
|
|
465
|
+
code;
|
|
466
|
+
/** Set for candidate-selection errors — the names the caller can pick from. */
|
|
467
|
+
candidates;
|
|
468
|
+
constructor(code, message, candidates) {
|
|
469
|
+
super(message);
|
|
470
|
+
this.name = "InstallSourceError";
|
|
471
|
+
this.code = code;
|
|
472
|
+
if (candidates) this.candidates = candidates;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const GITHUB_TREE_URL_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#@:]+?)(?:\.git)?\/tree\/([^/\s:]+)(?:\/([^\s:]+?))?\/?$/;
|
|
476
|
+
const GITHUB_URL_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#@:]+?)(?:\.git)?\/?$/;
|
|
477
|
+
const OWNER_REPO_RE = /^([\w.-]+)\/([\w.-]+)$/;
|
|
478
|
+
const GITHUB_REF_RE = /^[\w./-]+$/;
|
|
479
|
+
const NPM_NAME_RE = /^(@[\w.-]+\/[\w.-]+|[\w.-]+)(?:@([\w.^~><=*-]+))?$/;
|
|
480
|
+
/** Structural validation for a `:subdir` suffix — repo-relative, no escapes. */
|
|
481
|
+
function parseSubdir(rawSubdir, raw) {
|
|
482
|
+
const subdir = rawSubdir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
483
|
+
const segments = subdir.split("/");
|
|
484
|
+
if (subdir.length === 0 || subdir.startsWith("/") || subdir.includes("\\") || segments.some((s) => s.length === 0 || s === "." || s === "..")) throw new InstallSourceError("invalid-source", `"${raw}" has an invalid subdirectory — expected a repo-relative path (no leading /, no ..).`);
|
|
485
|
+
return subdir;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Classify a raw source string:
|
|
489
|
+
*
|
|
490
|
+
* - `github:owner/repo`, `owner/repo`, GitHub URLs (including
|
|
491
|
+
* `/tree/<ref>/<path>` links) → GitHub. Optional `#ref` (or the
|
|
492
|
+
* legacy `@ref`) pins a branch/tag; optional `:path/to/dir` roots the
|
|
493
|
+
* candidate search in a subdirectory: `owner/repo#branch:packages/x`.
|
|
494
|
+
* - `@scope/pkg[@version]`, `pkg[@version]` → npm (no subdir support —
|
|
495
|
+
* flavors expose their own selector for multi-candidate packages).
|
|
496
|
+
*
|
|
497
|
+
* The one ambiguity — `a/b` — is resolved as GitHub: npm's slash names
|
|
498
|
+
* are always scoped (`@scope/…`), so a bare `owner/repo` can only be a
|
|
499
|
+
* repo. Throws `invalid-source` on anything else.
|
|
500
|
+
*/
|
|
501
|
+
function parseInstallSource(raw) {
|
|
502
|
+
const spec = raw.trim();
|
|
503
|
+
if (spec.length === 0) throw new InstallSourceError("invalid-source", "Empty source. Expected `owner/repo`, `github:owner/repo`, a GitHub URL, or an npm package name.");
|
|
504
|
+
const explicitGithub = spec.startsWith("github:");
|
|
505
|
+
const body = explicitGithub ? spec.slice(7) : spec;
|
|
506
|
+
const tree = GITHUB_TREE_URL_RE.exec(body);
|
|
507
|
+
if (tree) return {
|
|
508
|
+
kind: "github",
|
|
509
|
+
owner: tree[1],
|
|
510
|
+
repo: tree[2],
|
|
511
|
+
ref: tree[3],
|
|
512
|
+
...tree[4] ? { subdir: parseSubdir(tree[4], raw) } : {}
|
|
513
|
+
};
|
|
514
|
+
const url = GITHUB_URL_RE.exec(body);
|
|
515
|
+
if (url) return {
|
|
516
|
+
kind: "github",
|
|
517
|
+
owner: url[1],
|
|
518
|
+
repo: url[2]
|
|
519
|
+
};
|
|
520
|
+
if (body.startsWith("@")) {
|
|
521
|
+
if (explicitGithub) throw new InstallSourceError("invalid-source", `"${raw}" mixes the github: prefix with an npm scoped name.`);
|
|
522
|
+
if (body.includes(":")) throw new InstallSourceError("invalid-source", `"${raw}": subdirectories (\`:path\`) are only supported for GitHub sources.`);
|
|
523
|
+
const m = NPM_NAME_RE.exec(body);
|
|
524
|
+
if (!m) throw new InstallSourceError("invalid-source", `"${raw}" is not a valid npm package spec.`);
|
|
525
|
+
return {
|
|
526
|
+
kind: "npm",
|
|
527
|
+
pkg: m[1],
|
|
528
|
+
...m[2] ? { version: m[2] } : {}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (body.includes("/")) {
|
|
532
|
+
const colonIdx = body.indexOf(":");
|
|
533
|
+
const specPart = colonIdx === -1 ? body : body.slice(0, colonIdx);
|
|
534
|
+
const subdir = colonIdx === -1 ? void 0 : parseSubdir(body.slice(colonIdx + 1), raw);
|
|
535
|
+
const hashIdx = specPart.indexOf("#");
|
|
536
|
+
const refIdx = hashIdx >= 0 ? hashIdx : specPart.indexOf("@");
|
|
537
|
+
const repoPart = refIdx === -1 ? specPart : specPart.slice(0, refIdx);
|
|
538
|
+
const ref = refIdx === -1 ? void 0 : specPart.slice(refIdx + 1);
|
|
539
|
+
const m = OWNER_REPO_RE.exec(repoPart);
|
|
540
|
+
if (!m || ref !== void 0 && !GITHUB_REF_RE.test(ref)) throw new InstallSourceError("invalid-source", `"${raw}" is not a valid GitHub repo spec (expected owner/repo, optionally #ref and :subdir).`);
|
|
541
|
+
return {
|
|
542
|
+
kind: "github",
|
|
543
|
+
owner: m[1],
|
|
544
|
+
repo: m[2],
|
|
545
|
+
...ref ? { ref } : {},
|
|
546
|
+
...subdir ? { subdir } : {}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (explicitGithub) throw new InstallSourceError("invalid-source", `"${raw}" is missing the repo part (expected github:owner/repo).`);
|
|
550
|
+
if (body.includes(":")) throw new InstallSourceError("invalid-source", `"${raw}": subdirectories (\`:path\`) are only supported for GitHub sources.`);
|
|
551
|
+
const m = NPM_NAME_RE.exec(body);
|
|
552
|
+
if (!m) throw new InstallSourceError("invalid-source", `"${raw}" is not a valid npm package spec.`);
|
|
553
|
+
return {
|
|
554
|
+
kind: "npm",
|
|
555
|
+
pkg: m[1],
|
|
556
|
+
...m[2] ? { version: m[2] } : {}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Materialize the source into `destDir` (created by the caller, assumed
|
|
561
|
+
* empty). Returns the provenance string plus, for git clones, the exact
|
|
562
|
+
* commit SHA (the tarball fallback can't know it — codeload doesn't
|
|
563
|
+
* expose the resolved commit).
|
|
564
|
+
*/
|
|
565
|
+
async function fetchInstallSource(source, destDir, io = {}, onProgress) {
|
|
566
|
+
const exec = io.exec ?? defaultExec;
|
|
567
|
+
const which = io.which ?? defaultWhich;
|
|
568
|
+
const fetchImpl = io.fetch ?? fetch;
|
|
569
|
+
if (source.kind === "github") {
|
|
570
|
+
const label = `${source.owner}/${source.repo}${source.ref ? `@${source.ref}` : ""}${source.subdir ? `:${source.subdir}` : ""}`;
|
|
571
|
+
if (which("git")) {
|
|
572
|
+
onProgress?.(`cloning github.com/${label}…`);
|
|
573
|
+
const args = [
|
|
574
|
+
"clone",
|
|
575
|
+
"--depth",
|
|
576
|
+
"1",
|
|
577
|
+
"--single-branch"
|
|
578
|
+
];
|
|
579
|
+
if (source.ref) args.push("--branch", source.ref);
|
|
580
|
+
args.push(`https://github.com/${source.owner}/${source.repo}.git`, destDir);
|
|
581
|
+
const res = await exec("git", args, {});
|
|
582
|
+
if (!res.ok) throw new InstallSourceError("fetch-failed", `git clone failed for ${label}: ${lastLine(res.stderr)}`);
|
|
583
|
+
const head = await exec("git", ["rev-parse", "HEAD"], { cwd: destDir });
|
|
584
|
+
const commit = head.ok && /^[0-9a-f]{40}$/.test(head.stdout.trim()) ? head.stdout.trim() : void 0;
|
|
585
|
+
rmSync(join(destDir, ".git"), {
|
|
586
|
+
recursive: true,
|
|
587
|
+
force: true
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
provenance: `github:${label}`,
|
|
591
|
+
...commit ? { commit } : {}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
onProgress?.(`downloading github.com/${label} tarball…`);
|
|
595
|
+
const ref = source.ref ?? "HEAD";
|
|
596
|
+
await downloadAndExtract(`https://codeload.github.com/${source.owner}/${source.repo}/tar.gz/${ref}`, destDir, {
|
|
597
|
+
exec,
|
|
598
|
+
fetchImpl,
|
|
599
|
+
which,
|
|
600
|
+
what: label
|
|
601
|
+
});
|
|
602
|
+
return { provenance: `github:${label}` };
|
|
603
|
+
}
|
|
604
|
+
onProgress?.(`resolving npm:${source.pkg}…`);
|
|
605
|
+
const { version, tarball } = await resolveNpmVersion(source, fetchImpl);
|
|
606
|
+
onProgress?.(`downloading npm:${source.pkg}@${version}…`);
|
|
607
|
+
await downloadAndExtract(tarball, destDir, {
|
|
608
|
+
exec,
|
|
609
|
+
fetchImpl,
|
|
610
|
+
which,
|
|
611
|
+
what: `${source.pkg}@${version}`
|
|
612
|
+
});
|
|
613
|
+
return { provenance: `npm:${source.pkg}@${version}` };
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Registry lookup shared by the fetch path and update no-op checks.
|
|
617
|
+
* Exact versions and dist-tags only — range resolution is out of scope.
|
|
618
|
+
*/
|
|
619
|
+
async function resolveNpmVersion(source, fetchImpl) {
|
|
620
|
+
const registryUrl = `https://registry.npmjs.org/${source.pkg.replace("/", "%2F")}`;
|
|
621
|
+
let meta;
|
|
622
|
+
try {
|
|
623
|
+
const res = await fetchImpl(registryUrl);
|
|
624
|
+
if (!res.ok) throw new Error(`registry returned ${res.status}`);
|
|
625
|
+
meta = await res.json();
|
|
626
|
+
} catch (err) {
|
|
627
|
+
throw new InstallSourceError("fetch-failed", `Could not resolve npm package "${source.pkg}": ${errorText(err)}`);
|
|
628
|
+
}
|
|
629
|
+
const version = source.version && meta.versions?.[source.version] ? source.version : meta["dist-tags"]?.[source.version ?? "latest"];
|
|
630
|
+
const tarball = version ? meta.versions?.[version]?.dist?.tarball : void 0;
|
|
631
|
+
if (!version || !tarball) throw new InstallSourceError("fetch-failed", `npm package "${source.pkg}" has no ${source.version ? `version/tag "${source.version}"` : "latest version"} (exact versions and dist-tags only — ranges are not resolved).`);
|
|
632
|
+
return {
|
|
633
|
+
version,
|
|
634
|
+
tarball
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* OOM guard: tarballs are buffered in memory before extraction, so an
|
|
639
|
+
* unexpectedly huge (or hostile) URL must fail fast instead of ballooning
|
|
640
|
+
* RSS. Install sources are source trees — 64MB is already generous.
|
|
641
|
+
*/
|
|
642
|
+
const MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
|
|
643
|
+
async function downloadAndExtract(url, destDir, opts) {
|
|
644
|
+
let bytes;
|
|
645
|
+
try {
|
|
646
|
+
const res = await opts.fetchImpl(url);
|
|
647
|
+
if (!res.ok) throw new Error(`download returned ${res.status}`);
|
|
648
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
649
|
+
if (declared > 67108864) throw new Error(`archive is ${Math.round(declared / 1024 / 1024)}MB — larger than the ${MAX_DOWNLOAD_BYTES / 1024 / 1024}MB limit`);
|
|
650
|
+
bytes = await res.arrayBuffer();
|
|
651
|
+
if (bytes.byteLength > 67108864) throw new Error(`archive is ${Math.round(bytes.byteLength / 1024 / 1024)}MB — larger than the ${MAX_DOWNLOAD_BYTES / 1024 / 1024}MB limit`);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
throw new InstallSourceError("fetch-failed", `Download failed for ${opts.what}: ${errorText(err)}`);
|
|
654
|
+
}
|
|
655
|
+
if (!opts.which("tar")) throw new InstallSourceError("fetch-failed", `Extracting ${opts.what} requires \`tar\` on PATH (or install git for the clone path).`);
|
|
656
|
+
const tarPath = join(destDir, ".zidane-install.tgz");
|
|
657
|
+
mkdirSync(destDir, { recursive: true });
|
|
658
|
+
writeFileSync(tarPath, new Uint8Array(bytes));
|
|
659
|
+
const res = await opts.exec("tar", [
|
|
660
|
+
"-xzf",
|
|
661
|
+
tarPath,
|
|
662
|
+
"-C",
|
|
663
|
+
destDir,
|
|
664
|
+
"--strip-components=1"
|
|
665
|
+
], {});
|
|
666
|
+
rmSync(tarPath, { force: true });
|
|
667
|
+
if (!res.ok) throw new InstallSourceError("fetch-failed", `Extracting ${opts.what} failed: ${lastLine(res.stderr)}`);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Resolve a `:subdir` inside a fetched tree with PHYSICAL containment:
|
|
671
|
+
* string-level prefix check first, then realpath both sides and re-check
|
|
672
|
+
* so a malicious repo shipping a symlink (`evil -> /`) can't walk the
|
|
673
|
+
* candidate search (and any follow-up validation) onto the local
|
|
674
|
+
* filesystem. Returns the physical directory, or a typed refusal the
|
|
675
|
+
* flavor maps onto its own error vocabulary.
|
|
676
|
+
*/
|
|
677
|
+
function resolveSubdirRoot(fetchedInto, subdir) {
|
|
678
|
+
const resolved = resolve(fetchedInto, subdir);
|
|
679
|
+
if (!resolved.startsWith(fetchedInto + sep)) return {
|
|
680
|
+
ok: false,
|
|
681
|
+
reason: "escape"
|
|
682
|
+
};
|
|
683
|
+
let physical = null;
|
|
684
|
+
try {
|
|
685
|
+
physical = realpathSync(resolved);
|
|
686
|
+
} catch {}
|
|
687
|
+
if (physical === null || !isDirectory(physical)) return {
|
|
688
|
+
ok: false,
|
|
689
|
+
reason: "missing"
|
|
690
|
+
};
|
|
691
|
+
const physicalRoot = realpathSync(fetchedInto);
|
|
692
|
+
if (physical !== physicalRoot && !physical.startsWith(physicalRoot + sep)) return {
|
|
693
|
+
ok: false,
|
|
694
|
+
reason: "escape-symlink"
|
|
695
|
+
};
|
|
696
|
+
return {
|
|
697
|
+
ok: true,
|
|
698
|
+
dir: physical
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function isDirectory(path) {
|
|
702
|
+
try {
|
|
703
|
+
return lstatSync(path).isDirectory();
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function defaultExec(cmd, args, opts) {
|
|
709
|
+
return new Promise((resolvePromise) => {
|
|
710
|
+
execFile(cmd, [...args], {
|
|
711
|
+
cwd: opts.cwd,
|
|
712
|
+
maxBuffer: 16 * 1024 * 1024
|
|
713
|
+
}, (err, stdout, stderr) => {
|
|
714
|
+
resolvePromise({
|
|
715
|
+
ok: !err,
|
|
716
|
+
stdout: String(stdout),
|
|
717
|
+
stderr: String(stderr)
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function defaultWhich(cmd) {
|
|
723
|
+
const exts = process.platform === "win32" ? [
|
|
724
|
+
".exe",
|
|
725
|
+
".cmd",
|
|
726
|
+
".bat",
|
|
727
|
+
""
|
|
728
|
+
] : [""];
|
|
729
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
730
|
+
if (!dir) continue;
|
|
731
|
+
for (const ext of exts) {
|
|
732
|
+
const full = resolve(dir, cmd + ext);
|
|
733
|
+
try {
|
|
734
|
+
if (lstatSync(full).isFile()) return full;
|
|
735
|
+
} catch {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
/** Last non-empty line of a command's output — the useful bit of tool noise. */
|
|
743
|
+
function lastLine(text) {
|
|
744
|
+
const lines = text.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
745
|
+
return lines[lines.length - 1]?.trim() ?? "";
|
|
746
|
+
}
|
|
747
|
+
function errorText(err) {
|
|
748
|
+
return err instanceof Error ? err.message : String(err);
|
|
749
|
+
}
|
|
750
|
+
//#endregion
|
|
751
|
+
//#region src/skills/activation.ts
|
|
752
|
+
function createSkillActivationState(options = {}) {
|
|
753
|
+
const byName = /* @__PURE__ */ new Map();
|
|
754
|
+
const maxActive = typeof options.maxActive === "number" && options.maxActive > 0 ? options.maxActive : void 0;
|
|
755
|
+
return {
|
|
756
|
+
active() {
|
|
757
|
+
return [...byName.values()];
|
|
758
|
+
},
|
|
759
|
+
isActive(name) {
|
|
760
|
+
return byName.has(name);
|
|
761
|
+
},
|
|
762
|
+
get(name) {
|
|
763
|
+
return byName.get(name);
|
|
764
|
+
},
|
|
765
|
+
activate(skill, via) {
|
|
766
|
+
if (byName.has(skill.name)) return "already-active";
|
|
767
|
+
if (maxActive !== void 0 && byName.size >= maxActive) return "cap-reached";
|
|
768
|
+
byName.set(skill.name, {
|
|
769
|
+
skill,
|
|
770
|
+
activatedAt: Date.now(),
|
|
771
|
+
activatedVia: via
|
|
772
|
+
});
|
|
773
|
+
return "ok";
|
|
774
|
+
},
|
|
775
|
+
deactivate(name) {
|
|
776
|
+
const existing = byName.get(name);
|
|
777
|
+
if (!existing) return void 0;
|
|
778
|
+
byName.delete(name);
|
|
779
|
+
return existing;
|
|
780
|
+
},
|
|
781
|
+
clear() {
|
|
782
|
+
const snapshot = [...byName.values()];
|
|
783
|
+
byName.clear();
|
|
784
|
+
return snapshot;
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
277
789
|
//#region src/skills/allowed-tools.ts
|
|
278
790
|
/** Tools that are always allowed regardless of the active skills' allow-list. */
|
|
279
791
|
const IMPLICITLY_ALLOWED_SKILL_TOOLS = [
|
|
@@ -1031,92 +1543,6 @@ async function resolveSkills(config) {
|
|
|
1031
1543
|
};
|
|
1032
1544
|
}
|
|
1033
1545
|
//#endregion
|
|
1034
|
-
|
|
1035
|
-
/** Regex to match !`command` patterns in skill instructions */
|
|
1036
|
-
const SHELL_INTERPOLATION_RE = /!`([^`]+)`/g;
|
|
1037
|
-
/**
|
|
1038
|
-
* Replace every !\`command\` pattern with a static placeholder, without
|
|
1039
|
-
* executing anything. Used when shell interpolation is disabled
|
|
1040
|
-
* (`SkillsConfig.allowShellInterpolation: false`): skill content can be
|
|
1041
|
-
* third-party input, and running its embedded commands on activation
|
|
1042
|
-
* bypasses the allowed-tools gate.
|
|
1043
|
-
*/
|
|
1044
|
-
function stripShellInterpolations(instructions) {
|
|
1045
|
-
return instructions.replace(SHELL_INTERPOLATION_RE, "[shell interpolation disabled]");
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Interpolate shell commands in skill instructions.
|
|
1049
|
-
*
|
|
1050
|
-
* Runs each !\`command\` through the execution context and replaces
|
|
1051
|
-
* the placeholder with the command's stdout. If a command fails,
|
|
1052
|
-
* the placeholder is replaced with an error message (and
|
|
1053
|
-
* `options.onFailure` is notified when provided).
|
|
1054
|
-
*
|
|
1055
|
-
* @param instructions - Raw skill instructions with potential !\`command\` patterns
|
|
1056
|
-
* @param execution - The execution context to run commands in
|
|
1057
|
-
* @param handle - The active execution handle
|
|
1058
|
-
* @returns Instructions with all !\`command\` patterns replaced by output
|
|
1059
|
-
*/
|
|
1060
|
-
async function interpolateShellCommands(instructions, execution, handle, options = {}) {
|
|
1061
|
-
const matches = [...instructions.matchAll(SHELL_INTERPOLATION_RE)];
|
|
1062
|
-
if (matches.length === 0) return instructions;
|
|
1063
|
-
const replacements = [];
|
|
1064
|
-
for (const match of matches) {
|
|
1065
|
-
const command = match[1];
|
|
1066
|
-
const index = match.index;
|
|
1067
|
-
const length = match[0].length;
|
|
1068
|
-
if (options.approve) {
|
|
1069
|
-
let approved = false;
|
|
1070
|
-
try {
|
|
1071
|
-
approved = await options.approve(command);
|
|
1072
|
-
} catch {
|
|
1073
|
-
approved = false;
|
|
1074
|
-
}
|
|
1075
|
-
if (!approved) {
|
|
1076
|
-
const placeholder = "[shell interpolation not approved]";
|
|
1077
|
-
options.onFailure?.(command, placeholder);
|
|
1078
|
-
replacements.push({
|
|
1079
|
-
index,
|
|
1080
|
-
length,
|
|
1081
|
-
output: placeholder
|
|
1082
|
-
});
|
|
1083
|
-
continue;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
const result = await execution.exec(handle, command, { timeout: 30 });
|
|
1088
|
-
if (result.exitCode === 0) replacements.push({
|
|
1089
|
-
index,
|
|
1090
|
-
length,
|
|
1091
|
-
output: result.stdout.trim()
|
|
1092
|
-
});
|
|
1093
|
-
else {
|
|
1094
|
-
const placeholder = `[command failed (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}]`;
|
|
1095
|
-
options.onFailure?.(command, placeholder);
|
|
1096
|
-
replacements.push({
|
|
1097
|
-
index,
|
|
1098
|
-
length,
|
|
1099
|
-
output: placeholder
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
} catch (err) {
|
|
1103
|
-
const placeholder = `[command error: ${errorMessage(err)}]`;
|
|
1104
|
-
options.onFailure?.(command, placeholder);
|
|
1105
|
-
replacements.push({
|
|
1106
|
-
index,
|
|
1107
|
-
length,
|
|
1108
|
-
output: placeholder
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
let result = instructions;
|
|
1113
|
-
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
1114
|
-
const { index, length, output } = replacements[i];
|
|
1115
|
-
result = result.slice(0, index) + output + result.slice(index + length);
|
|
1116
|
-
}
|
|
1117
|
-
return result;
|
|
1118
|
-
}
|
|
1119
|
-
//#endregion
|
|
1120
|
-
export { escapeXml as S, validateResourcePath as _, writeSkillsToDisk as a, validateSkillName as b, inferSource as c, buildCatalog as d, IMPLICITLY_ALLOWED_SKILL_TOOLS as f, parseAllowedToolPattern as g, matchesAllowedTool as h, writeSkillToDisk as i, parseFrontmatter as l, isToolAllowedByUnion as m, stripShellInterpolations as n, discoverSkills as o, installAllowedToolsGate as p, resolveSkills as r, getDefaultScanPaths as s, interpolateShellCommands as t, parseSkillFile as u, validateResourcePathReal as v, createSkillActivationState as x, validateSkillForWrite as y };
|
|
1546
|
+
export { validateSkillName as A, stripShellInterpolations as C, validateResourcePath as D, parseAllowedToolPattern as E, findGitRoot as M, validateResourcePathReal as O, interpolateShellCommands as S, matchesAllowedTool as T, lastLine as _, getDefaultScanPaths as a, resolveSubdirRoot as b, parseSkillFile as c, installAllowedToolsGate as d, createSkillActivationState as f, fetchInstallSource as g, defaultWhich as h, discoverSkills as i, projectUserPaths as j, validateSkillForWrite as k, buildCatalog as l, defaultExec as m, writeSkillToDisk as n, inferSource as o, InstallSourceError as p, writeSkillsToDisk as r, parseFrontmatter as s, resolveSkills as t, IMPLICITLY_ALLOWED_SKILL_TOOLS as u, parseInstallSource as v, isToolAllowedByUnion as w, escapeXml as x, resolveNpmVersion as y };
|
|
1121
1547
|
|
|
1122
|
-
//# sourceMappingURL=
|
|
1548
|
+
//# sourceMappingURL=resolve-Cr8JSL_O.js.map
|