terminal-pilot 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1076 -496
- package/dist/cli.js.map +4 -4
- package/dist/commands/close-session.js.map +1 -1
- package/dist/commands/create-session.js.map +1 -1
- package/dist/commands/fill.js.map +1 -1
- package/dist/commands/get-session.js.map +1 -1
- package/dist/commands/index.js +481 -163
- package/dist/commands/index.js.map +4 -4
- package/dist/commands/install.js +448 -130
- package/dist/commands/install.js.map +4 -4
- package/dist/commands/installer.js +154 -60
- package/dist/commands/installer.js.map +4 -4
- package/dist/commands/list-sessions.js.map +1 -1
- package/dist/commands/press-key.js.map +1 -1
- package/dist/commands/read-history.js.map +1 -1
- package/dist/commands/read-screen.js.map +1 -1
- package/dist/commands/resize.js.map +1 -1
- package/dist/commands/runtime.js.map +1 -1
- package/dist/commands/screenshot.js.map +1 -1
- package/dist/commands/send-signal.js.map +1 -1
- package/dist/commands/type.js.map +1 -1
- package/dist/commands/uninstall.js +187 -93
- package/dist/commands/uninstall.js.map +4 -4
- package/dist/commands/wait-for-exit.js.map +1 -1
- package/dist/commands/wait-for.js.map +1 -1
- package/dist/testing/cli-repl.js +1072 -492
- package/dist/testing/cli-repl.js.map +4 -4
- package/dist/testing/qa-cli.js +1082 -502
- package/dist/testing/qa-cli.js.map +4 -4
- package/node_modules/@poe-code/agent-skill-config/README.md +103 -0
- package/node_modules/@poe-code/agent-skill-config/dist/apply.d.ts +25 -0
- package/node_modules/@poe-code/agent-skill-config/dist/apply.js +159 -0
- package/node_modules/@poe-code/agent-skill-config/dist/bridge-active-skills.d.ts +23 -0
- package/node_modules/@poe-code/agent-skill-config/dist/bridge-active-skills.js +341 -0
- package/node_modules/@poe-code/agent-skill-config/dist/configs.d.ts +16 -0
- package/node_modules/@poe-code/agent-skill-config/dist/configs.js +70 -0
- package/node_modules/@poe-code/agent-skill-config/dist/exports.compile-check.d.ts +1 -0
- package/node_modules/@poe-code/agent-skill-config/dist/exports.compile-check.js +1 -0
- package/node_modules/@poe-code/agent-skill-config/dist/git-exclude.d.ts +8 -0
- package/node_modules/@poe-code/agent-skill-config/dist/git-exclude.js +159 -0
- package/node_modules/@poe-code/agent-skill-config/dist/index.d.ts +11 -0
- package/node_modules/@poe-code/agent-skill-config/dist/index.js +6 -0
- package/node_modules/@poe-code/agent-skill-config/dist/resolve-skill-reference.d.ts +22 -0
- package/node_modules/@poe-code/agent-skill-config/dist/resolve-skill-reference.js +87 -0
- package/node_modules/@poe-code/agent-skill-config/dist/templates/poe-generate.md +47 -0
- package/node_modules/@poe-code/agent-skill-config/dist/templates/terminal-pilot.md +45 -0
- package/node_modules/@poe-code/agent-skill-config/dist/templates.d.ts +3 -0
- package/node_modules/@poe-code/agent-skill-config/dist/templates.js +63 -0
- package/node_modules/@poe-code/agent-skill-config/dist/types.d.ts +16 -0
- package/node_modules/@poe-code/agent-skill-config/dist/types.js +1 -0
- package/node_modules/@poe-code/agent-skill-config/package.json +24 -0
- package/package.json +4 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @poe-code/agent-skill-config
|
|
2
|
+
|
|
3
|
+
`@poe-code/agent-skill-config` resolves skill references for CLI, SDK, pipeline, and ralph configs, installs declarative skill folders into native agent skill directories, and bridges active skills into the spawning agent at runtime.
|
|
4
|
+
|
|
5
|
+
## Skill References
|
|
6
|
+
|
|
7
|
+
Skill refs use one of two forms:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
"<name>" # bare: poe-code-native skill
|
|
11
|
+
"<agentId>/<name>" # agent-prefixed: agent-native skill
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
|
|
16
|
+
| Ref | Source |
|
|
17
|
+
|-----|--------|
|
|
18
|
+
| `"my-helper"` | `~/.poe-code/skills/my-helper` or `<cwd>/.poe-code/skills/my-helper` |
|
|
19
|
+
| `"claude/my-helper"` | `~/.claude/skills/my-helper` alias for `claude-code` |
|
|
20
|
+
| `"claude-code/my-helper"` | `~/.claude/skills/my-helper` canonical id |
|
|
21
|
+
| `"codex/my-helper"` | `~/.codex/skills/my-helper` |
|
|
22
|
+
|
|
23
|
+
The agent token accepts canonical ids, aliases, and any casing. It is normalized with `resolveAgentId` from `@poe-code/agent-defs`. The token is not the source agent's native directory name; the source path always comes from `agentSkillConfigs[canonicalId]`, so aliases map to the directory owned by that canonical agent.
|
|
24
|
+
|
|
25
|
+
## Resolution
|
|
26
|
+
|
|
27
|
+
Resolution is per ref. Project scope beats user scope; first hit wins.
|
|
28
|
+
|
|
29
|
+
Bare `<name>`:
|
|
30
|
+
|
|
31
|
+
1. `<cwd>/.poe-code/skills/<name>`
|
|
32
|
+
2. `~/.poe-code/skills/<name>`
|
|
33
|
+
|
|
34
|
+
Prefixed `<agentId>/<name>`:
|
|
35
|
+
|
|
36
|
+
1. `<cwd>/<agentId-local-skill-dir>/<name>`, for example `<cwd>/.claude/skills/<name>`
|
|
37
|
+
2. `~/<agentId-global-skill-dir>/<name>`, for example `~/.claude/skills/<name>`
|
|
38
|
+
|
|
39
|
+
Per-agent skill directories come from `agentSkillConfigs` in `configs.ts`; resolvers do not hard-code native agent paths.
|
|
40
|
+
|
|
41
|
+
## Bridge Contract
|
|
42
|
+
|
|
43
|
+
At spawn time, `bridgeActiveSkills(spawnAgentId, cwd, refs, homeDir, runId)` resolves every ref, then copies each resolved source folder into the spawning agent's native local skill directory under `cwd`, keyed by source basename:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
<cwd>/<spawn-agent-local-skill-dir>/<source-basename>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The source ref's agent prefix never appears in the target path. For a Claude Code spawn, both `"codex/my-helper"` and `"my-helper"` target `.claude/skills/my-helper`.
|
|
50
|
+
|
|
51
|
+
Resolution failures abort the whole bridge before any copy. Error messages distinguish:
|
|
52
|
+
|
|
53
|
+
| Kind | Meaning |
|
|
54
|
+
|------|---------|
|
|
55
|
+
| `malformed` | Bad syntax; expected `"<name>"` or `"<agentId>/<name>"`. |
|
|
56
|
+
| `unknown-agent` | Agent token does not resolve to a supported agent; the error lists supported agents. |
|
|
57
|
+
| `not-found` | No skill folder exists at any tier; the error lists searched paths in order. |
|
|
58
|
+
|
|
59
|
+
Collisions never abort. The bridge emits one `BridgeWarning` per skipped ref, leaves no state for that ref, and continues the batch.
|
|
60
|
+
|
|
61
|
+
| Warning kind | Skip condition |
|
|
62
|
+
|--------------|----------------|
|
|
63
|
+
| `local-collision` | Target folder already exists in the spawning agent's local skill dir. |
|
|
64
|
+
| `global-collision` | Target folder already exists in the spawning agent's global skill dir. |
|
|
65
|
+
| `self-reference` | The spawning agent references its own native skill. |
|
|
66
|
+
| `intra-batch-collision` | Two refs produce the same target basename; first input ref wins. |
|
|
67
|
+
|
|
68
|
+
Native skills are never overwritten. Callers, including the spawn runner, surface `manifest.warnings` through the design-system warning channel before launching the agent.
|
|
69
|
+
|
|
70
|
+
`cleanupBridgedSkills(manifest)` removes only bridge-created targets and empty parent directories recorded in the manifest. It is idempotent.
|
|
71
|
+
|
|
72
|
+
When bridge creates entries inside a git repository, `.git/info/exclude` gets a per-run marked block containing only successfully bridged target entries. Cleanup removes only that run's marked block.
|
|
73
|
+
|
|
74
|
+
## Public API
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import {
|
|
78
|
+
bridgeActiveSkills,
|
|
79
|
+
cleanupBridgedSkills,
|
|
80
|
+
resolveSkillReference,
|
|
81
|
+
configure,
|
|
82
|
+
unconfigure,
|
|
83
|
+
installSkill,
|
|
84
|
+
resolveAgentSupport,
|
|
85
|
+
getAgentConfig,
|
|
86
|
+
resolveSkillDir,
|
|
87
|
+
supportedAgents,
|
|
88
|
+
} from "@poe-code/agent-skill-config";
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Exported types include `AgentSkillConfig`, `AgentSupportResult`, `SkillScope`, `SkillResolution`, `SkillResolutionFailure`, `SkillSource`, `BridgeEntry`, `BridgeManifest`, `BridgeWarning`, `BridgeWarningKind`, `ApplyOptions`, `SkillFile`, `InstallSkillOptions`, and `InstallSkillResult`.
|
|
92
|
+
|
|
93
|
+
`agentSkillConfigs` is internal and is not exported.
|
|
94
|
+
|
|
95
|
+
## Config Options
|
|
96
|
+
|
|
97
|
+
This package exposes no user-facing config options.
|
|
98
|
+
|
|
99
|
+
Internal agent skill config has two fields: `localSkillDir` and `globalSkillDir`. Callers can read supported resolved config through `resolveAgentSupport()` or `getAgentConfig()`, and can turn a resolved config into an absolute path with `resolveSkillDir()`.
|
|
100
|
+
|
|
101
|
+
## Environment Variables
|
|
102
|
+
|
|
103
|
+
This package exposes no public environment variables.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ApplyOptions, SkillFile } from "./types.js";
|
|
2
|
+
export declare class UnsupportedAgentError extends Error {
|
|
3
|
+
constructor(agentId: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function configure(agentId: string, options: ApplyOptions): Promise<void>;
|
|
6
|
+
export declare function unconfigure(agentId: string, options: ApplyOptions & {
|
|
7
|
+
force?: boolean;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export type InstallSkillOptions = {
|
|
10
|
+
fs: ApplyOptions["fs"];
|
|
11
|
+
cwd: string;
|
|
12
|
+
homeDir: string;
|
|
13
|
+
scope: ApplyOptions["scope"];
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
observers?: ApplyOptions["observers"];
|
|
16
|
+
};
|
|
17
|
+
export type InstallSkillResult = {
|
|
18
|
+
skillPath: string;
|
|
19
|
+
displayPath: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Install a skill for an agent.
|
|
23
|
+
* Creates folder structure: skillDir/<skill.name>/SKILL.md
|
|
24
|
+
*/
|
|
25
|
+
export declare function installSkill(agentId: string, skill: SkillFile, options: InstallSkillOptions): Promise<InstallSkillResult>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { fileMutation, runMutations, templateMutation } from "@poe-code/config-mutations";
|
|
2
|
+
import { resolveAgentSupport } from "./configs.js";
|
|
3
|
+
import { createTemplateLoader } from "./templates.js";
|
|
4
|
+
export class UnsupportedAgentError extends Error {
|
|
5
|
+
constructor(agentId) {
|
|
6
|
+
super(`Unsupported agent: ${agentId}`);
|
|
7
|
+
this.name = "UnsupportedAgentError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function toHomeRelative(localSkillDir) {
|
|
11
|
+
if (localSkillDir.startsWith("~/") || localSkillDir === "~") {
|
|
12
|
+
return localSkillDir;
|
|
13
|
+
}
|
|
14
|
+
const normalized = localSkillDir.startsWith("./")
|
|
15
|
+
? localSkillDir.slice(2)
|
|
16
|
+
: localSkillDir;
|
|
17
|
+
return `~/${normalized}`;
|
|
18
|
+
}
|
|
19
|
+
const bundledSkillTemplateIds = ["poe-generate.md"];
|
|
20
|
+
async function pathExists(fs, targetPath) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.stat(targetPath);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function configure(agentId, options) {
|
|
33
|
+
const support = resolveAgentSupport(agentId);
|
|
34
|
+
if (support.status !== "supported") {
|
|
35
|
+
throw new UnsupportedAgentError(agentId);
|
|
36
|
+
}
|
|
37
|
+
const scope = options.scope ?? "global";
|
|
38
|
+
const config = support.config;
|
|
39
|
+
const skillDir = scope === "global" ? config.globalSkillDir : toHomeRelative(config.localSkillDir);
|
|
40
|
+
const homeDir = scope === "global" ? options.homeDir : options.cwd;
|
|
41
|
+
const templateLoader = createTemplateLoader();
|
|
42
|
+
for (const templateId of bundledSkillTemplateIds) {
|
|
43
|
+
const targetPath = `${scope === "global" ? options.homeDir : options.cwd}/${skillDir.slice(2)}/${templateId}`;
|
|
44
|
+
if (await pathExists(options.fs, targetPath)) {
|
|
45
|
+
const existing = await options.fs.readFile(targetPath, "utf8");
|
|
46
|
+
if (existing !== (await templateLoader(templateId))) {
|
|
47
|
+
throw new Error(`Skill already exists: ${targetPath}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await runMutations([
|
|
52
|
+
fileMutation.ensureDirectory({
|
|
53
|
+
path: skillDir,
|
|
54
|
+
label: `Ensure directory ${skillDir}`
|
|
55
|
+
}),
|
|
56
|
+
...bundledSkillTemplateIds.map((templateId) => templateMutation.write({
|
|
57
|
+
target: `${skillDir}/${templateId}`,
|
|
58
|
+
templateId,
|
|
59
|
+
label: `Write bundled skill ${templateId} to ${skillDir}`
|
|
60
|
+
}))
|
|
61
|
+
], {
|
|
62
|
+
fs: options.fs,
|
|
63
|
+
homeDir,
|
|
64
|
+
dryRun: options.dryRun,
|
|
65
|
+
observers: options.observers,
|
|
66
|
+
templates: templateLoader
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function unconfigure(agentId, options) {
|
|
70
|
+
const support = resolveAgentSupport(agentId);
|
|
71
|
+
if (support.status !== "supported") {
|
|
72
|
+
throw new UnsupportedAgentError(agentId);
|
|
73
|
+
}
|
|
74
|
+
const scope = options.scope ?? "global";
|
|
75
|
+
const config = support.config;
|
|
76
|
+
const skillDir = scope === "global" ? config.globalSkillDir : toHomeRelative(config.localSkillDir);
|
|
77
|
+
const homeDir = scope === "global" ? options.homeDir : options.cwd;
|
|
78
|
+
const templateLoader = createTemplateLoader();
|
|
79
|
+
const removeBundledSkills = [];
|
|
80
|
+
for (const templateId of bundledSkillTemplateIds) {
|
|
81
|
+
const targetPath = `${scope === "global" ? options.homeDir : options.cwd}/${skillDir.slice(2)}/${templateId}`;
|
|
82
|
+
if (await pathExists(options.fs, targetPath)) {
|
|
83
|
+
const existing = await options.fs.readFile(targetPath, "utf8");
|
|
84
|
+
if (existing === (await templateLoader(templateId))) {
|
|
85
|
+
removeBundledSkills.push(fileMutation.remove({
|
|
86
|
+
target: `${skillDir}/${templateId}`,
|
|
87
|
+
label: `Remove bundled skill ${templateId} from ${skillDir}`
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await runMutations([
|
|
93
|
+
...removeBundledSkills,
|
|
94
|
+
fileMutation.removeDirectory({
|
|
95
|
+
path: skillDir,
|
|
96
|
+
force: options.force,
|
|
97
|
+
label: `Remove skills directory ${skillDir}`
|
|
98
|
+
})
|
|
99
|
+
], {
|
|
100
|
+
fs: options.fs,
|
|
101
|
+
homeDir,
|
|
102
|
+
dryRun: options.dryRun,
|
|
103
|
+
observers: options.observers
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const SKILL_TEMPLATE_ID = "__skill_content__";
|
|
107
|
+
/**
|
|
108
|
+
* Install a skill for an agent.
|
|
109
|
+
* Creates folder structure: skillDir/<skill.name>/SKILL.md
|
|
110
|
+
*/
|
|
111
|
+
export async function installSkill(agentId, skill, options) {
|
|
112
|
+
const support = resolveAgentSupport(agentId);
|
|
113
|
+
if (support.status !== "supported") {
|
|
114
|
+
throw new UnsupportedAgentError(agentId);
|
|
115
|
+
}
|
|
116
|
+
const scope = options.scope ?? "local";
|
|
117
|
+
const config = support.config;
|
|
118
|
+
if (skill.name.length === 0 ||
|
|
119
|
+
skill.name === "." ||
|
|
120
|
+
skill.name === ".." ||
|
|
121
|
+
skill.name.includes("/") ||
|
|
122
|
+
skill.name.includes("\\") ||
|
|
123
|
+
skill.name.includes("\n") ||
|
|
124
|
+
skill.name.includes("\r")) {
|
|
125
|
+
throw new Error(`Invalid skill name: ${skill.name}`);
|
|
126
|
+
}
|
|
127
|
+
// Use home-relative paths for mutations (same pattern as configure/unconfigure)
|
|
128
|
+
const skillDir = scope === "global" ? config.globalSkillDir : toHomeRelative(config.localSkillDir);
|
|
129
|
+
const skillFolderPath = `${skillDir}/${skill.name}`;
|
|
130
|
+
const skillFilePath = `${skillFolderPath}/SKILL.md`;
|
|
131
|
+
const displayPath = `${scope === "global" ? config.globalSkillDir : config.localSkillDir}/${skill.name}/SKILL.md`;
|
|
132
|
+
const absoluteSkillPath = `${scope === "global" ? options.homeDir : options.cwd}/${skillFilePath.slice(2)}`;
|
|
133
|
+
if (await pathExists(options.fs, absoluteSkillPath)) {
|
|
134
|
+
throw new Error(`Skill already exists: ${displayPath}`);
|
|
135
|
+
}
|
|
136
|
+
await runMutations([
|
|
137
|
+
fileMutation.ensureDirectory({
|
|
138
|
+
path: skillFolderPath,
|
|
139
|
+
label: `Ensure skill directory ${skill.name}`
|
|
140
|
+
}),
|
|
141
|
+
templateMutation.write({
|
|
142
|
+
target: skillFilePath,
|
|
143
|
+
templateId: SKILL_TEMPLATE_ID,
|
|
144
|
+
label: `Write skill ${skill.name}`
|
|
145
|
+
})
|
|
146
|
+
], {
|
|
147
|
+
fs: options.fs,
|
|
148
|
+
homeDir: scope === "global" ? options.homeDir : options.cwd,
|
|
149
|
+
dryRun: options.dryRun,
|
|
150
|
+
observers: options.observers,
|
|
151
|
+
templates: async (templateId) => {
|
|
152
|
+
if (templateId === SKILL_TEMPLATE_ID) {
|
|
153
|
+
return skill.content;
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Unknown template: ${templateId}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return { skillPath: skillFilePath, displayPath };
|
|
159
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BridgeEntry {
|
|
2
|
+
ref: string;
|
|
3
|
+
sourcePath: string;
|
|
4
|
+
targetPath: string;
|
|
5
|
+
createdParents: string[];
|
|
6
|
+
}
|
|
7
|
+
export type BridgeWarningKind = "local-collision" | "global-collision" | "self-reference" | "intra-batch-collision";
|
|
8
|
+
export interface BridgeWarning {
|
|
9
|
+
kind: BridgeWarningKind;
|
|
10
|
+
ref: string;
|
|
11
|
+
sourcePath: string;
|
|
12
|
+
conflictingPath: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
export interface BridgeManifest {
|
|
16
|
+
spawnAgentId: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
runId: string;
|
|
19
|
+
entries: BridgeEntry[];
|
|
20
|
+
warnings: BridgeWarning[];
|
|
21
|
+
}
|
|
22
|
+
export declare function bridgeActiveSkills(spawnAgentId: string, cwd: string, refs: string[], homeDir: string, runId: string): BridgeManifest;
|
|
23
|
+
export declare function cleanupBridgedSkills(manifest: BridgeManifest): void;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getAgentConfig, resolveAgentSupport, resolveSkillDir, supportedAgents } from "./configs.js";
|
|
5
|
+
import { appendExcludeBlock, removeExcludeBlock } from "./git-exclude.js";
|
|
6
|
+
import { resolveSkillReference } from "./resolve-skill-reference.js";
|
|
7
|
+
const activeTargets = new Map();
|
|
8
|
+
const manifestStates = new WeakMap();
|
|
9
|
+
const ownershipFileName = ".poe-code-bridge-owner";
|
|
10
|
+
function isNodeError(error) {
|
|
11
|
+
return error instanceof Error && "code" in error;
|
|
12
|
+
}
|
|
13
|
+
function pathExists(targetPath) {
|
|
14
|
+
try {
|
|
15
|
+
fs.statSync(targetPath);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function assertNoSymbolicLink(targetPath) {
|
|
26
|
+
const parsed = path.parse(path.resolve(targetPath));
|
|
27
|
+
let current = parsed.root;
|
|
28
|
+
for (const segment of path.resolve(targetPath).slice(parsed.root.length).split(path.sep)) {
|
|
29
|
+
if (segment.length === 0) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
current = path.join(current, segment);
|
|
33
|
+
try {
|
|
34
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
35
|
+
throw new Error(`Refusing to bridge skills through symbolic link: ${current}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isDirectory(targetPath) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.statSync(targetPath).isDirectory();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function formatResolutionFailureError(failures) {
|
|
58
|
+
const malformed = failures.filter((failure) => failure.kind === "malformed");
|
|
59
|
+
const unknownAgent = failures.filter((failure) => failure.kind === "unknown-agent");
|
|
60
|
+
const notFound = failures.filter((failure) => failure.kind === "not-found");
|
|
61
|
+
const lines = [
|
|
62
|
+
`Failed to bridge active skills: ${failures.length} skill reference(s) could not be resolved.`
|
|
63
|
+
];
|
|
64
|
+
if (malformed.length > 0) {
|
|
65
|
+
lines.push("", "Malformed skill references:");
|
|
66
|
+
for (const failure of malformed) {
|
|
67
|
+
lines.push(`- ${failure.ref}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push('Expected syntax: "<name>" or "<agentId>/<name>".');
|
|
70
|
+
}
|
|
71
|
+
if (unknownAgent.length > 0) {
|
|
72
|
+
lines.push("", "Unknown agent references:");
|
|
73
|
+
for (const failure of unknownAgent) {
|
|
74
|
+
lines.push(`- ${failure.ref} (agent token: ${failure.agentInput})`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(`Supported agents: ${supportedAgents.join(", ")}.`);
|
|
77
|
+
}
|
|
78
|
+
if (notFound.length > 0) {
|
|
79
|
+
lines.push("", "Not found skill references.");
|
|
80
|
+
for (const failure of notFound) {
|
|
81
|
+
lines.push(`- ${failure.ref}`);
|
|
82
|
+
lines.push(" searched paths:");
|
|
83
|
+
for (const searchedPath of failure.searchedPaths) {
|
|
84
|
+
lines.push(` - ${searchedPath}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return new Error(lines.join("\n"));
|
|
89
|
+
}
|
|
90
|
+
function copyDirectory(sourcePath, targetPath) {
|
|
91
|
+
assertNoSymbolicLink(sourcePath);
|
|
92
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
93
|
+
for (const dirent of fs.readdirSync(sourcePath, { withFileTypes: true })) {
|
|
94
|
+
const childSource = path.join(sourcePath, dirent.name);
|
|
95
|
+
const childTarget = path.join(targetPath, dirent.name);
|
|
96
|
+
if (dirent.isSymbolicLink()) {
|
|
97
|
+
throw new Error(`Refusing to bridge skill containing symbolic link: ${childSource}`);
|
|
98
|
+
}
|
|
99
|
+
if (dirent.isDirectory()) {
|
|
100
|
+
copyDirectory(childSource, childTarget);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (dirent.isFile()) {
|
|
104
|
+
fs.copyFileSync(childSource, childTarget);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function directoryFingerprint(targetPath) {
|
|
109
|
+
const hash = createHash("sha256");
|
|
110
|
+
function visit(currentPath, relativePath) {
|
|
111
|
+
const stats = fs.lstatSync(currentPath);
|
|
112
|
+
if (stats.isSymbolicLink()) {
|
|
113
|
+
throw new Error(`Refusing to inspect bridged skill through symbolic link: ${currentPath}`);
|
|
114
|
+
}
|
|
115
|
+
if (stats.isDirectory()) {
|
|
116
|
+
hash.update(`d:${relativePath}\n`);
|
|
117
|
+
for (const name of fs.readdirSync(currentPath).sort()) {
|
|
118
|
+
visit(path.join(currentPath, name), path.join(relativePath, name));
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (stats.isFile()) {
|
|
123
|
+
hash.update(`f:${relativePath}\n`);
|
|
124
|
+
hash.update(fs.readFileSync(currentPath));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Refusing to bridge unsupported filesystem entry: ${currentPath}`);
|
|
128
|
+
}
|
|
129
|
+
visit(targetPath, ".");
|
|
130
|
+
return hash.digest("hex");
|
|
131
|
+
}
|
|
132
|
+
function ownershipPath(targetPath) {
|
|
133
|
+
return path.join(targetPath, ownershipFileName);
|
|
134
|
+
}
|
|
135
|
+
function hasOwnershipToken(targetPath, token) {
|
|
136
|
+
try {
|
|
137
|
+
return fs.readFileSync(ownershipPath(targetPath), "utf8") === token;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function collectMissingParents(targetPath) {
|
|
147
|
+
const parents = [];
|
|
148
|
+
let current = path.dirname(targetPath);
|
|
149
|
+
while (!pathExists(current)) {
|
|
150
|
+
parents.push(current);
|
|
151
|
+
const parent = path.dirname(current);
|
|
152
|
+
if (parent === current) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
current = parent;
|
|
156
|
+
}
|
|
157
|
+
return parents.reverse();
|
|
158
|
+
}
|
|
159
|
+
function removeDirectoryIfEmpty(targetPath) {
|
|
160
|
+
try {
|
|
161
|
+
fs.rmdirSync(targetPath);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (isNodeError(error) &&
|
|
165
|
+
(error.code === "ENOENT" || error.code === "ENOTEMPTY" || error.code === "EEXIST")) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function removeTarget(targetPath) {
|
|
172
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
function rollbackEntries(entries) {
|
|
175
|
+
for (const entry of [...entries].reverse()) {
|
|
176
|
+
const activeTarget = activeTargets.get(entry.targetPath);
|
|
177
|
+
if (activeTarget && activeTarget.references > 1) {
|
|
178
|
+
activeTarget.references -= 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
activeTargets.delete(entry.targetPath);
|
|
182
|
+
removeTarget(entry.targetPath);
|
|
183
|
+
for (const parent of [...(activeTarget?.createdParents ?? entry.createdParents)].reverse()) {
|
|
184
|
+
removeDirectoryIfEmpty(parent);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function toCwdRelative(cwd, targetPath) {
|
|
189
|
+
return path.relative(cwd, targetPath);
|
|
190
|
+
}
|
|
191
|
+
function warning(kind, ref, sourcePath, conflictingPath) {
|
|
192
|
+
const messages = {
|
|
193
|
+
"intra-batch-collision": `Skipping ${ref}: an earlier bridged skill already targets ${conflictingPath}.`,
|
|
194
|
+
"local-collision": `Skipping ${ref}: local skill already exists at ${conflictingPath}.`,
|
|
195
|
+
"global-collision": `Skipping ${ref}: global skill already exists at ${conflictingPath}.`,
|
|
196
|
+
"self-reference": `Skipping ${ref}: spawning agent already sees this native skill at ${conflictingPath}.`
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
kind,
|
|
200
|
+
ref,
|
|
201
|
+
sourcePath,
|
|
202
|
+
conflictingPath,
|
|
203
|
+
message: messages[kind]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export function bridgeActiveSkills(spawnAgentId, cwd, refs, homeDir, runId) {
|
|
207
|
+
const spawnConfig = getAgentConfig(spawnAgentId);
|
|
208
|
+
const spawnSupport = resolveAgentSupport(spawnAgentId);
|
|
209
|
+
if (!spawnConfig || spawnSupport.status !== "supported" || !spawnSupport.id) {
|
|
210
|
+
throw new Error(`Unsupported spawn agent "${spawnAgentId}". Supported agents: ${supportedAgents.join(", ")}.`);
|
|
211
|
+
}
|
|
212
|
+
const targetDir = resolveSkillDir(spawnConfig, "local", cwd);
|
|
213
|
+
const globalTargetDir = resolveSkillDir(spawnConfig, "global", cwd, homeDir);
|
|
214
|
+
const resolutions = refs.map((ref) => resolveSkillReference(ref, cwd, homeDir));
|
|
215
|
+
const failures = resolutions.filter((resolution) => resolution.kind !== "resolved");
|
|
216
|
+
if (failures.length > 0) {
|
|
217
|
+
throw formatResolutionFailureError(failures);
|
|
218
|
+
}
|
|
219
|
+
const sources = resolutions.map((source, index) => ({
|
|
220
|
+
ref: refs[index],
|
|
221
|
+
source: source,
|
|
222
|
+
targetPath: path.resolve(targetDir, source.name),
|
|
223
|
+
globalTargetPath: path.resolve(globalTargetDir, source.name)
|
|
224
|
+
}));
|
|
225
|
+
const entries = [];
|
|
226
|
+
const warnings = [];
|
|
227
|
+
const claimedTargets = new Set();
|
|
228
|
+
try {
|
|
229
|
+
for (const item of sources) {
|
|
230
|
+
if (claimedTargets.has(item.targetPath)) {
|
|
231
|
+
warnings.push(warning("intra-batch-collision", item.ref, item.source.sourcePath, item.targetPath));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (item.source.sourceAgentId === spawnSupport.id) {
|
|
235
|
+
warnings.push(warning("self-reference", item.ref, item.source.sourcePath, item.source.sourcePath));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const activeTarget = activeTargets.get(item.targetPath);
|
|
239
|
+
if (activeTarget && pathExists(item.targetPath)) {
|
|
240
|
+
if (activeTarget.sourcePath === item.source.sourcePath &&
|
|
241
|
+
hasOwnershipToken(item.targetPath, activeTarget.token) &&
|
|
242
|
+
directoryFingerprint(item.targetPath) === activeTarget.fingerprint) {
|
|
243
|
+
activeTarget.references += 1;
|
|
244
|
+
claimedTargets.add(item.targetPath);
|
|
245
|
+
entries.push({
|
|
246
|
+
ref: item.ref,
|
|
247
|
+
sourcePath: item.source.sourcePath,
|
|
248
|
+
targetPath: item.targetPath,
|
|
249
|
+
createdParents: []
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
activeTargets.delete(item.targetPath);
|
|
254
|
+
}
|
|
255
|
+
if (pathExists(item.targetPath)) {
|
|
256
|
+
warnings.push(warning("local-collision", item.ref, item.source.sourcePath, item.targetPath));
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (isDirectory(item.globalTargetPath)) {
|
|
260
|
+
warnings.push(warning("global-collision", item.ref, item.source.sourcePath, item.globalTargetPath));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const createdParents = collectMissingParents(item.targetPath);
|
|
264
|
+
assertNoSymbolicLink(path.dirname(item.targetPath));
|
|
265
|
+
fs.mkdirSync(path.dirname(item.targetPath), { recursive: true });
|
|
266
|
+
const token = randomUUID();
|
|
267
|
+
try {
|
|
268
|
+
copyDirectory(item.source.sourcePath, item.targetPath);
|
|
269
|
+
fs.writeFileSync(ownershipPath(item.targetPath), token, "utf8");
|
|
270
|
+
const fingerprint = directoryFingerprint(item.targetPath);
|
|
271
|
+
claimedTargets.add(item.targetPath);
|
|
272
|
+
entries.push({
|
|
273
|
+
ref: item.ref,
|
|
274
|
+
sourcePath: item.source.sourcePath,
|
|
275
|
+
targetPath: item.targetPath,
|
|
276
|
+
createdParents
|
|
277
|
+
});
|
|
278
|
+
activeTargets.set(item.targetPath, {
|
|
279
|
+
createdParents,
|
|
280
|
+
fingerprint,
|
|
281
|
+
sourcePath: item.source.sourcePath,
|
|
282
|
+
references: 1,
|
|
283
|
+
token
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
removeTarget(item.targetPath);
|
|
288
|
+
for (const parent of [...createdParents].reverse()) {
|
|
289
|
+
removeDirectoryIfEmpty(parent);
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let excludeBlockId;
|
|
295
|
+
if (entries.length > 0) {
|
|
296
|
+
excludeBlockId = appendExcludeBlock(cwd, runId, entries.map((entry) => toCwdRelative(cwd, entry.targetPath)));
|
|
297
|
+
}
|
|
298
|
+
const manifest = {
|
|
299
|
+
spawnAgentId,
|
|
300
|
+
cwd,
|
|
301
|
+
runId,
|
|
302
|
+
entries,
|
|
303
|
+
warnings
|
|
304
|
+
};
|
|
305
|
+
manifestStates.set(manifest, { excludeBlockId });
|
|
306
|
+
return manifest;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
rollbackEntries(entries);
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export function cleanupBridgedSkills(manifest) {
|
|
314
|
+
const state = manifestStates.get(manifest);
|
|
315
|
+
if (state?.cleaned) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
removeExcludeBlock(manifest.cwd, state?.excludeBlockId ?? manifest.runId);
|
|
319
|
+
for (const entry of manifest.entries) {
|
|
320
|
+
const activeTarget = activeTargets.get(entry.targetPath);
|
|
321
|
+
if (!activeTarget) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (activeTarget.references > 1) {
|
|
325
|
+
activeTarget.references -= 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
activeTargets.delete(entry.targetPath);
|
|
329
|
+
if (pathExists(entry.targetPath) &&
|
|
330
|
+
hasOwnershipToken(entry.targetPath, activeTarget.token) &&
|
|
331
|
+
directoryFingerprint(entry.targetPath) === activeTarget.fingerprint) {
|
|
332
|
+
removeTarget(entry.targetPath);
|
|
333
|
+
for (const parent of [...activeTarget.createdParents].reverse()) {
|
|
334
|
+
removeDirectoryIfEmpty(parent);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (state) {
|
|
339
|
+
state.cleaned = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AgentSkillConfig {
|
|
2
|
+
globalSkillDir: string;
|
|
3
|
+
localSkillDir: string;
|
|
4
|
+
}
|
|
5
|
+
export type SkillScope = "global" | "local";
|
|
6
|
+
export declare const supportedAgents: readonly string[];
|
|
7
|
+
export type AgentSupportStatus = "supported" | "unsupported" | "unknown";
|
|
8
|
+
export interface AgentSupportResult {
|
|
9
|
+
status: AgentSupportStatus;
|
|
10
|
+
input: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
config?: AgentSkillConfig;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveAgentSupport(input: string, registry?: Record<string, AgentSkillConfig>): AgentSupportResult;
|
|
15
|
+
export declare function getAgentConfig(agentId: string): AgentSkillConfig | undefined;
|
|
16
|
+
export declare function resolveSkillDir(config: AgentSkillConfig, scope: SkillScope, cwd: string, homeDir?: string): string;
|