@codeharbor/agent-playbook 0.1.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/LICENSE +21 -0
- package/README.md +42 -0
- package/bin/agent-playbook.js +14 -0
- package/package.json +24 -0
- package/src/cli.js +1101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agent Playbook Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @codeharbor/agent-playbook
|
|
2
|
+
|
|
3
|
+
One-command installer and workflow fixer for agent-playbook across Claude Code and Codex.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm dlx @codeharbor/agent-playbook init
|
|
9
|
+
# or
|
|
10
|
+
npm exec -- @codeharbor/agent-playbook init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Project-only setup:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm dlx @codeharbor/agent-playbook init --project
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What It Does
|
|
20
|
+
- Links skills to `~/.claude/skills` and `~/.codex/skills` (or project `.claude/.codex`).
|
|
21
|
+
- Installs a stable CLI copy under `~/.claude/agent-playbook/` for hook execution.
|
|
22
|
+
- Adds Claude hooks for SessionEnd (session logs) and PostToolUse (self-improve MVP).
|
|
23
|
+
- Records a metadata block in `~/.codex/config.toml`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
- `agent-playbook init [--project] [--copy] [--hooks] [--no-hooks] [--session-dir <path>] [--dry-run] [--repo <path>]`
|
|
27
|
+
- `agent-playbook status`
|
|
28
|
+
- `agent-playbook doctor`
|
|
29
|
+
- `agent-playbook repair`
|
|
30
|
+
- `agent-playbook uninstall`
|
|
31
|
+
- `agent-playbook session-log [--session-dir <path>]`
|
|
32
|
+
- `agent-playbook self-improve`
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
- Default session logs go to repo `sessions/` if a Git root is found; otherwise `~/.claude/sessions/`.
|
|
36
|
+
- Hooks are merged without overwriting existing user hooks.
|
|
37
|
+
- Requires Node.js 18+.
|
|
38
|
+
|
|
39
|
+
## Advanced
|
|
40
|
+
- Override Claude/Codex config paths for testing:
|
|
41
|
+
- `AGENT_PLAYBOOK_CLAUDE_DIR=/tmp/claude`
|
|
42
|
+
- `AGENT_PLAYBOOK_CODEX_DIR=/tmp/codex`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { main } = require("../src/cli");
|
|
6
|
+
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
const cliPath = path.resolve(__dirname, "agent-playbook.js");
|
|
9
|
+
|
|
10
|
+
main(argv, { cliPath }).catch((error) => {
|
|
11
|
+
const message = error && error.stack ? error.stack : String(error || "Unknown error");
|
|
12
|
+
console.error(message);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeharbor/agent-playbook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-click installer and workflow fixer for agent-playbook across Claude Code and Codex.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-playbook": "bin/agent-playbook.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
|
+
"lint": "node -c src/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"templates",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PACKAGE_NAME = "@codeharbor/agent-playbook";
|
|
8
|
+
const APP_NAME = "agent-playbook";
|
|
9
|
+
const SKILLS_DIR_NAME = "skills";
|
|
10
|
+
const DEFAULT_SESSION_DIR = "sessions";
|
|
11
|
+
const LOCAL_CLI_DIR = "agent-playbook";
|
|
12
|
+
const HOOK_SOURCE_VALUE = "agent-playbook";
|
|
13
|
+
|
|
14
|
+
const packageJson = readJsonSafe(path.join(__dirname, "..", "package.json"));
|
|
15
|
+
const VERSION = packageJson.version || "0.0.0";
|
|
16
|
+
|
|
17
|
+
function main(argv, context) {
|
|
18
|
+
const parsed = parseArgs(argv);
|
|
19
|
+
const command = parsed.command || "help";
|
|
20
|
+
const options = parsed.options;
|
|
21
|
+
|
|
22
|
+
switch (command) {
|
|
23
|
+
case "init":
|
|
24
|
+
return handleInit(options, context);
|
|
25
|
+
case "status":
|
|
26
|
+
return handleStatus(options);
|
|
27
|
+
case "doctor":
|
|
28
|
+
return handleDoctor(options);
|
|
29
|
+
case "repair":
|
|
30
|
+
return handleRepair({ ...options, repair: true }, context);
|
|
31
|
+
case "uninstall":
|
|
32
|
+
return handleUninstall(options);
|
|
33
|
+
case "session-log":
|
|
34
|
+
return handleSessionLog(options);
|
|
35
|
+
case "self-improve":
|
|
36
|
+
return handleSelfImprove(options);
|
|
37
|
+
case "help":
|
|
38
|
+
case "--help":
|
|
39
|
+
case "-h":
|
|
40
|
+
default:
|
|
41
|
+
printHelp();
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printHelp() {
|
|
47
|
+
const text = [
|
|
48
|
+
`${APP_NAME} ${VERSION}`,
|
|
49
|
+
"",
|
|
50
|
+
"Usage:",
|
|
51
|
+
` ${APP_NAME} init [--project] [--copy] [--hooks] [--no-hooks] [--session-dir <path>] [--dry-run] [--repo <path>]`,
|
|
52
|
+
` ${APP_NAME} status [--project] [--repo <path>]`,
|
|
53
|
+
` ${APP_NAME} doctor [--project] [--repo <path>]`,
|
|
54
|
+
` ${APP_NAME} repair [--project] [--repo <path>]`,
|
|
55
|
+
` ${APP_NAME} uninstall [--project] [--repo <path>]`,
|
|
56
|
+
"",
|
|
57
|
+
"Hook commands:",
|
|
58
|
+
` ${APP_NAME} session-log [--session-dir <path>]`,
|
|
59
|
+
` ${APP_NAME} self-improve`,
|
|
60
|
+
];
|
|
61
|
+
console.log(text.join("\n"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseArgs(argv) {
|
|
65
|
+
const valueFlags = new Set(["session-dir", "repo", "transcript-path", "cwd", "hook-source"]);
|
|
66
|
+
const options = {};
|
|
67
|
+
const positionals = [];
|
|
68
|
+
let command = null;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
71
|
+
const arg = argv[i];
|
|
72
|
+
if (!command && !arg.startsWith("-")) {
|
|
73
|
+
command = arg;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (arg.startsWith("--no-")) {
|
|
78
|
+
const key = arg.slice(5);
|
|
79
|
+
options[key] = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (arg.startsWith("--")) {
|
|
84
|
+
const eqIndex = arg.indexOf("=");
|
|
85
|
+
const key = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
|
|
86
|
+
const hasValue = eqIndex !== -1;
|
|
87
|
+
|
|
88
|
+
if (hasValue) {
|
|
89
|
+
options[key] = arg.slice(eqIndex + 1);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (valueFlags.has(key)) {
|
|
94
|
+
const next = argv[i + 1];
|
|
95
|
+
if (next && !next.startsWith("-")) {
|
|
96
|
+
options[key] = next;
|
|
97
|
+
i += 1;
|
|
98
|
+
} else {
|
|
99
|
+
options[key] = "";
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
options[key] = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
positionals.push(arg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { command, options, positionals };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleInit(options, context) {
|
|
115
|
+
const settings = resolveSettings(options, context);
|
|
116
|
+
const hooksEnabled = options.hooks !== false;
|
|
117
|
+
const repoRoot = settings.repoRoot;
|
|
118
|
+
const warnings = [];
|
|
119
|
+
|
|
120
|
+
if (!settings.skillsSource) {
|
|
121
|
+
if (options.repair) {
|
|
122
|
+
warnings.push("Skills directory not found; skipping skill linking.");
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error("Unable to locate skills directory. Run from the agent-playbook repo or pass --repo.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ensureDir(settings.claudeSkillsDir, options["dry-run"]);
|
|
129
|
+
ensureDir(settings.codexSkillsDir, options["dry-run"]);
|
|
130
|
+
|
|
131
|
+
const manifest = {
|
|
132
|
+
name: APP_NAME,
|
|
133
|
+
version: VERSION,
|
|
134
|
+
installedAt: new Date().toISOString(),
|
|
135
|
+
repoRoot,
|
|
136
|
+
copyMode: Boolean(options.copy),
|
|
137
|
+
links: {
|
|
138
|
+
claude: [],
|
|
139
|
+
codex: [],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
let claudeLinks = { created: [], skipped: [] };
|
|
144
|
+
let codexLinks = { created: [], skipped: [] };
|
|
145
|
+
if (settings.skillsSource) {
|
|
146
|
+
claudeLinks = linkSkills(settings.skillsSource, settings.claudeSkillsDir, options);
|
|
147
|
+
codexLinks = linkSkills(settings.skillsSource, settings.codexSkillsDir, options);
|
|
148
|
+
manifest.links.claude = claudeLinks.created;
|
|
149
|
+
manifest.links.codex = codexLinks.created;
|
|
150
|
+
|
|
151
|
+
if (!options["dry-run"]) {
|
|
152
|
+
writeJson(path.join(settings.claudeSkillsDir, ".agent-playbook.json"), manifest);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hooksEnabled) {
|
|
157
|
+
const hookCommandPath = ensureLocalCli(settings, context, options);
|
|
158
|
+
const hookUpdated = updateClaudeSettings(settings, hookCommandPath, options);
|
|
159
|
+
if (hookUpdated === false) {
|
|
160
|
+
warnings.push("Unable to update Claude settings (invalid JSON).");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateCodexConfig(settings, options);
|
|
165
|
+
|
|
166
|
+
printInitSummary(settings, hooksEnabled, options, claudeLinks, codexLinks, warnings);
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleStatus(options) {
|
|
171
|
+
const settings = resolveSettings(options, {});
|
|
172
|
+
const status = collectStatus(settings);
|
|
173
|
+
printStatus(status);
|
|
174
|
+
return Promise.resolve();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleDoctor(options) {
|
|
178
|
+
const settings = resolveSettings(options, {});
|
|
179
|
+
const status = collectStatus(settings);
|
|
180
|
+
const issues = summarizeIssues(status);
|
|
181
|
+
|
|
182
|
+
printStatus(status);
|
|
183
|
+
if (issues.length) {
|
|
184
|
+
console.error("\nIssues detected:");
|
|
185
|
+
issues.forEach((issue) => console.error(`- ${issue}`));
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
} else {
|
|
188
|
+
console.log("\nNo critical issues detected.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return Promise.resolve();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleUninstall(options) {
|
|
195
|
+
const settings = resolveSettings(options, {});
|
|
196
|
+
const manifestPath = path.join(settings.claudeSkillsDir, ".agent-playbook.json");
|
|
197
|
+
const manifest = readJsonSafe(manifestPath);
|
|
198
|
+
|
|
199
|
+
if (manifest && manifest.links) {
|
|
200
|
+
removeLinks(manifest.links.claude || []);
|
|
201
|
+
removeLinks(manifest.links.codex || []);
|
|
202
|
+
safeUnlink(manifestPath);
|
|
203
|
+
} else {
|
|
204
|
+
console.log("No manifest found. Skipping link removal.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
removeHooks(settings);
|
|
208
|
+
removeCodexConfig(settings);
|
|
209
|
+
removeLocalCli(settings);
|
|
210
|
+
console.log("Uninstall complete.");
|
|
211
|
+
return Promise.resolve();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function handleSessionLog(options) {
|
|
215
|
+
const input = await readStdinJson();
|
|
216
|
+
const transcriptPath = options["transcript-path"] || input.transcript_path;
|
|
217
|
+
const cwd = options.cwd || input.cwd || process.cwd();
|
|
218
|
+
const sessionId = input.session_id || "unknown";
|
|
219
|
+
const sessionDir = resolveSessionDir(options["session-dir"], cwd);
|
|
220
|
+
|
|
221
|
+
ensureDir(sessionDir, false);
|
|
222
|
+
|
|
223
|
+
const events = transcriptPath ? readTranscript(transcriptPath) : [];
|
|
224
|
+
const insights = collectTranscriptInsights(events);
|
|
225
|
+
const lastUserPrompt = insights.lastUserPrompt;
|
|
226
|
+
const topic = buildTopic(lastUserPrompt, cwd);
|
|
227
|
+
const fileName = `${formatDate(new Date())}-${topic}.md`;
|
|
228
|
+
const outputPath = resolveUniquePath(path.join(sessionDir, fileName));
|
|
229
|
+
const summary = buildSessionSummary(insights, sessionId, cwd);
|
|
230
|
+
|
|
231
|
+
fs.writeFileSync(outputPath, summary, "utf8");
|
|
232
|
+
console.error(`Session log saved to ${outputPath}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function handleSelfImprove(options) {
|
|
236
|
+
const input = await readStdinJson();
|
|
237
|
+
const cwd = input.cwd || process.cwd();
|
|
238
|
+
const sessionId = input.session_id || "unknown";
|
|
239
|
+
const transcriptPath = input.transcript_path || "";
|
|
240
|
+
const now = new Date();
|
|
241
|
+
const memoryRoot = path.join(os.homedir(), ".claude", "memory");
|
|
242
|
+
const episodicDir = path.join(memoryRoot, "episodic", String(now.getFullYear()));
|
|
243
|
+
const workingDir = path.join(memoryRoot, "working");
|
|
244
|
+
|
|
245
|
+
ensureDir(episodicDir, false);
|
|
246
|
+
ensureDir(workingDir, false);
|
|
247
|
+
|
|
248
|
+
const entry = {
|
|
249
|
+
id: `ep-${now.toISOString()}`.replace(/[:.]/g, "-"),
|
|
250
|
+
timestamp: now.toISOString(),
|
|
251
|
+
session_id: sessionId,
|
|
252
|
+
cwd,
|
|
253
|
+
transcript_path: transcriptPath,
|
|
254
|
+
hook_event: input.hook_event_name || "PostToolUse",
|
|
255
|
+
tool_name: input.tool_name || "",
|
|
256
|
+
tool_input: input.tool_input || "",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const entryPath = path.join(episodicDir, `${entry.id}.json`);
|
|
260
|
+
fs.writeFileSync(entryPath, JSON.stringify(entry, null, 2));
|
|
261
|
+
|
|
262
|
+
const workingPath = path.join(workingDir, "current_session.json");
|
|
263
|
+
fs.writeFileSync(workingPath, JSON.stringify(entry, null, 2));
|
|
264
|
+
console.error(`Self-improvement entry saved to ${entryPath}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveSettings(options, context) {
|
|
268
|
+
const cwd = process.cwd();
|
|
269
|
+
const repoRoot = options.repo ? path.resolve(options.repo) : findRepoRoot(cwd);
|
|
270
|
+
const skillsSource = findSkillsSource(repoRoot || cwd);
|
|
271
|
+
const projectMode = Boolean(options.project);
|
|
272
|
+
|
|
273
|
+
const envClaudeDir = process.env.AGENT_PLAYBOOK_CLAUDE_DIR;
|
|
274
|
+
const envCodexDir = process.env.AGENT_PLAYBOOK_CODEX_DIR;
|
|
275
|
+
const claudeDir = envClaudeDir
|
|
276
|
+
? path.resolve(envClaudeDir)
|
|
277
|
+
: projectMode
|
|
278
|
+
? path.join(repoRoot || cwd, ".claude")
|
|
279
|
+
: path.join(os.homedir(), ".claude");
|
|
280
|
+
const codexDir = envCodexDir
|
|
281
|
+
? path.resolve(envCodexDir)
|
|
282
|
+
: projectMode
|
|
283
|
+
? path.join(repoRoot || cwd, ".codex")
|
|
284
|
+
: path.join(os.homedir(), ".codex");
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
cwd,
|
|
288
|
+
repoRoot: repoRoot || cwd,
|
|
289
|
+
skillsSource,
|
|
290
|
+
projectMode,
|
|
291
|
+
cliPath: context && context.cliPath ? context.cliPath : null,
|
|
292
|
+
claudeDir,
|
|
293
|
+
codexDir,
|
|
294
|
+
claudeSkillsDir: path.join(claudeDir, SKILLS_DIR_NAME),
|
|
295
|
+
codexSkillsDir: path.join(codexDir, SKILLS_DIR_NAME),
|
|
296
|
+
claudeSettingsPath: path.join(claudeDir, "settings.json"),
|
|
297
|
+
codexConfigPath: path.join(codexDir, "config.toml"),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function findRepoRoot(startDir) {
|
|
302
|
+
let current = startDir;
|
|
303
|
+
while (current && current !== path.dirname(current)) {
|
|
304
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
305
|
+
return current;
|
|
306
|
+
}
|
|
307
|
+
current = path.dirname(current);
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function findSkillsSource(startDir) {
|
|
313
|
+
let current = startDir;
|
|
314
|
+
while (current && current !== path.dirname(current)) {
|
|
315
|
+
const candidate = path.join(current, SKILLS_DIR_NAME);
|
|
316
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
317
|
+
const routerPath = path.join(candidate, "skill-router", "SKILL.md");
|
|
318
|
+
if (fs.existsSync(routerPath)) {
|
|
319
|
+
return candidate;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
current = path.dirname(current);
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function linkSkills(sourceDir, targetDir, options) {
|
|
328
|
+
const created = [];
|
|
329
|
+
const skipped = [];
|
|
330
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
331
|
+
|
|
332
|
+
entries.forEach((entry) => {
|
|
333
|
+
if (!entry.isDirectory()) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (entry.name.startsWith(".")) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const skillDir = path.join(sourceDir, entry.name);
|
|
341
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
342
|
+
if (!fs.existsSync(skillFile)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
347
|
+
if (fs.existsSync(targetPath)) {
|
|
348
|
+
if (isSameLink(targetPath, skillDir)) {
|
|
349
|
+
skipped.push({ source: skillDir, target: targetPath, reason: "already linked" });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
skipped.push({ source: skillDir, target: targetPath, reason: "exists" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (options["dry-run"]) {
|
|
357
|
+
created.push({ source: skillDir, target: targetPath, mode: options.copy ? "copy" : "link", dryRun: true });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (options.copy) {
|
|
362
|
+
fs.cpSync(skillDir, targetPath, { recursive: true });
|
|
363
|
+
created.push({ source: skillDir, target: targetPath, mode: "copy" });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
368
|
+
try {
|
|
369
|
+
fs.symlinkSync(skillDir, targetPath, linkType);
|
|
370
|
+
created.push({ source: skillDir, target: targetPath, mode: "link" });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
fs.cpSync(skillDir, targetPath, { recursive: true });
|
|
373
|
+
created.push({ source: skillDir, target: targetPath, mode: "copy", fallback: "symlink_failed" });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return { created, skipped };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function ensureLocalCli(settings, context, options) {
|
|
381
|
+
const baseDir = settings.projectMode ? settings.claudeDir : settings.claudeDir;
|
|
382
|
+
const cliRoot = path.join(baseDir, LOCAL_CLI_DIR);
|
|
383
|
+
const targetBin = path.join(cliRoot, "bin", "agent-playbook.js");
|
|
384
|
+
const sourceRoot = path.resolve(__dirname, "..");
|
|
385
|
+
|
|
386
|
+
if (options["dry-run"]) {
|
|
387
|
+
return targetBin;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
ensureDir(cliRoot, false);
|
|
391
|
+
fs.cpSync(path.join(sourceRoot, "bin"), path.join(cliRoot, "bin"), { recursive: true });
|
|
392
|
+
fs.cpSync(path.join(sourceRoot, "src"), path.join(cliRoot, "src"), { recursive: true });
|
|
393
|
+
fs.cpSync(path.join(sourceRoot, "package.json"), path.join(cliRoot, "package.json"));
|
|
394
|
+
|
|
395
|
+
fs.chmodSync(targetBin, 0o755);
|
|
396
|
+
return targetBin;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function updateClaudeSettings(settings, cliPath, options) {
|
|
400
|
+
const settingsPath = settings.claudeSettingsPath;
|
|
401
|
+
const existing = readJsonSafe(settingsPath);
|
|
402
|
+
if (existing === null && fs.existsSync(settingsPath)) {
|
|
403
|
+
console.error("Warning: unable to parse Claude settings.json, skipping hook update.");
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
const data = existing || {};
|
|
407
|
+
|
|
408
|
+
data.hooks = data.hooks || {};
|
|
409
|
+
|
|
410
|
+
let sessionCommand = buildHookCommand(cliPath, "session-log");
|
|
411
|
+
sessionCommand = `${sessionCommand} --hook-source ${HOOK_SOURCE_VALUE}`;
|
|
412
|
+
if (options["session-dir"]) {
|
|
413
|
+
const sessionDir = path.resolve(options["session-dir"]);
|
|
414
|
+
sessionCommand = `${sessionCommand} --session-dir \"${sessionDir}\"`;
|
|
415
|
+
}
|
|
416
|
+
const improveCommand = `${buildHookCommand(cliPath, "self-improve")} --hook-source ${HOOK_SOURCE_VALUE}`;
|
|
417
|
+
|
|
418
|
+
ensureHook(data.hooks, "SessionEnd", null, sessionCommand);
|
|
419
|
+
ensureHook(data.hooks, "PostToolUse", "*", improveCommand);
|
|
420
|
+
|
|
421
|
+
data.agentPlaybook = {
|
|
422
|
+
version: VERSION,
|
|
423
|
+
installedAt: new Date().toISOString(),
|
|
424
|
+
cliPath,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
if (!options["dry-run"]) {
|
|
428
|
+
backupFile(settingsPath);
|
|
429
|
+
ensureDir(path.dirname(settingsPath), false);
|
|
430
|
+
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2));
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function removeHooks(settings) {
|
|
436
|
+
const settingsPath = settings.claudeSettingsPath;
|
|
437
|
+
const data = readJsonSafe(settingsPath);
|
|
438
|
+
if (!data || !data.hooks) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const marker = `--hook-source ${HOOK_SOURCE_VALUE}`;
|
|
443
|
+
data.hooks = removeHookCommand(data.hooks, "SessionEnd", marker);
|
|
444
|
+
data.hooks = removeHookCommand(data.hooks, "PostToolUse", marker);
|
|
445
|
+
|
|
446
|
+
delete data.agentPlaybook;
|
|
447
|
+
|
|
448
|
+
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function updateCodexConfig(settings, options) {
|
|
452
|
+
if (options["dry-run"]) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
ensureDir(settings.codexDir, false);
|
|
457
|
+
const configPath = settings.codexConfigPath;
|
|
458
|
+
const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
459
|
+
const updated = upsertCodexBlock(content, {
|
|
460
|
+
version: VERSION,
|
|
461
|
+
installed_at: new Date().toISOString(),
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
backupFile(configPath);
|
|
465
|
+
fs.writeFileSync(configPath, updated);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function removeCodexConfig(settings) {
|
|
469
|
+
const configPath = settings.codexConfigPath;
|
|
470
|
+
if (!fs.existsSync(configPath)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
474
|
+
const cleaned = removeCodexBlock(content);
|
|
475
|
+
fs.writeFileSync(configPath, cleaned);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function removeLocalCli(settings) {
|
|
479
|
+
const cliRoot = path.join(settings.claudeDir, LOCAL_CLI_DIR);
|
|
480
|
+
if (fs.existsSync(cliRoot)) {
|
|
481
|
+
fs.rmSync(cliRoot, { recursive: true, force: true });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function removeLinks(links) {
|
|
486
|
+
links.forEach((link) => {
|
|
487
|
+
if (!link || !link.target) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
safeUnlink(link.target);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function ensureHook(hooks, eventName, matcher, command) {
|
|
495
|
+
hooks[eventName] = hooks[eventName] || [];
|
|
496
|
+
const entries = hooks[eventName];
|
|
497
|
+
|
|
498
|
+
let entry = entries.find((item) => (matcher ? item.matcher === matcher : !item.matcher));
|
|
499
|
+
if (!entry) {
|
|
500
|
+
entry = matcher ? { matcher, hooks: [] } : { hooks: [] };
|
|
501
|
+
entries.push(entry);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
entry.hooks = entry.hooks || [];
|
|
505
|
+
const exists = entry.hooks.some((hook) => hook.command === command);
|
|
506
|
+
if (!exists) {
|
|
507
|
+
entry.hooks.push({ type: "command", command });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function removeHookCommand(hooks, eventName, command) {
|
|
512
|
+
const entries = hooks[eventName];
|
|
513
|
+
if (!entries) {
|
|
514
|
+
return hooks;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
hooks[eventName] = entries
|
|
518
|
+
.map((entry) => {
|
|
519
|
+
const nextHooks = (entry.hooks || []).filter((hook) => !String(hook.command || "").includes(command));
|
|
520
|
+
return { ...entry, hooks: nextHooks };
|
|
521
|
+
})
|
|
522
|
+
.filter((entry) => (entry.hooks || []).length > 0);
|
|
523
|
+
|
|
524
|
+
if (!hooks[eventName].length) {
|
|
525
|
+
delete hooks[eventName];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return hooks;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function upsertCodexBlock(content, values) {
|
|
532
|
+
const cleaned = removeCodexBlock(content);
|
|
533
|
+
const lines = [
|
|
534
|
+
cleaned.trimEnd(),
|
|
535
|
+
"",
|
|
536
|
+
"[agent_playbook]",
|
|
537
|
+
`version = \"${values.version}\"`,
|
|
538
|
+
`installed_at = \"${values.installed_at}\"`,
|
|
539
|
+
"",
|
|
540
|
+
];
|
|
541
|
+
return lines.join("\n");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function removeCodexBlock(content) {
|
|
545
|
+
const pattern = /^\[agent_playbook\][\s\S]*?(?=^\[|\s*$)/m;
|
|
546
|
+
return content.replace(pattern, "").trimEnd();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function buildHookCommand(cliPath, subcommand) {
|
|
550
|
+
const quoted = cliPath.includes(" ") ? `\"${cliPath}\"` : cliPath;
|
|
551
|
+
return `${quoted} ${subcommand}`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function resolveSessionDir(explicit, cwd) {
|
|
555
|
+
if (explicit) {
|
|
556
|
+
return path.resolve(explicit);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const repoRoot = findRepoRoot(cwd);
|
|
560
|
+
if (repoRoot) {
|
|
561
|
+
return path.join(repoRoot, DEFAULT_SESSION_DIR);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return path.join(os.homedir(), ".claude", DEFAULT_SESSION_DIR);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function readTranscript(transcriptPath) {
|
|
568
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
const lines = fs.readFileSync(transcriptPath, "utf8").split("\n");
|
|
572
|
+
const events = [];
|
|
573
|
+
|
|
574
|
+
lines.forEach((line) => {
|
|
575
|
+
if (!line.trim()) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
events.push(JSON.parse(line));
|
|
580
|
+
} catch (error) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
return events;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function resolveUniquePath(filePath) {
|
|
589
|
+
if (!fs.existsSync(filePath)) {
|
|
590
|
+
return filePath;
|
|
591
|
+
}
|
|
592
|
+
const parsed = path.parse(filePath);
|
|
593
|
+
let counter = 1;
|
|
594
|
+
let candidate = filePath;
|
|
595
|
+
while (fs.existsSync(candidate)) {
|
|
596
|
+
candidate = path.join(parsed.dir, `${parsed.name}-${counter}${parsed.ext}`);
|
|
597
|
+
counter += 1;
|
|
598
|
+
}
|
|
599
|
+
return candidate;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function collectTranscriptInsights(events) {
|
|
603
|
+
const insights = {
|
|
604
|
+
userMessages: [],
|
|
605
|
+
assistantMessages: [],
|
|
606
|
+
commands: [],
|
|
607
|
+
files: [],
|
|
608
|
+
questions: [],
|
|
609
|
+
lastUserPrompt: "",
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
events.forEach((event) => {
|
|
613
|
+
const role = getEventRole(event);
|
|
614
|
+
const text = extractEventText(event);
|
|
615
|
+
|
|
616
|
+
if (!text) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (role === "user") {
|
|
621
|
+
insights.userMessages.push(text);
|
|
622
|
+
insights.lastUserPrompt = text;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (role === "assistant") {
|
|
626
|
+
insights.assistantMessages.push(text);
|
|
627
|
+
insights.commands.push(...extractCommands(text));
|
|
628
|
+
insights.questions.push(...extractQuestions(text));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
insights.files.push(...extractFilePaths(text));
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
insights.commands = uniqueList(insights.commands, 12);
|
|
635
|
+
insights.files = uniqueList(insights.files, 12);
|
|
636
|
+
insights.questions = uniqueList(insights.questions, 8);
|
|
637
|
+
|
|
638
|
+
return insights;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function getEventRole(event) {
|
|
642
|
+
if (!event) {
|
|
643
|
+
return "";
|
|
644
|
+
}
|
|
645
|
+
if (event.message && typeof event.message.role === "string") {
|
|
646
|
+
return event.message.role;
|
|
647
|
+
}
|
|
648
|
+
if (typeof event.role === "string") {
|
|
649
|
+
return event.role;
|
|
650
|
+
}
|
|
651
|
+
if (event.type === "user" || event.type === "assistant") {
|
|
652
|
+
return event.type;
|
|
653
|
+
}
|
|
654
|
+
return "";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function extractEventText(event) {
|
|
658
|
+
if (!event) {
|
|
659
|
+
return "";
|
|
660
|
+
}
|
|
661
|
+
if (event.message) {
|
|
662
|
+
if (event.message.content) {
|
|
663
|
+
return extractText(event.message.content);
|
|
664
|
+
}
|
|
665
|
+
if (event.message.text) {
|
|
666
|
+
return extractText(event.message.text);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (event.content) {
|
|
670
|
+
return extractText(event.content);
|
|
671
|
+
}
|
|
672
|
+
if (event.text) {
|
|
673
|
+
return extractText(event.text);
|
|
674
|
+
}
|
|
675
|
+
return "";
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function extractCommands(text) {
|
|
679
|
+
const commands = [];
|
|
680
|
+
if (!text) {
|
|
681
|
+
return commands;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const paramRegex = /<parameter name=\"command\">([\s\S]*?)<\/parameter>/g;
|
|
685
|
+
let match = paramRegex.exec(text);
|
|
686
|
+
while (match) {
|
|
687
|
+
commands.push(...splitCommands(match[1]));
|
|
688
|
+
match = paramRegex.exec(text);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const fenceRegex = /```(?:bash|sh|zsh|shell)?\n([\s\S]*?)```/g;
|
|
692
|
+
match = fenceRegex.exec(text);
|
|
693
|
+
while (match) {
|
|
694
|
+
commands.push(...splitCommands(match[1]));
|
|
695
|
+
match = fenceRegex.exec(text);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return commands.map((cmd) => cmd.trim()).filter(Boolean);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function splitCommands(block) {
|
|
702
|
+
return String(block || "")
|
|
703
|
+
.split("\n")
|
|
704
|
+
.map((line) => line.replace(/^\s*\$\s?/, "").trim())
|
|
705
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function extractQuestions(text) {
|
|
709
|
+
if (!text) {
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
712
|
+
const questions = [];
|
|
713
|
+
const lines = text.split("\n");
|
|
714
|
+
lines.forEach((line) => {
|
|
715
|
+
const trimmed = line.trim();
|
|
716
|
+
if (trimmed.includes("?") && trimmed.length <= 200) {
|
|
717
|
+
questions.push(trimmed.replace(/^[*-]\s*/, ""));
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
return questions;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function extractFilePaths(text) {
|
|
724
|
+
if (!text) {
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
const regex = /\b[\w./~\-]+?\.(?:md|mdx|json|jsonl|js|ts|tsx|jsx|py|sh|toml|yaml|yml|txt|lock)\b/gi;
|
|
728
|
+
const matches = text.match(regex);
|
|
729
|
+
return matches ? matches : [];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function extractText(content) {
|
|
733
|
+
if (!content) {
|
|
734
|
+
return "";
|
|
735
|
+
}
|
|
736
|
+
if (typeof content === "string") {
|
|
737
|
+
return content;
|
|
738
|
+
}
|
|
739
|
+
if (Array.isArray(content)) {
|
|
740
|
+
return content
|
|
741
|
+
.map((item) => {
|
|
742
|
+
if (typeof item === "string") {
|
|
743
|
+
return item;
|
|
744
|
+
}
|
|
745
|
+
if (item && typeof item.text === "string") {
|
|
746
|
+
return item.text;
|
|
747
|
+
}
|
|
748
|
+
return "";
|
|
749
|
+
})
|
|
750
|
+
.join("\n")
|
|
751
|
+
.trim();
|
|
752
|
+
}
|
|
753
|
+
if (content && typeof content.text === "string") {
|
|
754
|
+
return content.text;
|
|
755
|
+
}
|
|
756
|
+
return "";
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function buildTopic(prompt, cwd) {
|
|
760
|
+
if (prompt) {
|
|
761
|
+
return slugify(prompt).slice(0, 40) || "session";
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return slugify(path.basename(cwd)) || "session";
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function buildSessionSummary(insights, sessionId, cwd) {
|
|
768
|
+
const now = new Date();
|
|
769
|
+
const date = formatDate(now);
|
|
770
|
+
const repoRoot = findRepoRoot(cwd) || cwd;
|
|
771
|
+
const title = insights.lastUserPrompt ? trimTo(insights.lastUserPrompt, 60) : "Session";
|
|
772
|
+
const actions = insights.commands.length
|
|
773
|
+
? insights.commands.map((cmd) => `- [x] \`${trimTo(cmd, 120)}\``)
|
|
774
|
+
: ["- [ ] (auto) No commands captured"];
|
|
775
|
+
const relatedFiles = insights.files.length
|
|
776
|
+
? insights.files.map((file) => `- \`${file}\``)
|
|
777
|
+
: ["- (auto) None captured"];
|
|
778
|
+
const questions = insights.questions.length
|
|
779
|
+
? insights.questions.map((question) => `- ${question}`)
|
|
780
|
+
: ["- (auto) None captured"];
|
|
781
|
+
|
|
782
|
+
const summaryLines = [
|
|
783
|
+
`# Session: ${title}`,
|
|
784
|
+
"",
|
|
785
|
+
`**Date**: ${date}`,
|
|
786
|
+
`**Duration**: unknown`,
|
|
787
|
+
`**Context**: ${repoRoot}`,
|
|
788
|
+
"",
|
|
789
|
+
"## Summary",
|
|
790
|
+
"Auto-generated session log.",
|
|
791
|
+
`- Messages: ${insights.userMessages.length} user, ${insights.assistantMessages.length} assistant`,
|
|
792
|
+
`- Commands detected: ${insights.commands.length}`,
|
|
793
|
+
`- Files referenced: ${insights.files.length}`,
|
|
794
|
+
insights.lastUserPrompt
|
|
795
|
+
? `- Last user prompt: ${trimTo(insights.lastUserPrompt, 120)}`
|
|
796
|
+
: "- Last user prompt: (not available)",
|
|
797
|
+
"",
|
|
798
|
+
"## Key Decisions",
|
|
799
|
+
"1. (auto) No structured decisions extracted",
|
|
800
|
+
"",
|
|
801
|
+
"## Actions Taken",
|
|
802
|
+
...actions,
|
|
803
|
+
"",
|
|
804
|
+
"## Technical Notes",
|
|
805
|
+
`Session ID: ${sessionId}`,
|
|
806
|
+
`Working directory: ${cwd}`,
|
|
807
|
+
"",
|
|
808
|
+
"## Open Questions / Follow-ups",
|
|
809
|
+
...questions,
|
|
810
|
+
"",
|
|
811
|
+
"## Related Files",
|
|
812
|
+
...relatedFiles,
|
|
813
|
+
"",
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
return summaryLines.join("\n");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function collectStatus(settings) {
|
|
820
|
+
const claudeSettings = readJsonSafe(settings.claudeSettingsPath);
|
|
821
|
+
return {
|
|
822
|
+
skillsSource: settings.skillsSource,
|
|
823
|
+
claudeSettingsPath: settings.claudeSettingsPath,
|
|
824
|
+
codexConfigPath: settings.codexConfigPath,
|
|
825
|
+
claudeSkillsDir: settings.claudeSkillsDir,
|
|
826
|
+
codexSkillsDir: settings.codexSkillsDir,
|
|
827
|
+
claudeSettingsReadable: claudeSettings !== null || !fs.existsSync(settings.claudeSettingsPath),
|
|
828
|
+
codexBlockPresent: hasCodexBlock(settings.codexConfigPath),
|
|
829
|
+
hooksInstalled: hasHooks(settings.claudeSettingsPath),
|
|
830
|
+
manifestPresent: fs.existsSync(path.join(settings.claudeSkillsDir, ".agent-playbook.json")),
|
|
831
|
+
localCliPresent: fs.existsSync(path.join(settings.claudeDir, LOCAL_CLI_DIR, "bin", "agent-playbook.js")),
|
|
832
|
+
claudeSkillCount: countSkills(settings.claudeSkillsDir),
|
|
833
|
+
codexSkillCount: countSkills(settings.codexSkillsDir),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function hasHooks(settingsPath) {
|
|
838
|
+
const data = readJsonSafe(settingsPath);
|
|
839
|
+
if (!data || !data.hooks) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
const sessionHook = (data.hooks.SessionEnd || []).some((entry) =>
|
|
843
|
+
(entry.hooks || []).some((hook) => String(hook.command || "").includes("session-log"))
|
|
844
|
+
);
|
|
845
|
+
const improveHook = (data.hooks.PostToolUse || []).some((entry) =>
|
|
846
|
+
(entry.hooks || []).some((hook) => String(hook.command || "").includes("self-improve"))
|
|
847
|
+
);
|
|
848
|
+
return sessionHook && improveHook;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function summarizeIssues(status) {
|
|
852
|
+
const issues = [];
|
|
853
|
+
if (!status.skillsSource) {
|
|
854
|
+
issues.push("skills source not found (run init from repo or use --repo)");
|
|
855
|
+
}
|
|
856
|
+
if (!status.claudeSettingsReadable) {
|
|
857
|
+
issues.push("unable to parse ~/.claude/settings.json");
|
|
858
|
+
}
|
|
859
|
+
if (!status.manifestPresent) {
|
|
860
|
+
issues.push("missing skill manifest (.agent-playbook.json)");
|
|
861
|
+
}
|
|
862
|
+
if (!status.hooksInstalled) {
|
|
863
|
+
issues.push("hooks not installed");
|
|
864
|
+
}
|
|
865
|
+
if (!status.localCliPresent) {
|
|
866
|
+
issues.push("local CLI not installed under ~/.claude/agent-playbook");
|
|
867
|
+
}
|
|
868
|
+
if (!status.codexBlockPresent) {
|
|
869
|
+
issues.push("Codex config missing agent_playbook block");
|
|
870
|
+
}
|
|
871
|
+
return issues;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function printStatus(status) {
|
|
875
|
+
console.log("Agent Playbook Status:");
|
|
876
|
+
console.log(`- Skills source: ${status.skillsSource || "(not found)"}`);
|
|
877
|
+
console.log(`- Claude settings: ${status.claudeSettingsPath}`);
|
|
878
|
+
console.log(`- Codex config: ${status.codexConfigPath}`);
|
|
879
|
+
console.log(`- Claude skills: ${status.claudeSkillsDir}`);
|
|
880
|
+
console.log(`- Codex skills: ${status.codexSkillsDir}`);
|
|
881
|
+
console.log(`- Claude skills count: ${status.claudeSkillCount}`);
|
|
882
|
+
console.log(`- Codex skills count: ${status.codexSkillCount}`);
|
|
883
|
+
console.log(`- Hooks installed: ${status.hooksInstalled ? "yes" : "no"}`);
|
|
884
|
+
console.log(`- Manifest present: ${status.manifestPresent ? "yes" : "no"}`);
|
|
885
|
+
console.log(`- Local CLI present: ${status.localCliPresent ? "yes" : "no"}`);
|
|
886
|
+
console.log(`- Codex config block: ${status.codexBlockPresent ? "yes" : "no"}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function printInitSummary(settings, hooksEnabled, options, claudeLinks, codexLinks, warnings) {
|
|
890
|
+
console.log("Init complete.");
|
|
891
|
+
console.log(`- Claude skills: ${settings.claudeSkillsDir}`);
|
|
892
|
+
console.log(`- Codex skills: ${settings.codexSkillsDir}`);
|
|
893
|
+
console.log(`- Hooks: ${hooksEnabled ? "enabled" : "disabled"}`);
|
|
894
|
+
console.log(`- Linked skills: ${claudeLinks.created.length + codexLinks.created.length}`);
|
|
895
|
+
if (claudeLinks.skipped.length || codexLinks.skipped.length) {
|
|
896
|
+
console.log("- Some skills were skipped due to existing paths.");
|
|
897
|
+
}
|
|
898
|
+
if (warnings && warnings.length) {
|
|
899
|
+
warnings.forEach((warning) => console.log(`- Warning: ${warning}`));
|
|
900
|
+
}
|
|
901
|
+
if (options["dry-run"]) {
|
|
902
|
+
console.log("- Dry run: no changes written.");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function uniqueList(items, limit) {
|
|
907
|
+
const seen = new Set();
|
|
908
|
+
const result = [];
|
|
909
|
+
items.forEach((item) => {
|
|
910
|
+
const value = String(item || "").trim();
|
|
911
|
+
if (!value || seen.has(value)) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
seen.add(value);
|
|
915
|
+
result.push(value);
|
|
916
|
+
if (limit && result.length >= limit) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function countSkills(dirPath) {
|
|
924
|
+
if (!fs.existsSync(dirPath)) {
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
928
|
+
let count = 0;
|
|
929
|
+
entries.forEach((entry) => {
|
|
930
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const skillPath = path.join(dirPath, entry.name);
|
|
934
|
+
const skillFile = path.join(skillPath, "SKILL.md");
|
|
935
|
+
if (fs.existsSync(skillFile)) {
|
|
936
|
+
count += 1;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
return count;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function hasCodexBlock(configPath) {
|
|
943
|
+
if (!fs.existsSync(configPath)) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
947
|
+
return /^\[agent_playbook\]/m.test(content);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function backupFile(filePath) {
|
|
951
|
+
if (!fs.existsSync(filePath)) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const backupPath = `${filePath}.bak`;
|
|
955
|
+
if (!fs.existsSync(backupPath)) {
|
|
956
|
+
fs.copyFileSync(filePath, backupPath);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function isSameLink(targetPath, sourcePath) {
|
|
961
|
+
try {
|
|
962
|
+
const stat = fs.lstatSync(targetPath);
|
|
963
|
+
if (!stat.isSymbolicLink()) {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const realTarget = fs.realpathSync(targetPath);
|
|
967
|
+
return realTarget === sourcePath;
|
|
968
|
+
} catch (error) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function ensureDir(dirPath, dryRun) {
|
|
974
|
+
if (dryRun) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function readJsonSafe(filePath) {
|
|
981
|
+
if (!fs.existsSync(filePath)) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
986
|
+
} catch (error) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function writeJson(filePath, data) {
|
|
992
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function safeUnlink(targetPath) {
|
|
996
|
+
if (!fs.existsSync(targetPath)) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function slugify(value) {
|
|
1003
|
+
return String(value || "")
|
|
1004
|
+
.toLowerCase()
|
|
1005
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1006
|
+
.replace(/^-+|-+$/g, "")
|
|
1007
|
+
.replace(/-+/g, "-");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function trimTo(value, length) {
|
|
1011
|
+
const text = String(value || "");
|
|
1012
|
+
if (text.length <= length) {
|
|
1013
|
+
return text;
|
|
1014
|
+
}
|
|
1015
|
+
return `${text.slice(0, length - 3)}...`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function formatDate(date) {
|
|
1019
|
+
return date.toISOString().slice(0, 10);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function readStdinJson() {
|
|
1023
|
+
return new Promise((resolve) => {
|
|
1024
|
+
if (process.stdin.isTTY) {
|
|
1025
|
+
resolve({});
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
let input = "";
|
|
1029
|
+
process.stdin.setEncoding("utf8");
|
|
1030
|
+
process.stdin.on("data", (chunk) => {
|
|
1031
|
+
input += chunk;
|
|
1032
|
+
});
|
|
1033
|
+
process.stdin.on("end", () => {
|
|
1034
|
+
if (!input.trim()) {
|
|
1035
|
+
resolve({});
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
resolve(JSON.parse(input));
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
resolve({});
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
module.exports = { main };
|
|
1048
|
+
function handleRepair(options, context) {
|
|
1049
|
+
const settings = resolveSettings(options, context);
|
|
1050
|
+
const status = collectStatus(settings);
|
|
1051
|
+
const warnings = [];
|
|
1052
|
+
|
|
1053
|
+
if (!settings.skillsSource) {
|
|
1054
|
+
warnings.push("Skills directory not found; skipping skill linking.");
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (!options["dry-run"]) {
|
|
1058
|
+
ensureDir(settings.claudeSkillsDir, false);
|
|
1059
|
+
ensureDir(settings.codexSkillsDir, false);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (!status.localCliPresent) {
|
|
1063
|
+
ensureLocalCli(settings, context, options);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (!status.hooksInstalled) {
|
|
1067
|
+
const updated = updateClaudeSettings(
|
|
1068
|
+
settings,
|
|
1069
|
+
path.join(settings.claudeDir, LOCAL_CLI_DIR, "bin", "agent-playbook.js"),
|
|
1070
|
+
options
|
|
1071
|
+
);
|
|
1072
|
+
if (updated === false) {
|
|
1073
|
+
warnings.push("Unable to update Claude settings (invalid JSON).");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (!status.codexBlockPresent) {
|
|
1078
|
+
updateCodexConfig(settings, options);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (settings.skillsSource) {
|
|
1082
|
+
linkSkills(settings.skillsSource, settings.claudeSkillsDir, options);
|
|
1083
|
+
linkSkills(settings.skillsSource, settings.codexSkillsDir, options);
|
|
1084
|
+
if (!options["dry-run"]) {
|
|
1085
|
+
const manifestPath = path.join(settings.claudeSkillsDir, ".agent-playbook.json");
|
|
1086
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1087
|
+
writeJson(manifestPath, {
|
|
1088
|
+
name: APP_NAME,
|
|
1089
|
+
version: VERSION,
|
|
1090
|
+
installedAt: new Date().toISOString(),
|
|
1091
|
+
repairedAt: new Date().toISOString(),
|
|
1092
|
+
repoRoot: settings.repoRoot,
|
|
1093
|
+
links: { claude: [], codex: [] },
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
printInitSummary(settings, true, options, { created: [], skipped: [] }, { created: [], skipped: [] }, warnings);
|
|
1100
|
+
return Promise.resolve();
|
|
1101
|
+
}
|