@logbookfordevs/afk 0.5.2
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 +240 -0
- package/dist/agents.js +59 -0
- package/dist/brand.js +60 -0
- package/dist/cli.js +625 -0
- package/dist/delegates.js +220 -0
- package/dist/fs-utils.js +120 -0
- package/dist/hooks.js +217 -0
- package/dist/index.js +4 -0
- package/dist/interactive.js +273 -0
- package/dist/manifest-configure.js +300 -0
- package/dist/manifest-show.js +207 -0
- package/dist/manifest.js +409 -0
- package/dist/paths.js +17 -0
- package/dist/prompt-ui.js +109 -0
- package/dist/prompt.js +8 -0
- package/dist/rules.js +209 -0
- package/dist/setup.js +225 -0
- package/dist/skills.js +114 -0
- package/dist/terminal-theme.js +37 -0
- package/dist/types.js +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { bold, paint, reset, terminalPalette } from "./terminal-theme.js";
|
|
2
|
+
let promptStep = 0;
|
|
3
|
+
export const DEFAULT_CHECKED = false;
|
|
4
|
+
export const defaultCheckedDetail = DEFAULT_CHECKED
|
|
5
|
+
? "Everything starts selected. Use space to unselect anything you want to skip."
|
|
6
|
+
: "Nothing starts selected. Use space to choose what you want to include.";
|
|
7
|
+
export function resetPromptSteps() {
|
|
8
|
+
promptStep = 0;
|
|
9
|
+
}
|
|
10
|
+
export function renderPromptStep(title, detail) {
|
|
11
|
+
promptStep += 1;
|
|
12
|
+
const step = String(promptStep).padStart(2, "0");
|
|
13
|
+
const lines = [
|
|
14
|
+
"",
|
|
15
|
+
`${chartLine("┌")} ${badge(`step ${step}`)} ${bold}${title}${reset}`,
|
|
16
|
+
];
|
|
17
|
+
if (detail) {
|
|
18
|
+
lines.push(`${chartLine("│")} ${muted(detail)}`);
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
export const afkSelectTheme = {
|
|
23
|
+
prefix: {
|
|
24
|
+
idle: sea("◇"),
|
|
25
|
+
done: signal("◆"),
|
|
26
|
+
},
|
|
27
|
+
icon: {
|
|
28
|
+
cursor: brass("◆"),
|
|
29
|
+
},
|
|
30
|
+
style: {
|
|
31
|
+
answer: (text) => sea(text),
|
|
32
|
+
message: (text) => `${bold}${text}${reset}`,
|
|
33
|
+
description: (text) => muted(text),
|
|
34
|
+
highlight: (text) => brass(text),
|
|
35
|
+
help: (text) => muted(text),
|
|
36
|
+
key: (text) => sea(`<${text}>`),
|
|
37
|
+
keysHelpTip: (keys) => formatKeys(keys),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export const afkCheckboxTheme = {
|
|
41
|
+
prefix: {
|
|
42
|
+
idle: sea("◇"),
|
|
43
|
+
done: signal("◆"),
|
|
44
|
+
},
|
|
45
|
+
icon: {
|
|
46
|
+
checked: brass("■"),
|
|
47
|
+
unchecked: muted("□"),
|
|
48
|
+
cursor: signal("◆ "),
|
|
49
|
+
},
|
|
50
|
+
style: {
|
|
51
|
+
answer: (text) => sea(text),
|
|
52
|
+
message: (text) => `${bold}${text}${reset}`,
|
|
53
|
+
description: (text) => muted(text),
|
|
54
|
+
highlight: (text) => brass(text),
|
|
55
|
+
help: (text) => muted(text),
|
|
56
|
+
key: (text) => sea(`<${text}>`),
|
|
57
|
+
disabledChoice: (text) => muted(`- ${text}`),
|
|
58
|
+
renderSelectedChoices: (selectedChoices) => {
|
|
59
|
+
if (selectedChoices.length === 0) {
|
|
60
|
+
return "none selected";
|
|
61
|
+
}
|
|
62
|
+
if (selectedChoices.length <= 3) {
|
|
63
|
+
return selectedChoices.map((choice) => choice.short).join(", ");
|
|
64
|
+
}
|
|
65
|
+
return `${selectedChoices.length} selected`;
|
|
66
|
+
},
|
|
67
|
+
keysHelpTip: (keys) => formatKeys(keys),
|
|
68
|
+
},
|
|
69
|
+
helpMode: "always",
|
|
70
|
+
};
|
|
71
|
+
export const afkPromptTheme = {
|
|
72
|
+
prefix: {
|
|
73
|
+
idle: sea("◇"),
|
|
74
|
+
done: signal("◆"),
|
|
75
|
+
},
|
|
76
|
+
style: {
|
|
77
|
+
answer: (text) => sea(text),
|
|
78
|
+
message: (text) => `${bold}${text}${reset}`,
|
|
79
|
+
error: (text) => ember(`> ${text}`),
|
|
80
|
+
defaultAnswer: (text) => muted(`(${text})`),
|
|
81
|
+
help: (text) => muted(text),
|
|
82
|
+
highlight: (text) => brass(text),
|
|
83
|
+
key: (text) => sea(`<${text}>`),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
function formatKeys(keys) {
|
|
87
|
+
return muted(keys.map(([key, action]) => `${sea(key)} ${action}`).join(" · "));
|
|
88
|
+
}
|
|
89
|
+
function badge(value) {
|
|
90
|
+
return `${signal(" ")}${signal(value)}${signal(" ")}`;
|
|
91
|
+
}
|
|
92
|
+
function sea(value) {
|
|
93
|
+
return paint(terminalPalette.harbor, value);
|
|
94
|
+
}
|
|
95
|
+
function brass(value) {
|
|
96
|
+
return paint(terminalPalette.brass, value);
|
|
97
|
+
}
|
|
98
|
+
function signal(value) {
|
|
99
|
+
return paint(terminalPalette.rust, value);
|
|
100
|
+
}
|
|
101
|
+
function muted(value) {
|
|
102
|
+
return paint(terminalPalette.driftwood, value);
|
|
103
|
+
}
|
|
104
|
+
function ember(value) {
|
|
105
|
+
return paint(terminalPalette.ember, value);
|
|
106
|
+
}
|
|
107
|
+
function chartLine(value) {
|
|
108
|
+
return paint(terminalPalette.sienna, value);
|
|
109
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
export async function confirm(message) {
|
|
4
|
+
const reader = createInterface({ input, output });
|
|
5
|
+
const answer = await reader.question(`${message} [y/N] `);
|
|
6
|
+
reader.close();
|
|
7
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
8
|
+
}
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { applyOperation, backupTarget, formatOperation, isSymlink, pathExists, readText, summarizeOperations } from "./fs-utils.js";
|
|
3
|
+
import { filterAgents } from "./agents.js";
|
|
4
|
+
import { loadRulesManifest } from "./manifest.js";
|
|
5
|
+
const afkRegionStart = "<!-- AFK:RULES:START -->";
|
|
6
|
+
const afkRegionEnd = "<!-- AFK:RULES:END -->";
|
|
7
|
+
const legacyImportStart = "<!-- AFK:IMPORT:START -->";
|
|
8
|
+
const legacyImportEnd = "<!-- AFK:IMPORT:END -->";
|
|
9
|
+
const globalRulesAgents = ["antigravity", "codex", "opencode"];
|
|
10
|
+
export async function syncRules(runtime, options) {
|
|
11
|
+
const content = await loadRulesContent(options);
|
|
12
|
+
const operations = planRulesSync(options, content);
|
|
13
|
+
if (options.dryRun) {
|
|
14
|
+
printOperations(runtime, "Rules sync plan", operations);
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
for (const operation of operations) {
|
|
18
|
+
applyOperation(operation);
|
|
19
|
+
}
|
|
20
|
+
runtime.io.stdout(`\nRules synced: ${summarizeOperations(operations)}.`);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
export function planRulesSync(options, content) {
|
|
24
|
+
const timestamp = compactTimestamp();
|
|
25
|
+
const operations = [];
|
|
26
|
+
const normalizedRules = normalizeAfkRules(content.afk);
|
|
27
|
+
if (options.setupScope === "project") {
|
|
28
|
+
return planProjectRules(options, normalizedRules, timestamp);
|
|
29
|
+
}
|
|
30
|
+
for (const agent of filterAgents(options.agents, globalRulesAgents)) {
|
|
31
|
+
operations.push(...removeLegacySidecars(dirname(agentRulesDestination(options.homeDir, agent)), timestamp));
|
|
32
|
+
operations.push(...upsertManagedRulesRegion(agentRulesDestination(options.homeDir, agent), normalizedRules, timestamp));
|
|
33
|
+
}
|
|
34
|
+
if (shouldConfigureClaude(options.agents)) {
|
|
35
|
+
operations.push(...planClaudeRules(options.homeDir, { afk: normalizedRules }, timestamp));
|
|
36
|
+
}
|
|
37
|
+
return operations;
|
|
38
|
+
}
|
|
39
|
+
function planProjectRules(options, normalizedRules, timestamp) {
|
|
40
|
+
const operations = [];
|
|
41
|
+
const selected = filterAgents(options.agents, ["antigravity", "claude", "codex", "opencode"]);
|
|
42
|
+
const hostPaths = new Set();
|
|
43
|
+
for (const agent of selected) {
|
|
44
|
+
hostPaths.add(projectRulesDestination(options.cwd, agent));
|
|
45
|
+
}
|
|
46
|
+
for (const path of hostPaths) {
|
|
47
|
+
operations.push(...upsertManagedRulesRegion(path, normalizedRules, timestamp));
|
|
48
|
+
}
|
|
49
|
+
return operations;
|
|
50
|
+
}
|
|
51
|
+
async function loadRulesContent(options) {
|
|
52
|
+
const manifest = loadRulesManifest(options);
|
|
53
|
+
if (!manifest.url) {
|
|
54
|
+
return { afk: "" };
|
|
55
|
+
}
|
|
56
|
+
const source = options.rulesSource === "manifest" ? manifest.source : options.rulesSource;
|
|
57
|
+
const agents = source === "local"
|
|
58
|
+
? await readLocalRule(options.repoDir, localRulesPath(manifest.url))
|
|
59
|
+
: await fetchGithubRule(manifest.url);
|
|
60
|
+
return {
|
|
61
|
+
afk: normalizeAfkRules(agents),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function readLocalRule(repoDir, file) {
|
|
65
|
+
return readText(join(repoDir, file));
|
|
66
|
+
}
|
|
67
|
+
async function fetchGithubRule(url) {
|
|
68
|
+
const response = await fetch(url);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`Could not fetch ${url}: ${response.status} ${response.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
return response.text();
|
|
73
|
+
}
|
|
74
|
+
function localRulesPath(url) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = new URL(url);
|
|
77
|
+
const rawGithubMatch = parsed.hostname === "raw.githubusercontent.com" ? parsed.pathname.match(/^\/[^/]+\/[^/]+\/[^/]+\/(.+)$/) : null;
|
|
78
|
+
if (rawGithubMatch?.[1]) {
|
|
79
|
+
return rawGithubMatch[1];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return url;
|
|
84
|
+
}
|
|
85
|
+
return url.replace(/^\/+/, "");
|
|
86
|
+
}
|
|
87
|
+
function normalizeAfkRules(content) {
|
|
88
|
+
const withoutImports = content
|
|
89
|
+
.split(/\r?\n/)
|
|
90
|
+
.filter((line) => !isMarkdownImportLine(line))
|
|
91
|
+
.join("\n");
|
|
92
|
+
return ensureTrailingNewline(withoutImports);
|
|
93
|
+
}
|
|
94
|
+
function isMarkdownImportLine(line) {
|
|
95
|
+
return /^@[^\s]+\.md$/i.test(line.trim());
|
|
96
|
+
}
|
|
97
|
+
function ensureTrailingNewline(content) {
|
|
98
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
99
|
+
}
|
|
100
|
+
function upsertManagedRulesRegion(path, afkRules, timestamp) {
|
|
101
|
+
const operations = [];
|
|
102
|
+
const region = renderManagedRulesRegion(afkRules);
|
|
103
|
+
if (isSymlink(path)) {
|
|
104
|
+
const current = pathExists(path) ? readText(path) : "";
|
|
105
|
+
const next = current ? updateManagedRulesRegion(current, region) : region;
|
|
106
|
+
operations.push({ type: "remove", path });
|
|
107
|
+
operations.push({ type: "write", path, content: next });
|
|
108
|
+
return operations;
|
|
109
|
+
}
|
|
110
|
+
if (!pathExists(path)) {
|
|
111
|
+
operations.push({ type: "write", path, content: region });
|
|
112
|
+
return operations;
|
|
113
|
+
}
|
|
114
|
+
const current = readText(path);
|
|
115
|
+
const next = updateManagedRulesRegion(current, region);
|
|
116
|
+
if (current === next) {
|
|
117
|
+
operations.push({ type: "skip", path, reason: "AFK rules region already current" });
|
|
118
|
+
return operations;
|
|
119
|
+
}
|
|
120
|
+
const backup = backupTarget(path, timestamp);
|
|
121
|
+
if (backup) {
|
|
122
|
+
operations.push(backup);
|
|
123
|
+
}
|
|
124
|
+
operations.push({ type: "write", path, content: next });
|
|
125
|
+
return operations;
|
|
126
|
+
}
|
|
127
|
+
function planClaudeRules(homeDir, content, timestamp) {
|
|
128
|
+
const claudeDir = join(homeDir, ".claude");
|
|
129
|
+
const operations = [{ type: "mkdir", path: claudeDir }];
|
|
130
|
+
operations.push(...removeLegacySidecars(claudeDir, timestamp));
|
|
131
|
+
operations.push(...upsertManagedRulesRegion(join(claudeDir, "CLAUDE.md"), content.afk, timestamp));
|
|
132
|
+
return operations;
|
|
133
|
+
}
|
|
134
|
+
function shouldConfigureClaude(agents) {
|
|
135
|
+
return agents.length === 0 || agents.includes("claude");
|
|
136
|
+
}
|
|
137
|
+
function removeLegacySidecars(directory, timestamp) {
|
|
138
|
+
const operations = [];
|
|
139
|
+
for (const filename of ["AFK.md", "AFK_WORKFLOW.md"]) {
|
|
140
|
+
const path = join(directory, filename);
|
|
141
|
+
if (!pathExists(path) && !isSymlink(path)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const backup = backupTarget(path, timestamp);
|
|
145
|
+
if (backup) {
|
|
146
|
+
operations.push(backup);
|
|
147
|
+
}
|
|
148
|
+
operations.push({ type: "remove", path });
|
|
149
|
+
}
|
|
150
|
+
return operations;
|
|
151
|
+
}
|
|
152
|
+
function agentRulesDestination(homeDir, agent) {
|
|
153
|
+
switch (agent) {
|
|
154
|
+
case "codex":
|
|
155
|
+
return join(homeDir, ".codex", "AGENTS.md");
|
|
156
|
+
case "antigravity":
|
|
157
|
+
return join(homeDir, ".gemini", "GEMINI.md");
|
|
158
|
+
case "opencode":
|
|
159
|
+
return join(homeDir, ".config", "opencode", "AGENTS.md");
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unsupported linked rules agent: ${agent}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function projectRulesDestination(cwd, agent) {
|
|
165
|
+
switch (agent) {
|
|
166
|
+
case "claude":
|
|
167
|
+
return join(cwd, "CLAUDE.md");
|
|
168
|
+
case "antigravity":
|
|
169
|
+
return join(cwd, "GEMINI.md");
|
|
170
|
+
case "codex":
|
|
171
|
+
case "opencode":
|
|
172
|
+
return join(cwd, "AGENTS.md");
|
|
173
|
+
case "cursor-local":
|
|
174
|
+
return join(cwd, ".cursor", "rules", "afk.mdc");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function renderManagedRulesRegion(afkRules) {
|
|
178
|
+
return ensureTrailingNewline([afkRegionStart, ensureTrailingNewline(afkRules).trimEnd(), afkRegionEnd, ""].join("\n"));
|
|
179
|
+
}
|
|
180
|
+
function updateManagedRulesRegion(current, region) {
|
|
181
|
+
if (current.includes(afkRegionStart)) {
|
|
182
|
+
const pattern = new RegExp(`${escapeRegExp(afkRegionStart)}[\\s\\S]*?${escapeRegExp(afkRegionEnd)}\\n?`);
|
|
183
|
+
return ensureTrailingNewline(current.replace(pattern, region));
|
|
184
|
+
}
|
|
185
|
+
if (current.includes(legacyImportStart)) {
|
|
186
|
+
const pattern = new RegExp(`${escapeRegExp(legacyImportStart)}[\\s\\S]*?${escapeRegExp(legacyImportEnd)}\\n?`);
|
|
187
|
+
return ensureTrailingNewline(current.replace(pattern, region));
|
|
188
|
+
}
|
|
189
|
+
const lines = current.split(/\r?\n/);
|
|
190
|
+
const firstLocalImportIndex = lines.findIndex((line) => line === "@RTK.md" || line === "<!-- OMC:IMPORT:START -->");
|
|
191
|
+
if (firstLocalImportIndex >= 0) {
|
|
192
|
+
const before = lines.slice(0, firstLocalImportIndex).join("\n").trimEnd();
|
|
193
|
+
const after = lines.slice(firstLocalImportIndex).join("\n").trimStart();
|
|
194
|
+
return ensureTrailingNewline([before, region, after].filter(Boolean).join("\n\n"));
|
|
195
|
+
}
|
|
196
|
+
return `${region}\n${current}`;
|
|
197
|
+
}
|
|
198
|
+
function escapeRegExp(value) {
|
|
199
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
200
|
+
}
|
|
201
|
+
function compactTimestamp() {
|
|
202
|
+
return new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
|
|
203
|
+
}
|
|
204
|
+
function printOperations(runtime, title, operations) {
|
|
205
|
+
runtime.io.stdout(`\n${title}`);
|
|
206
|
+
for (const operation of operations) {
|
|
207
|
+
runtime.io.stdout(`- ${formatOperation(operation)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { syncRules } from "./rules.js";
|
|
2
|
+
import { syncHooks } from "./hooks.js";
|
|
3
|
+
import { syncSkillInvocationPolicy } from "./skills.js";
|
|
4
|
+
import { buildMcpCommands, buildSkillCommands, buildUtilityCommands, runDelegateCommands } from "./delegates.js";
|
|
5
|
+
import { renderBanner, renderSetupOutro, sectionTitle, muted } from "./brand.js";
|
|
6
|
+
import { selectHooksInstall, selectMcpsInstall, selectRulesSync, selectSetup, selectSkillsInstall, selectUtilsInstall } from "./interactive.js";
|
|
7
|
+
import { applyOperation, formatOperation, summarizeOperations } from "./fs-utils.js";
|
|
8
|
+
import { ensureLocalManifests } from "./manifest.js";
|
|
9
|
+
import { defaultCheckedDetail } from "./prompt-ui.js";
|
|
10
|
+
export async function runSetup(runtime, options) {
|
|
11
|
+
runtime.io.stdout(renderBanner());
|
|
12
|
+
if (options.refreshDefaults) {
|
|
13
|
+
runtime.io.stdout(options.manifestLocal
|
|
14
|
+
? "Refreshing project AFK manifests from your configured defaults source."
|
|
15
|
+
: "Refreshing global AFK manifests from your configured defaults source.");
|
|
16
|
+
return ensureManifestFiles(runtime, options);
|
|
17
|
+
}
|
|
18
|
+
runtime.io.stdout("Choose the parts of your AI field setup you want AFK to prepare.");
|
|
19
|
+
runtime.io.stdout(muted(defaultCheckedDetail));
|
|
20
|
+
const manifestCode = await ensureManifestFiles(runtime, options);
|
|
21
|
+
if (manifestCode !== 0 || options.initOnly) {
|
|
22
|
+
return manifestCode;
|
|
23
|
+
}
|
|
24
|
+
const selection = await selectSetup(options);
|
|
25
|
+
const selectedOptions = {
|
|
26
|
+
...options,
|
|
27
|
+
agents: selection.agents,
|
|
28
|
+
setupScope: selection.setupScope,
|
|
29
|
+
scopeExplicit: true,
|
|
30
|
+
selectedSkillIds: selection.skillIds,
|
|
31
|
+
selectedSkillAgentIds: selection.skillAgents,
|
|
32
|
+
selectedMcpIds: selection.mcpIds,
|
|
33
|
+
selectedUtilIds: selection.utilIds,
|
|
34
|
+
selectedHookIds: selection.hookIds,
|
|
35
|
+
};
|
|
36
|
+
if (selection.areas.length === 0) {
|
|
37
|
+
runtime.io.stdout("\nNothing selected. No changes planned.");
|
|
38
|
+
runtime.io.stdout(renderSetupOutro({
|
|
39
|
+
dryRun: options.dryRun,
|
|
40
|
+
failed: false,
|
|
41
|
+
scopeLabel: scopeLabel(selection.setupScope, options.cwd),
|
|
42
|
+
areas: ["none"],
|
|
43
|
+
}));
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
runtime.io.stdout("\nSetup path");
|
|
47
|
+
runtime.io.stdout(`- Scope: ${scopeLabel(selection.setupScope, options.cwd)}`);
|
|
48
|
+
runtime.io.stdout(`- Areas: ${selection.areas.join(", ")}`);
|
|
49
|
+
if (selection.agents.length > 0) {
|
|
50
|
+
runtime.io.stdout(`- Agents: ${selection.agents.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
if (selection.skillAgents.length > 0) {
|
|
53
|
+
runtime.io.stdout(`- Additional skill agents: ${selection.skillAgents.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
const failures = [];
|
|
56
|
+
for (const area of selection.areas) {
|
|
57
|
+
runtime.io.stdout(`\n${sectionTitle(areaLabel(area))}`);
|
|
58
|
+
const areaOptions = area === "hooks" ? { ...selectedOptions, agents: selection.hookAgents } : selectedOptions;
|
|
59
|
+
const code = await runArea(area, runtime, areaOptions);
|
|
60
|
+
if (code !== 0) {
|
|
61
|
+
failures.push({ area, code });
|
|
62
|
+
runtime.io.stderr(`${areaLabel(area)} failed with exit code ${code}. Continuing with remaining setup areas.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (failures.length > 0) {
|
|
66
|
+
runtime.io.stdout("\nSetup completed with failures:");
|
|
67
|
+
for (const failure of failures) {
|
|
68
|
+
runtime.io.stdout(`- ${areaLabel(failure.area)} exited with code ${failure.code}`);
|
|
69
|
+
}
|
|
70
|
+
runtime.io.stdout(renderSetupOutro({
|
|
71
|
+
dryRun: options.dryRun,
|
|
72
|
+
failed: true,
|
|
73
|
+
scopeLabel: scopeLabel(selection.setupScope, options.cwd),
|
|
74
|
+
areas: selection.areas.map(areaLabel),
|
|
75
|
+
}));
|
|
76
|
+
return failures[0]?.code ?? 1;
|
|
77
|
+
}
|
|
78
|
+
runtime.io.stdout(renderSetupOutro({
|
|
79
|
+
dryRun: options.dryRun,
|
|
80
|
+
failed: false,
|
|
81
|
+
scopeLabel: scopeLabel(selection.setupScope, options.cwd),
|
|
82
|
+
areas: selection.areas.map(areaLabel),
|
|
83
|
+
}));
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
export async function runArea(area, runtime, options) {
|
|
87
|
+
const manifestCode = await ensureManifestFiles(runtime, options);
|
|
88
|
+
if (manifestCode !== 0 || options.initOnly) {
|
|
89
|
+
return manifestCode;
|
|
90
|
+
}
|
|
91
|
+
switch (area) {
|
|
92
|
+
case "rules": {
|
|
93
|
+
const selectedOptions = await resolveRulesOptions(options);
|
|
94
|
+
return syncRules(runtime, selectedOptions);
|
|
95
|
+
}
|
|
96
|
+
case "skills": {
|
|
97
|
+
const selectedOptions = await resolveSkillOptions(options);
|
|
98
|
+
if (!selectedOptions.yes && selectedOptions.selectedSkillIds.length === 0) {
|
|
99
|
+
runtime.io.stdout("\nNo skills selected. No changes planned.");
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
const code = await runDelegateCommands(runtime, buildSkillCommands(selectedOptions), selectedOptions);
|
|
103
|
+
if (code === 0) {
|
|
104
|
+
syncSkillInvocationPolicy(runtime, selectedOptions);
|
|
105
|
+
}
|
|
106
|
+
return code;
|
|
107
|
+
}
|
|
108
|
+
case "mcps": {
|
|
109
|
+
const selectedOptions = await resolveMcpOptions(options);
|
|
110
|
+
if (!selectedOptions.yes && selectedOptions.selectedMcpIds.length === 0) {
|
|
111
|
+
runtime.io.stdout("\nNo MCPs selected. No changes planned.");
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
return runDelegateCommands(runtime, buildMcpCommands(selectedOptions), selectedOptions);
|
|
115
|
+
}
|
|
116
|
+
case "utils": {
|
|
117
|
+
const selectedOptions = await resolveUtilityOptions(options);
|
|
118
|
+
if (!selectedOptions.yes && selectedOptions.selectedUtilIds.length === 0) {
|
|
119
|
+
runtime.io.stdout("\nNo utilities selected. No changes planned.");
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
return runDelegateCommands(runtime, buildUtilityCommands(selectedOptions), {
|
|
123
|
+
...options,
|
|
124
|
+
continueOnError: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
case "hooks": {
|
|
128
|
+
const selectedOptions = await resolveHookOptions(options);
|
|
129
|
+
if (!selectedOptions.yes && (selectedOptions.selectedHookIds.length === 0 || selectedOptions.agents.length === 0)) {
|
|
130
|
+
runtime.io.stdout("\nNo hooks selected. No changes planned.");
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
return syncHooks(runtime, selectedOptions);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function resolveRulesOptions(options) {
|
|
138
|
+
if (options.yes || options.agents.length > 0) {
|
|
139
|
+
return options;
|
|
140
|
+
}
|
|
141
|
+
const selection = await selectRulesSync(options);
|
|
142
|
+
return {
|
|
143
|
+
...options,
|
|
144
|
+
agents: selection.agents,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function resolveSkillOptions(options) {
|
|
148
|
+
if (options.yes || options.selectedSkillIds.length > 0) {
|
|
149
|
+
return options;
|
|
150
|
+
}
|
|
151
|
+
const selection = await selectSkillsInstall(options);
|
|
152
|
+
return {
|
|
153
|
+
...options,
|
|
154
|
+
selectedSkillIds: selection.skillIds,
|
|
155
|
+
selectedSkillAgentIds: selection.skillAgents,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function resolveMcpOptions(options) {
|
|
159
|
+
if (options.yes || options.selectedMcpIds.length > 0) {
|
|
160
|
+
return options;
|
|
161
|
+
}
|
|
162
|
+
const selection = await selectMcpsInstall(options);
|
|
163
|
+
return {
|
|
164
|
+
...options,
|
|
165
|
+
agents: selection.agents,
|
|
166
|
+
selectedMcpIds: selection.mcpIds,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function resolveUtilityOptions(options) {
|
|
170
|
+
if (options.yes || options.selectedUtilIds.length > 0) {
|
|
171
|
+
return options;
|
|
172
|
+
}
|
|
173
|
+
const selection = await selectUtilsInstall(options);
|
|
174
|
+
return {
|
|
175
|
+
...options,
|
|
176
|
+
agents: selection.agents,
|
|
177
|
+
selectedUtilIds: selection.utilIds,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function resolveHookOptions(options) {
|
|
181
|
+
if (options.yes || options.selectedHookIds.length > 0) {
|
|
182
|
+
return options;
|
|
183
|
+
}
|
|
184
|
+
const selection = await selectHooksInstall(options);
|
|
185
|
+
return {
|
|
186
|
+
...options,
|
|
187
|
+
agents: selection.agents,
|
|
188
|
+
selectedHookIds: selection.hookIds,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function ensureManifestFiles(runtime, options) {
|
|
192
|
+
const operations = await ensureLocalManifests(options);
|
|
193
|
+
if (operations.length === 0) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (options.dryRun) {
|
|
197
|
+
runtime.io.stdout(`\n${sectionTitle("Local Manifests")}`);
|
|
198
|
+
for (const operation of operations) {
|
|
199
|
+
runtime.io.stdout(`- ${formatOperation(operation)}`);
|
|
200
|
+
}
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
for (const operation of operations) {
|
|
204
|
+
applyOperation(operation);
|
|
205
|
+
}
|
|
206
|
+
runtime.io.stdout(`\nLocal manifests prepared: ${summarizeOperations(operations)}.`);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
function areaLabel(area) {
|
|
210
|
+
switch (area) {
|
|
211
|
+
case "rules":
|
|
212
|
+
return "Rules";
|
|
213
|
+
case "skills":
|
|
214
|
+
return "Skills";
|
|
215
|
+
case "mcps":
|
|
216
|
+
return "MCPs";
|
|
217
|
+
case "utils":
|
|
218
|
+
return "Utils";
|
|
219
|
+
case "hooks":
|
|
220
|
+
return "Hooks";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function scopeLabel(scope, cwd) {
|
|
224
|
+
return scope === "global" ? "Global field kit" : `This project only (${cwd})`;
|
|
225
|
+
}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { applyOperation, formatOperation, pathExists, readText, summarizeOperations } from "./fs-utils.js";
|
|
4
|
+
import { loadSkillManifest } from "./manifest.js";
|
|
5
|
+
export function planSkillInvocationPolicy(options) {
|
|
6
|
+
const manifest = loadSkillManifest(options);
|
|
7
|
+
const selected = options.selectedSkillIds.length > 0
|
|
8
|
+
? manifest.items.filter((item) => options.selectedSkillIds.includes(item.id))
|
|
9
|
+
: manifest.items.filter((item) => item.default);
|
|
10
|
+
const manualSkills = selected.filter((item) => item.autoInvocation === false);
|
|
11
|
+
const operations = [];
|
|
12
|
+
const plannedRealPaths = new Set();
|
|
13
|
+
for (const item of manualSkills) {
|
|
14
|
+
for (const skillDir of skillDirectories(options, item)) {
|
|
15
|
+
operations.push(...planManualSkillInvocation(skillDir, plannedRealPaths));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return operations;
|
|
19
|
+
}
|
|
20
|
+
export function syncSkillInvocationPolicy(runtime, options) {
|
|
21
|
+
const operations = planSkillInvocationPolicy(options);
|
|
22
|
+
if (operations.length === 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (options.dryRun) {
|
|
26
|
+
runtime.io.stdout("\nSkill invocation policy plan");
|
|
27
|
+
for (const operation of operations) {
|
|
28
|
+
runtime.io.stdout(`- ${formatOperation(operation)}`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const operation of operations) {
|
|
33
|
+
applyOperation(operation);
|
|
34
|
+
}
|
|
35
|
+
runtime.io.stdout(`\nSkill invocation policies synced: ${summarizeOperations(operations)}.`);
|
|
36
|
+
}
|
|
37
|
+
function skillDirectories(options, item) {
|
|
38
|
+
const root = options.setupScope === "global" ? options.homeDir : options.cwd;
|
|
39
|
+
return [
|
|
40
|
+
join(root, ".agents", "skills", item.id),
|
|
41
|
+
join(root, ".claude", "skills", item.id),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function planManualSkillInvocation(skillDir, plannedRealPaths) {
|
|
45
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
46
|
+
const openaiYaml = join(skillDir, "agents", "openai.yaml");
|
|
47
|
+
const operations = [];
|
|
48
|
+
if (!pathExists(skillMd)) {
|
|
49
|
+
return operations;
|
|
50
|
+
}
|
|
51
|
+
const skillMdRealPath = realPathOrSelf(skillMd);
|
|
52
|
+
if (!plannedRealPaths.has(skillMdRealPath)) {
|
|
53
|
+
plannedRealPaths.add(skillMdRealPath);
|
|
54
|
+
const nextSkillMd = upsertFrontmatterBoolean(readText(skillMd), "disable-model-invocation", true);
|
|
55
|
+
if (nextSkillMd !== readText(skillMd)) {
|
|
56
|
+
operations.push({ type: "write", path: skillMd, content: nextSkillMd });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const openaiYamlRealPath = realPathOrSelf(openaiYaml);
|
|
60
|
+
if (!plannedRealPaths.has(openaiYamlRealPath)) {
|
|
61
|
+
plannedRealPaths.add(openaiYamlRealPath);
|
|
62
|
+
const currentOpenAiYaml = pathExists(openaiYaml) ? readText(openaiYaml) : "";
|
|
63
|
+
const nextOpenAiYaml = upsertOpenAiImplicitInvocation(currentOpenAiYaml, false);
|
|
64
|
+
if (nextOpenAiYaml !== currentOpenAiYaml) {
|
|
65
|
+
operations.push({ type: "write", path: openaiYaml, content: nextOpenAiYaml });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return operations;
|
|
69
|
+
}
|
|
70
|
+
export function upsertFrontmatterBoolean(markdown, key, value) {
|
|
71
|
+
const line = `${key}: ${value ? "true" : "false"}`;
|
|
72
|
+
if (!markdown.startsWith("---\n")) {
|
|
73
|
+
return `---\n${line}\n---\n\n${markdown}`;
|
|
74
|
+
}
|
|
75
|
+
const end = markdown.indexOf("\n---", 4);
|
|
76
|
+
if (end === -1) {
|
|
77
|
+
return `---\n${line}\n---\n\n${markdown.replace(/^---\n?/, "")}`;
|
|
78
|
+
}
|
|
79
|
+
const frontmatter = markdown.slice(4, end);
|
|
80
|
+
const body = markdown.slice(end);
|
|
81
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}:\\s*(true|false)\\s*$`, "m");
|
|
82
|
+
const nextFrontmatter = (pattern.test(frontmatter)
|
|
83
|
+
? frontmatter.replace(pattern, line)
|
|
84
|
+
: `${frontmatter.trimEnd()}\n${line}`)
|
|
85
|
+
.trimEnd();
|
|
86
|
+
return `---\n${nextFrontmatter}${body}`;
|
|
87
|
+
}
|
|
88
|
+
export function upsertOpenAiImplicitInvocation(yaml, value) {
|
|
89
|
+
const line = ` allow_implicit_invocation: ${value ? "true" : "false"}`;
|
|
90
|
+
if (!yaml.trim()) {
|
|
91
|
+
return `policy:\n${line}\n`;
|
|
92
|
+
}
|
|
93
|
+
if (/^\s*allow_implicit_invocation:\s*(true|false)\s*$/m.test(yaml)) {
|
|
94
|
+
return ensureTrailingNewline(yaml.replace(/^\s*allow_implicit_invocation:\s*(true|false)\s*$/m, line));
|
|
95
|
+
}
|
|
96
|
+
if (/^policy:\s*$/m.test(yaml)) {
|
|
97
|
+
return ensureTrailingNewline(yaml.replace(/^policy:\s*$/m, `policy:\n${line}`));
|
|
98
|
+
}
|
|
99
|
+
return `${ensureTrailingNewline(yaml)}policy:\n${line}\n`;
|
|
100
|
+
}
|
|
101
|
+
function realPathOrSelf(path) {
|
|
102
|
+
try {
|
|
103
|
+
return realpathSync(path);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return path;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function ensureTrailingNewline(value) {
|
|
110
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
111
|
+
}
|
|
112
|
+
function escapeRegExp(value) {
|
|
113
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
|
+
}
|