agent-first-data 0.20.0 → 0.22.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 +1 -10
- package/dist/cli.d.ts +24 -11
- package/dist/cli.js +68 -21
- package/package.json +1 -9
- package/assets/skills/agent-first-data/SKILL.md +0 -280
- package/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -66
- package/assets/skills/agent-first-data/references/registry.json +0 -251
- package/assets/skills/agent-first-data/references/rules.md +0 -358
- package/dist/skill.d.ts +0 -96
- package/dist/skill.js +0 -493
- package/dist/stream_redirect.d.ts +0 -21
- package/dist/stream_redirect.js +0 -128
package/dist/skill.d.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reusable Agent Skill installer for spore CLIs.
|
|
3
|
-
*
|
|
4
|
-
* A spore that embeds its `SKILL.md` describes itself with a {@link SkillSpec} and calls
|
|
5
|
-
* {@link runSkillAdmin} to install, uninstall, or report status of that skill across
|
|
6
|
-
* supported coding agents (Codex, Claude Code, opencode, Hermes).
|
|
7
|
-
*
|
|
8
|
-
* The function performs the filesystem work and returns the protocol value (rendered by
|
|
9
|
-
* the caller with `render`) or throws a {@link SkillError}. It never writes to
|
|
10
|
-
* stdout/stderr itself.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Identity of the skill being managed and the tool that manages it.
|
|
14
|
-
*
|
|
15
|
-
* `name` is both the skill directory name and the `name:` front-matter field. `source` is
|
|
16
|
-
* the bundled `SKILL.md`. `title` is a human label for error messages. `markerSlug` seeds
|
|
17
|
-
* the managed-skill marker and the `Generated by <slug> skill install` comment, and is
|
|
18
|
-
* referenced in hints (e.g. `afwidget`).
|
|
19
|
-
*/
|
|
20
|
-
export interface SkillSpec {
|
|
21
|
-
name: string;
|
|
22
|
-
source: string;
|
|
23
|
-
title: string;
|
|
24
|
-
markerSlug: string;
|
|
25
|
-
}
|
|
26
|
-
/** The skill action to perform. */
|
|
27
|
-
export type SkillAction = "status" | "install" | "uninstall";
|
|
28
|
-
/** Where to install the skill. */
|
|
29
|
-
export type SkillScope = "personal" | "workspace";
|
|
30
|
-
/** Which agent target(s) to manage. */
|
|
31
|
-
export type SkillAgentSelection = "all" | "codex" | "claude-code" | "opencode" | "hermes";
|
|
32
|
-
/** A concrete agent a skill is installed for (no `all`). */
|
|
33
|
-
export type SkillAgent = "codex" | "claude-code" | "opencode" | "hermes";
|
|
34
|
-
/** Options shared by every skill action. */
|
|
35
|
-
export interface SkillOptions {
|
|
36
|
-
agent: SkillAgentSelection;
|
|
37
|
-
scope: SkillScope;
|
|
38
|
-
/** Explicit skills directory; requires a single concrete `agent`. */
|
|
39
|
-
skillsDir?: string;
|
|
40
|
-
/** Overwrite or remove a skill that this tool did not manage. */
|
|
41
|
-
force?: boolean;
|
|
42
|
-
}
|
|
43
|
-
/** A skill admin failure with an operator-facing message and optional hint. */
|
|
44
|
-
export declare class SkillError extends Error {
|
|
45
|
-
readonly hint?: string;
|
|
46
|
-
constructor(message: string, hint?: string);
|
|
47
|
-
}
|
|
48
|
-
/** Per-target outcome of `status` / `install`. */
|
|
49
|
-
export interface SkillTargetStatus {
|
|
50
|
-
agent: SkillAgent;
|
|
51
|
-
scope: SkillScope;
|
|
52
|
-
skills_dir: string;
|
|
53
|
-
skill_path: string;
|
|
54
|
-
installed: boolean;
|
|
55
|
-
managed: boolean;
|
|
56
|
-
valid: boolean;
|
|
57
|
-
current: boolean;
|
|
58
|
-
validation_error: string | null;
|
|
59
|
-
}
|
|
60
|
-
/** Per-target outcome of `uninstall`. */
|
|
61
|
-
export interface SkillUninstallStatus {
|
|
62
|
-
agent: SkillAgent;
|
|
63
|
-
scope: SkillScope;
|
|
64
|
-
skills_dir: string;
|
|
65
|
-
skill_path: string;
|
|
66
|
-
removed: boolean;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The result of a skill action. Read fields off the discriminated union (narrow on
|
|
70
|
-
* `code`), or pass it to `render` to serialize — every field is already JSON-shaped.
|
|
71
|
-
*/
|
|
72
|
-
export type SkillReport = {
|
|
73
|
-
code: "skill_status";
|
|
74
|
-
skill: string;
|
|
75
|
-
installed_all: boolean;
|
|
76
|
-
valid_all: boolean;
|
|
77
|
-
current_all: boolean;
|
|
78
|
-
targets: SkillTargetStatus[];
|
|
79
|
-
} | {
|
|
80
|
-
code: "skill_install";
|
|
81
|
-
skill: string;
|
|
82
|
-
installed: boolean;
|
|
83
|
-
targets: SkillTargetStatus[];
|
|
84
|
-
hint: string;
|
|
85
|
-
} | {
|
|
86
|
-
code: "skill_uninstall";
|
|
87
|
-
skill: string;
|
|
88
|
-
removed_any: boolean;
|
|
89
|
-
targets: SkillUninstallStatus[];
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* Install, uninstall, or report status of `spec`'s skill across the selected agent
|
|
93
|
-
* target(s). Returns the protocol value (caller renders with `render`) or throws a
|
|
94
|
-
* {@link SkillError}. Does not touch stdout/stderr.
|
|
95
|
-
*/
|
|
96
|
-
export declare function runSkillAdmin(spec: SkillSpec, action: SkillAction, options: SkillOptions): SkillReport;
|
package/dist/skill.js
DELETED
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reusable Agent Skill installer for spore CLIs.
|
|
3
|
-
*
|
|
4
|
-
* A spore that embeds its `SKILL.md` describes itself with a {@link SkillSpec} and calls
|
|
5
|
-
* {@link runSkillAdmin} to install, uninstall, or report status of that skill across
|
|
6
|
-
* supported coding agents (Codex, Claude Code, opencode, Hermes).
|
|
7
|
-
*
|
|
8
|
-
* The function performs the filesystem work and returns the protocol value (rendered by
|
|
9
|
-
* the caller with `render`) or throws a {@link SkillError}. It never writes to
|
|
10
|
-
* stdout/stderr itself.
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from "node:fs";
|
|
13
|
-
import { basename, join } from "node:path";
|
|
14
|
-
const SKILL_FILE_NAME = "SKILL.md";
|
|
15
|
-
const FNV1A64_OFFSET = 0xcbf29ce484222325n;
|
|
16
|
-
const FNV1A64_PRIME = 0x100000001b3n;
|
|
17
|
-
const FNV1A64_MASK = 0xffffffffffffffffn;
|
|
18
|
-
/** A skill admin failure with an operator-facing message and optional hint. */
|
|
19
|
-
export class SkillError extends Error {
|
|
20
|
-
hint;
|
|
21
|
-
constructor(message, hint) {
|
|
22
|
-
super(message);
|
|
23
|
-
this.name = "SkillError";
|
|
24
|
-
this.hint = hint;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Install, uninstall, or report status of `spec`'s skill across the selected agent
|
|
29
|
-
* target(s). Returns the protocol value (caller renders with `render`) or throws a
|
|
30
|
-
* {@link SkillError}. Does not touch stdout/stderr.
|
|
31
|
-
*/
|
|
32
|
-
export function runSkillAdmin(spec, action, options) {
|
|
33
|
-
validateSpec(spec);
|
|
34
|
-
switch (action) {
|
|
35
|
-
case "status":
|
|
36
|
-
return status(spec, options);
|
|
37
|
-
case "install":
|
|
38
|
-
return install(spec, options);
|
|
39
|
-
case "uninstall":
|
|
40
|
-
return uninstall(spec, options);
|
|
41
|
-
default:
|
|
42
|
-
throw new SkillError(`unknown skill action '${action}'`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function status(spec, options) {
|
|
46
|
-
const targets = resolveTargets(spec, options);
|
|
47
|
-
const statuses = targets.map((target) => targetStatus(spec, target));
|
|
48
|
-
return {
|
|
49
|
-
code: "skill_status",
|
|
50
|
-
skill: spec.name,
|
|
51
|
-
installed_all: statuses.every((s) => s.installed),
|
|
52
|
-
valid_all: statuses.every((s) => s.valid),
|
|
53
|
-
current_all: statuses.every((s) => s.current),
|
|
54
|
-
targets: statuses,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function install(spec, options) {
|
|
58
|
-
validateSkillText(spec, spec.source);
|
|
59
|
-
const targets = resolveTargets(spec, options);
|
|
60
|
-
const content = managedSkillContents(spec);
|
|
61
|
-
const installed = [];
|
|
62
|
-
for (const target of targets) {
|
|
63
|
-
fs.mkdirSync(target.skillDir, { recursive: true });
|
|
64
|
-
const st = lstatOrNull(target.skillPath);
|
|
65
|
-
if (st !== null) {
|
|
66
|
-
if (st.isSymbolicLink()) {
|
|
67
|
-
if (!options.force) {
|
|
68
|
-
throw new SkillError(`refusing to overwrite symlinked skill at ${target.skillPath}`, "pass --force to replace the symlink itself, or choose another --skills-dir");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else if (!isManagedOrBundledSkill(spec, target.skillPath) && !options.force) {
|
|
72
|
-
throw new SkillError(`refusing to overwrite unmanaged skill at ${target.skillPath}`, "pass --force to replace it, or choose another --skills-dir");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
writeSkillAtomic(target, content);
|
|
76
|
-
validateInstalledSkill(spec, target.skillPath);
|
|
77
|
-
installed.push(targetStatus(spec, target));
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
code: "skill_install",
|
|
81
|
-
skill: spec.name,
|
|
82
|
-
installed: true,
|
|
83
|
-
targets: installed,
|
|
84
|
-
hint: "restart the agent so it reloads installed skills",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
function uninstall(spec, options) {
|
|
88
|
-
const targets = resolveTargets(spec, options);
|
|
89
|
-
const removed = [];
|
|
90
|
-
for (const target of targets) {
|
|
91
|
-
const st = lstatOrNull(target.skillPath);
|
|
92
|
-
if (st === null) {
|
|
93
|
-
removed.push(targetUninstallStatus(target, false));
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (st.isSymbolicLink()) {
|
|
97
|
-
if (!options.force) {
|
|
98
|
-
throw new SkillError(`refusing to remove symlinked skill at ${target.skillPath}`, "pass --force to remove the symlink itself");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else if (!isManagedOrBundledSkill(spec, target.skillPath) && !options.force) {
|
|
102
|
-
throw new SkillError(`refusing to remove unmanaged skill at ${target.skillPath}`, `only skills generated by ${spec.markerSlug} skill install can be removed without --force`);
|
|
103
|
-
}
|
|
104
|
-
fs.rmSync(target.skillPath);
|
|
105
|
-
// Prune the now-empty skill dir; ignore if not empty.
|
|
106
|
-
try {
|
|
107
|
-
fs.rmdirSync(target.skillDir);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// directory not empty or already gone
|
|
111
|
-
}
|
|
112
|
-
removed.push(targetUninstallStatus(target, true));
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
code: "skill_uninstall",
|
|
116
|
-
skill: spec.name,
|
|
117
|
-
removed_any: removed.some((s) => s.removed),
|
|
118
|
-
targets: removed,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function resolveTargets(spec, options) {
|
|
122
|
-
if (options.skillsDir !== undefined && options.agent === "all") {
|
|
123
|
-
throw new SkillError("--skills-dir requires a single --agent", "custom skills directories are ambiguous when --agent all is used");
|
|
124
|
-
}
|
|
125
|
-
const { agent, scope } = options;
|
|
126
|
-
if (agent === "all" && scope === "personal") {
|
|
127
|
-
return [
|
|
128
|
-
resolveTarget(spec, "codex", "personal", undefined),
|
|
129
|
-
resolveTarget(spec, "claude-code", "personal", undefined),
|
|
130
|
-
resolveTarget(spec, "opencode", "personal", undefined),
|
|
131
|
-
resolveTarget(spec, "hermes", "personal", undefined),
|
|
132
|
-
];
|
|
133
|
-
}
|
|
134
|
-
if (agent === "all" && scope === "workspace") {
|
|
135
|
-
return [
|
|
136
|
-
resolveTarget(spec, "codex", "workspace", undefined),
|
|
137
|
-
resolveTarget(spec, "claude-code", "workspace", undefined),
|
|
138
|
-
resolveTarget(spec, "opencode", "workspace", undefined),
|
|
139
|
-
resolveTarget(spec, "hermes", "workspace", undefined),
|
|
140
|
-
];
|
|
141
|
-
}
|
|
142
|
-
if (agent === "codex") {
|
|
143
|
-
return [resolveTarget(spec, "codex", scope, options.skillsDir)];
|
|
144
|
-
}
|
|
145
|
-
if (agent === "claude-code") {
|
|
146
|
-
return [resolveTarget(spec, "claude-code", scope, options.skillsDir)];
|
|
147
|
-
}
|
|
148
|
-
if (agent === "opencode") {
|
|
149
|
-
return [resolveTarget(spec, "opencode", scope, options.skillsDir)];
|
|
150
|
-
}
|
|
151
|
-
if (agent === "hermes") {
|
|
152
|
-
return [resolveTarget(spec, "hermes", scope, options.skillsDir)];
|
|
153
|
-
}
|
|
154
|
-
throw new SkillError(`invalid --agent '${agent}'`);
|
|
155
|
-
}
|
|
156
|
-
function resolveTarget(spec, agent, scope, skillsDir) {
|
|
157
|
-
const base = skillsDir !== undefined ? expandTilde(skillsDir) : defaultSkillsDir(agent, scope);
|
|
158
|
-
const skillDir = join(base, spec.name);
|
|
159
|
-
return {
|
|
160
|
-
agent,
|
|
161
|
-
scope,
|
|
162
|
-
skillsDir: base,
|
|
163
|
-
skillDir,
|
|
164
|
-
skillPath: join(skillDir, SKILL_FILE_NAME),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
function defaultSkillsDir(agent, scope) {
|
|
168
|
-
if (agent === "codex") {
|
|
169
|
-
if (scope !== "personal") {
|
|
170
|
-
return join(process.cwd(), ".codex", "skills");
|
|
171
|
-
}
|
|
172
|
-
const codexHome = process.env["CODEX_HOME"];
|
|
173
|
-
if (codexHome !== undefined) {
|
|
174
|
-
return join(codexHome, "skills");
|
|
175
|
-
}
|
|
176
|
-
return join(homeDir(), ".codex", "skills");
|
|
177
|
-
}
|
|
178
|
-
if (agent === "claude-code") {
|
|
179
|
-
if (scope !== "personal") {
|
|
180
|
-
return join(process.cwd(), ".claude", "skills");
|
|
181
|
-
}
|
|
182
|
-
return join(homeDir(), ".claude", "skills");
|
|
183
|
-
}
|
|
184
|
-
if (agent === "opencode") {
|
|
185
|
-
if (scope !== "personal") {
|
|
186
|
-
return join(process.cwd(), ".opencode", "skills");
|
|
187
|
-
}
|
|
188
|
-
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
189
|
-
if (xdg !== undefined) {
|
|
190
|
-
return join(xdg, "opencode", "skills");
|
|
191
|
-
}
|
|
192
|
-
return join(homeDir(), ".config", "opencode", "skills");
|
|
193
|
-
}
|
|
194
|
-
// hermes
|
|
195
|
-
if (scope !== "personal") {
|
|
196
|
-
return join(process.cwd(), ".hermes", "skills");
|
|
197
|
-
}
|
|
198
|
-
const hermesHome = process.env["HERMES_HOME"];
|
|
199
|
-
if (hermesHome !== undefined) {
|
|
200
|
-
return join(hermesHome, "skills");
|
|
201
|
-
}
|
|
202
|
-
return join(homeDir(), ".hermes", "skills");
|
|
203
|
-
}
|
|
204
|
-
function targetStatus(spec, target) {
|
|
205
|
-
const st = lstatOrNull(target.skillPath);
|
|
206
|
-
const installed = st !== null;
|
|
207
|
-
let valid = false;
|
|
208
|
-
let current = false;
|
|
209
|
-
let managed = false;
|
|
210
|
-
let validationError = null;
|
|
211
|
-
if (st?.isSymbolicLink()) {
|
|
212
|
-
validationError = "target SKILL.md is a symlink; refusing to follow it";
|
|
213
|
-
}
|
|
214
|
-
else if (st?.isFile()) {
|
|
215
|
-
const text = fs.readFileSync(target.skillPath, "utf-8");
|
|
216
|
-
managed = skillTextIsManagedOrBundled(spec, text);
|
|
217
|
-
current = !skillTextHasLegacyMarker(spec, text) && normalizeSkillText(spec, text) === normalizeSkillText(spec, spec.source);
|
|
218
|
-
const error = validateSkillTextError(spec, text);
|
|
219
|
-
if (error === null) {
|
|
220
|
-
valid = true;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
validationError = error;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else if (installed) {
|
|
227
|
-
validationError = "target SKILL.md is not a regular file";
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
agent: target.agent,
|
|
231
|
-
scope: target.scope,
|
|
232
|
-
skills_dir: target.skillsDir,
|
|
233
|
-
skill_path: target.skillPath,
|
|
234
|
-
installed,
|
|
235
|
-
managed,
|
|
236
|
-
valid,
|
|
237
|
-
current,
|
|
238
|
-
validation_error: validationError,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function targetUninstallStatus(target, removed) {
|
|
242
|
-
return {
|
|
243
|
-
agent: target.agent,
|
|
244
|
-
scope: target.scope,
|
|
245
|
-
skills_dir: target.skillsDir,
|
|
246
|
-
skill_path: target.skillPath,
|
|
247
|
-
removed,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function generatedBy(spec) {
|
|
251
|
-
return `Generated by ${spec.markerSlug} skill install`;
|
|
252
|
-
}
|
|
253
|
-
function marker(spec) {
|
|
254
|
-
return `${spec.markerSlug}-managed-skill: true`;
|
|
255
|
-
}
|
|
256
|
-
function sourceHash(spec) {
|
|
257
|
-
let hash = FNV1A64_OFFSET;
|
|
258
|
-
for (const byte of Buffer.from(spec.source, "utf-8")) {
|
|
259
|
-
hash ^= BigInt(byte);
|
|
260
|
-
hash = (hash * FNV1A64_PRIME) & FNV1A64_MASK;
|
|
261
|
-
}
|
|
262
|
-
return hash.toString(16).padStart(16, "0");
|
|
263
|
-
}
|
|
264
|
-
function managedMarkerBlock(spec) {
|
|
265
|
-
const slug = spec.markerSlug;
|
|
266
|
-
return [
|
|
267
|
-
"<!--",
|
|
268
|
-
generatedBy(spec),
|
|
269
|
-
`${slug}-managed-skill: true`,
|
|
270
|
-
`${slug}-managed-skill-name: ${spec.name}`,
|
|
271
|
-
`${slug}-managed-skill-source-hash-fnv1a64: ${sourceHash(spec)}`,
|
|
272
|
-
"-->",
|
|
273
|
-
].join("\n");
|
|
274
|
-
}
|
|
275
|
-
function legacyMarkerBlock(spec) {
|
|
276
|
-
return `<!-- ${generatedBy(spec)} -->\n<!-- ${marker(spec)} -->`;
|
|
277
|
-
}
|
|
278
|
-
function managedSkillContents(spec) {
|
|
279
|
-
const block = managedMarkerBlock(spec);
|
|
280
|
-
const lines = splitLines(spec.source);
|
|
281
|
-
const out = [];
|
|
282
|
-
let inserted = false;
|
|
283
|
-
lines.forEach((line, i) => {
|
|
284
|
-
out.push(line);
|
|
285
|
-
if (i === 0)
|
|
286
|
-
return;
|
|
287
|
-
if (!inserted && line.trim() === "---") {
|
|
288
|
-
out.push(block, "");
|
|
289
|
-
inserted = true;
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
if (!inserted) {
|
|
293
|
-
out.push(block);
|
|
294
|
-
}
|
|
295
|
-
return out.map((line) => line + "\n").join("");
|
|
296
|
-
}
|
|
297
|
-
function validateInstalledSkill(spec, path) {
|
|
298
|
-
validateSkillText(spec, fs.readFileSync(path, "utf-8"));
|
|
299
|
-
}
|
|
300
|
-
function validateSkillText(spec, text) {
|
|
301
|
-
const error = validateSkillTextError(spec, text);
|
|
302
|
-
if (error !== null) {
|
|
303
|
-
const hint = error.includes("does not match expected")
|
|
304
|
-
? `set front matter name to ${spec.name}`
|
|
305
|
-
: "quote scalar values that contain ': ', especially description";
|
|
306
|
-
throw new SkillError(`invalid ${spec.title} skill front matter: ${error}`, hint);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function validateSkillTextError(spec, text) {
|
|
310
|
-
const parsed = parseSkillFrontmatter(text);
|
|
311
|
-
if (parsed.error !== null) {
|
|
312
|
-
return parsed.error;
|
|
313
|
-
}
|
|
314
|
-
if (parsed.name !== spec.name) {
|
|
315
|
-
return `name ${JSON.stringify(parsed.name)} does not match expected ${JSON.stringify(spec.name)}`;
|
|
316
|
-
}
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
function validateSkillFrontmatter(text) {
|
|
320
|
-
return parseSkillFrontmatter(text).error;
|
|
321
|
-
}
|
|
322
|
-
function parseSkillFrontmatter(text) {
|
|
323
|
-
const lines = splitLines(text);
|
|
324
|
-
if (lines.length === 0) {
|
|
325
|
-
return { error: "missing YAML front matter" };
|
|
326
|
-
}
|
|
327
|
-
if (lines[0].trim() !== "---") {
|
|
328
|
-
return { error: "missing opening --- YAML front matter delimiter" };
|
|
329
|
-
}
|
|
330
|
-
let foundEnd = false;
|
|
331
|
-
let name;
|
|
332
|
-
let hasDescription = false;
|
|
333
|
-
for (let idx = 1; idx < lines.length; idx++) {
|
|
334
|
-
const line = lines[idx];
|
|
335
|
-
const lineNo = idx + 1;
|
|
336
|
-
const trimmed = line.trim();
|
|
337
|
-
if (trimmed === "---") {
|
|
338
|
-
foundEnd = true;
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
if (line.startsWith(" ") || line.startsWith("\t")) {
|
|
345
|
-
return { error: `line ${lineNo}: nested YAML is not supported here` };
|
|
346
|
-
}
|
|
347
|
-
const colon = line.indexOf(":");
|
|
348
|
-
if (colon === -1) {
|
|
349
|
-
return { error: `line ${lineNo}: expected key: value` };
|
|
350
|
-
}
|
|
351
|
-
const key = line.slice(0, colon).trim();
|
|
352
|
-
if (key === "") {
|
|
353
|
-
return { error: `line ${lineNo}: empty key` };
|
|
354
|
-
}
|
|
355
|
-
const value = line.slice(colon + 1).replace(/^\s+/, "");
|
|
356
|
-
if (key === "name")
|
|
357
|
-
name = parseFrontmatterScalar(value);
|
|
358
|
-
if (key === "description")
|
|
359
|
-
hasDescription = true;
|
|
360
|
-
if (value.startsWith('"') || value.startsWith("'")) {
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
if (value.includes(": ")) {
|
|
364
|
-
return { error: `line ${lineNo}: unquoted scalar contains ': '; quote the value` };
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (!foundEnd) {
|
|
368
|
-
return { error: "missing closing --- YAML front matter delimiter" };
|
|
369
|
-
}
|
|
370
|
-
if (name === undefined) {
|
|
371
|
-
return { error: "missing required name field" };
|
|
372
|
-
}
|
|
373
|
-
if (!hasDescription) {
|
|
374
|
-
return { error: "missing required description field" };
|
|
375
|
-
}
|
|
376
|
-
return { name, error: null };
|
|
377
|
-
}
|
|
378
|
-
function parseFrontmatterScalar(value) {
|
|
379
|
-
value = value.trim();
|
|
380
|
-
if (value.length >= 2) {
|
|
381
|
-
const first = value[0];
|
|
382
|
-
const last = value[value.length - 1];
|
|
383
|
-
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
384
|
-
return value.slice(1, -1);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return value;
|
|
388
|
-
}
|
|
389
|
-
function isManagedOrBundledSkill(spec, path) {
|
|
390
|
-
const st = lstatOrNull(path);
|
|
391
|
-
if (st === null) {
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
if (st.isSymbolicLink()) {
|
|
395
|
-
throw new SkillError(`refusing to inspect symlinked skill at ${path}`, "pass --force to replace or remove the symlink itself");
|
|
396
|
-
}
|
|
397
|
-
return skillTextIsManagedOrBundled(spec, fs.readFileSync(path, "utf-8"));
|
|
398
|
-
}
|
|
399
|
-
function skillTextIsManagedOrBundled(spec, text) {
|
|
400
|
-
if (skillTextHasCurrentMarker(spec, text) || skillTextHasLegacyMarker(spec, text)) {
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
return normalizeSkillText(spec, text) === normalizeSkillText(spec, spec.source);
|
|
404
|
-
}
|
|
405
|
-
function skillTextHasCurrentMarker(spec, text) {
|
|
406
|
-
return text.replace(/\r\n/g, "\n").includes(managedMarkerBlock(spec));
|
|
407
|
-
}
|
|
408
|
-
function skillTextHasLegacyMarker(spec, text) {
|
|
409
|
-
return text.replace(/\r\n/g, "\n").includes(legacyMarkerBlock(spec));
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Drop the managed-marker blocks, then collapse runs of blank lines to one so that the
|
|
413
|
-
* blank line `managedSkillContents` inserts after the marker block does not make a
|
|
414
|
-
* managed install compare unequal to the bundled source.
|
|
415
|
-
*/
|
|
416
|
-
function normalizeSkillText(spec, text) {
|
|
417
|
-
text = text.replace(/\r\n/g, "\n");
|
|
418
|
-
text = text.replace(managedMarkerBlock(spec), "");
|
|
419
|
-
text = text.replace(legacyMarkerBlock(spec), "");
|
|
420
|
-
const out = [];
|
|
421
|
-
for (const line of splitLines(text)) {
|
|
422
|
-
const trimmed = line.trim();
|
|
423
|
-
if (trimmed === "" && out.length > 0 && out[out.length - 1].trim() === "") {
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
out.push(line);
|
|
427
|
-
}
|
|
428
|
-
return out.join("\n").trim();
|
|
429
|
-
}
|
|
430
|
-
function homeDir() {
|
|
431
|
-
const home = process.env["HOME"] ?? process.env["USERPROFILE"];
|
|
432
|
-
if (home !== undefined) {
|
|
433
|
-
return home;
|
|
434
|
-
}
|
|
435
|
-
throw new SkillError("cannot determine home directory", "pass --skills-dir explicitly");
|
|
436
|
-
}
|
|
437
|
-
function expandTilde(input) {
|
|
438
|
-
if (input === "~") {
|
|
439
|
-
return homeDir();
|
|
440
|
-
}
|
|
441
|
-
if (input.startsWith("~/")) {
|
|
442
|
-
return join(homeDir(), input.slice(2));
|
|
443
|
-
}
|
|
444
|
-
return input;
|
|
445
|
-
}
|
|
446
|
-
function validateSpec(spec) {
|
|
447
|
-
validateSlug("skill name", spec.name);
|
|
448
|
-
validateSlug("marker slug", spec.markerSlug);
|
|
449
|
-
validateSkillText(spec, spec.source);
|
|
450
|
-
}
|
|
451
|
-
function validateSlug(field, value) {
|
|
452
|
-
if (slugIsValid(value)) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
throw new SkillError(`invalid ${field} ${JSON.stringify(value)}: expected a lowercase slug matching [a-z0-9][a-z0-9-]*[a-z0-9]`, "use lowercase ASCII letters, digits, and single hyphen-separated words");
|
|
456
|
-
}
|
|
457
|
-
function slugIsValid(value) {
|
|
458
|
-
if (value.length === 0)
|
|
459
|
-
return false;
|
|
460
|
-
const isLowerAlnum = (ch) => (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9");
|
|
461
|
-
return isLowerAlnum(value[0]) && isLowerAlnum(value[value.length - 1]) &&
|
|
462
|
-
[...value].every((ch) => isLowerAlnum(ch) || ch === "-");
|
|
463
|
-
}
|
|
464
|
-
function lstatOrNull(path) {
|
|
465
|
-
return fs.lstatSync(path, { throwIfNoEntry: false }) ?? null;
|
|
466
|
-
}
|
|
467
|
-
function writeSkillAtomic(target, content) {
|
|
468
|
-
const tmp = join(target.skillDir, `.${basename(SKILL_FILE_NAME)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
469
|
-
try {
|
|
470
|
-
fs.writeFileSync(tmp, content);
|
|
471
|
-
fs.renameSync(tmp, target.skillPath);
|
|
472
|
-
}
|
|
473
|
-
catch (error) {
|
|
474
|
-
try {
|
|
475
|
-
fs.rmSync(tmp, { force: true });
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
// ignore cleanup errors
|
|
479
|
-
}
|
|
480
|
-
throw error;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Mirror Rust's `str::lines`: split on `\n` (normalizing `\r\n`), dropping a single
|
|
485
|
-
* trailing empty element from a final newline.
|
|
486
|
-
*/
|
|
487
|
-
function splitLines(s) {
|
|
488
|
-
const lines = s.replace(/\r\n/g, "\n").split("\n");
|
|
489
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
490
|
-
lines.pop();
|
|
491
|
-
}
|
|
492
|
-
return lines;
|
|
493
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional stdout/stderr file redirection for AFDATA CLIs.
|
|
3
|
-
*
|
|
4
|
-
* This redirects Node's process stdout/stderr file descriptors to append-only
|
|
5
|
-
* files. It is not an AFDATA formatter and does not convert stderr diagnostics
|
|
6
|
-
* into JSON.
|
|
7
|
-
*/
|
|
8
|
-
export declare const STDOUT_FILE_ARG = "--stdout-file";
|
|
9
|
-
export declare const STDERR_FILE_ARG = "--stderr-file";
|
|
10
|
-
export interface StreamRedirectConfig {
|
|
11
|
-
stdoutFile?: string;
|
|
12
|
-
stderrFile?: string;
|
|
13
|
-
}
|
|
14
|
-
export interface InstalledStreamRedirect {
|
|
15
|
-
stdoutFile?: string;
|
|
16
|
-
stderrFile?: string;
|
|
17
|
-
processLifetime: true;
|
|
18
|
-
}
|
|
19
|
-
export declare function configFromRawArgs(args: readonly string[]): StreamRedirectConfig | undefined;
|
|
20
|
-
export declare function installStreamRedirectFromRawArgs(args?: string[]): InstalledStreamRedirect | undefined;
|
|
21
|
-
export declare function installStreamRedirect(config: StreamRedirectConfig): InstalledStreamRedirect | undefined;
|