@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,220 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { addMcpAgentNames } from "./agents.js";
|
|
4
|
+
import { loadMcpManifest, loadSkillManifest, loadUtilityManifest } from "./manifest.js";
|
|
5
|
+
export function buildSkillCommands(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 || options.includeExternal);
|
|
10
|
+
return buildSkillSourceCommands(selected, "Shared skills", buildSkillsAgentArgs(options.selectedSkillAgentIds), options.setupScope);
|
|
11
|
+
}
|
|
12
|
+
export function buildMcpCommands(options) {
|
|
13
|
+
const manifest = loadMcpManifest(options);
|
|
14
|
+
const agentArgs = buildAddMcpAgentArgs(options.agents, options.yes, options.setupScope);
|
|
15
|
+
if (options.agents.length > 0 && agentArgs.length === 0) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return manifest.items
|
|
19
|
+
.filter((item) => options.selectedMcpIds.length === 0 ? item.default : options.selectedMcpIds.includes(item.id))
|
|
20
|
+
.map((item) => ({
|
|
21
|
+
label: item.label,
|
|
22
|
+
command: "npx",
|
|
23
|
+
args: [
|
|
24
|
+
"add-mcp",
|
|
25
|
+
item.source.replace("${HOME}", options.homeDir),
|
|
26
|
+
...item.args,
|
|
27
|
+
...(options.setupScope === "global" ? ["-g"] : []),
|
|
28
|
+
...agentArgs,
|
|
29
|
+
...(options.yes ? ["-y"] : []),
|
|
30
|
+
],
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function buildUtilityCommands(options) {
|
|
34
|
+
const manifest = loadUtilityManifest(options);
|
|
35
|
+
const selected = options.selectedUtilIds.length > 0
|
|
36
|
+
? manifest.items.filter((item) => options.selectedUtilIds.includes(item.id))
|
|
37
|
+
: manifest.items.filter((item) => item.default);
|
|
38
|
+
return selected.flatMap((item) => [
|
|
39
|
+
buildUtilityInstallCommand(item),
|
|
40
|
+
...buildUtilityPostInstallCommands(item, options),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
export async function runDelegateCommands(runtime, commands, options) {
|
|
44
|
+
const failures = [];
|
|
45
|
+
for (const item of commands) {
|
|
46
|
+
runtime.io.stdout(`\n${item.label}`);
|
|
47
|
+
if (item.cwd) {
|
|
48
|
+
runtime.io.stdout(`(in ${item.cwd})`);
|
|
49
|
+
}
|
|
50
|
+
runtime.io.stdout(`$ ${item.command} ${item.args.map(quoteArg).join(" ")}`);
|
|
51
|
+
if (options.dryRun) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (item.cwd) {
|
|
55
|
+
mkdirSync(item.cwd, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
const result = await runtime.spawn(item.command, item.args, item.cwd ?? options.cwd ?? options.repoDir);
|
|
58
|
+
if (result.code !== 0) {
|
|
59
|
+
if (options.continueOnError) {
|
|
60
|
+
failures.push({ label: item.label, code: result.code });
|
|
61
|
+
runtime.io.stderr(`Warning: ${item.label} failed with exit code ${result.code}. Continuing.`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return result.code;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (failures.length > 0) {
|
|
68
|
+
runtime.io.stdout("\nSome delegated commands failed:");
|
|
69
|
+
for (const failure of failures) {
|
|
70
|
+
runtime.io.stdout(`- ${failure.label} exited with code ${failure.code}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
function buildAddMcpAgentArgs(agents, nonInteractive, scope) {
|
|
76
|
+
const selected = agents.length > 0 ? agents : nonInteractive ? defaultMcpAgents(scope) : [];
|
|
77
|
+
const args = [];
|
|
78
|
+
for (const agent of selected) {
|
|
79
|
+
if (scope === "project" && agent === "antigravity") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const addMcpName = addMcpAgentNames[agent];
|
|
83
|
+
if (addMcpName) {
|
|
84
|
+
args.push("-a", addMcpName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
function defaultMcpAgents(scope) {
|
|
90
|
+
return scope === "global"
|
|
91
|
+
? ["antigravity", "claude", "codex", "opencode"]
|
|
92
|
+
: ["claude", "codex", "opencode"];
|
|
93
|
+
}
|
|
94
|
+
function buildUtilityInstallCommand(item) {
|
|
95
|
+
return {
|
|
96
|
+
label: `${item.label} / install`,
|
|
97
|
+
command: item.install.command,
|
|
98
|
+
args: item.install.args,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function buildUtilityPostInstallCommands(item, options) {
|
|
102
|
+
if (typeof item.postInstall === "object") {
|
|
103
|
+
return [{
|
|
104
|
+
label: item.postInstall.label ?? `${item.label} / post-install`,
|
|
105
|
+
command: item.postInstall.command,
|
|
106
|
+
args: item.postInstall.args,
|
|
107
|
+
}];
|
|
108
|
+
}
|
|
109
|
+
if (item.postInstall !== "rtk-init") {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const selectedAgents = filterUtilityAgents(options.agents);
|
|
113
|
+
return selectedAgents.map((agent) => ({
|
|
114
|
+
label: `RTK / init ${agentLabel(agent)}`,
|
|
115
|
+
command: "rtk",
|
|
116
|
+
args: rtkInitArgs(agent, options.setupScope),
|
|
117
|
+
...(options.setupScope === "global" && agent === "codex" ? { cwd: join(options.homeDir, ".codex") } : {}),
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
function defaultUtilityAgents() {
|
|
121
|
+
return ["antigravity", "claude", "codex", "opencode"];
|
|
122
|
+
}
|
|
123
|
+
function filterUtilityAgents(agents) {
|
|
124
|
+
if (agents.length === 0) {
|
|
125
|
+
return defaultUtilityAgents();
|
|
126
|
+
}
|
|
127
|
+
return agents.filter((agent) => defaultUtilityAgents().includes(agent));
|
|
128
|
+
}
|
|
129
|
+
function rtkInitArgs(agent, scope) {
|
|
130
|
+
if (scope === "project") {
|
|
131
|
+
switch (agent) {
|
|
132
|
+
case "claude":
|
|
133
|
+
return ["init"];
|
|
134
|
+
case "codex":
|
|
135
|
+
return ["init", "--codex"];
|
|
136
|
+
case "antigravity":
|
|
137
|
+
return ["init", "--agent", "antigravity"];
|
|
138
|
+
case "opencode":
|
|
139
|
+
return ["init", "--opencode"];
|
|
140
|
+
case "cursor-local":
|
|
141
|
+
return ["init"];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
switch (agent) {
|
|
145
|
+
case "claude":
|
|
146
|
+
return ["init", "--global"];
|
|
147
|
+
case "codex":
|
|
148
|
+
return ["init", "--codex"];
|
|
149
|
+
case "antigravity":
|
|
150
|
+
// Antigravity still consumes the global Gemini rules host, so global RTK uses
|
|
151
|
+
// the Gemini compatibility initializer while project setup uses Antigravity.
|
|
152
|
+
return ["init", "--global", "--gemini"];
|
|
153
|
+
case "opencode":
|
|
154
|
+
return ["init", "--global", "--opencode"];
|
|
155
|
+
case "cursor-local":
|
|
156
|
+
return ["init", "--global"];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function agentLabel(agent) {
|
|
160
|
+
switch (agent) {
|
|
161
|
+
case "claude":
|
|
162
|
+
return "Claude Code";
|
|
163
|
+
case "codex":
|
|
164
|
+
return "Codex";
|
|
165
|
+
case "antigravity":
|
|
166
|
+
return "Antigravity";
|
|
167
|
+
case "opencode":
|
|
168
|
+
return "OpenCode";
|
|
169
|
+
case "cursor-local":
|
|
170
|
+
return "Cursor Local";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function buildSkillsAgentArgs(agents) {
|
|
174
|
+
return agents.flatMap((agent) => ["--agent", agent]);
|
|
175
|
+
}
|
|
176
|
+
function buildSkillSourceCommands(items, labelPrefix, targetArgs, scope) {
|
|
177
|
+
const bySource = new Map();
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
bySource.set(item.source, [...(bySource.get(item.source) ?? []), item]);
|
|
180
|
+
}
|
|
181
|
+
return [...bySource.entries()].map(([source, sourceItems]) => ({
|
|
182
|
+
label: `${labelPrefix} / ${sourceLabel(source)}`,
|
|
183
|
+
command: "npx",
|
|
184
|
+
args: [
|
|
185
|
+
"skills",
|
|
186
|
+
"add",
|
|
187
|
+
source,
|
|
188
|
+
...(scope === "global" ? ["--global"] : []),
|
|
189
|
+
"--yes",
|
|
190
|
+
...skillSelectionArgs(sourceItems),
|
|
191
|
+
...targetArgs,
|
|
192
|
+
],
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
function skillSelectionArgs(items) {
|
|
196
|
+
const skillIds = items.map((item) => skillIdFromArgs(item.args));
|
|
197
|
+
if (skillIds.some((id) => !id)) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
return ["--skill", ...skillIds.filter((id) => Boolean(id))];
|
|
201
|
+
}
|
|
202
|
+
function skillIdFromArgs(args) {
|
|
203
|
+
const index = args.indexOf("--skill");
|
|
204
|
+
return index >= 0 ? args[index + 1] ?? null : null;
|
|
205
|
+
}
|
|
206
|
+
function sourceLabel(source) {
|
|
207
|
+
if (source.includes("logbookfordevs/ai-field-kit")) {
|
|
208
|
+
return "AI Field Kit";
|
|
209
|
+
}
|
|
210
|
+
if (source.includes("addyosmani/agent-skills")) {
|
|
211
|
+
return "Agent Skills";
|
|
212
|
+
}
|
|
213
|
+
return source;
|
|
214
|
+
}
|
|
215
|
+
function quoteArg(value) {
|
|
216
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(value)) {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
return JSON.stringify(value);
|
|
220
|
+
}
|
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { constants, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync, copyFileSync, accessSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
export const managedMarker = ".ai-field-kit-managed";
|
|
4
|
+
export function pathExists(path) {
|
|
5
|
+
return existsSync(path);
|
|
6
|
+
}
|
|
7
|
+
export function isFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
return lstatSync(path).isFile();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isDirectory(path) {
|
|
16
|
+
try {
|
|
17
|
+
return lstatSync(path).isDirectory();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function isSymlink(path) {
|
|
24
|
+
try {
|
|
25
|
+
return lstatSync(path).isSymbolicLink();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function canExecute(path) {
|
|
32
|
+
try {
|
|
33
|
+
accessSync(path, constants.X_OK);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function readText(path) {
|
|
41
|
+
return readFileSync(path, "utf8");
|
|
42
|
+
}
|
|
43
|
+
export function ensureParent(path) {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
export function backupTarget(path, timestamp) {
|
|
47
|
+
if (isFile(path) && !isSymlink(path)) {
|
|
48
|
+
return {
|
|
49
|
+
type: "backup",
|
|
50
|
+
source: path,
|
|
51
|
+
target: `${path}.bak.${timestamp}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export function applyOperation(operation) {
|
|
57
|
+
switch (operation.type) {
|
|
58
|
+
case "mkdir":
|
|
59
|
+
mkdirSync(operation.path, { recursive: true });
|
|
60
|
+
return;
|
|
61
|
+
case "remove":
|
|
62
|
+
rmSync(operation.path, { recursive: true, force: true });
|
|
63
|
+
return;
|
|
64
|
+
case "symlink":
|
|
65
|
+
ensureParent(operation.target);
|
|
66
|
+
symlinkSync(operation.source, operation.target);
|
|
67
|
+
return;
|
|
68
|
+
case "copy":
|
|
69
|
+
ensureParent(operation.target);
|
|
70
|
+
copyFileSync(operation.source, operation.target);
|
|
71
|
+
return;
|
|
72
|
+
case "write":
|
|
73
|
+
ensureParent(operation.path);
|
|
74
|
+
writeFileSync(operation.path, operation.content);
|
|
75
|
+
return;
|
|
76
|
+
case "backup":
|
|
77
|
+
ensureParent(operation.target);
|
|
78
|
+
renameSync(operation.source, operation.target);
|
|
79
|
+
return;
|
|
80
|
+
case "skip":
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function formatOperation(operation) {
|
|
85
|
+
switch (operation.type) {
|
|
86
|
+
case "mkdir":
|
|
87
|
+
return `mkdir ${operation.path}`;
|
|
88
|
+
case "remove":
|
|
89
|
+
return `remove ${operation.path}`;
|
|
90
|
+
case "symlink":
|
|
91
|
+
return `link ${operation.target} -> ${operation.source}`;
|
|
92
|
+
case "copy":
|
|
93
|
+
return `copy ${operation.source} -> ${operation.target}`;
|
|
94
|
+
case "write":
|
|
95
|
+
return `write ${operation.path}`;
|
|
96
|
+
case "backup":
|
|
97
|
+
return `backup ${operation.source} -> ${operation.target}`;
|
|
98
|
+
case "skip":
|
|
99
|
+
return `skip ${operation.path} (${operation.reason})`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function summarizeOperations(operations) {
|
|
103
|
+
const counts = new Map();
|
|
104
|
+
for (const operation of operations) {
|
|
105
|
+
counts.set(operation.type, (counts.get(operation.type) ?? 0) + 1);
|
|
106
|
+
}
|
|
107
|
+
const parts = [
|
|
108
|
+
formatCount("created directories", counts.get("mkdir") ?? 0),
|
|
109
|
+
formatCount("wrote files", counts.get("write") ?? 0),
|
|
110
|
+
formatCount("linked files", counts.get("symlink") ?? 0),
|
|
111
|
+
formatCount("copied files", counts.get("copy") ?? 0),
|
|
112
|
+
formatCount("backed up files", counts.get("backup") ?? 0),
|
|
113
|
+
formatCount("removed files", counts.get("remove") ?? 0),
|
|
114
|
+
formatCount("skipped unchanged or unmanaged files", counts.get("skip") ?? 0),
|
|
115
|
+
].filter(Boolean);
|
|
116
|
+
return parts.length > 0 ? parts.join(", ") : "no file changes";
|
|
117
|
+
}
|
|
118
|
+
function formatCount(label, count) {
|
|
119
|
+
return count > 0 ? `${count} ${label}` : "";
|
|
120
|
+
}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { applyOperation, formatOperation, pathExists, readText, summarizeOperations } from "./fs-utils.js";
|
|
4
|
+
import { loadHookManifest } from "./manifest.js";
|
|
5
|
+
const hookAgents = ["codex", "claude", "cursor-local"];
|
|
6
|
+
export async function syncHooks(runtime, options) {
|
|
7
|
+
const operations = await planHooksSync(options);
|
|
8
|
+
if (options.dryRun) {
|
|
9
|
+
runtime.io.stdout("\nHooks install plan");
|
|
10
|
+
for (const operation of operations) {
|
|
11
|
+
runtime.io.stdout(`- ${formatOperation(operation)}`);
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
for (const operation of operations) {
|
|
16
|
+
applyOperation(operation);
|
|
17
|
+
}
|
|
18
|
+
runtime.io.stdout(`\nHooks installed: ${summarizeOperations(operations)}.`);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
export async function planHooksSync(options) {
|
|
22
|
+
const manifest = loadHookManifest(options);
|
|
23
|
+
const selected = selectHookItems(manifest.items, options.selectedHookIds);
|
|
24
|
+
const operations = [];
|
|
25
|
+
for (const item of selected) {
|
|
26
|
+
const sourceContent = await loadHookSource(item.source, options);
|
|
27
|
+
for (const agent of selectedHookAgents(options.agents, item.agents)) {
|
|
28
|
+
operations.push(...planAgentHook(agent, options, item, sourceContent));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return operations;
|
|
32
|
+
}
|
|
33
|
+
function selectHookItems(items, selectedHookIds) {
|
|
34
|
+
if (selectedHookIds.length > 0) {
|
|
35
|
+
return items.filter((item) => selectedHookIds.includes(item.id));
|
|
36
|
+
}
|
|
37
|
+
return items.filter((item) => item.default);
|
|
38
|
+
}
|
|
39
|
+
function selectedHookAgents(selected, supported) {
|
|
40
|
+
return selected.filter((agent) => hookAgents.includes(agent) && supported.includes(agent));
|
|
41
|
+
}
|
|
42
|
+
function planAgentHook(agent, options, item, sourceContent) {
|
|
43
|
+
const scriptPath = agentScriptPath(agent, options, item);
|
|
44
|
+
const configPath = agentConfigPath(agent, options);
|
|
45
|
+
const current = pathExists(configPath) ? readText(configPath) : "";
|
|
46
|
+
const command = buildHookCommand(item, scriptPath, agent);
|
|
47
|
+
const config = mergeAgentHookConfig(agent, current, command, item);
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
type: "write",
|
|
51
|
+
path: scriptPath,
|
|
52
|
+
content: sourceContent,
|
|
53
|
+
},
|
|
54
|
+
{ type: "write", path: configPath, content: `${JSON.stringify(config, null, 2)}\n` },
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function agentScriptPath(agent, options, item) {
|
|
58
|
+
const base = options.setupScope === "project" ? options.cwd : options.homeDir;
|
|
59
|
+
const filename = safeHookFilename(item);
|
|
60
|
+
switch (agent) {
|
|
61
|
+
case "codex":
|
|
62
|
+
return join(base, ".codex", "hooks", filename);
|
|
63
|
+
case "claude":
|
|
64
|
+
return join(base, ".claude", "hooks", filename);
|
|
65
|
+
case "cursor-local":
|
|
66
|
+
return join(base, ".cursor", "hooks", filename);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function agentConfigPath(agent, options) {
|
|
70
|
+
const base = options.setupScope === "project" ? options.cwd : options.homeDir;
|
|
71
|
+
switch (agent) {
|
|
72
|
+
case "codex":
|
|
73
|
+
return join(base, ".codex", "hooks.json");
|
|
74
|
+
case "claude":
|
|
75
|
+
return join(base, ".claude", "settings.json");
|
|
76
|
+
case "cursor-local":
|
|
77
|
+
return join(base, ".cursor", "hooks.json");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function mergeAgentHookConfig(agent, current, command, item) {
|
|
81
|
+
const config = parseJsonObject(current);
|
|
82
|
+
const hooks = getOrCreateRecord(config, "hooks");
|
|
83
|
+
if (agent === "cursor-local") {
|
|
84
|
+
config.version = typeof config.version === "number" ? config.version : 1;
|
|
85
|
+
mergeCursorStopHook(hooks, command, item);
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
mergeCodexClaudeStopHook(hooks, command, item);
|
|
89
|
+
return config;
|
|
90
|
+
}
|
|
91
|
+
function mergeCodexClaudeStopHook(hooks, command, item) {
|
|
92
|
+
const entries = getOrCreateArray(hooks, "Stop");
|
|
93
|
+
const handler = {
|
|
94
|
+
type: "command",
|
|
95
|
+
command,
|
|
96
|
+
statusMessage: "Checking AFK tracking",
|
|
97
|
+
};
|
|
98
|
+
upsertMatcherHook(entries, "", handler, command, item);
|
|
99
|
+
}
|
|
100
|
+
function mergeCursorStopHook(hooks, command, item) {
|
|
101
|
+
const entries = getOrCreateArray(hooks, "stop");
|
|
102
|
+
const hook = { command };
|
|
103
|
+
if (entries.some((entry) => isRecord(entry) && entry.command === command)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const filtered = entries.filter((entry) => !isRecord(entry) || !isManagedHookCommand(entry.command, item));
|
|
107
|
+
filtered.push(hook);
|
|
108
|
+
hooks.stop = filtered;
|
|
109
|
+
}
|
|
110
|
+
function upsertMatcherHook(entries, matcher, handler, command, item) {
|
|
111
|
+
const existingGroup = entries.find((entry) => isRecord(entry) && entry.matcher === matcher);
|
|
112
|
+
const group = isRecord(existingGroup) ? existingGroup : { matcher, hooks: [] };
|
|
113
|
+
if (!isRecord(existingGroup)) {
|
|
114
|
+
entries.push(group);
|
|
115
|
+
}
|
|
116
|
+
const hooks = Array.isArray(group.hooks) ? group.hooks : [];
|
|
117
|
+
const filtered = hooks.filter((hook) => !isRecord(hook) || (!isManagedHookCommand(hook.command, item) && hook.command !== command));
|
|
118
|
+
filtered.push(handler);
|
|
119
|
+
group.hooks = filtered;
|
|
120
|
+
}
|
|
121
|
+
function buildHookCommand(item, hookFile, agent) {
|
|
122
|
+
const args = item.args.map((arg) => replaceHookPlaceholders(arg, hookFile, agent));
|
|
123
|
+
return [item.command, ...args].map(quoteArg).join(" ");
|
|
124
|
+
}
|
|
125
|
+
function replaceHookPlaceholders(value, hookFile, agent) {
|
|
126
|
+
return value
|
|
127
|
+
.replaceAll("${HOOK_FILE}", hookFile)
|
|
128
|
+
.replaceAll("${AGENT}", agent);
|
|
129
|
+
}
|
|
130
|
+
function safeHookFilename(item) {
|
|
131
|
+
const sourceName = filenameFromSource(item.source);
|
|
132
|
+
if (sourceName) {
|
|
133
|
+
return sourceName;
|
|
134
|
+
}
|
|
135
|
+
return `${item.id.replace(/[^a-z0-9._-]+/gi, "-")}.js`;
|
|
136
|
+
}
|
|
137
|
+
function filenameFromSource(source) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = new URL(source);
|
|
140
|
+
return basename(parsed.pathname);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return basename(source);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function loadHookSource(source, options) {
|
|
147
|
+
if (/^https?:\/\//.test(source)) {
|
|
148
|
+
const response = await fetch(source);
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`Could not fetch hook source ${source}: ${response.status} ${response.statusText}`);
|
|
151
|
+
}
|
|
152
|
+
return ensureTrailingNewline(await response.text());
|
|
153
|
+
}
|
|
154
|
+
const path = resolveHookSourcePath(source, options);
|
|
155
|
+
return ensureTrailingNewline(readFileSync(path, "utf8"));
|
|
156
|
+
}
|
|
157
|
+
function resolveHookSourcePath(source, options) {
|
|
158
|
+
const candidates = [
|
|
159
|
+
isAbsolute(source) ? source : resolve(options.cwd, source),
|
|
160
|
+
isAbsolute(source) ? source : resolve(options.repoDir, source),
|
|
161
|
+
];
|
|
162
|
+
for (const candidate of unique(candidates)) {
|
|
163
|
+
if (existsSync(candidate)) {
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Missing hook source: ${source}`);
|
|
168
|
+
}
|
|
169
|
+
function unique(values) {
|
|
170
|
+
return [...new Set(values)];
|
|
171
|
+
}
|
|
172
|
+
function ensureTrailingNewline(content) {
|
|
173
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
174
|
+
}
|
|
175
|
+
function isManagedHookCommand(value, item) {
|
|
176
|
+
const filename = safeHookFilename(item);
|
|
177
|
+
return typeof value === "string" && filename.length > 0 && value.includes(filename);
|
|
178
|
+
}
|
|
179
|
+
function quoteArg(value) {
|
|
180
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(value)) {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
return JSON.stringify(value);
|
|
184
|
+
}
|
|
185
|
+
function parseJsonObject(content) {
|
|
186
|
+
if (!content.trim()) {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(content);
|
|
191
|
+
return isRecord(parsed) ? parsed : {};
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function getOrCreateRecord(record, key) {
|
|
198
|
+
const value = record[key];
|
|
199
|
+
if (isRecord(value)) {
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
const next = {};
|
|
203
|
+
record[key] = next;
|
|
204
|
+
return next;
|
|
205
|
+
}
|
|
206
|
+
function getOrCreateArray(record, key) {
|
|
207
|
+
const value = record[key];
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
const next = [];
|
|
212
|
+
record[key] = next;
|
|
213
|
+
return next;
|
|
214
|
+
}
|
|
215
|
+
function isRecord(value) {
|
|
216
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
217
|
+
}
|
package/dist/index.js
ADDED