caik-cli 0.1.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/api-6OX4ICXN.js +9 -0
- package/dist/auto-improve-skills-2COKTU5C.js +8 -0
- package/dist/autoresearch-Y7WW6L4O.js +24 -0
- package/dist/chunk-2YHUDOJL.js +54 -0
- package/dist/chunk-3TXNZINH.js +775 -0
- package/dist/chunk-5MHNQAV4.js +317 -0
- package/dist/chunk-7AIZTHHZ.js +152 -0
- package/dist/chunk-D4IM3YRX.js +166 -0
- package/dist/chunk-DJJHS7KK.js +62 -0
- package/dist/chunk-DKZBQRR3.js +91 -0
- package/dist/chunk-FLSHJZLC.js +613 -0
- package/dist/chunk-H2ZKCXMJ.js +202 -0
- package/dist/chunk-ILMOSMD3.js +83 -0
- package/dist/chunk-KYTHKH6V.js +79 -0
- package/dist/chunk-LTKHLRM4.js +272 -0
- package/dist/chunk-T32AEP3O.js +146 -0
- package/dist/chunk-T73Z5UMA.js +14437 -0
- package/dist/chunk-TFKT7V7H.js +1545 -0
- package/dist/chunk-US4CYDNS.js +524 -0
- package/dist/chunk-ZLRN7Q7C.js +27 -0
- package/dist/claude-code-6DF4YARB.js +8 -0
- package/dist/config-CS7734SA.js +24 -0
- package/dist/correction-classifier-TLPKRNLI.js +93 -0
- package/dist/cursor-Z4XXDCAM.js +8 -0
- package/dist/daemon/autoresearch-2MAEM2YI.js +272 -0
- package/dist/daemon/chunk-545XA5CB.js +77 -0
- package/dist/daemon/chunk-HEYFAUHL.js +90 -0
- package/dist/daemon/chunk-MLKGABMK.js +9 -0
- package/dist/daemon/chunk-NJICGNCK.js +150 -0
- package/dist/daemon/chunk-OD5NUFH2.js +181 -0
- package/dist/daemon/chunk-SM2FSXIP.js +60 -0
- package/dist/daemon/chunk-UMDJFPN6.js +163 -0
- package/dist/daemon/config-F7HE3JRY.js +23 -0
- package/dist/daemon/db-QEXVVTAL.js +15 -0
- package/dist/daemon/eval-generator-OR2FAYLB.js +316 -0
- package/dist/daemon/improver-TGEK6MPE.js +186 -0
- package/dist/daemon/llm-FUJ2TBYT.js +11 -0
- package/dist/daemon/nudge-detector-NFRHWZY6.js +140 -0
- package/dist/daemon/platform-7N3LQDIB.js +16381 -0
- package/dist/daemon/registry-FI4GTO3H.js +20 -0
- package/dist/daemon/server.js +356 -0
- package/dist/daemon/trace-store-T7XFGQSX.js +19 -0
- package/dist/daemon-UXYMG46V.js +85 -0
- package/dist/db-TLNRIXLK.js +18 -0
- package/dist/eval-generator-GGMRPO3K.js +21 -0
- package/dist/eval-runner-EF4K6T5Y.js +15 -0
- package/dist/index.js +8033 -568
- package/dist/llm-3UUZX6PX.js +12 -0
- package/dist/platform-52NREMBS.js +33 -0
- package/dist/repo-installer-K6ADOW3E.js +25 -0
- package/dist/setup-P744STZE.js +16 -0
- package/dist/test-loop-Y7QQE55P.js +127 -0
- package/dist/trace-store-FVLMNNDK.js +20 -0
- package/package.json +9 -3
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ClaudeCodeAdapter
|
|
4
|
+
} from "./chunk-US4CYDNS.js";
|
|
5
|
+
import {
|
|
6
|
+
CLI_NPX_PACKAGE,
|
|
7
|
+
CursorAdapter
|
|
8
|
+
} from "./chunk-T73Z5UMA.js";
|
|
9
|
+
|
|
10
|
+
// src/platform/detect.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
|
|
16
|
+
// src/platform/platform-configs.ts
|
|
17
|
+
var platformConfigs = {
|
|
18
|
+
windsurf: {
|
|
19
|
+
label: "Windsurf",
|
|
20
|
+
tier: "cli-mcp",
|
|
21
|
+
homeDirs: [".windsurf"],
|
|
22
|
+
cwdDirs: [".windsurf"],
|
|
23
|
+
binaries: [],
|
|
24
|
+
mcpConfigPath: "~/.windsurf/mcp.json",
|
|
25
|
+
mcpConfigKey: "mcpServers",
|
|
26
|
+
skillsDir: null,
|
|
27
|
+
localSkillsDir: ".agents/skills/caik",
|
|
28
|
+
capabilities: { hooks: false, mcp: true, skills: true }
|
|
29
|
+
},
|
|
30
|
+
copilot: {
|
|
31
|
+
label: "GitHub Copilot",
|
|
32
|
+
tier: "cli-mcp",
|
|
33
|
+
homeDirs: [],
|
|
34
|
+
cwdDirs: [".github/copilot"],
|
|
35
|
+
binaries: [],
|
|
36
|
+
mcpConfigPath: ".github/copilot/mcp.json",
|
|
37
|
+
mcpConfigKey: "mcpServers",
|
|
38
|
+
skillsDir: null,
|
|
39
|
+
localSkillsDir: ".github/copilot/skills/caik",
|
|
40
|
+
capabilities: { hooks: false, mcp: true, skills: true }
|
|
41
|
+
},
|
|
42
|
+
"gemini-cli": {
|
|
43
|
+
label: "Gemini CLI",
|
|
44
|
+
tier: "cli-mcp",
|
|
45
|
+
homeDirs: [".gemini"],
|
|
46
|
+
cwdDirs: [".gemini"],
|
|
47
|
+
binaries: ["gemini"],
|
|
48
|
+
mcpConfigPath: "~/.gemini/settings.json",
|
|
49
|
+
mcpConfigKey: "mcpServers",
|
|
50
|
+
skillsDir: "~/.gemini/skills/caik",
|
|
51
|
+
localSkillsDir: ".gemini/skills/caik",
|
|
52
|
+
capabilities: { hooks: false, mcp: true, skills: true }
|
|
53
|
+
},
|
|
54
|
+
aider: {
|
|
55
|
+
label: "Aider",
|
|
56
|
+
tier: "cli-mcp",
|
|
57
|
+
homeDirs: [".aider"],
|
|
58
|
+
cwdDirs: [],
|
|
59
|
+
binaries: ["aider"],
|
|
60
|
+
mcpConfigPath: "~/.aider.conf.yml",
|
|
61
|
+
mcpConfigKey: "manual",
|
|
62
|
+
skillsDir: null,
|
|
63
|
+
localSkillsDir: null,
|
|
64
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
65
|
+
},
|
|
66
|
+
cline: {
|
|
67
|
+
label: "Cline",
|
|
68
|
+
tier: "cli-mcp",
|
|
69
|
+
homeDirs: [".cline"],
|
|
70
|
+
cwdDirs: [".cline"],
|
|
71
|
+
binaries: [],
|
|
72
|
+
mcpConfigPath: "~/cline_mcp_settings.json",
|
|
73
|
+
mcpConfigKey: "mcpServers",
|
|
74
|
+
skillsDir: "~/.cline/skills/caik",
|
|
75
|
+
localSkillsDir: ".cline/skills/caik",
|
|
76
|
+
capabilities: { hooks: false, mcp: true, skills: true }
|
|
77
|
+
},
|
|
78
|
+
"roo-code": {
|
|
79
|
+
label: "Roo Code",
|
|
80
|
+
tier: "cli-mcp",
|
|
81
|
+
homeDirs: [".roo"],
|
|
82
|
+
cwdDirs: [".roo"],
|
|
83
|
+
binaries: [],
|
|
84
|
+
mcpConfigPath: "~/.roo/mcp.json",
|
|
85
|
+
mcpConfigKey: "mcpServers",
|
|
86
|
+
skillsDir: "~/.roo/skills/caik",
|
|
87
|
+
localSkillsDir: ".roo/skills/caik",
|
|
88
|
+
capabilities: { hooks: false, mcp: true, skills: true }
|
|
89
|
+
},
|
|
90
|
+
"amazon-q": {
|
|
91
|
+
label: "Amazon Q",
|
|
92
|
+
tier: "cli-mcp",
|
|
93
|
+
homeDirs: [".aws/amazonq"],
|
|
94
|
+
cwdDirs: [],
|
|
95
|
+
binaries: ["q"],
|
|
96
|
+
mcpConfigPath: "~/.aws/amazonq/mcp.json",
|
|
97
|
+
mcpConfigKey: "mcpServers",
|
|
98
|
+
skillsDir: null,
|
|
99
|
+
localSkillsDir: null,
|
|
100
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
101
|
+
},
|
|
102
|
+
devin: {
|
|
103
|
+
label: "Devin",
|
|
104
|
+
tier: "cli-mcp",
|
|
105
|
+
homeDirs: [],
|
|
106
|
+
cwdDirs: [],
|
|
107
|
+
binaries: ["devin"],
|
|
108
|
+
mcpConfigPath: "~/devin.json",
|
|
109
|
+
mcpConfigKey: "mcpServers",
|
|
110
|
+
skillsDir: null,
|
|
111
|
+
localSkillsDir: null,
|
|
112
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
113
|
+
},
|
|
114
|
+
goose: {
|
|
115
|
+
label: "Goose",
|
|
116
|
+
tier: "cli-mcp",
|
|
117
|
+
homeDirs: [".goose"],
|
|
118
|
+
cwdDirs: [".goose"],
|
|
119
|
+
binaries: ["goose"],
|
|
120
|
+
mcpConfigPath: "~/.goose/config.yaml",
|
|
121
|
+
mcpConfigKey: "manual",
|
|
122
|
+
skillsDir: null,
|
|
123
|
+
localSkillsDir: null,
|
|
124
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
125
|
+
},
|
|
126
|
+
warp: {
|
|
127
|
+
label: "Warp",
|
|
128
|
+
tier: "cli-mcp",
|
|
129
|
+
homeDirs: [".warp"],
|
|
130
|
+
cwdDirs: [".warp"],
|
|
131
|
+
binaries: ["warp"],
|
|
132
|
+
mcpConfigPath: "~/.warp/mcp.json",
|
|
133
|
+
mcpConfigKey: "mcpServers",
|
|
134
|
+
skillsDir: null,
|
|
135
|
+
localSkillsDir: null,
|
|
136
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
137
|
+
},
|
|
138
|
+
zed: {
|
|
139
|
+
label: "Zed",
|
|
140
|
+
tier: "cli-mcp",
|
|
141
|
+
homeDirs: [".config/zed"],
|
|
142
|
+
cwdDirs: [".zed"],
|
|
143
|
+
binaries: ["zed"],
|
|
144
|
+
mcpConfigPath: "~/.config/zed/settings.json",
|
|
145
|
+
mcpConfigKey: "manual",
|
|
146
|
+
skillsDir: null,
|
|
147
|
+
localSkillsDir: null,
|
|
148
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
149
|
+
},
|
|
150
|
+
augment: {
|
|
151
|
+
label: "Augment",
|
|
152
|
+
tier: "cli-mcp",
|
|
153
|
+
homeDirs: [".augment"],
|
|
154
|
+
cwdDirs: [".augment"],
|
|
155
|
+
binaries: [],
|
|
156
|
+
mcpConfigPath: "~/.augment/mcp.json",
|
|
157
|
+
mcpConfigKey: "mcpServers",
|
|
158
|
+
skillsDir: null,
|
|
159
|
+
localSkillsDir: null,
|
|
160
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
161
|
+
},
|
|
162
|
+
replit: {
|
|
163
|
+
label: "Replit",
|
|
164
|
+
tier: "cli-mcp",
|
|
165
|
+
homeDirs: [".replit"],
|
|
166
|
+
cwdDirs: [".replit"],
|
|
167
|
+
binaries: [],
|
|
168
|
+
mcpConfigPath: "~/.replit/mcp.json",
|
|
169
|
+
mcpConfigKey: "mcpServers",
|
|
170
|
+
skillsDir: null,
|
|
171
|
+
localSkillsDir: null,
|
|
172
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
173
|
+
},
|
|
174
|
+
// Generic fallbacks — no auto-detection, manual config only
|
|
175
|
+
"generic-mcp": {
|
|
176
|
+
label: "Generic MCP",
|
|
177
|
+
tier: "cli-mcp",
|
|
178
|
+
homeDirs: [],
|
|
179
|
+
cwdDirs: [],
|
|
180
|
+
binaries: [],
|
|
181
|
+
mcpConfigPath: "",
|
|
182
|
+
mcpConfigKey: "mcpServers",
|
|
183
|
+
skillsDir: null,
|
|
184
|
+
localSkillsDir: null,
|
|
185
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
186
|
+
},
|
|
187
|
+
"generic-agent": {
|
|
188
|
+
label: "Generic Agent",
|
|
189
|
+
tier: "cli-mcp",
|
|
190
|
+
homeDirs: [],
|
|
191
|
+
cwdDirs: [],
|
|
192
|
+
binaries: [],
|
|
193
|
+
mcpConfigPath: "",
|
|
194
|
+
mcpConfigKey: "mcpServers",
|
|
195
|
+
skillsDir: null,
|
|
196
|
+
localSkillsDir: null,
|
|
197
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
198
|
+
},
|
|
199
|
+
"generic-ide": {
|
|
200
|
+
label: "Generic IDE",
|
|
201
|
+
tier: "cli-mcp",
|
|
202
|
+
homeDirs: [],
|
|
203
|
+
cwdDirs: [],
|
|
204
|
+
binaries: [],
|
|
205
|
+
mcpConfigPath: "",
|
|
206
|
+
mcpConfigKey: "mcpServers",
|
|
207
|
+
skillsDir: null,
|
|
208
|
+
localSkillsDir: null,
|
|
209
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
210
|
+
},
|
|
211
|
+
generic: {
|
|
212
|
+
label: "Generic",
|
|
213
|
+
tier: "cli-mcp",
|
|
214
|
+
homeDirs: [],
|
|
215
|
+
cwdDirs: [],
|
|
216
|
+
binaries: [],
|
|
217
|
+
mcpConfigPath: "",
|
|
218
|
+
mcpConfigKey: "mcpServers",
|
|
219
|
+
skillsDir: null,
|
|
220
|
+
localSkillsDir: null,
|
|
221
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/platform/detect.ts
|
|
226
|
+
function commandExists(cmd) {
|
|
227
|
+
try {
|
|
228
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
229
|
+
return true;
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function detectClaudeCode() {
|
|
235
|
+
const home = homedir();
|
|
236
|
+
const claudeDir = join(home, ".claude");
|
|
237
|
+
if (!existsSync(claudeDir)) return null;
|
|
238
|
+
const hasSettings = existsSync(join(claudeDir, "settings.json"));
|
|
239
|
+
const hasMcpConfig = existsSync(join(claudeDir, "mcp.json"));
|
|
240
|
+
const hasAnyMarker = hasSettings || hasMcpConfig || existsSync(join(claudeDir, "statsig"));
|
|
241
|
+
if (!hasAnyMarker) return null;
|
|
242
|
+
const configPaths = [];
|
|
243
|
+
if (hasSettings) configPaths.push(join(claudeDir, "settings.json"));
|
|
244
|
+
if (hasMcpConfig) configPaths.push(join(claudeDir, "mcp.json"));
|
|
245
|
+
return {
|
|
246
|
+
name: "claude-code",
|
|
247
|
+
tier: "full-plugin",
|
|
248
|
+
configPaths,
|
|
249
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function detectOpenClaw() {
|
|
253
|
+
const home = homedir();
|
|
254
|
+
const openclawDir = join(home, ".openclaw");
|
|
255
|
+
const cwdConfig = join(process.cwd(), "openclaw.json");
|
|
256
|
+
const inPath = commandExists("openclaw");
|
|
257
|
+
if (!existsSync(openclawDir) && !existsSync(cwdConfig) && !inPath) return null;
|
|
258
|
+
const configPaths = [];
|
|
259
|
+
if (existsSync(cwdConfig)) configPaths.push(cwdConfig);
|
|
260
|
+
if (existsSync(join(openclawDir, "openclaw.json"))) {
|
|
261
|
+
configPaths.push(join(openclawDir, "openclaw.json"));
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
name: "openclaw",
|
|
265
|
+
tier: "hook-enabled",
|
|
266
|
+
configPaths,
|
|
267
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function detectCursor() {
|
|
271
|
+
const home = homedir();
|
|
272
|
+
const cursorDir = join(home, ".cursor");
|
|
273
|
+
const cwdCursor = join(process.cwd(), ".cursor");
|
|
274
|
+
if (!existsSync(cursorDir) && !existsSync(cwdCursor)) return null;
|
|
275
|
+
const configPaths = [];
|
|
276
|
+
if (existsSync(join(cwdCursor, "mcp.json"))) {
|
|
277
|
+
configPaths.push(join(cwdCursor, "mcp.json"));
|
|
278
|
+
}
|
|
279
|
+
if (existsSync(join(cursorDir, "mcp.json"))) {
|
|
280
|
+
configPaths.push(join(cursorDir, "mcp.json"));
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
name: "cursor",
|
|
284
|
+
tier: "hook-enabled",
|
|
285
|
+
configPaths,
|
|
286
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function detectCodex() {
|
|
290
|
+
const home = homedir();
|
|
291
|
+
const codexDir = join(home, ".codex");
|
|
292
|
+
if (!existsSync(codexDir) && !commandExists("codex")) return null;
|
|
293
|
+
if (existsSync(codexDir)) {
|
|
294
|
+
const hasConfig = existsSync(join(codexDir, "config.toml"));
|
|
295
|
+
const hasHooks = existsSync(join(codexDir, "hooks.json"));
|
|
296
|
+
const hasMcp = existsSync(join(codexDir, "mcp.json"));
|
|
297
|
+
if (!hasConfig && !hasHooks && !hasMcp) {
|
|
298
|
+
if (!commandExists("codex")) return null;
|
|
299
|
+
}
|
|
300
|
+
const configPaths = [];
|
|
301
|
+
if (hasHooks) configPaths.push(join(codexDir, "hooks.json"));
|
|
302
|
+
if (hasMcp) configPaths.push(join(codexDir, "mcp.json"));
|
|
303
|
+
if (hasConfig) configPaths.push(join(codexDir, "config.toml"));
|
|
304
|
+
return {
|
|
305
|
+
name: "codex",
|
|
306
|
+
tier: "full-plugin",
|
|
307
|
+
configPaths,
|
|
308
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
name: "codex",
|
|
313
|
+
tier: "full-plugin",
|
|
314
|
+
configPaths: [],
|
|
315
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function expandHome(p) {
|
|
319
|
+
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
320
|
+
if (p === "~") return homedir();
|
|
321
|
+
return p;
|
|
322
|
+
}
|
|
323
|
+
function createConfigDetector(name, config) {
|
|
324
|
+
return () => {
|
|
325
|
+
const home = homedir();
|
|
326
|
+
const cwd = process.cwd();
|
|
327
|
+
let found = false;
|
|
328
|
+
for (const dir of config.homeDirs) {
|
|
329
|
+
if (existsSync(join(home, dir))) {
|
|
330
|
+
found = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!found) {
|
|
335
|
+
for (const dir of config.cwdDirs) {
|
|
336
|
+
if (existsSync(join(cwd, dir))) {
|
|
337
|
+
found = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!found) {
|
|
343
|
+
for (const bin of config.binaries) {
|
|
344
|
+
if (commandExists(bin)) {
|
|
345
|
+
found = true;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!found) return null;
|
|
351
|
+
const configPaths = [];
|
|
352
|
+
if (config.mcpConfigPath) {
|
|
353
|
+
const resolved = config.mcpConfigPath.startsWith("~/") ? expandHome(config.mcpConfigPath) : join(cwd, config.mcpConfigPath);
|
|
354
|
+
if (existsSync(resolved)) {
|
|
355
|
+
configPaths.push(resolved);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
name,
|
|
360
|
+
tier: config.tier,
|
|
361
|
+
configPaths,
|
|
362
|
+
capabilities: config.capabilities
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
var customDetectors = [
|
|
367
|
+
detectClaudeCode,
|
|
368
|
+
detectOpenClaw,
|
|
369
|
+
detectCursor,
|
|
370
|
+
detectCodex
|
|
371
|
+
];
|
|
372
|
+
var genericFallbacks = /* @__PURE__ */ new Set(["generic-mcp", "generic-agent", "generic-ide", "generic"]);
|
|
373
|
+
var configDetectors = [];
|
|
374
|
+
var configDetectorMap = {};
|
|
375
|
+
for (const [name, config] of Object.entries(platformConfigs)) {
|
|
376
|
+
if (genericFallbacks.has(name)) continue;
|
|
377
|
+
const detector = createConfigDetector(name, config);
|
|
378
|
+
configDetectors.push(detector);
|
|
379
|
+
configDetectorMap[name] = detector;
|
|
380
|
+
}
|
|
381
|
+
var allDetectors = [
|
|
382
|
+
...customDetectors,
|
|
383
|
+
...configDetectors
|
|
384
|
+
];
|
|
385
|
+
function detectPlatforms() {
|
|
386
|
+
const detected = [];
|
|
387
|
+
for (const detect of allDetectors) {
|
|
388
|
+
try {
|
|
389
|
+
const result = detect();
|
|
390
|
+
if (result) detected.push(result);
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return detected;
|
|
395
|
+
}
|
|
396
|
+
function detectPlatform(name) {
|
|
397
|
+
const customMap = {
|
|
398
|
+
"claude-code": detectClaudeCode,
|
|
399
|
+
"openclaw": detectOpenClaw,
|
|
400
|
+
"cursor": detectCursor,
|
|
401
|
+
"codex": detectCodex
|
|
402
|
+
};
|
|
403
|
+
const customDetector = customMap[name];
|
|
404
|
+
if (customDetector) {
|
|
405
|
+
try {
|
|
406
|
+
return customDetector();
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const configDetector = configDetectorMap[name];
|
|
412
|
+
if (configDetector) {
|
|
413
|
+
try {
|
|
414
|
+
return configDetector();
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/platform/openclaw.ts
|
|
423
|
+
import {
|
|
424
|
+
existsSync as existsSync2,
|
|
425
|
+
readFileSync,
|
|
426
|
+
writeFileSync,
|
|
427
|
+
mkdirSync,
|
|
428
|
+
rmSync
|
|
429
|
+
} from "fs";
|
|
430
|
+
import { execSync as execSync2 } from "child_process";
|
|
431
|
+
import { join as join2, dirname } from "path";
|
|
432
|
+
import { homedir as homedir2 } from "os";
|
|
433
|
+
var MCP_KEY = "caik";
|
|
434
|
+
var OpenClawAdapter = class {
|
|
435
|
+
name = "openclaw";
|
|
436
|
+
tier = "hook-enabled";
|
|
437
|
+
// --- Path helpers --------------------------------------------------------
|
|
438
|
+
get openclawDir() {
|
|
439
|
+
return join2(homedir2(), ".openclaw");
|
|
440
|
+
}
|
|
441
|
+
get managedHooksDir() {
|
|
442
|
+
return join2(this.openclawDir, "hooks");
|
|
443
|
+
}
|
|
444
|
+
get caikHookDir() {
|
|
445
|
+
return join2(this.managedHooksDir, "caik-contributions");
|
|
446
|
+
}
|
|
447
|
+
/** OpenClaw's own config — NOT where MCP servers go. */
|
|
448
|
+
get openclawConfigPath() {
|
|
449
|
+
return join2(this.openclawDir, "openclaw.json");
|
|
450
|
+
}
|
|
451
|
+
/** Project-level `.mcp.json` — where MCP servers are registered. */
|
|
452
|
+
get mcpConfigPath() {
|
|
453
|
+
return join2(process.cwd(), ".mcp.json");
|
|
454
|
+
}
|
|
455
|
+
/** Skills directory following OpenClaw convention. */
|
|
456
|
+
get skillsBaseDir() {
|
|
457
|
+
return join2(homedir2(), ".agents", "skills");
|
|
458
|
+
}
|
|
459
|
+
get skillDir() {
|
|
460
|
+
return join2(this.skillsBaseDir, "caik");
|
|
461
|
+
}
|
|
462
|
+
get skillLockPath() {
|
|
463
|
+
return join2(homedir2(), ".agents", ".skill-lock.json");
|
|
464
|
+
}
|
|
465
|
+
// --- JSON helpers --------------------------------------------------------
|
|
466
|
+
readJson(path) {
|
|
467
|
+
if (!existsSync2(path)) return {};
|
|
468
|
+
try {
|
|
469
|
+
const raw = readFileSync(path, "utf-8");
|
|
470
|
+
const parsed = JSON.parse(raw);
|
|
471
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
472
|
+
return parsed;
|
|
473
|
+
}
|
|
474
|
+
return {};
|
|
475
|
+
} catch {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
writeJson(path, data) {
|
|
480
|
+
const dir = dirname(path);
|
|
481
|
+
if (!existsSync2(dir)) {
|
|
482
|
+
mkdirSync(dir, { recursive: true });
|
|
483
|
+
}
|
|
484
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
485
|
+
}
|
|
486
|
+
readSkillLock() {
|
|
487
|
+
if (!existsSync2(this.skillLockPath)) {
|
|
488
|
+
return { version: 3, skills: {} };
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const raw = readFileSync(this.skillLockPath, "utf-8");
|
|
492
|
+
const parsed = JSON.parse(raw);
|
|
493
|
+
return {
|
|
494
|
+
version: parsed.version ?? 3,
|
|
495
|
+
skills: parsed.skills ?? {}
|
|
496
|
+
};
|
|
497
|
+
} catch {
|
|
498
|
+
return { version: 3, skills: {} };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
writeSkillLock(lock) {
|
|
502
|
+
const dir = dirname(this.skillLockPath);
|
|
503
|
+
if (!existsSync2(dir)) {
|
|
504
|
+
mkdirSync(dir, { recursive: true });
|
|
505
|
+
}
|
|
506
|
+
writeFileSync(
|
|
507
|
+
this.skillLockPath,
|
|
508
|
+
JSON.stringify(lock, null, 2) + "\n",
|
|
509
|
+
"utf-8"
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
/** Check if `openclaw` binary is in PATH. */
|
|
513
|
+
isInPath() {
|
|
514
|
+
try {
|
|
515
|
+
execSync2("which openclaw", { stdio: "ignore" });
|
|
516
|
+
return true;
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// --- PlatformAdapter implementation --------------------------------------
|
|
522
|
+
async detect() {
|
|
523
|
+
const hasDir = existsSync2(this.openclawDir);
|
|
524
|
+
const inPath = this.isInPath();
|
|
525
|
+
const hasCwdConfig = existsSync2(join2(process.cwd(), "openclaw.json"));
|
|
526
|
+
if (!hasDir && !inPath && !hasCwdConfig) return null;
|
|
527
|
+
const configPaths = [];
|
|
528
|
+
if (hasCwdConfig) {
|
|
529
|
+
configPaths.push(join2(process.cwd(), "openclaw.json"));
|
|
530
|
+
}
|
|
531
|
+
if (existsSync2(this.openclawConfigPath)) {
|
|
532
|
+
configPaths.push(this.openclawConfigPath);
|
|
533
|
+
}
|
|
534
|
+
if (existsSync2(this.mcpConfigPath)) {
|
|
535
|
+
configPaths.push(this.mcpConfigPath);
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
name: this.name,
|
|
539
|
+
tier: this.tier,
|
|
540
|
+
configPaths,
|
|
541
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async registerMcp(serverConfig) {
|
|
545
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
546
|
+
const servers = config.mcpServers ?? {};
|
|
547
|
+
servers[MCP_KEY] = {
|
|
548
|
+
type: "stdio",
|
|
549
|
+
command: serverConfig.command,
|
|
550
|
+
args: serverConfig.args,
|
|
551
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
552
|
+
};
|
|
553
|
+
config.mcpServers = servers;
|
|
554
|
+
this.writeJson(this.mcpConfigPath, config);
|
|
555
|
+
}
|
|
556
|
+
async unregisterMcp() {
|
|
557
|
+
if (!existsSync2(this.mcpConfigPath)) return;
|
|
558
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
559
|
+
const servers = config.mcpServers;
|
|
560
|
+
if (!servers || !(MCP_KEY in servers)) return;
|
|
561
|
+
delete servers[MCP_KEY];
|
|
562
|
+
config.mcpServers = servers;
|
|
563
|
+
this.writeJson(this.mcpConfigPath, config);
|
|
564
|
+
}
|
|
565
|
+
get pluginDir() {
|
|
566
|
+
return join2(homedir2(), ".openclaw", "plugins", "caik");
|
|
567
|
+
}
|
|
568
|
+
async registerHooks(_hookConfig) {
|
|
569
|
+
if (!existsSync2(this.caikHookDir)) {
|
|
570
|
+
mkdirSync(this.caikHookDir, { recursive: true });
|
|
571
|
+
}
|
|
572
|
+
writeFileSync(
|
|
573
|
+
join2(this.caikHookDir, "HOOK.md"),
|
|
574
|
+
HOOK_MD_CONTENT,
|
|
575
|
+
"utf-8"
|
|
576
|
+
);
|
|
577
|
+
writeFileSync(
|
|
578
|
+
join2(this.caikHookDir, "handler.js"),
|
|
579
|
+
HANDLER_JS_CONTENT,
|
|
580
|
+
"utf-8"
|
|
581
|
+
);
|
|
582
|
+
await this.installPlugin();
|
|
583
|
+
}
|
|
584
|
+
async installPlugin() {
|
|
585
|
+
try {
|
|
586
|
+
if (!existsSync2(this.pluginDir)) {
|
|
587
|
+
mkdirSync(this.pluginDir, { recursive: true });
|
|
588
|
+
}
|
|
589
|
+
const thisDir = dirname(new URL(import.meta.url).pathname);
|
|
590
|
+
const pluginSources = [
|
|
591
|
+
join2(thisDir, "..", "..", "openclaw-plugin", "dist"),
|
|
592
|
+
// monorepo sibling
|
|
593
|
+
join2(thisDir, "..", "..", "..", "openclaw-plugin", "dist")
|
|
594
|
+
// npm installed
|
|
595
|
+
];
|
|
596
|
+
let pluginDistDir = null;
|
|
597
|
+
for (const src of pluginSources) {
|
|
598
|
+
if (existsSync2(join2(src, "index.js"))) {
|
|
599
|
+
pluginDistDir = src;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const manifestSources = [
|
|
604
|
+
join2(thisDir, "..", "..", "openclaw-plugin", "openclaw.plugin.json"),
|
|
605
|
+
join2(thisDir, "..", "..", "..", "openclaw-plugin", "openclaw.plugin.json")
|
|
606
|
+
];
|
|
607
|
+
for (const src of manifestSources) {
|
|
608
|
+
if (existsSync2(src)) {
|
|
609
|
+
const manifest = readFileSync(src, "utf-8");
|
|
610
|
+
writeFileSync(join2(this.pluginDir, "openclaw.plugin.json"), manifest, "utf-8");
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (pluginDistDir) {
|
|
615
|
+
const indexJs = readFileSync(join2(pluginDistDir, "index.js"), "utf-8");
|
|
616
|
+
writeFileSync(join2(this.pluginDir, "index.js"), indexJs, "utf-8");
|
|
617
|
+
}
|
|
618
|
+
const config = this.readJson(this.openclawConfigPath);
|
|
619
|
+
if (!config.plugins) config.plugins = {};
|
|
620
|
+
if (!config.plugins.entries) config.plugins.entries = {};
|
|
621
|
+
config.plugins.entries.caik = { enabled: true };
|
|
622
|
+
if (!config.plugins.load) config.plugins.load = {};
|
|
623
|
+
if (!config.plugins.load.paths) config.plugins.load.paths = [];
|
|
624
|
+
const paths = config.plugins.load.paths;
|
|
625
|
+
if (!paths.includes(this.pluginDir)) {
|
|
626
|
+
paths.push(this.pluginDir);
|
|
627
|
+
}
|
|
628
|
+
this.writeJson(this.openclawConfigPath, config);
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async unregisterHooks() {
|
|
633
|
+
if (existsSync2(this.caikHookDir)) {
|
|
634
|
+
try {
|
|
635
|
+
rmSync(this.caikHookDir, { recursive: true, force: true });
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async installSkill(_slug, content, files) {
|
|
641
|
+
if (!existsSync2(this.skillDir)) {
|
|
642
|
+
mkdirSync(this.skillDir, { recursive: true });
|
|
643
|
+
}
|
|
644
|
+
writeFileSync(join2(this.skillDir, "SKILL.md"), content, "utf-8");
|
|
645
|
+
if (files) {
|
|
646
|
+
for (const file of files) {
|
|
647
|
+
const filePath = join2(this.skillDir, file.path);
|
|
648
|
+
const fileDir = dirname(filePath);
|
|
649
|
+
if (!existsSync2(fileDir)) {
|
|
650
|
+
mkdirSync(fileDir, { recursive: true });
|
|
651
|
+
}
|
|
652
|
+
writeFileSync(filePath, file.content, "utf-8");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const lock = this.readSkillLock();
|
|
656
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
657
|
+
const existing = lock.skills["caik"];
|
|
658
|
+
lock.skills["caik"] = {
|
|
659
|
+
source: CLI_NPX_PACKAGE,
|
|
660
|
+
sourceType: "local",
|
|
661
|
+
sourceUrl: "",
|
|
662
|
+
skillPath: "skills/caik/SKILL.md",
|
|
663
|
+
skillFolderHash: "",
|
|
664
|
+
installedAt: existing?.installedAt ?? now,
|
|
665
|
+
updatedAt: now
|
|
666
|
+
};
|
|
667
|
+
this.writeSkillLock(lock);
|
|
668
|
+
}
|
|
669
|
+
async uninstallSkill(_slug) {
|
|
670
|
+
if (existsSync2(this.skillDir)) {
|
|
671
|
+
try {
|
|
672
|
+
rmSync(this.skillDir, { recursive: true, force: true });
|
|
673
|
+
} catch {
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const lock = this.readSkillLock();
|
|
677
|
+
if ("caik" in lock.skills) {
|
|
678
|
+
delete lock.skills["caik"];
|
|
679
|
+
this.writeSkillLock(lock);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
getConfigPath() {
|
|
683
|
+
return this.openclawConfigPath;
|
|
684
|
+
}
|
|
685
|
+
async readConfig() {
|
|
686
|
+
return this.readJson(this.openclawConfigPath);
|
|
687
|
+
}
|
|
688
|
+
async isRegistered() {
|
|
689
|
+
if (!existsSync2(this.mcpConfigPath)) return false;
|
|
690
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
691
|
+
const servers = config.mcpServers;
|
|
692
|
+
return !!servers && MCP_KEY in servers;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
var HOOK_MD_CONTENT = `---
|
|
696
|
+
name: caik-contributions
|
|
697
|
+
description: "Track artifact usage to build your CAIK contribution level and community karma"
|
|
698
|
+
metadata:
|
|
699
|
+
{
|
|
700
|
+
"openclaw":
|
|
701
|
+
{
|
|
702
|
+
"emoji": "\u{1F4E6}",
|
|
703
|
+
"events": ["session_start", "session_end", "after_tool_call", "agent_end", "gateway_start"],
|
|
704
|
+
"install": [{ "id": "local", "kind": "local", "label": "CAIK CLI hook pack" }],
|
|
705
|
+
},
|
|
706
|
+
}
|
|
707
|
+
---
|
|
708
|
+
|
|
709
|
+
# CAIK Contribution Tracking
|
|
710
|
+
|
|
711
|
+
Reports session lifecycle and tool usage events to the CAIK API to build your contribution level and community karma.
|
|
712
|
+
|
|
713
|
+
## What It Does
|
|
714
|
+
|
|
715
|
+
- **Session start** (\`session_start\`): Computes stack fingerprint, starts daemon, records session
|
|
716
|
+
- **Tool use** (\`after_tool_call\`): Buffers tool execution events with success/failure signals
|
|
717
|
+
- **Session end** (\`session_end\`, \`agent_end\`): Computes session shape, flushes events, records session end
|
|
718
|
+
- **Gateway start** (\`gateway_start\`): Clears session state on gateway restart
|
|
719
|
+
|
|
720
|
+
## Privacy
|
|
721
|
+
|
|
722
|
+
- Only sends: event type, platform name, tool name, success/failure, timestamp
|
|
723
|
+
- At Collective level: adds stack fingerprint, session shape, agent model
|
|
724
|
+
- No code, file contents, or conversation data is transmitted
|
|
725
|
+
- Contribution tracking can be disabled: \`caik config set contributionLevel none\`
|
|
726
|
+
|
|
727
|
+
## Configuration
|
|
728
|
+
|
|
729
|
+
Set \`CAIK_API_URL\` and \`CAIK_API_KEY\` environment variables, or configure via \`caik init --auth\`.
|
|
730
|
+
`;
|
|
731
|
+
var HANDLER_JS_CONTENT = `/**
|
|
732
|
+
* CAIK contribution tracking hook for OpenClaw.
|
|
733
|
+
*
|
|
734
|
+
* Uses OpenClaw lifecycle hooks (session_start, after_tool_call, session_end, agent_end,
|
|
735
|
+
* gateway_start) for rich event data. Delegates to the CLI for session-start/end
|
|
736
|
+
* (full processing: fingerprint, daemon, metrics) and buffers tool events in-process
|
|
737
|
+
* for speed.
|
|
738
|
+
*
|
|
739
|
+
* Fire-and-forget \u2014 never blocks the agent, never throws.
|
|
740
|
+
*/
|
|
741
|
+
import { spawn } from "node:child_process";
|
|
742
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
743
|
+
import { join, dirname } from "node:path";
|
|
744
|
+
import { homedir } from "node:os";
|
|
745
|
+
import { fileURLToPath } from "node:url";
|
|
746
|
+
|
|
747
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
748
|
+
const __dirname = dirname(__filename);
|
|
749
|
+
|
|
750
|
+
const CAIK_DIR = join(homedir(), ".caik");
|
|
751
|
+
const PENDING_PATH = join(CAIK_DIR, "pending-events.json");
|
|
752
|
+
const CONFIG_PATH = join(CAIK_DIR, "config.json");
|
|
753
|
+
const DAEMON_PORT = process.env.CAIK_DAEMON_PORT ?? "37778";
|
|
754
|
+
|
|
755
|
+
// \u2500\u2500\u2500 Discovery logging \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
756
|
+
// Logs first event of each type to learn OpenClaw's event field names.
|
|
757
|
+
// Remove once field names are confirmed.
|
|
758
|
+
const seenEventTypes = new Set();
|
|
759
|
+
function logEventShape(event, ctx) {
|
|
760
|
+
const name = event?.hookName ?? event?.type ?? "unknown";
|
|
761
|
+
if (seenEventTypes.has(name)) return;
|
|
762
|
+
seenEventTypes.add(name);
|
|
763
|
+
try {
|
|
764
|
+
if (!existsSync(CAIK_DIR)) mkdirSync(CAIK_DIR, { recursive: true });
|
|
765
|
+
const summary = {};
|
|
766
|
+
if (ctx) {
|
|
767
|
+
for (const [k, v] of Object.entries(ctx)) {
|
|
768
|
+
if (typeof v === "string" && v.length > 200) summary[k] = v.slice(0, 200) + "...";
|
|
769
|
+
else if (typeof v === "object" && v !== null) summary[k] = "[" + typeof v + ": " + Object.keys(v).join(",") + "]";
|
|
770
|
+
else summary[k] = v;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
appendFileSync(
|
|
774
|
+
join(CAIK_DIR, "openclaw-events-captured.jsonl"),
|
|
775
|
+
JSON.stringify({
|
|
776
|
+
hookName: name,
|
|
777
|
+
eventKeys: Object.keys(event ?? {}),
|
|
778
|
+
ctxKeys: Object.keys(ctx ?? {}),
|
|
779
|
+
event,
|
|
780
|
+
ctxSummary: summary,
|
|
781
|
+
}) + "\\n",
|
|
782
|
+
);
|
|
783
|
+
} catch { /* ignore */ }
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// \u2500\u2500\u2500 Disk-backed session tracking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
787
|
+
// Persisted to disk because OpenClaw runs each hook invocation as a separate
|
|
788
|
+
// process, so in-memory state is lost between events.
|
|
789
|
+
const SESSION_STATE_PATH = join(CAIK_DIR, "openclaw-sessions.json");
|
|
790
|
+
|
|
791
|
+
function loadSessionState() {
|
|
792
|
+
try { return existsSync(SESSION_STATE_PATH) ? JSON.parse(readFileSync(SESSION_STATE_PATH, "utf-8")) : {}; }
|
|
793
|
+
catch { return {}; }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function saveSessionState(state) {
|
|
797
|
+
try {
|
|
798
|
+
if (!existsSync(CAIK_DIR)) mkdirSync(CAIK_DIR, { recursive: true });
|
|
799
|
+
writeFileSync(SESSION_STATE_PATH, JSON.stringify(state), "utf-8");
|
|
800
|
+
} catch { /* ignore */ }
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function getOrCreateSessionId(event) {
|
|
804
|
+
const key = event?.agentId ?? event?.runId ?? event?.sessionId ?? "default";
|
|
805
|
+
const state = loadSessionState();
|
|
806
|
+
if (!state[key]) {
|
|
807
|
+
state[key] = "caik-oc-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8);
|
|
808
|
+
saveSessionState(state);
|
|
809
|
+
}
|
|
810
|
+
return state[key];
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function removeSessionId(sessionId) {
|
|
814
|
+
const state = loadSessionState();
|
|
815
|
+
for (const k of Object.keys(state)) {
|
|
816
|
+
if (state[k] === sessionId) delete state[k];
|
|
817
|
+
}
|
|
818
|
+
saveSessionState(state);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function clearAllSessions() {
|
|
822
|
+
try { writeFileSync(SESSION_STATE_PATH, "{}", "utf-8"); }
|
|
823
|
+
catch { /* ignore */ }
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
827
|
+
function loadConfig() {
|
|
828
|
+
try { return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")); }
|
|
829
|
+
catch { return {}; }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function shouldSend() {
|
|
833
|
+
const cfg = loadConfig();
|
|
834
|
+
if (cfg.contributions === false || cfg.contribution === false) return false;
|
|
835
|
+
return cfg.contributionLevel !== "none";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// \u2500\u2500\u2500 CLI delegation (session-start/end only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
839
|
+
function findCli() {
|
|
840
|
+
try {
|
|
841
|
+
const local = join(__dirname, "..", "..", "..", "cli", "dist", "index.js");
|
|
842
|
+
if (existsSync(local)) return [process.execPath, local];
|
|
843
|
+
} catch { /* ignore */ }
|
|
844
|
+
return ["npx", "-y", "${CLI_NPX_PACKAGE}"];
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function runCliHook(subcommand, stdinData) {
|
|
848
|
+
return new Promise((resolve) => {
|
|
849
|
+
const [cmd, ...args] = findCli();
|
|
850
|
+
const proc = spawn(cmd, [...args, "hook", subcommand], {
|
|
851
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
852
|
+
timeout: 8000,
|
|
853
|
+
env: { ...process.env, CAIK_OPENCLAW: "1" },
|
|
854
|
+
});
|
|
855
|
+
if (stdinData) {
|
|
856
|
+
proc.stdin.write(JSON.stringify(stdinData));
|
|
857
|
+
proc.stdin.end();
|
|
858
|
+
}
|
|
859
|
+
proc.on("close", resolve);
|
|
860
|
+
proc.on("error", () => resolve(1));
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// \u2500\u2500\u2500 Fast in-process event buffering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
865
|
+
function bufferEvent(event) {
|
|
866
|
+
try {
|
|
867
|
+
if (!existsSync(CAIK_DIR)) mkdirSync(CAIK_DIR, { recursive: true });
|
|
868
|
+
let events = [];
|
|
869
|
+
try { events = JSON.parse(readFileSync(PENDING_PATH, "utf-8")); } catch { /* empty */ }
|
|
870
|
+
if (!Array.isArray(events)) events = [];
|
|
871
|
+
events.push(event);
|
|
872
|
+
writeFileSync(PENDING_PATH, JSON.stringify(events), "utf-8");
|
|
873
|
+
} catch { /* ignore */ }
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// \u2500\u2500\u2500 Daemon observation posting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
877
|
+
function postToDaemon(obs) {
|
|
878
|
+
try {
|
|
879
|
+
fetch("http://127.0.0.1:" + DAEMON_PORT + "/observations", {
|
|
880
|
+
method: "POST",
|
|
881
|
+
headers: { "Content-Type": "application/json" },
|
|
882
|
+
body: JSON.stringify(obs),
|
|
883
|
+
signal: AbortSignal.timeout(1000),
|
|
884
|
+
}).catch(() => {});
|
|
885
|
+
} catch { /* ignore */ }
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// \u2500\u2500\u2500 Main handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
889
|
+
const handler = async (event, ctx) => {
|
|
890
|
+
try {
|
|
891
|
+
logEventShape(event, ctx);
|
|
892
|
+
if (!shouldSend()) return;
|
|
893
|
+
// If the native plugin is active, skip hook pack to avoid double-counting
|
|
894
|
+
if (process.env.CAIK_PLUGIN_ACTIVE === "1") return;
|
|
895
|
+
|
|
896
|
+
const hookName = event?.hookName ?? event?.type ?? "";
|
|
897
|
+
const timestamp = new Date().toISOString();
|
|
898
|
+
|
|
899
|
+
switch (hookName) {
|
|
900
|
+
case "session_start": {
|
|
901
|
+
const sessionId = getOrCreateSessionId(event);
|
|
902
|
+
await runCliHook("session-start", {
|
|
903
|
+
hook_event_name: "session-start",
|
|
904
|
+
session_id: sessionId,
|
|
905
|
+
model: event?.model ?? ctx?.model ?? undefined,
|
|
906
|
+
cwd: event?.cwd ?? ctx?.cwd ?? ctx?.workspace ?? process.cwd(),
|
|
907
|
+
});
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
case "after_tool_call": {
|
|
912
|
+
const sessionId = getOrCreateSessionId(event);
|
|
913
|
+
const toolName = event?.toolName ?? event?.tool ?? event?.name ?? event?.action ?? "unknown";
|
|
914
|
+
const success = event?.error == null && event?.success !== false;
|
|
915
|
+
|
|
916
|
+
bufferEvent({
|
|
917
|
+
type: "tool_use",
|
|
918
|
+
platform: "openclaw",
|
|
919
|
+
sessionId,
|
|
920
|
+
tool: toolName,
|
|
921
|
+
success,
|
|
922
|
+
timestamp,
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
postToDaemon({
|
|
926
|
+
sessionId,
|
|
927
|
+
slug: toolName,
|
|
928
|
+
tool: toolName,
|
|
929
|
+
success,
|
|
930
|
+
platform: "openclaw",
|
|
931
|
+
timestamp,
|
|
932
|
+
});
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
case "session_end":
|
|
937
|
+
case "agent_end": {
|
|
938
|
+
const key = event?.agentId ?? event?.runId ?? event?.sessionId ?? "default";
|
|
939
|
+
const state = loadSessionState();
|
|
940
|
+
const sessionId = state[key] ?? "unknown";
|
|
941
|
+
await runCliHook("session-end", {
|
|
942
|
+
hook_event_name: "session-end",
|
|
943
|
+
session_id: sessionId,
|
|
944
|
+
reason: hookName,
|
|
945
|
+
cwd: event?.cwd ?? ctx?.cwd ?? ctx?.workspace ?? process.cwd(),
|
|
946
|
+
});
|
|
947
|
+
removeSessionId(sessionId);
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
case "gateway_start": {
|
|
952
|
+
clearAllSessions();
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch { /* never fail */ }
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
export default handler;
|
|
960
|
+
`;
|
|
961
|
+
|
|
962
|
+
// src/platform/codex.ts
|
|
963
|
+
import {
|
|
964
|
+
existsSync as existsSync3,
|
|
965
|
+
readFileSync as readFileSync2,
|
|
966
|
+
writeFileSync as writeFileSync2,
|
|
967
|
+
mkdirSync as mkdirSync2,
|
|
968
|
+
readdirSync,
|
|
969
|
+
rmSync as rmSync2
|
|
970
|
+
} from "fs";
|
|
971
|
+
import { execSync as execSync3 } from "child_process";
|
|
972
|
+
import { join as join3 } from "path";
|
|
973
|
+
import { homedir as homedir3 } from "os";
|
|
974
|
+
var MCP_KEY2 = "caik";
|
|
975
|
+
var PLUGIN_MANAGED_SKILLS = [
|
|
976
|
+
"caik",
|
|
977
|
+
"caik-discover",
|
|
978
|
+
"caik-observe",
|
|
979
|
+
"caik-improve",
|
|
980
|
+
"caik-status"
|
|
981
|
+
];
|
|
982
|
+
var CodexAdapter = class {
|
|
983
|
+
name = "codex";
|
|
984
|
+
tier = "full-plugin";
|
|
985
|
+
get codexDir() {
|
|
986
|
+
return join3(homedir3(), ".codex");
|
|
987
|
+
}
|
|
988
|
+
get configTomlPath() {
|
|
989
|
+
return join3(this.codexDir, "config.toml");
|
|
990
|
+
}
|
|
991
|
+
get hooksPath() {
|
|
992
|
+
return join3(this.codexDir, "hooks.json");
|
|
993
|
+
}
|
|
994
|
+
get skillsDir() {
|
|
995
|
+
return join3(this.codexDir, "skills");
|
|
996
|
+
}
|
|
997
|
+
get localSkillsDir() {
|
|
998
|
+
return join3(process.cwd(), ".agents", "skills");
|
|
999
|
+
}
|
|
1000
|
+
get pluginsCacheDir() {
|
|
1001
|
+
return join3(this.codexDir, "plugins", "cache");
|
|
1002
|
+
}
|
|
1003
|
+
// ---------------------------------------------------------------------------
|
|
1004
|
+
// JSON helpers (for hooks.json)
|
|
1005
|
+
// ---------------------------------------------------------------------------
|
|
1006
|
+
readJson(path) {
|
|
1007
|
+
if (!existsSync3(path)) return {};
|
|
1008
|
+
try {
|
|
1009
|
+
const raw = readFileSync2(path, "utf-8");
|
|
1010
|
+
const parsed = JSON.parse(raw);
|
|
1011
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1012
|
+
return parsed;
|
|
1013
|
+
}
|
|
1014
|
+
return {};
|
|
1015
|
+
} catch {
|
|
1016
|
+
return {};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
writeJson(path, data) {
|
|
1020
|
+
const dir = join3(path, "..");
|
|
1021
|
+
if (!existsSync3(dir)) {
|
|
1022
|
+
mkdirSync2(dir, { recursive: true });
|
|
1023
|
+
}
|
|
1024
|
+
writeFileSync2(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1025
|
+
}
|
|
1026
|
+
// ---------------------------------------------------------------------------
|
|
1027
|
+
// TOML helpers (for config.toml — read-only, writes go through codex CLI)
|
|
1028
|
+
// ---------------------------------------------------------------------------
|
|
1029
|
+
/**
|
|
1030
|
+
* Check if a key exists in config.toml (simple regex, no TOML parser needed).
|
|
1031
|
+
*/
|
|
1032
|
+
configTomlHasKey(key) {
|
|
1033
|
+
if (!existsSync3(this.configTomlPath)) return false;
|
|
1034
|
+
try {
|
|
1035
|
+
const content = readFileSync2(this.configTomlPath, "utf-8");
|
|
1036
|
+
return content.includes(`[mcp_servers.${key}]`);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Check if `codex` CLI is available in PATH.
|
|
1043
|
+
*/
|
|
1044
|
+
codexCliAvailable() {
|
|
1045
|
+
try {
|
|
1046
|
+
execSync3("which codex", { stdio: "pipe" });
|
|
1047
|
+
return true;
|
|
1048
|
+
} catch {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Ensure `codex_hooks = true` is set in config.toml [features] section.
|
|
1054
|
+
* Codex requires this feature flag for hooks to fire.
|
|
1055
|
+
*/
|
|
1056
|
+
ensureHooksEnabled() {
|
|
1057
|
+
if (!existsSync3(this.configTomlPath)) return;
|
|
1058
|
+
try {
|
|
1059
|
+
const content = readFileSync2(this.configTomlPath, "utf-8");
|
|
1060
|
+
if (content.includes("codex_hooks")) return;
|
|
1061
|
+
if (content.includes("[features]")) {
|
|
1062
|
+
const updated = content.replace(
|
|
1063
|
+
/(\[features\]\n)/,
|
|
1064
|
+
"$1codex_hooks = true\n"
|
|
1065
|
+
);
|
|
1066
|
+
writeFileSync2(this.configTomlPath, updated, "utf-8");
|
|
1067
|
+
} else {
|
|
1068
|
+
writeFileSync2(
|
|
1069
|
+
this.configTomlPath,
|
|
1070
|
+
content.trimEnd() + "\n\n[features]\ncodex_hooks = true\n",
|
|
1071
|
+
"utf-8"
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// ---------------------------------------------------------------------------
|
|
1078
|
+
// Plugin detection
|
|
1079
|
+
// ---------------------------------------------------------------------------
|
|
1080
|
+
/**
|
|
1081
|
+
* Check if CAIK is installed as a Codex plugin.
|
|
1082
|
+
* Checks plugins cache directory and config.toml [plugins] section.
|
|
1083
|
+
*/
|
|
1084
|
+
isPluginInstalled() {
|
|
1085
|
+
if (existsSync3(this.pluginsCacheDir)) {
|
|
1086
|
+
try {
|
|
1087
|
+
const publishers = readdirSync(this.pluginsCacheDir);
|
|
1088
|
+
for (const publisher of publishers) {
|
|
1089
|
+
const caikDir = join3(this.pluginsCacheDir, publisher, "caik");
|
|
1090
|
+
if (existsSync3(caikDir)) {
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (existsSync3(this.configTomlPath)) {
|
|
1098
|
+
try {
|
|
1099
|
+
const content = readFileSync2(this.configTomlPath, "utf-8");
|
|
1100
|
+
if (/\[plugins\.[^\]]*caik/i.test(content)) {
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
// ---------------------------------------------------------------------------
|
|
1109
|
+
// PlatformAdapter implementation
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
async detect() {
|
|
1112
|
+
if (!existsSync3(this.codexDir)) return null;
|
|
1113
|
+
const hasHooks = existsSync3(this.hooksPath);
|
|
1114
|
+
const hasConfig = existsSync3(this.configTomlPath);
|
|
1115
|
+
if (!hasHooks && !hasConfig) return null;
|
|
1116
|
+
const configPaths = [];
|
|
1117
|
+
if (hasConfig) configPaths.push(this.configTomlPath);
|
|
1118
|
+
if (hasHooks) configPaths.push(this.hooksPath);
|
|
1119
|
+
return {
|
|
1120
|
+
name: this.name,
|
|
1121
|
+
tier: this.tier,
|
|
1122
|
+
configPaths,
|
|
1123
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
async registerMcp(serverConfig) {
|
|
1127
|
+
if (this.isPluginInstalled()) return;
|
|
1128
|
+
const serverKey = serverConfig.key ?? MCP_KEY2;
|
|
1129
|
+
if (this.configTomlHasKey(serverKey)) return;
|
|
1130
|
+
if (this.codexCliAvailable()) {
|
|
1131
|
+
try {
|
|
1132
|
+
const args = [serverConfig.command, ...serverConfig.args];
|
|
1133
|
+
const envFlags = serverConfig.env ? Object.entries(serverConfig.env).map(([k, v]) => `--env ${k}=${v}`).join(" ") : "";
|
|
1134
|
+
const cmd = `codex mcp add ${serverKey} ${envFlags} -- ${args.join(" ")}`;
|
|
1135
|
+
execSync3(cmd, { stdio: "pipe", timeout: 1e4 });
|
|
1136
|
+
return;
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const envSection = serverConfig.env ? Object.entries(serverConfig.env).map(([k, v]) => `${k} = "${v}"`).join("\n") : "";
|
|
1141
|
+
const tomlSnippet = [
|
|
1142
|
+
`[mcp_servers.${serverKey}]`,
|
|
1143
|
+
`command = "${serverConfig.command}"`,
|
|
1144
|
+
`args = ${JSON.stringify(serverConfig.args)}`,
|
|
1145
|
+
...envSection ? [`
|
|
1146
|
+
[mcp_servers.${serverKey}.env]`, envSection] : []
|
|
1147
|
+
].join("\n");
|
|
1148
|
+
process.stdout.write(
|
|
1149
|
+
`
|
|
1150
|
+
Add this to ~/.codex/config.toml:
|
|
1151
|
+
|
|
1152
|
+
${tomlSnippet}
|
|
1153
|
+
|
|
1154
|
+
Or run: codex mcp add ${serverKey} -- ${serverConfig.command} ${serverConfig.args.join(" ")}
|
|
1155
|
+
|
|
1156
|
+
`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
async unregisterMcp() {
|
|
1160
|
+
if (!this.configTomlHasKey(MCP_KEY2)) return;
|
|
1161
|
+
if (this.codexCliAvailable()) {
|
|
1162
|
+
try {
|
|
1163
|
+
execSync3(`codex mcp remove ${MCP_KEY2}`, { stdio: "pipe", timeout: 1e4 });
|
|
1164
|
+
return;
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
process.stdout.write(
|
|
1169
|
+
`
|
|
1170
|
+
Remove [mcp_servers.${MCP_KEY2}] from ~/.codex/config.toml
|
|
1171
|
+
Or run: codex mcp remove ${MCP_KEY2}
|
|
1172
|
+
|
|
1173
|
+
`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
async registerHooks(hookConfig) {
|
|
1177
|
+
if (this.isPluginInstalled()) return;
|
|
1178
|
+
this.ensureHooksEnabled();
|
|
1179
|
+
const hooksFile = this.readJson(this.hooksPath);
|
|
1180
|
+
const existingHooks = hooksFile.hooks ?? {};
|
|
1181
|
+
for (const [event, entries] of Object.entries(hookConfig.hooks)) {
|
|
1182
|
+
const newEntries = Array.isArray(entries) ? entries : [entries];
|
|
1183
|
+
const existing = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
|
|
1184
|
+
const filtered = existing.filter((group) => {
|
|
1185
|
+
if (typeof group === "object" && group !== null) {
|
|
1186
|
+
const g = group;
|
|
1187
|
+
if (Array.isArray(g.hooks)) {
|
|
1188
|
+
const hasCAIK = g.hooks.some(
|
|
1189
|
+
(h) => typeof h.command === "string" && h.command.toLowerCase().includes("caik")
|
|
1190
|
+
);
|
|
1191
|
+
if (hasCAIK) return false;
|
|
1192
|
+
}
|
|
1193
|
+
if ("command" in g) {
|
|
1194
|
+
return !g.command.toLowerCase().includes("caik");
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return true;
|
|
1198
|
+
});
|
|
1199
|
+
const wrappedEntries = newEntries.map((entry) => {
|
|
1200
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1201
|
+
const e = entry;
|
|
1202
|
+
const hookEntry = {
|
|
1203
|
+
type: "command",
|
|
1204
|
+
timeout: 10,
|
|
1205
|
+
...e
|
|
1206
|
+
};
|
|
1207
|
+
return { hooks: [hookEntry] };
|
|
1208
|
+
}
|
|
1209
|
+
return entry;
|
|
1210
|
+
});
|
|
1211
|
+
existingHooks[event] = [...filtered, ...wrappedEntries];
|
|
1212
|
+
}
|
|
1213
|
+
hooksFile.hooks = existingHooks;
|
|
1214
|
+
this.writeJson(this.hooksPath, hooksFile);
|
|
1215
|
+
}
|
|
1216
|
+
async unregisterHooks() {
|
|
1217
|
+
if (!existsSync3(this.hooksPath)) return;
|
|
1218
|
+
const hooksFile = this.readJson(this.hooksPath);
|
|
1219
|
+
const hooks = hooksFile.hooks;
|
|
1220
|
+
if (!hooks) return;
|
|
1221
|
+
for (const event of Object.keys(hooks)) {
|
|
1222
|
+
const entries = hooks[event];
|
|
1223
|
+
if (!Array.isArray(entries)) continue;
|
|
1224
|
+
hooks[event] = entries.filter((group) => {
|
|
1225
|
+
if (typeof group === "object" && group !== null) {
|
|
1226
|
+
const g = group;
|
|
1227
|
+
if (Array.isArray(g.hooks)) {
|
|
1228
|
+
const hasCAIK = g.hooks.some(
|
|
1229
|
+
(h) => typeof h.command === "string" && h.command.includes("caik")
|
|
1230
|
+
);
|
|
1231
|
+
if (hasCAIK) return false;
|
|
1232
|
+
}
|
|
1233
|
+
if ("command" in g) {
|
|
1234
|
+
return !g.command.includes("caik");
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return true;
|
|
1238
|
+
});
|
|
1239
|
+
if (hooks[event].length === 0) {
|
|
1240
|
+
delete hooks[event];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (Object.keys(hooks).length === 0) {
|
|
1244
|
+
delete hooksFile.hooks;
|
|
1245
|
+
} else {
|
|
1246
|
+
hooksFile.hooks = hooks;
|
|
1247
|
+
}
|
|
1248
|
+
this.writeJson(this.hooksPath, hooksFile);
|
|
1249
|
+
}
|
|
1250
|
+
async installSkill(slug, content, files, local) {
|
|
1251
|
+
if (!local && this.isPluginInstalled() && PLUGIN_MANAGED_SKILLS.includes(slug)) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const baseDir = local ? this.localSkillsDir : this.skillsDir;
|
|
1255
|
+
const skillDir = join3(baseDir, slug);
|
|
1256
|
+
if (!existsSync3(skillDir)) {
|
|
1257
|
+
mkdirSync2(skillDir, { recursive: true });
|
|
1258
|
+
}
|
|
1259
|
+
writeFileSync2(join3(skillDir, "SKILL.md"), content, "utf-8");
|
|
1260
|
+
if (files) {
|
|
1261
|
+
for (const file of files) {
|
|
1262
|
+
const filePath = join3(skillDir, file.path);
|
|
1263
|
+
const fileDir = join3(filePath, "..");
|
|
1264
|
+
if (!existsSync3(fileDir)) {
|
|
1265
|
+
mkdirSync2(fileDir, { recursive: true });
|
|
1266
|
+
}
|
|
1267
|
+
writeFileSync2(filePath, file.content, "utf-8");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async uninstallSkill(slug, local) {
|
|
1272
|
+
const baseDir = local ? this.localSkillsDir : this.skillsDir;
|
|
1273
|
+
const skillDir = join3(baseDir, slug);
|
|
1274
|
+
if (!existsSync3(skillDir)) return;
|
|
1275
|
+
try {
|
|
1276
|
+
rmSync2(skillDir, { recursive: true, force: true });
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
getConfigPath() {
|
|
1281
|
+
return this.configTomlPath;
|
|
1282
|
+
}
|
|
1283
|
+
async readConfig() {
|
|
1284
|
+
if (!existsSync3(this.configTomlPath)) return {};
|
|
1285
|
+
try {
|
|
1286
|
+
const content = readFileSync2(this.configTomlPath, "utf-8");
|
|
1287
|
+
return { _raw: content, _format: "toml" };
|
|
1288
|
+
} catch {
|
|
1289
|
+
return {};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async isRegistered() {
|
|
1293
|
+
if (this.isPluginInstalled()) return true;
|
|
1294
|
+
return this.configTomlHasKey(MCP_KEY2);
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
// src/platform/generic.ts
|
|
1299
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, rmSync as rmSync3 } from "fs";
|
|
1300
|
+
import { execSync as execSync4 } from "child_process";
|
|
1301
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
1302
|
+
import { homedir as homedir4 } from "os";
|
|
1303
|
+
var MCP_KEY3 = "caik";
|
|
1304
|
+
function expandHome2(p) {
|
|
1305
|
+
if (p.startsWith("~/")) return join4(homedir4(), p.slice(2));
|
|
1306
|
+
if (p === "~") return homedir4();
|
|
1307
|
+
return p;
|
|
1308
|
+
}
|
|
1309
|
+
function commandExists2(cmd) {
|
|
1310
|
+
try {
|
|
1311
|
+
execSync4(`which ${cmd}`, { stdio: "pipe" });
|
|
1312
|
+
return true;
|
|
1313
|
+
} catch {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function readJsonFile(path) {
|
|
1318
|
+
try {
|
|
1319
|
+
if (!existsSync4(path)) return null;
|
|
1320
|
+
const raw = readFileSync3(path, "utf-8");
|
|
1321
|
+
return JSON.parse(raw);
|
|
1322
|
+
} catch {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function writeJsonFile(path, data) {
|
|
1327
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1328
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1329
|
+
}
|
|
1330
|
+
var GenericAdapter = class {
|
|
1331
|
+
name;
|
|
1332
|
+
tier;
|
|
1333
|
+
config;
|
|
1334
|
+
constructor(name, config) {
|
|
1335
|
+
this.name = name;
|
|
1336
|
+
this.config = config ?? null;
|
|
1337
|
+
this.tier = config?.tier ?? "cli-mcp";
|
|
1338
|
+
}
|
|
1339
|
+
// ── Detection ─────────────────────────────────────────────
|
|
1340
|
+
async detect() {
|
|
1341
|
+
if (!this.config) return null;
|
|
1342
|
+
const home = homedir4();
|
|
1343
|
+
const cwd = process.cwd();
|
|
1344
|
+
let found = false;
|
|
1345
|
+
for (const dir of this.config.homeDirs) {
|
|
1346
|
+
if (existsSync4(join4(home, dir))) {
|
|
1347
|
+
found = true;
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (!found) {
|
|
1352
|
+
for (const dir of this.config.cwdDirs) {
|
|
1353
|
+
if (existsSync4(join4(cwd, dir))) {
|
|
1354
|
+
found = true;
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!found) {
|
|
1360
|
+
for (const bin of this.config.binaries) {
|
|
1361
|
+
if (commandExists2(bin)) {
|
|
1362
|
+
found = true;
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (!found) return null;
|
|
1368
|
+
const configPaths = [];
|
|
1369
|
+
if (this.config.mcpConfigPath) {
|
|
1370
|
+
const resolved = this.resolveConfigPath();
|
|
1371
|
+
if (resolved && existsSync4(resolved)) {
|
|
1372
|
+
configPaths.push(resolved);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
name: this.name,
|
|
1377
|
+
tier: this.tier,
|
|
1378
|
+
configPaths,
|
|
1379
|
+
capabilities: this.config.capabilities
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
// ── MCP Registration ──────────────────────────────────────
|
|
1383
|
+
async registerMcp(serverConfig) {
|
|
1384
|
+
const configPath = this.resolveConfigPath();
|
|
1385
|
+
if (!configPath || this.config?.mcpConfigKey === "manual") {
|
|
1386
|
+
this.printManualInstructions(serverConfig);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const config = readJsonFile(configPath) ?? {};
|
|
1390
|
+
const key = this.config?.mcpConfigKey ?? "mcpServers";
|
|
1391
|
+
const servers = config[key] ?? {};
|
|
1392
|
+
const serverKey = serverConfig.key ?? MCP_KEY3;
|
|
1393
|
+
const { key: _key, ...mcpEntry } = serverConfig;
|
|
1394
|
+
servers[serverKey] = mcpEntry;
|
|
1395
|
+
config[key] = servers;
|
|
1396
|
+
writeJsonFile(configPath, config);
|
|
1397
|
+
}
|
|
1398
|
+
async unregisterMcp() {
|
|
1399
|
+
const configPath = this.resolveConfigPath();
|
|
1400
|
+
if (!configPath || this.config?.mcpConfigKey === "manual") {
|
|
1401
|
+
process.stdout.write(
|
|
1402
|
+
`
|
|
1403
|
+
To unregister CAIK from ${this.name}, remove the "caik" entry
|
|
1404
|
+
from the MCP section in your configuration file.
|
|
1405
|
+
|
|
1406
|
+
`
|
|
1407
|
+
);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const config = readJsonFile(configPath);
|
|
1411
|
+
if (!config) return;
|
|
1412
|
+
const key = this.config?.mcpConfigKey ?? "mcpServers";
|
|
1413
|
+
const servers = config[key];
|
|
1414
|
+
if (!servers?.[MCP_KEY3]) return;
|
|
1415
|
+
delete servers[MCP_KEY3];
|
|
1416
|
+
if (Object.keys(servers).length === 0) {
|
|
1417
|
+
delete config[key];
|
|
1418
|
+
}
|
|
1419
|
+
writeJsonFile(configPath, config);
|
|
1420
|
+
}
|
|
1421
|
+
// ── Skills ────────────────────────────────────────────────
|
|
1422
|
+
async installSkill(slug, content, files, local) {
|
|
1423
|
+
const baseDir = this.resolveSkillsDir(local);
|
|
1424
|
+
if (!baseDir) return;
|
|
1425
|
+
const skillDir = join4(baseDir, slug);
|
|
1426
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
1427
|
+
writeFileSync3(join4(skillDir, "SKILL.md"), content, "utf-8");
|
|
1428
|
+
if (files) {
|
|
1429
|
+
for (const file of files) {
|
|
1430
|
+
const filePath = join4(skillDir, file.path);
|
|
1431
|
+
mkdirSync3(dirname2(filePath), { recursive: true });
|
|
1432
|
+
writeFileSync3(filePath, file.content, "utf-8");
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async uninstallSkill(slug, local) {
|
|
1437
|
+
const baseDir = this.resolveSkillsDir(local);
|
|
1438
|
+
if (!baseDir) return;
|
|
1439
|
+
const skillDir = join4(baseDir, slug);
|
|
1440
|
+
try {
|
|
1441
|
+
if (existsSync4(skillDir)) {
|
|
1442
|
+
rmSync3(skillDir, { recursive: true, force: true });
|
|
1443
|
+
}
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// ── Config Accessors ──────────────────────────────────────
|
|
1448
|
+
getConfigPath() {
|
|
1449
|
+
return this.resolveConfigPath() ?? "";
|
|
1450
|
+
}
|
|
1451
|
+
async readConfig() {
|
|
1452
|
+
const configPath = this.resolveConfigPath();
|
|
1453
|
+
if (!configPath) return {};
|
|
1454
|
+
return readJsonFile(configPath) ?? {};
|
|
1455
|
+
}
|
|
1456
|
+
async isRegistered() {
|
|
1457
|
+
const configPath = this.resolveConfigPath();
|
|
1458
|
+
if (!configPath || this.config?.mcpConfigKey === "manual") return false;
|
|
1459
|
+
const config = readJsonFile(configPath);
|
|
1460
|
+
if (!config) return false;
|
|
1461
|
+
const key = this.config?.mcpConfigKey ?? "mcpServers";
|
|
1462
|
+
const servers = config[key];
|
|
1463
|
+
return Boolean(servers?.[MCP_KEY3]);
|
|
1464
|
+
}
|
|
1465
|
+
// ── Private Helpers ───────────────────────────────────────
|
|
1466
|
+
/** Resolve the MCP config path, expanding ~ and handling cwd-relative paths */
|
|
1467
|
+
resolveConfigPath() {
|
|
1468
|
+
if (!this.config?.mcpConfigPath) return null;
|
|
1469
|
+
const raw = this.config.mcpConfigPath;
|
|
1470
|
+
if (raw.startsWith("~/") || raw.startsWith("~")) {
|
|
1471
|
+
return expandHome2(raw);
|
|
1472
|
+
}
|
|
1473
|
+
return join4(process.cwd(), raw);
|
|
1474
|
+
}
|
|
1475
|
+
/** Resolve the skills directory for global or local scope */
|
|
1476
|
+
resolveSkillsDir(local) {
|
|
1477
|
+
if (!this.config) return null;
|
|
1478
|
+
if (local && this.config.localSkillsDir) {
|
|
1479
|
+
return join4(process.cwd(), this.config.localSkillsDir);
|
|
1480
|
+
}
|
|
1481
|
+
if (!local && this.config.skillsDir) {
|
|
1482
|
+
return expandHome2(this.config.skillsDir);
|
|
1483
|
+
}
|
|
1484
|
+
if (local && this.config.skillsDir) {
|
|
1485
|
+
return expandHome2(this.config.skillsDir);
|
|
1486
|
+
}
|
|
1487
|
+
if (!local && this.config.localSkillsDir) {
|
|
1488
|
+
return join4(process.cwd(), this.config.localSkillsDir);
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
/** Print manual MCP setup instructions for platforms with non-JSON configs */
|
|
1493
|
+
printManualInstructions(serverConfig) {
|
|
1494
|
+
const snippet = {
|
|
1495
|
+
mcpServers: {
|
|
1496
|
+
caik: {
|
|
1497
|
+
command: serverConfig.command,
|
|
1498
|
+
args: serverConfig.args,
|
|
1499
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
const json = JSON.stringify(snippet, null, 2);
|
|
1504
|
+
const configFile = this.config?.mcpConfigPath ? `Config file: ${this.config.mcpConfigPath}` : `Refer to your platform's documentation for the correct config file location.`;
|
|
1505
|
+
process.stdout.write(
|
|
1506
|
+
`
|
|
1507
|
+
CAIK MCP server configuration for ${this.config?.label ?? this.name}:
|
|
1508
|
+
|
|
1509
|
+
Add the following to your MCP configuration:
|
|
1510
|
+
|
|
1511
|
+
${json}
|
|
1512
|
+
|
|
1513
|
+
${configFile}
|
|
1514
|
+
|
|
1515
|
+
`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// src/platform/index.ts
|
|
1521
|
+
var customAdapters = {
|
|
1522
|
+
"claude-code": () => new ClaudeCodeAdapter(),
|
|
1523
|
+
"openclaw": () => new OpenClawAdapter(),
|
|
1524
|
+
"cursor": () => new CursorAdapter(),
|
|
1525
|
+
"codex": () => new CodexAdapter()
|
|
1526
|
+
};
|
|
1527
|
+
function getPlatformAdapter(name) {
|
|
1528
|
+
const custom = customAdapters[name];
|
|
1529
|
+
if (custom) return custom();
|
|
1530
|
+
const config = platformConfigs[name];
|
|
1531
|
+
return new GenericAdapter(name, config);
|
|
1532
|
+
}
|
|
1533
|
+
function getDefaultMcpConfig() {
|
|
1534
|
+
return {
|
|
1535
|
+
command: "npx",
|
|
1536
|
+
args: ["-y", "@caik.dev/mcp"]
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
export {
|
|
1541
|
+
detectPlatforms,
|
|
1542
|
+
detectPlatform,
|
|
1543
|
+
getPlatformAdapter,
|
|
1544
|
+
getDefaultMcpConfig
|
|
1545
|
+
};
|