agent-trellis 0.8.0 → 0.9.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/README.md +45 -206
- package/dist/adapters/zcode.d.ts +21 -0
- package/dist/adapters/zcode.js +234 -0
- package/dist/cli.js +119 -10
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -1
- package/dist/commands/mcp.d.ts +56 -14
- package/dist/commands/mcp.js +105 -28
- package/dist/commands/mcpGateway.js +2 -1
- package/dist/commands/mcpImport.d.ts +21 -2
- package/dist/commands/mcpImport.js +30 -7
- package/dist/commands/memory.d.ts +13 -1
- package/dist/commands/memory.js +15 -0
- package/dist/commands/migrate.js +4 -1
- package/dist/commands/onboard.js +15 -4
- package/dist/commands/remoteSkill.d.ts +90 -0
- package/dist/commands/remoteSkill.js +392 -0
- package/dist/commands/rollback.js +55 -1
- package/dist/commands/secretsAudit.js +4 -1
- package/dist/commands/skill.d.ts +37 -6
- package/dist/commands/skill.js +73 -15
- package/dist/commands/sync.js +3 -1
- package/dist/core/adapter.d.ts +11 -1
- package/dist/core/canonical.d.ts +17 -1
- package/dist/core/canonical.js +54 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +1 -0
- package/dist/lib/agentBridge.d.ts +169 -0
- package/dist/lib/agentBridge.js +367 -0
- package/dist/lib/agentBridgeProvider.d.ts +14 -0
- package/dist/lib/agentBridgeProvider.js +95 -0
- package/dist/lib/backup.d.ts +42 -0
- package/dist/lib/backup.js +36 -3
- package/dist/lib/chatAgents.d.ts +42 -0
- package/dist/lib/chatAgents.js +59 -0
- package/dist/lib/dirDigest.d.ts +11 -0
- package/dist/lib/dirDigest.js +52 -0
- package/dist/lib/envVarNames.d.ts +3 -0
- package/dist/lib/envVarNames.js +19 -0
- package/dist/lib/installAgent.d.ts +1 -1
- package/dist/lib/installAgent.js +1 -1
- package/dist/lib/mcpMigrateRead.d.ts +1 -0
- package/dist/lib/mcpMigrateRead.js +19 -1
- package/dist/lib/realHomeSnapshot.js +5 -0
- package/dist/lib/remoteSkillLock.d.ts +26 -0
- package/dist/lib/remoteSkillLock.js +88 -0
- package/dist/lib/remoteSkillSource.d.ts +45 -0
- package/dist/lib/remoteSkillSource.js +206 -0
- package/dist/lib/runtimeControlProvider.js +2 -0
- package/dist/lib/taskStore.js +1 -1
- package/dist/lib/terminalPicker.js +33 -6
- package/dist/lib/zcodeOwnership.d.ts +17 -0
- package/dist/lib/zcodeOwnership.js +37 -0
- package/dist/pi-bridge/bundle.js +2 -1
- package/dist/probes/codex.d.ts +7 -0
- package/dist/probes/codex.js +37 -5
- package/dist/probes/zcode.d.ts +32 -0
- package/dist/probes/zcode.js +140 -0
- package/docs/architecture.md +9 -5
- package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
- package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
- package/docs/assets/poster-series-v3/README.md +60 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
- package/docs/getting-started.md +244 -14
- package/docs/overview.md +6 -3
- package/docs/overview.zh-CN.md +5 -3
- package/docs/roadmap.md +2 -1
- package/docs/video/README.md +76 -0
- package/docs/video/build/audio/01-opening.aiff +0 -0
- package/docs/video/build/audio/01-opening.mp3 +0 -0
- package/docs/video/build/audio/02-pain.aiff +0 -0
- package/docs/video/build/audio/02-pain.mp3 +0 -0
- package/docs/video/build/audio/03-solution.aiff +0 -0
- package/docs/video/build/audio/03-solution.mp3 +0 -0
- package/docs/video/build/audio/04-install.aiff +0 -0
- package/docs/video/build/audio/04-install.mp3 +0 -0
- package/docs/video/build/audio/04-setup.mp3 +0 -0
- package/docs/video/build/audio/05-dryrun.mp3 +0 -0
- package/docs/video/build/audio/05-init.aiff +0 -0
- package/docs/video/build/audio/05-init.mp3 +0 -0
- package/docs/video/build/audio/05-interactive.mp3 +0 -0
- package/docs/video/build/audio/06-dryrun.aiff +0 -0
- package/docs/video/build/audio/06-dryrun.mp3 +0 -0
- package/docs/video/build/audio/06-onboard.mp3 +0 -0
- package/docs/video/build/audio/06-run.mp3 +0 -0
- package/docs/video/build/audio/07-onboard.aiff +0 -0
- package/docs/video/build/audio/07-onboard.mp3 +0 -0
- package/docs/video/build/audio/07-verify.mp3 +0 -0
- package/docs/video/build/audio/08-closing.mp3 +0 -0
- package/docs/video/build/audio/08-doctor.aiff +0 -0
- package/docs/video/build/audio/08-doctor.mp3 +0 -0
- package/docs/video/build/audio/09-rollback.aiff +0 -0
- package/docs/video/build/audio/09-rollback.mp3 +0 -0
- package/docs/video/build/audio/10-principles.aiff +0 -0
- package/docs/video/build/audio/10-principles.mp3 +0 -0
- package/docs/video/build/audio/11-closing.aiff +0 -0
- package/docs/video/build/audio/11-closing.mp3 +0 -0
- package/docs/video/build/frames/01-opening-0.png +0 -0
- package/docs/video/build/frames/02-pain-1.png +0 -0
- package/docs/video/build/frames/02-pain-2.png +0 -0
- package/docs/video/build/frames/03-solution-3.png +0 -0
- package/docs/video/build/frames/04-install-4.png +0 -0
- package/docs/video/build/frames/04-setup-4.png +0 -0
- package/docs/video/build/frames/04-setup-5.png +0 -0
- package/docs/video/build/frames/05-dryrun-6.png +0 -0
- package/docs/video/build/frames/05-init-5.png +0 -0
- package/docs/video/build/frames/05-interactive-5.png +0 -0
- package/docs/video/build/frames/05-interactive-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-7.png +0 -0
- package/docs/video/build/frames/06-onboard-7.png +0 -0
- package/docs/video/build/frames/06-onboard-8.png +0 -0
- package/docs/video/build/frames/06-run-7.png +0 -0
- package/docs/video/build/frames/06-run-8.png +0 -0
- package/docs/video/build/frames/07-onboard-10.png +0 -0
- package/docs/video/build/frames/07-onboard-7.png +0 -0
- package/docs/video/build/frames/07-onboard-8.png +0 -0
- package/docs/video/build/frames/07-onboard-9.png +0 -0
- package/docs/video/build/frames/07-verify-10.png +0 -0
- package/docs/video/build/frames/07-verify-8.png +0 -0
- package/docs/video/build/frames/07-verify-9.png +0 -0
- package/docs/video/build/frames/08-closing-10.png +0 -0
- package/docs/video/build/frames/08-closing-11.png +0 -0
- package/docs/video/build/frames/08-doctor-11.png +0 -0
- package/docs/video/build/frames/08-doctor-9.png +0 -0
- package/docs/video/build/frames/09-rollback-10.png +0 -0
- package/docs/video/build/frames/09-rollback-12.png +0 -0
- package/docs/video/build/frames/10-principles-11.png +0 -0
- package/docs/video/build/frames/10-principles-13.png +0 -0
- package/docs/video/build/frames/11-closing-12.png +0 -0
- package/docs/video/build/frames/11-closing-14.png +0 -0
- package/docs/video/build/segments/01-opening.mp4 +0 -0
- package/docs/video/build/segments/01-opening.txt +3 -0
- package/docs/video/build/segments/02-pain.mp4 +0 -0
- package/docs/video/build/segments/02-pain.txt +5 -0
- package/docs/video/build/segments/03-solution.mp4 +0 -0
- package/docs/video/build/segments/03-solution.txt +3 -0
- package/docs/video/build/segments/04-install.mp4 +0 -0
- package/docs/video/build/segments/04-install.txt +3 -0
- package/docs/video/build/segments/04-setup.mp4 +0 -0
- package/docs/video/build/segments/04-setup.txt +3 -0
- package/docs/video/build/segments/05-dryrun.mp4 +0 -0
- package/docs/video/build/segments/05-dryrun.txt +3 -0
- package/docs/video/build/segments/05-init.mp4 +0 -0
- package/docs/video/build/segments/05-init.txt +3 -0
- package/docs/video/build/segments/05-interactive.mp4 +0 -0
- package/docs/video/build/segments/05-interactive.txt +5 -0
- package/docs/video/build/segments/06-dryrun.mp4 +0 -0
- package/docs/video/build/segments/06-dryrun.txt +3 -0
- package/docs/video/build/segments/06-onboard.mp4 +0 -0
- package/docs/video/build/segments/06-onboard.txt +5 -0
- package/docs/video/build/segments/06-run.mp4 +0 -0
- package/docs/video/build/segments/06-run.txt +3 -0
- package/docs/video/build/segments/07-onboard.mp4 +0 -0
- package/docs/video/build/segments/07-onboard.txt +5 -0
- package/docs/video/build/segments/07-verify.mp4 +0 -0
- package/docs/video/build/segments/07-verify.txt +5 -0
- package/docs/video/build/segments/08-closing.mp4 +0 -0
- package/docs/video/build/segments/08-closing.txt +3 -0
- package/docs/video/build/segments/08-doctor.mp4 +0 -0
- package/docs/video/build/segments/08-doctor.txt +3 -0
- package/docs/video/build/segments/09-rollback.mp4 +0 -0
- package/docs/video/build/segments/09-rollback.txt +3 -0
- package/docs/video/build/segments/10-principles.mp4 +0 -0
- package/docs/video/build/segments/10-principles.txt +3 -0
- package/docs/video/build/segments/11-closing.mp4 +0 -0
- package/docs/video/build/segments/11-closing.txt +3 -0
- package/docs/video/build/segments/final.txt +8 -0
- package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
- package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
- package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
- package/docs/video/build-ai/audio/04-how.mp3 +0 -0
- package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
- package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
- package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
- package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
- package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
- package/docs/video/build-ai/clips/04-how.mp4 +0 -0
- package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
- package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
- package/docs/video/build-ai/first-frame-02-pain.png +0 -0
- package/docs/video/build-ai/first-frame-03-solution.png +0 -0
- package/docs/video/build-ai/first-frame-04-how.png +0 -0
- package/docs/video/build-ai/first-frame-05-safe.png +0 -0
- package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
- package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
- package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
- package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
- package/docs/video/build-ai/segments/04-how.mp4 +0 -0
- package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
- package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
- package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
- package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
- package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
- package/docs/video/build-ai/segments/cap-04-how.png +0 -0
- package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
- package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
- package/docs/video/build-ai/segments/final.txt +6 -0
- package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
- package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
- package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
- package/docs/video/build-posters/audio/04-how.mp3 +0 -0
- package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
- package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
- package/docs/video/build-posters/cap-01-hook.png +0 -0
- package/docs/video/build-posters/cap-02-pain.png +0 -0
- package/docs/video/build-posters/cap-03-solution.png +0 -0
- package/docs/video/build-posters/cap-04-how.png +0 -0
- package/docs/video/build-posters/cap-05-safe.png +0 -0
- package/docs/video/build-posters/cap-06-closing.png +0 -0
- package/docs/video/build-posters/card-01-hook.png +0 -0
- package/docs/video/build-posters/card-06-closing.png +0 -0
- package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
- package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
- package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
- package/docs/video/build-posters/segments/04-how.mp4 +0 -0
- package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
- package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
- package/docs/video/build-posters/segments/final.txt +6 -0
- package/docs/video/generate-video-ai.mjs +334 -0
- package/docs/video/generate-video-posters.mjs +204 -0
- package/docs/video/generate-video.mjs +426 -0
- package/docs/video/outline.zh-CN.md +102 -0
- package/docs/video/trellis-demo-video-ai.mp4 +0 -0
- package/docs/video/trellis-demo-video.mp4 +0 -0
- package/docs/video/voiceover.zh-CN.md +49 -0
- package/package.json +5 -2
- package/schema/servers.example.yaml +1 -1
- package/docs/assets/agent-trellis-architecture-horizontal.svg +0 -80
- package/docs/assets/agent-trellis-architecture-vertical.svg +0 -82
- package/docs/assets/trellis-mark.svg +0 -12
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config for the GUI's in-app chat feature: which CLI agents can be chatted
|
|
3
|
+
* with from the desktop app, keyed by an arbitrary string id (not `AgentId`
|
|
4
|
+
* — chat targets are not necessarily fully-managed Agents with adapters,
|
|
5
|
+
* sync, and probes; they're just "a non-interactive CLI I can spawn and
|
|
6
|
+
* stream from"). Kept as its own file + config path, sibling to
|
|
7
|
+
* `agent-bridge.yaml`, so delegation's `ALL_AGENTS` constraint (needed for
|
|
8
|
+
* the cross-Agent-delegation use case) never has to bend for chat's needs.
|
|
9
|
+
*
|
|
10
|
+
* Configuration lives at `~/.trellis/mcp/chat-agents.yaml`. Its absence
|
|
11
|
+
* means zero targets, not an error — the same "optional, additive" shape
|
|
12
|
+
* as `agent-bridge.yaml`.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { parse as parseYaml } from "yaml";
|
|
17
|
+
const OUTPUT_FORMATS = ["text", "json", "stream-json"];
|
|
18
|
+
const STREAM_PROTOCOLS = ["claude", "zcode"];
|
|
19
|
+
export function chatAgentsPath(homeDir) {
|
|
20
|
+
return join(homeDir, ".trellis", "mcp", "chat-agents.yaml");
|
|
21
|
+
}
|
|
22
|
+
export function loadChatAgentConfig(homeDir) {
|
|
23
|
+
const path = chatAgentsPath(homeDir);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return { targets: {} };
|
|
26
|
+
const parsed = (parseYaml(readFileSync(path, "utf-8")) ?? {});
|
|
27
|
+
const targets = {};
|
|
28
|
+
for (const [id, raw] of Object.entries(parsed.targets ?? {})) {
|
|
29
|
+
if (!raw)
|
|
30
|
+
continue;
|
|
31
|
+
if (!raw.label || !raw.label.trim()) {
|
|
32
|
+
throw new Error(`chat-agents.yaml: target "${id}" must declare a non-empty "label"`);
|
|
33
|
+
}
|
|
34
|
+
if (!raw.command || !Array.isArray(raw.args) || raw.args.length === 0) {
|
|
35
|
+
throw new Error(`chat-agents.yaml: target "${id}" must declare "command" and a non-empty "args"`);
|
|
36
|
+
}
|
|
37
|
+
if (raw.resumeArgs !== undefined && (!Array.isArray(raw.resumeArgs) || raw.resumeArgs.length === 0)) {
|
|
38
|
+
throw new Error(`chat-agents.yaml: target "${id}" has an invalid "resumeArgs"`);
|
|
39
|
+
}
|
|
40
|
+
if (raw.outputFormat !== undefined && !OUTPUT_FORMATS.includes(raw.outputFormat)) {
|
|
41
|
+
throw new Error(`chat-agents.yaml: target "${id}" has unknown outputFormat "${raw.outputFormat}"`);
|
|
42
|
+
}
|
|
43
|
+
if (raw.streamProtocol !== undefined && !STREAM_PROTOCOLS.includes(raw.streamProtocol)) {
|
|
44
|
+
throw new Error(`chat-agents.yaml: target "${id}" has unknown streamProtocol "${raw.streamProtocol}"`);
|
|
45
|
+
}
|
|
46
|
+
targets[id] = {
|
|
47
|
+
label: raw.label,
|
|
48
|
+
command: raw.command,
|
|
49
|
+
args: raw.args,
|
|
50
|
+
...(raw.resumeArgs ? { resumeArgs: raw.resumeArgs } : {}),
|
|
51
|
+
...(raw.outputFormat ? { outputFormat: raw.outputFormat } : {}),
|
|
52
|
+
...(raw.streamProtocol ? { streamProtocol: raw.streamProtocol } : {}),
|
|
53
|
+
...(raw.timeoutMs ? { timeoutMs: raw.timeoutMs } : {}),
|
|
54
|
+
...(raw.tags ? { tags: raw.tags } : {}),
|
|
55
|
+
...(raw.persona ? { persona: raw.persona } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { targets };
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic digest for a directory tree. Paths, empty directories,
|
|
3
|
+
* file bytes, and link targets all contribute to the digest so it can be
|
|
4
|
+
* used for provenance and for detecting edits before a remote update.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Hashes a tree without following symbolic links. A caller that needs a
|
|
8
|
+
* stricter source policy can reject links separately; keeping their target in
|
|
9
|
+
* this digest means a locally added link is still detected as drift.
|
|
10
|
+
*/
|
|
11
|
+
export declare function directoryDigest(dir: string): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deterministic digest for a directory tree. Paths, empty directories,
|
|
3
|
+
* file bytes, and link targets all contribute to the digest so it can be
|
|
4
|
+
* used for provenance and for detecting edits before a remote update.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { lstatSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
8
|
+
import { join, relative, sep } from "node:path";
|
|
9
|
+
function portablePath(path) {
|
|
10
|
+
return path.split(sep).join("/");
|
|
11
|
+
}
|
|
12
|
+
function addPart(hash, value) {
|
|
13
|
+
hash.update(String(Buffer.byteLength(value)));
|
|
14
|
+
hash.update(":");
|
|
15
|
+
hash.update(value);
|
|
16
|
+
hash.update("\0");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hashes a tree without following symbolic links. A caller that needs a
|
|
20
|
+
* stricter source policy can reject links separately; keeping their target in
|
|
21
|
+
* this digest means a locally added link is still detected as drift.
|
|
22
|
+
*/
|
|
23
|
+
export function directoryDigest(dir) {
|
|
24
|
+
const hash = createHash("sha256");
|
|
25
|
+
const walk = (current) => {
|
|
26
|
+
const rel = portablePath(relative(dir, current)) || ".";
|
|
27
|
+
const stat = lstatSync(current);
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
addPart(hash, "directory");
|
|
30
|
+
addPart(hash, rel);
|
|
31
|
+
for (const name of readdirSync(current).sort((a, b) => a.localeCompare(b))) {
|
|
32
|
+
walk(join(current, name));
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (stat.isFile()) {
|
|
37
|
+
addPart(hash, "file");
|
|
38
|
+
addPart(hash, rel);
|
|
39
|
+
addPart(hash, readFileSync(current));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (stat.isSymbolicLink()) {
|
|
43
|
+
addPart(hash, "symlink");
|
|
44
|
+
addPart(hash, rel);
|
|
45
|
+
addPart(hash, readlinkSync(current));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Cannot digest unsupported filesystem entry: ${current}`);
|
|
49
|
+
};
|
|
50
|
+
walk(dir);
|
|
51
|
+
return `sha256:${hash.digest("hex")}`;
|
|
52
|
+
}
|
|
@@ -44,6 +44,9 @@ export declare function declaredEnvNames(def: McpServerDef): string[];
|
|
|
44
44
|
* different command, not this extractor's job.
|
|
45
45
|
*/
|
|
46
46
|
export declare function extractJsonEnvVarNames(content: string): string[];
|
|
47
|
+
/** ZCode stores the same server value shape under `mcp.servers` rather than
|
|
48
|
+
* the top-level `mcpServers` map used by Claude Code and Kiro. */
|
|
49
|
+
export declare function extractZcodeEnvVarNames(content: string): string[];
|
|
47
50
|
/** Codex's config is TOML. The shapes audit needs: every `env_vars = [...]`
|
|
48
51
|
* line's quoted string contents, plus a `bearer_token_env_var = "VAR"`
|
|
49
52
|
* line's bare name (a name directly, not a `${VAR}` template — Codex's
|
package/dist/lib/envVarNames.js
CHANGED
|
@@ -71,6 +71,25 @@ export function extractJsonEnvVarNames(content) {
|
|
|
71
71
|
}
|
|
72
72
|
return names;
|
|
73
73
|
}
|
|
74
|
+
/** ZCode stores the same server value shape under `mcp.servers` rather than
|
|
75
|
+
* the top-level `mcpServers` map used by Claude Code and Kiro. */
|
|
76
|
+
export function extractZcodeEnvVarNames(content) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(content);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const names = [];
|
|
85
|
+
for (const server of Object.values(parsed.mcp?.servers ?? {})) {
|
|
86
|
+
for (const value of Object.values(server.env ?? {}))
|
|
87
|
+
names.push(...extractTemplateVarNames(value));
|
|
88
|
+
for (const value of Object.values(server.headers ?? {}))
|
|
89
|
+
names.push(...extractTemplateVarNames(value));
|
|
90
|
+
}
|
|
91
|
+
return names;
|
|
92
|
+
}
|
|
74
93
|
const ENV_VARS_LINE_RE = /^\s*env_vars\s*=\s*\[(.*)\]\s*$/;
|
|
75
94
|
const BEARER_TOKEN_ENV_VAR_LINE_RE = /^\s*bearer_token_env_var\s*=\s*"([^"]*)"\s*$/;
|
|
76
95
|
const QUOTED_STRING_RE = /"((?:[^"\\]|\\.)*)"/g;
|
|
@@ -10,7 +10,7 @@ import type { AgentId } from "../core/types.js";
|
|
|
10
10
|
/** Only agents with a real `npm install -g <pkg>` command — Kiro's own
|
|
11
11
|
* `INSTALL_HINTS` entry is a download URL, not a package, and is never
|
|
12
12
|
* looked up here. */
|
|
13
|
-
export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro" | "kimi-code">, string>;
|
|
13
|
+
export declare const NPM_INSTALLABLE: Record<Exclude<AgentId, "kiro" | "kimi-code" | "zcode">, string>;
|
|
14
14
|
export interface ConfirmAndInstallOptions {
|
|
15
15
|
/** Test/real seam, same pattern as onboard's `promptForAgent` — a real
|
|
16
16
|
* terminal confirmation by default. */
|
package/dist/lib/installAgent.js
CHANGED
|
@@ -32,7 +32,7 @@ function runInstallReal(pkg) {
|
|
|
32
32
|
execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
|
|
33
33
|
}
|
|
34
34
|
export async function confirmAndInstall(agent, opts = {}) {
|
|
35
|
-
if (agent === "kiro" || agent === "kimi-code") {
|
|
35
|
+
if (agent === "kiro" || agent === "kimi-code" || agent === "zcode") {
|
|
36
36
|
return { installed: false, installable: false };
|
|
37
37
|
}
|
|
38
38
|
const pkg = NPM_INSTALLABLE[agent];
|
|
@@ -38,6 +38,7 @@ export declare function resolvedEnvTextMap(def: Pick<McpServerDef, "env" | "envA
|
|
|
38
38
|
export declare function readClaudeCodeMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
39
39
|
export declare function readKiroMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
40
40
|
export declare function readKimiCodeMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
41
|
+
export declare function readZcodeMcpDefs(homeDir: string): McpMigrateReadResult;
|
|
41
42
|
export interface CodexMcpEntryRich {
|
|
42
43
|
name: string;
|
|
43
44
|
enabled: boolean;
|
|
@@ -15,6 +15,8 @@ import { readFileSync } from "node:fs";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { readJsonFile } from "./probeCommon.js";
|
|
17
17
|
import { readServerEnvTable } from "./tomlSection.js";
|
|
18
|
+
import { resolveZcodeProfile } from "../probes/zcode.js";
|
|
19
|
+
import { codexEnv } from "../probes/codex.js";
|
|
18
20
|
const EMPTY_RESULT = { entries: [], unsupported: [] };
|
|
19
21
|
/**
|
|
20
22
|
* Trellis's own `env` (self-referencing name) vs. `envAliases`
|
|
@@ -111,6 +113,22 @@ export function readKiroMcpDefs(homeDir) {
|
|
|
111
113
|
export function readKimiCodeMcpDefs(homeDir) {
|
|
112
114
|
return readRichJsonMcpDefs(join(homeDir, ".kimi-code", "mcp.json"));
|
|
113
115
|
}
|
|
116
|
+
export function readZcodeMcpDefs(homeDir) {
|
|
117
|
+
const profile = resolveZcodeProfile(homeDir);
|
|
118
|
+
if (!profile)
|
|
119
|
+
return EMPTY_RESULT;
|
|
120
|
+
const parsed = readJsonFile(profile.configPath);
|
|
121
|
+
const entries = [];
|
|
122
|
+
const unsupported = [];
|
|
123
|
+
for (const [name, raw] of Object.entries(parsed?.mcp?.servers ?? {})) {
|
|
124
|
+
const def = fromRichJsonServerDef(raw);
|
|
125
|
+
if (def)
|
|
126
|
+
entries.push({ name, def });
|
|
127
|
+
else
|
|
128
|
+
unsupported.push({ name, reason: "stdio entry has no command — malformed, skipped" });
|
|
129
|
+
}
|
|
130
|
+
return { entries, unsupported };
|
|
131
|
+
}
|
|
114
132
|
/**
|
|
115
133
|
* Pure conversion half — separated from the subprocess/file-read effects
|
|
116
134
|
* below so the stdio/non-stdio/malformed decision logic is unit-testable
|
|
@@ -202,7 +220,7 @@ export function readCodexMcpDefs(homeDir) {
|
|
|
202
220
|
// `[]`, not the real machine's servers), so this must be scoped to
|
|
203
221
|
// `homeDir` explicitly — `src/probes/codex.ts`'s own equivalent call
|
|
204
222
|
// has this same gap, unaddressed there; not touched by this change.
|
|
205
|
-
raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000, env:
|
|
223
|
+
raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000, env: codexEnv(homeDir) });
|
|
206
224
|
}
|
|
207
225
|
catch {
|
|
208
226
|
return EMPTY_RESULT;
|
|
@@ -21,6 +21,11 @@ export const REAL_HOME_ALLOWLIST = [
|
|
|
21
21
|
".codex/config.toml",
|
|
22
22
|
".agents/skills",
|
|
23
23
|
".codex/skills",
|
|
24
|
+
// zcode (src/probes/zcode.ts)
|
|
25
|
+
".zcode/cli/config.json",
|
|
26
|
+
".zcode/cli/setting.json",
|
|
27
|
+
".zcode/skills",
|
|
28
|
+
".zcode/AGENTS.md",
|
|
24
29
|
// kiro (src/probes/kiro.ts)
|
|
25
30
|
".kiro/settings/mcp.json",
|
|
26
31
|
".kiro/skills",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Durable provenance for remote Skills in ~/.trellis/skills.lock.json. */
|
|
2
|
+
import type { BackupSession } from "./backup.js";
|
|
3
|
+
export declare const REMOTE_SKILL_LOCK_VERSION = 1;
|
|
4
|
+
export interface RemoteSkillLockEntry {
|
|
5
|
+
source: string;
|
|
6
|
+
requestedRef: string;
|
|
7
|
+
commit: string;
|
|
8
|
+
subdirectory: string;
|
|
9
|
+
digest: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RemoteSkillLock {
|
|
12
|
+
version: typeof REMOTE_SKILL_LOCK_VERSION;
|
|
13
|
+
skills: Record<string, RemoteSkillLockEntry>;
|
|
14
|
+
}
|
|
15
|
+
export type RemoteSkillLockRead = {
|
|
16
|
+
ok: true;
|
|
17
|
+
lock: RemoteSkillLock;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function remoteSkillLockPath(homeDir: string): string;
|
|
23
|
+
export declare function emptyRemoteSkillLock(): RemoteSkillLock;
|
|
24
|
+
export declare function readRemoteSkillLock(homeDir: string): RemoteSkillLockRead;
|
|
25
|
+
export declare function serializeRemoteSkillLock(lock: RemoteSkillLock): string;
|
|
26
|
+
export declare function writeRemoteSkillLock(homeDir: string, lock: RemoteSkillLock, backup?: BackupSession): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Durable provenance for remote Skills in ~/.trellis/skills.lock.json. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export const REMOTE_SKILL_LOCK_VERSION = 1;
|
|
5
|
+
export function remoteSkillLockPath(homeDir) {
|
|
6
|
+
return join(homeDir, ".trellis", "skills.lock.json");
|
|
7
|
+
}
|
|
8
|
+
export function emptyRemoteSkillLock() {
|
|
9
|
+
return { version: REMOTE_SKILL_LOCK_VERSION, skills: {} };
|
|
10
|
+
}
|
|
11
|
+
function isObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function validSource(value) {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return false;
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(value);
|
|
19
|
+
return parsed.protocol === "https:"
|
|
20
|
+
&& parsed.hostname === "github.com"
|
|
21
|
+
&& !parsed.username
|
|
22
|
+
&& !parsed.password
|
|
23
|
+
&& !parsed.search
|
|
24
|
+
&& !parsed.hash
|
|
25
|
+
&& /^\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9_.-]+\.git$/.test(parsed.pathname);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function validEntry(value) {
|
|
32
|
+
if (!isObject(value))
|
|
33
|
+
return false;
|
|
34
|
+
const keys = Object.keys(value).sort();
|
|
35
|
+
if (keys.join(",") !== "commit,digest,requestedRef,source,subdirectory")
|
|
36
|
+
return false;
|
|
37
|
+
return validSource(value.source)
|
|
38
|
+
&& typeof value.requestedRef === "string" && value.requestedRef.length > 0 && !/[\0\r\n]/.test(value.requestedRef)
|
|
39
|
+
&& typeof value.commit === "string" && /^[0-9a-f]{40}$/.test(value.commit)
|
|
40
|
+
&& typeof value.subdirectory === "string" && value.subdirectory.length > 0 && !value.subdirectory.startsWith("/") && !value.subdirectory.split("/").includes("..")
|
|
41
|
+
&& typeof value.digest === "string" && /^sha256:[0-9a-f]{64}$/.test(value.digest);
|
|
42
|
+
}
|
|
43
|
+
function validateLock(value) {
|
|
44
|
+
if (!isObject(value) || value.version !== REMOTE_SKILL_LOCK_VERSION || !isObject(value.skills) || Object.keys(value).some((key) => key !== "version" && key !== "skills")) {
|
|
45
|
+
throw new Error("expected version 1 and a skills object");
|
|
46
|
+
}
|
|
47
|
+
const skills = {};
|
|
48
|
+
for (const [name, entry] of Object.entries(value.skills)) {
|
|
49
|
+
if (!name || name.includes("/") || name.includes("\\") || !validEntry(entry)) {
|
|
50
|
+
throw new Error(`invalid provenance entry for Skill ${JSON.stringify(name)}`);
|
|
51
|
+
}
|
|
52
|
+
skills[name] = entry;
|
|
53
|
+
}
|
|
54
|
+
return { version: REMOTE_SKILL_LOCK_VERSION, skills };
|
|
55
|
+
}
|
|
56
|
+
export function readRemoteSkillLock(homeDir) {
|
|
57
|
+
const path = remoteSkillLockPath(homeDir);
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return { ok: true, lock: emptyRemoteSkillLock() };
|
|
61
|
+
return { ok: true, lock: validateLock(JSON.parse(readFileSync(path, "utf8"))) };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return { ok: false, error: `Cannot use ${path}: ${error instanceof Error ? error.message : String(error)}` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function serializeRemoteSkillLock(lock) {
|
|
68
|
+
const valid = validateLock(lock);
|
|
69
|
+
const skills = Object.fromEntries(Object.entries(valid.skills).sort(([a], [b]) => a.localeCompare(b)));
|
|
70
|
+
return `${JSON.stringify({ version: REMOTE_SKILL_LOCK_VERSION, skills }, null, 2)}\n`;
|
|
71
|
+
}
|
|
72
|
+
export function writeRemoteSkillLock(homeDir, lock, backup) {
|
|
73
|
+
const path = remoteSkillLockPath(homeDir);
|
|
74
|
+
const content = serializeRemoteSkillLock(lock);
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
if (backup) {
|
|
77
|
+
backup.writeFile(path, content);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const temporary = join(dirname(path), `.${path.split("/").pop()}.${process.pid}.${Date.now()}.tmp`);
|
|
81
|
+
try {
|
|
82
|
+
writeFileSync(temporary, content, { mode: 0o600 });
|
|
83
|
+
renameSync(temporary, path);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
rmSync(temporary, { force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Skill source handling. The public command accepts only GitHub
|
|
3
|
+
* identities, while the lower-level fetch function intentionally accepts a
|
|
4
|
+
* repository URL so unit and sandbox tests can use a local bare Git remote.
|
|
5
|
+
* No command in this module evaluates checked-out content.
|
|
6
|
+
*/
|
|
7
|
+
export interface GitHubRepository {
|
|
8
|
+
source: string;
|
|
9
|
+
url: string;
|
|
10
|
+
owner: string;
|
|
11
|
+
repository: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RemoteSkillCandidate {
|
|
14
|
+
name: string;
|
|
15
|
+
directory: string;
|
|
16
|
+
subdirectory: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SelectedRemoteSkill extends RemoteSkillCandidate {
|
|
19
|
+
digest: string;
|
|
20
|
+
}
|
|
21
|
+
export interface FetchedRemoteRepository {
|
|
22
|
+
repository: {
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
requestedRef: string;
|
|
26
|
+
commit: string;
|
|
27
|
+
root: string;
|
|
28
|
+
candidates: readonly RemoteSkillCandidate[];
|
|
29
|
+
select(name: string): SelectedRemoteSkill;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
/** Accept the exact two source syntaxes Trellis documents, with no URL state
|
|
33
|
+
* that could put a credential in the provenance lock. */
|
|
34
|
+
export declare function normalizeGitHubRepository(source: string): GitHubRepository;
|
|
35
|
+
/**
|
|
36
|
+
* Fetches a single immutable commit into a system temporary directory. It is
|
|
37
|
+
* exported with a URL-shaped repository argument to let tests exercise the
|
|
38
|
+
* real Git protocol against a local bare repo; CLI callers first use
|
|
39
|
+
* normalizeGitHubRepository and therefore cannot pass arbitrary hosts.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fetchRemoteRepository(repository: {
|
|
42
|
+
url: string;
|
|
43
|
+
}, opts?: {
|
|
44
|
+
branch?: string;
|
|
45
|
+
}): FetchedRemoteRepository;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Skill source handling. The public command accepts only GitHub
|
|
3
|
+
* identities, while the lower-level fetch function intentionally accepts a
|
|
4
|
+
* repository URL so unit and sandbox tests can use a local bare Git remote.
|
|
5
|
+
* No command in this module evaluates checked-out content.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { lstatSync, mkdtempSync, readdirSync, realpathSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { basename, join, relative, resolve, sep } from "node:path";
|
|
11
|
+
import { directoryDigest } from "./dirDigest.js";
|
|
12
|
+
function git(args, cwd) {
|
|
13
|
+
try {
|
|
14
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
18
|
+
throw new Error(`Git could not read the remote Skill source: ${detail}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Accept the exact two source syntaxes Trellis documents, with no URL state
|
|
22
|
+
* that could put a credential in the provenance lock. */
|
|
23
|
+
export function normalizeGitHubRepository(source) {
|
|
24
|
+
const shorthand = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,38})?)\/([A-Za-z0-9_.-]+)$/.exec(source);
|
|
25
|
+
if (shorthand) {
|
|
26
|
+
const [, owner, repository] = shorthand;
|
|
27
|
+
return { source, owner: owner, repository: repository, url: `https://github.com/${owner}/${repository}.git` };
|
|
28
|
+
}
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(source);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error(`Remote Skill source must be GitHub owner/repository or an HTTPS github.com URL: ${source}`);
|
|
35
|
+
}
|
|
36
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
37
|
+
if (parsed.protocol !== "https:"
|
|
38
|
+
|| parsed.hostname.toLowerCase() !== "github.com"
|
|
39
|
+
|| parsed.username
|
|
40
|
+
|| parsed.password
|
|
41
|
+
|| parsed.search
|
|
42
|
+
|| parsed.hash
|
|
43
|
+
|| parts.length !== 2
|
|
44
|
+
|| !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})?$/.test(parts[0])
|
|
45
|
+
|| !/^[A-Za-z0-9_.-]+(?:\.git)?$/.test(parts[1])) {
|
|
46
|
+
throw new Error(`Remote Skill source must be a credential-free HTTPS github.com repository URL: ${source}`);
|
|
47
|
+
}
|
|
48
|
+
const owner = parts[0];
|
|
49
|
+
const repository = parts[1].replace(/\.git$/i, "");
|
|
50
|
+
return { source, owner, repository, url: `https://github.com/${owner}/${repository}.git` };
|
|
51
|
+
}
|
|
52
|
+
function parseLsRemote(output) {
|
|
53
|
+
return output.split("\n").flatMap((line) => {
|
|
54
|
+
const match = /^([0-9a-f]{40})\t(.+)$/.exec(line.trim());
|
|
55
|
+
return match ? [{ hash: match[1], ref: match[2] }] : [];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function resolveDefaultRef(url) {
|
|
59
|
+
const output = git(["ls-remote", "--symref", url, "HEAD"]);
|
|
60
|
+
const branch = /^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/m.exec(output)?.[1];
|
|
61
|
+
const commit = parseLsRemote(output).find((entry) => entry.ref === "HEAD")?.hash;
|
|
62
|
+
if (!branch || !commit)
|
|
63
|
+
throw new Error(`Could not resolve the default branch for ${url}`);
|
|
64
|
+
return { requestedRef: branch, commit };
|
|
65
|
+
}
|
|
66
|
+
function resolveRequestedRef(url, requestedRef) {
|
|
67
|
+
if (!requestedRef || /[\0\r\n]/.test(requestedRef))
|
|
68
|
+
throw new Error("--branch must be a non-empty Git ref");
|
|
69
|
+
const entries = parseLsRemote(git(["ls-remote", url, requestedRef]));
|
|
70
|
+
const peeled = entries.find((entry) => entry.ref.endsWith("^{}"));
|
|
71
|
+
const direct = entries.find((entry) => !entry.ref.endsWith("^{}"));
|
|
72
|
+
const selected = peeled ?? direct;
|
|
73
|
+
if (!selected)
|
|
74
|
+
throw new Error(`Remote source has no ref named ${requestedRef}`);
|
|
75
|
+
return { requestedRef, commit: selected.hash };
|
|
76
|
+
}
|
|
77
|
+
function isInside(root, target) {
|
|
78
|
+
const rel = relative(root, target);
|
|
79
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !rel.startsWith("..\/"));
|
|
80
|
+
}
|
|
81
|
+
function walkDirectories(root) {
|
|
82
|
+
const dirs = [];
|
|
83
|
+
const walk = (current) => {
|
|
84
|
+
dirs.push(current);
|
|
85
|
+
for (const name of readdirSync(current).sort((a, b) => a.localeCompare(b))) {
|
|
86
|
+
// The Git administration directory is implementation state, never
|
|
87
|
+
// source content to inspect for a Skill candidate.
|
|
88
|
+
if (current === root && name === ".git")
|
|
89
|
+
continue;
|
|
90
|
+
const entry = join(current, name);
|
|
91
|
+
const stat = lstatSync(entry);
|
|
92
|
+
if (stat.isDirectory())
|
|
93
|
+
walk(entry);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(root);
|
|
97
|
+
return dirs;
|
|
98
|
+
}
|
|
99
|
+
function candidatesIn(root) {
|
|
100
|
+
return walkDirectories(root).flatMap((directory) => {
|
|
101
|
+
const names = readdirSync(directory);
|
|
102
|
+
const exact = names.find((name) => name === "SKILL.md");
|
|
103
|
+
if (!exact)
|
|
104
|
+
return [];
|
|
105
|
+
const entry = join(directory, exact);
|
|
106
|
+
// A non-file `SKILL.md` is deliberately not advertised as a valid
|
|
107
|
+
// candidate; selecting this directory produces a precise error below.
|
|
108
|
+
if (!lstatSync(entry).isFile())
|
|
109
|
+
return [];
|
|
110
|
+
return [{ name: basename(directory), directory, subdirectory: relative(root, directory).split(sep).join("/") }];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function validateSelectedDirectory(root, directory, expectedName) {
|
|
114
|
+
if (basename(directory) !== expectedName)
|
|
115
|
+
throw new Error(`Selected remote Skill directory is not named ${expectedName}`);
|
|
116
|
+
const rootReal = realpathSync(root);
|
|
117
|
+
const walk = (current) => {
|
|
118
|
+
const stat = lstatSync(current);
|
|
119
|
+
if (stat.isSymbolicLink())
|
|
120
|
+
throw new Error(`Remote Skill ${expectedName} contains a symbolic link: ${current}`);
|
|
121
|
+
const resolved = realpathSync(current);
|
|
122
|
+
if (!isInside(rootReal, resolved))
|
|
123
|
+
throw new Error(`Remote Skill ${expectedName} resolves outside the fetched repository`);
|
|
124
|
+
if (!stat.isDirectory())
|
|
125
|
+
return;
|
|
126
|
+
for (const name of readdirSync(current))
|
|
127
|
+
walk(join(current, name));
|
|
128
|
+
};
|
|
129
|
+
const entry = join(directory, "SKILL.md");
|
|
130
|
+
const entryStat = lstatSync(entry);
|
|
131
|
+
if (entryStat.isSymbolicLink()) {
|
|
132
|
+
const resolvedEntry = realpathSync(entry);
|
|
133
|
+
if (!isInside(rootReal, resolvedEntry))
|
|
134
|
+
throw new Error(`Remote Skill ${expectedName} entry file resolves outside the fetched repository`);
|
|
135
|
+
throw new Error(`Remote Skill ${expectedName} entry file must not be a symbolic link`);
|
|
136
|
+
}
|
|
137
|
+
if (!entryStat.isFile())
|
|
138
|
+
throw new Error(`Remote Skill ${expectedName} has no regular, exact-case SKILL.md`);
|
|
139
|
+
walk(directory);
|
|
140
|
+
return {
|
|
141
|
+
name: expectedName,
|
|
142
|
+
directory,
|
|
143
|
+
subdirectory: relative(root, directory).split(sep).join("/"),
|
|
144
|
+
digest: directoryDigest(directory),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetches a single immutable commit into a system temporary directory. It is
|
|
149
|
+
* exported with a URL-shaped repository argument to let tests exercise the
|
|
150
|
+
* real Git protocol against a local bare repo; CLI callers first use
|
|
151
|
+
* normalizeGitHubRepository and therefore cannot pass arbitrary hosts.
|
|
152
|
+
*/
|
|
153
|
+
export function fetchRemoteRepository(repository, opts = {}) {
|
|
154
|
+
const resolvedRef = opts.branch === undefined ? resolveDefaultRef(repository.url) : resolveRequestedRef(repository.url, opts.branch);
|
|
155
|
+
const temp = mkdtempSync(join(tmpdir(), "trellis-remote-skill-"));
|
|
156
|
+
let disposed = false;
|
|
157
|
+
try {
|
|
158
|
+
git(["init", "--quiet"], temp);
|
|
159
|
+
git(["remote", "add", "origin", repository.url], temp);
|
|
160
|
+
git(["fetch", "--quiet", "--depth=1", "origin", resolvedRef.commit], temp);
|
|
161
|
+
git(["checkout", "--quiet", "--detach", "FETCH_HEAD"], temp);
|
|
162
|
+
const actualCommit = git(["rev-parse", "HEAD"], temp).trim();
|
|
163
|
+
if (!/^[0-9a-f]{40}$/.test(actualCommit))
|
|
164
|
+
throw new Error("Git did not produce an immutable commit identifier");
|
|
165
|
+
const root = resolve(temp);
|
|
166
|
+
const candidates = candidatesIn(root);
|
|
167
|
+
return {
|
|
168
|
+
repository: { url: repository.url },
|
|
169
|
+
requestedRef: resolvedRef.requestedRef,
|
|
170
|
+
commit: actualCommit,
|
|
171
|
+
root,
|
|
172
|
+
candidates,
|
|
173
|
+
select(name) {
|
|
174
|
+
const matchingDirectories = walkDirectories(root).filter((directory) => basename(directory) === name);
|
|
175
|
+
if (matchingDirectories.length === 0) {
|
|
176
|
+
const available = candidates.map((candidate) => candidate.name).sort().join(", ") || "none";
|
|
177
|
+
throw new Error(`Remote source has no Skill named ${name}. Available Skills: ${available}`);
|
|
178
|
+
}
|
|
179
|
+
const withEntry = matchingDirectories.filter((directory) => {
|
|
180
|
+
try {
|
|
181
|
+
return readdirSync(directory).includes("SKILL.md");
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (withEntry.length !== 1) {
|
|
188
|
+
if (withEntry.length === 0)
|
|
189
|
+
throw new Error(`Remote Skill ${name} has no regular, exact-case SKILL.md`);
|
|
190
|
+
throw new Error(`Remote source contains multiple Skills named ${name}; select a repository with one matching Skill`);
|
|
191
|
+
}
|
|
192
|
+
return validateSelectedDirectory(root, withEntry[0], name);
|
|
193
|
+
},
|
|
194
|
+
dispose() {
|
|
195
|
+
if (disposed)
|
|
196
|
+
return;
|
|
197
|
+
disposed = true;
|
|
198
|
+
rmSync(temp, { recursive: true, force: true });
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
rmSync(temp, { recursive: true, force: true });
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -9,6 +9,7 @@ import * as codexProbe from "../probes/codex.js";
|
|
|
9
9
|
import * as kiroProbe from "../probes/kiro.js";
|
|
10
10
|
import * as piProbe from "../probes/pi.js";
|
|
11
11
|
import * as kimiCodeProbe from "../probes/kimi-code.js";
|
|
12
|
+
import * as zcodeProbe from "../probes/zcode.js";
|
|
12
13
|
import { TRELLIS_VERSION } from "./cliMetadata.js";
|
|
13
14
|
import { inspectMemoryReadiness } from "./memoryStatus.js";
|
|
14
15
|
const MAX_INSTRUCTIONS_BYTES = 256 * 1024;
|
|
@@ -37,6 +38,7 @@ const PROBES = {
|
|
|
37
38
|
kiro: (homeDir) => kiroProbe.probe(homeDir),
|
|
38
39
|
pi: (homeDir) => piProbe.probe(homeDir),
|
|
39
40
|
"kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
|
|
41
|
+
zcode: (homeDir) => zcodeProbe.probe(homeDir),
|
|
40
42
|
};
|
|
41
43
|
function textResult(value) {
|
|
42
44
|
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: value };
|
package/dist/lib/taskStore.js
CHANGED
|
@@ -42,7 +42,7 @@ function now() {
|
|
|
42
42
|
return new Date().toISOString();
|
|
43
43
|
}
|
|
44
44
|
function assertAgent(value, label) {
|
|
45
|
-
if (value !== "claude-code" && value !== "codex" && value !== "kiro" && value !== "pi" && value !== "kimi-code") {
|
|
45
|
+
if (value !== "claude-code" && value !== "codex" && value !== "kiro" && value !== "pi" && value !== "kimi-code" && value !== "zcode") {
|
|
46
46
|
throw new Error(`${label} must be a supported Agent id`);
|
|
47
47
|
}
|
|
48
48
|
}
|