@caik.dev/cli 0.1.1 → 0.1.3
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/dist/index.js +2622 -209
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { dirname as
|
|
8
|
-
import
|
|
7
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
8
|
+
import chalk3 from "chalk";
|
|
9
9
|
|
|
10
10
|
// src/errors.ts
|
|
11
11
|
var CaikError = class extends Error {
|
|
@@ -165,6 +165,11 @@ function writeConfig(config) {
|
|
|
165
165
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
166
166
|
chmodSync(path, 384);
|
|
167
167
|
}
|
|
168
|
+
function setApiKey(key) {
|
|
169
|
+
const config = readConfig();
|
|
170
|
+
config.apiKey = key;
|
|
171
|
+
writeConfig(config);
|
|
172
|
+
}
|
|
168
173
|
function resolveConfig(opts) {
|
|
169
174
|
const config = readConfig();
|
|
170
175
|
return {
|
|
@@ -186,6 +191,9 @@ function error(msg) {
|
|
|
186
191
|
function info(msg) {
|
|
187
192
|
return chalk.cyan(`\u2192 ${msg}`);
|
|
188
193
|
}
|
|
194
|
+
function warn(msg) {
|
|
195
|
+
return chalk.yellow(`\u26A0 ${msg}`);
|
|
196
|
+
}
|
|
189
197
|
function dim(msg) {
|
|
190
198
|
return chalk.dim(msg);
|
|
191
199
|
}
|
|
@@ -291,226 +299,1534 @@ ${result.total} result${result.total !== 1 ? "s" : ""} found.`);
|
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
// src/commands/install.ts
|
|
302
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
303
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
|
|
304
|
+
import { dirname as dirname3, resolve } from "path";
|
|
305
|
+
import { execSync as execSync3 } from "child_process";
|
|
306
|
+
import { createInterface } from "readline/promises";
|
|
307
|
+
|
|
308
|
+
// src/platform/detect.ts
|
|
294
309
|
import { existsSync as existsSync2 } from "fs";
|
|
295
|
-
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
296
|
-
import { dirname, resolve } from "path";
|
|
297
310
|
import { execSync } from "child_process";
|
|
298
|
-
import {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
import { join as join2 } from "path";
|
|
312
|
+
import { homedir as homedir2 } from "os";
|
|
313
|
+
function commandExists(cmd) {
|
|
314
|
+
try {
|
|
315
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
316
|
+
return true;
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
304
320
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
function detectClaudeCode() {
|
|
322
|
+
const home = homedir2();
|
|
323
|
+
const claudeDir = join2(home, ".claude");
|
|
324
|
+
if (!existsSync2(claudeDir)) return null;
|
|
325
|
+
const hasSettings = existsSync2(join2(claudeDir, "settings.json"));
|
|
326
|
+
const hasMcpConfig = existsSync2(join2(claudeDir, "mcp.json"));
|
|
327
|
+
const hasAnyMarker = hasSettings || hasMcpConfig || existsSync2(join2(claudeDir, "statsig"));
|
|
328
|
+
if (!hasAnyMarker) return null;
|
|
329
|
+
const configPaths = [];
|
|
330
|
+
if (hasSettings) configPaths.push(join2(claudeDir, "settings.json"));
|
|
331
|
+
if (hasMcpConfig) configPaths.push(join2(claudeDir, "mcp.json"));
|
|
332
|
+
return {
|
|
333
|
+
name: "claude-code",
|
|
334
|
+
tier: "full-plugin",
|
|
335
|
+
configPaths,
|
|
336
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function detectOpenClaw() {
|
|
340
|
+
const home = homedir2();
|
|
341
|
+
const openclawDir = join2(home, ".openclaw");
|
|
342
|
+
const cwdConfig = join2(process.cwd(), "openclaw.json");
|
|
343
|
+
const inPath = commandExists("openclaw");
|
|
344
|
+
if (!existsSync2(openclawDir) && !existsSync2(cwdConfig) && !inPath) return null;
|
|
345
|
+
const configPaths = [];
|
|
346
|
+
if (existsSync2(cwdConfig)) configPaths.push(cwdConfig);
|
|
347
|
+
if (existsSync2(join2(openclawDir, "openclaw.json"))) {
|
|
348
|
+
configPaths.push(join2(openclawDir, "openclaw.json"));
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
name: "openclaw",
|
|
352
|
+
tier: "hook-enabled",
|
|
353
|
+
configPaths,
|
|
354
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function detectCursor() {
|
|
358
|
+
const home = homedir2();
|
|
359
|
+
const cursorDir = join2(home, ".cursor");
|
|
360
|
+
const cwdCursor = join2(process.cwd(), ".cursor");
|
|
361
|
+
if (!existsSync2(cursorDir) && !existsSync2(cwdCursor)) return null;
|
|
362
|
+
const configPaths = [];
|
|
363
|
+
if (existsSync2(join2(cwdCursor, "mcp.json"))) {
|
|
364
|
+
configPaths.push(join2(cwdCursor, "mcp.json"));
|
|
365
|
+
}
|
|
366
|
+
if (existsSync2(join2(cursorDir, "mcp.json"))) {
|
|
367
|
+
configPaths.push(join2(cursorDir, "mcp.json"));
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
name: "cursor",
|
|
371
|
+
tier: "hook-enabled",
|
|
372
|
+
configPaths,
|
|
373
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function detectCodex() {
|
|
377
|
+
if (!commandExists("codex")) return null;
|
|
378
|
+
return {
|
|
379
|
+
name: "codex",
|
|
380
|
+
tier: "cli-mcp",
|
|
381
|
+
configPaths: [],
|
|
382
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function detectWindsurf() {
|
|
386
|
+
const home = homedir2();
|
|
387
|
+
const windsurfDir = join2(home, ".windsurf");
|
|
388
|
+
const cwdWindsurf = join2(process.cwd(), ".windsurf");
|
|
389
|
+
if (!existsSync2(windsurfDir) && !existsSync2(cwdWindsurf)) return null;
|
|
390
|
+
const configPaths = [];
|
|
391
|
+
if (existsSync2(join2(cwdWindsurf, "mcp.json"))) {
|
|
392
|
+
configPaths.push(join2(cwdWindsurf, "mcp.json"));
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
name: "windsurf",
|
|
396
|
+
tier: "cli-mcp",
|
|
397
|
+
configPaths,
|
|
398
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
var detectors = [
|
|
402
|
+
detectClaudeCode,
|
|
403
|
+
detectOpenClaw,
|
|
404
|
+
detectCursor,
|
|
405
|
+
detectCodex,
|
|
406
|
+
detectWindsurf
|
|
407
|
+
];
|
|
408
|
+
function detectPlatforms() {
|
|
409
|
+
const detected = [];
|
|
410
|
+
for (const detect of detectors) {
|
|
411
|
+
try {
|
|
412
|
+
const result = detect();
|
|
413
|
+
if (result) detected.push(result);
|
|
414
|
+
} catch {
|
|
322
415
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
416
|
+
}
|
|
417
|
+
return detected;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/platform/registry.ts
|
|
421
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
422
|
+
import { join as join3 } from "path";
|
|
423
|
+
var REGISTRY_VERSION = 1;
|
|
424
|
+
function getRegistryPath() {
|
|
425
|
+
return join3(getConfigDir(), "registry.json");
|
|
426
|
+
}
|
|
427
|
+
function emptyRegistry() {
|
|
428
|
+
return { version: REGISTRY_VERSION, entries: [] };
|
|
429
|
+
}
|
|
430
|
+
function readRegistry() {
|
|
431
|
+
const path = getRegistryPath();
|
|
432
|
+
if (!existsSync3(path)) return emptyRegistry();
|
|
433
|
+
try {
|
|
434
|
+
const raw = readFileSync2(path, "utf-8");
|
|
435
|
+
const parsed = JSON.parse(raw);
|
|
436
|
+
if (!parsed.entries || !Array.isArray(parsed.entries)) return emptyRegistry();
|
|
437
|
+
return parsed;
|
|
438
|
+
} catch {
|
|
439
|
+
return emptyRegistry();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function writeRegistry(registry) {
|
|
443
|
+
const dir = getConfigDir();
|
|
444
|
+
if (!existsSync3(dir)) {
|
|
445
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
446
|
+
}
|
|
447
|
+
const path = getRegistryPath();
|
|
448
|
+
writeFileSync2(path, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
449
|
+
}
|
|
450
|
+
function upsertRegistryEntry(entry) {
|
|
451
|
+
const registry = readRegistry();
|
|
452
|
+
const idx = registry.entries.findIndex(
|
|
453
|
+
(e) => e.slug === entry.slug && e.platform === entry.platform
|
|
454
|
+
);
|
|
455
|
+
if (idx >= 0) {
|
|
456
|
+
registry.entries[idx] = entry;
|
|
457
|
+
} else {
|
|
458
|
+
registry.entries.push(entry);
|
|
459
|
+
}
|
|
460
|
+
writeRegistry(registry);
|
|
461
|
+
}
|
|
462
|
+
function removeRegistryEntry(slug, platform2) {
|
|
463
|
+
const registry = readRegistry();
|
|
464
|
+
const idx = registry.entries.findIndex(
|
|
465
|
+
(e) => e.slug === slug && e.platform === platform2
|
|
466
|
+
);
|
|
467
|
+
if (idx < 0) return null;
|
|
468
|
+
const [removed] = registry.entries.splice(idx, 1);
|
|
469
|
+
writeRegistry(registry);
|
|
470
|
+
return removed;
|
|
471
|
+
}
|
|
472
|
+
function findRegistryEntry(slug, platform2) {
|
|
473
|
+
const registry = readRegistry();
|
|
474
|
+
return registry.entries.find(
|
|
475
|
+
(e) => e.slug === slug && (!platform2 || e.platform === platform2)
|
|
476
|
+
) ?? null;
|
|
477
|
+
}
|
|
478
|
+
function listRegistryEntries(platform2) {
|
|
479
|
+
const registry = readRegistry();
|
|
480
|
+
if (!platform2) return registry.entries;
|
|
481
|
+
return registry.entries.filter((e) => e.platform === platform2);
|
|
482
|
+
}
|
|
483
|
+
function cleanupFiles(entry) {
|
|
484
|
+
const failed = [];
|
|
485
|
+
for (const file of entry.files) {
|
|
486
|
+
try {
|
|
487
|
+
if (existsSync3(file)) {
|
|
488
|
+
unlinkSync(file);
|
|
337
489
|
}
|
|
490
|
+
} catch {
|
|
491
|
+
failed.push(file);
|
|
338
492
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
493
|
+
}
|
|
494
|
+
return failed;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/platform/claude-code.ts
|
|
498
|
+
import {
|
|
499
|
+
existsSync as existsSync4,
|
|
500
|
+
readFileSync as readFileSync3,
|
|
501
|
+
writeFileSync as writeFileSync3,
|
|
502
|
+
mkdirSync as mkdirSync3,
|
|
503
|
+
readdirSync,
|
|
504
|
+
rmSync
|
|
505
|
+
} from "fs";
|
|
506
|
+
import { join as join4 } from "path";
|
|
507
|
+
import { homedir as homedir3 } from "os";
|
|
508
|
+
var MCP_KEY = "caik";
|
|
509
|
+
var ClaudeCodeAdapter = class {
|
|
510
|
+
name = "claude-code";
|
|
511
|
+
tier = "full-plugin";
|
|
512
|
+
get claudeDir() {
|
|
513
|
+
return join4(homedir3(), ".claude");
|
|
514
|
+
}
|
|
515
|
+
get mcpPath() {
|
|
516
|
+
return join4(this.claudeDir, "mcp.json");
|
|
517
|
+
}
|
|
518
|
+
get settingsPath() {
|
|
519
|
+
return join4(this.claudeDir, "settings.json");
|
|
520
|
+
}
|
|
521
|
+
get skillsDir() {
|
|
522
|
+
return join4(this.claudeDir, "skills", "caik");
|
|
523
|
+
}
|
|
524
|
+
get pluginsCacheDir() {
|
|
525
|
+
return join4(this.claudeDir, "plugins", "cache");
|
|
526
|
+
}
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// JSON helpers
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
readJson(path) {
|
|
531
|
+
if (!existsSync4(path)) return {};
|
|
532
|
+
try {
|
|
533
|
+
const raw = readFileSync3(path, "utf-8");
|
|
534
|
+
const parsed = JSON.parse(raw);
|
|
535
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
536
|
+
return parsed;
|
|
364
537
|
}
|
|
365
|
-
|
|
538
|
+
return {};
|
|
539
|
+
} catch {
|
|
540
|
+
return {};
|
|
366
541
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
writeFileSync2(file.resolvedPath, file.content, "utf-8");
|
|
542
|
+
}
|
|
543
|
+
writeJson(path, data) {
|
|
544
|
+
const dir = join4(path, "..");
|
|
545
|
+
if (!existsSync4(dir)) {
|
|
546
|
+
mkdirSync3(dir, { recursive: true });
|
|
373
547
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
548
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
549
|
+
}
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
// Plugin detection
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
/**
|
|
554
|
+
* Check if CAIK is installed as a Claude Code plugin.
|
|
555
|
+
* Looks for "caik" in enabledPlugins in settings.json, or for a caik
|
|
556
|
+
* directory in the plugins cache.
|
|
557
|
+
*/
|
|
558
|
+
isPluginInstalled() {
|
|
559
|
+
const settings = this.readJson(this.settingsPath);
|
|
560
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
561
|
+
if (enabledPlugins) {
|
|
562
|
+
for (const key of Object.keys(enabledPlugins)) {
|
|
563
|
+
if (key.toLowerCase().includes("caik") && enabledPlugins[key] !== false) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
388
566
|
}
|
|
389
567
|
}
|
|
390
|
-
if (
|
|
568
|
+
if (existsSync4(this.pluginsCacheDir)) {
|
|
391
569
|
try {
|
|
392
|
-
|
|
570
|
+
const publishers = readdirSync(this.pluginsCacheDir);
|
|
571
|
+
for (const publisher of publishers) {
|
|
572
|
+
const publisherDir = join4(this.pluginsCacheDir, publisher);
|
|
573
|
+
const caikDir = join4(publisherDir, "caik");
|
|
574
|
+
if (existsSync4(caikDir)) {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
393
578
|
} catch {
|
|
394
579
|
}
|
|
395
580
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Returns the current install method:
|
|
585
|
+
* - "plugin" if CAIK plugin is installed via marketplace
|
|
586
|
+
* - "manual" if CAIK is registered in mcp.json manually
|
|
587
|
+
* - "none" if not installed
|
|
588
|
+
*/
|
|
589
|
+
getInstallMethod() {
|
|
590
|
+
if (this.isPluginInstalled()) return "plugin";
|
|
591
|
+
if (existsSync4(this.mcpPath)) {
|
|
592
|
+
const config = this.readJson(this.mcpPath);
|
|
593
|
+
const servers = config.mcpServers;
|
|
594
|
+
if (servers && MCP_KEY in servers) return "manual";
|
|
400
595
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
596
|
+
return "none";
|
|
597
|
+
}
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// PlatformAdapter implementation
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
async detect() {
|
|
602
|
+
if (!existsSync4(this.claudeDir)) return null;
|
|
603
|
+
const hasSettings = existsSync4(this.settingsPath);
|
|
604
|
+
const hasMcp = existsSync4(this.mcpPath);
|
|
605
|
+
const hasStatsig = existsSync4(join4(this.claudeDir, "statsig"));
|
|
606
|
+
if (!hasSettings && !hasMcp && !hasStatsig) return null;
|
|
607
|
+
const configPaths = [];
|
|
608
|
+
if (hasSettings) configPaths.push(this.settingsPath);
|
|
609
|
+
if (hasMcp) configPaths.push(this.mcpPath);
|
|
610
|
+
return {
|
|
611
|
+
name: this.name,
|
|
612
|
+
tier: this.tier,
|
|
613
|
+
configPaths,
|
|
614
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async registerMcp(serverConfig) {
|
|
618
|
+
if (this.isPluginInstalled()) {
|
|
619
|
+
console.log(
|
|
620
|
+
"CAIK plugin is installed \u2014 MCP server is managed by the plugin."
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const config = this.readJson(this.mcpPath);
|
|
625
|
+
const servers = config.mcpServers ?? {};
|
|
626
|
+
servers[MCP_KEY] = {
|
|
627
|
+
command: serverConfig.command,
|
|
628
|
+
args: serverConfig.args,
|
|
629
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
630
|
+
};
|
|
631
|
+
config.mcpServers = servers;
|
|
632
|
+
this.writeJson(this.mcpPath, config);
|
|
633
|
+
}
|
|
634
|
+
async unregisterMcp() {
|
|
635
|
+
if (!existsSync4(this.mcpPath)) return;
|
|
636
|
+
const config = this.readJson(this.mcpPath);
|
|
637
|
+
const servers = config.mcpServers;
|
|
638
|
+
if (!servers || !(MCP_KEY in servers)) return;
|
|
639
|
+
delete servers[MCP_KEY];
|
|
640
|
+
config.mcpServers = servers;
|
|
641
|
+
this.writeJson(this.mcpPath, config);
|
|
642
|
+
}
|
|
643
|
+
async registerHooks(hookConfig) {
|
|
644
|
+
if (this.isPluginInstalled()) {
|
|
645
|
+
console.log(
|
|
646
|
+
"CAIK plugin is installed \u2014 hooks are managed by the plugin."
|
|
647
|
+
);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const settings = this.readJson(this.settingsPath);
|
|
651
|
+
const existingHooks = settings.hooks ?? {};
|
|
652
|
+
for (const [event, entries] of Object.entries(hookConfig.hooks)) {
|
|
653
|
+
const newEntries = Array.isArray(entries) ? entries : [entries];
|
|
654
|
+
const existing = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
|
|
655
|
+
const filtered = existing.filter((group) => {
|
|
656
|
+
if (typeof group === "object" && group !== null) {
|
|
657
|
+
const g = group;
|
|
658
|
+
if (Array.isArray(g.hooks)) {
|
|
659
|
+
const hasCAIK = g.hooks.some(
|
|
660
|
+
(h) => typeof h.command === "string" && h.command.includes("caik")
|
|
661
|
+
);
|
|
662
|
+
if (hasCAIK) return false;
|
|
663
|
+
}
|
|
664
|
+
if ("command" in g) {
|
|
665
|
+
return !g.command.includes("caik");
|
|
666
|
+
}
|
|
439
667
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
668
|
+
return true;
|
|
669
|
+
});
|
|
670
|
+
const wrappedEntries = newEntries.map((entry) => {
|
|
671
|
+
if (typeof entry === "object" && entry !== null) {
|
|
672
|
+
const e = entry;
|
|
673
|
+
const hookEntry = {
|
|
674
|
+
type: "command",
|
|
675
|
+
timeout: 10,
|
|
676
|
+
...e
|
|
677
|
+
};
|
|
678
|
+
return { hooks: [hookEntry] };
|
|
679
|
+
}
|
|
680
|
+
return entry;
|
|
681
|
+
});
|
|
682
|
+
existingHooks[event] = [...filtered, ...wrappedEntries];
|
|
443
683
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
684
|
+
settings.hooks = existingHooks;
|
|
685
|
+
this.writeJson(this.settingsPath, settings);
|
|
686
|
+
}
|
|
687
|
+
async unregisterHooks() {
|
|
688
|
+
if (!existsSync4(this.settingsPath)) return;
|
|
689
|
+
const settings = this.readJson(this.settingsPath);
|
|
690
|
+
const hooks = settings.hooks;
|
|
691
|
+
if (!hooks) return;
|
|
692
|
+
for (const event of Object.keys(hooks)) {
|
|
693
|
+
const entries = hooks[event];
|
|
694
|
+
if (!Array.isArray(entries)) continue;
|
|
695
|
+
hooks[event] = entries.filter((group) => {
|
|
696
|
+
if (typeof group === "object" && group !== null) {
|
|
697
|
+
const g = group;
|
|
698
|
+
if (Array.isArray(g.hooks)) {
|
|
699
|
+
const hasCAIK = g.hooks.some(
|
|
700
|
+
(h) => typeof h.command === "string" && h.command.includes("caik")
|
|
701
|
+
);
|
|
702
|
+
if (hasCAIK) return false;
|
|
703
|
+
}
|
|
704
|
+
if ("command" in g) {
|
|
705
|
+
return !g.command.includes("caik");
|
|
706
|
+
}
|
|
462
707
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
708
|
+
return true;
|
|
709
|
+
});
|
|
710
|
+
if (hooks[event].length === 0) {
|
|
711
|
+
delete hooks[event];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (Object.keys(hooks).length === 0) {
|
|
715
|
+
delete settings.hooks;
|
|
716
|
+
} else {
|
|
717
|
+
settings.hooks = hooks;
|
|
718
|
+
}
|
|
719
|
+
this.writeJson(this.settingsPath, settings);
|
|
720
|
+
}
|
|
721
|
+
async installSkill(slug, content, files) {
|
|
722
|
+
const skillDir = join4(this.skillsDir, slug);
|
|
723
|
+
if (!existsSync4(skillDir)) {
|
|
724
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
725
|
+
}
|
|
726
|
+
writeFileSync3(join4(skillDir, "SKILL.md"), content, "utf-8");
|
|
727
|
+
if (files) {
|
|
728
|
+
for (const file of files) {
|
|
729
|
+
const filePath = join4(skillDir, file.path);
|
|
730
|
+
const fileDir = join4(filePath, "..");
|
|
731
|
+
if (!existsSync4(fileDir)) {
|
|
732
|
+
mkdirSync3(fileDir, { recursive: true });
|
|
470
733
|
}
|
|
471
|
-
|
|
472
|
-
} catch {
|
|
473
|
-
data.apiReachable = false;
|
|
474
|
-
data.authenticated = false;
|
|
734
|
+
writeFileSync3(filePath, file.content, "utf-8");
|
|
475
735
|
}
|
|
476
|
-
outputResult(data, { json: true });
|
|
477
|
-
return;
|
|
478
736
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
737
|
+
}
|
|
738
|
+
async uninstallSkill(slug) {
|
|
739
|
+
const skillDir = join4(this.skillsDir, slug);
|
|
740
|
+
if (!existsSync4(skillDir)) return;
|
|
484
741
|
try {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
742
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
getConfigPath() {
|
|
747
|
+
return this.mcpPath;
|
|
748
|
+
}
|
|
749
|
+
async readConfig() {
|
|
750
|
+
return this.readJson(this.mcpPath);
|
|
751
|
+
}
|
|
752
|
+
async isRegistered() {
|
|
753
|
+
if (this.isPluginInstalled()) return true;
|
|
754
|
+
if (!existsSync4(this.mcpPath)) return false;
|
|
755
|
+
const config = this.readJson(this.mcpPath);
|
|
756
|
+
const servers = config.mcpServers;
|
|
757
|
+
return !!servers && MCP_KEY in servers;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/platform/openclaw.ts
|
|
762
|
+
import {
|
|
763
|
+
existsSync as existsSync5,
|
|
764
|
+
readFileSync as readFileSync4,
|
|
765
|
+
writeFileSync as writeFileSync4,
|
|
766
|
+
mkdirSync as mkdirSync4,
|
|
767
|
+
rmSync as rmSync2
|
|
768
|
+
} from "fs";
|
|
769
|
+
import { execSync as execSync2 } from "child_process";
|
|
770
|
+
import { join as join5, dirname } from "path";
|
|
771
|
+
import { homedir as homedir4 } from "os";
|
|
772
|
+
var MCP_KEY2 = "caik";
|
|
773
|
+
var OpenClawAdapter = class {
|
|
774
|
+
name = "openclaw";
|
|
775
|
+
tier = "hook-enabled";
|
|
776
|
+
// --- Path helpers --------------------------------------------------------
|
|
777
|
+
get openclawDir() {
|
|
778
|
+
return join5(homedir4(), ".openclaw");
|
|
779
|
+
}
|
|
780
|
+
get managedHooksDir() {
|
|
781
|
+
return join5(this.openclawDir, "hooks");
|
|
782
|
+
}
|
|
783
|
+
get caikHookDir() {
|
|
784
|
+
return join5(this.managedHooksDir, "caik-contributions");
|
|
785
|
+
}
|
|
786
|
+
/** OpenClaw's own config — NOT where MCP servers go. */
|
|
787
|
+
get openclawConfigPath() {
|
|
788
|
+
return join5(this.openclawDir, "openclaw.json");
|
|
789
|
+
}
|
|
790
|
+
/** Project-level `.mcp.json` — where MCP servers are registered. */
|
|
791
|
+
get mcpConfigPath() {
|
|
792
|
+
return join5(process.cwd(), ".mcp.json");
|
|
793
|
+
}
|
|
794
|
+
/** Skills directory following OpenClaw convention. */
|
|
795
|
+
get skillsBaseDir() {
|
|
796
|
+
return join5(homedir4(), ".agents", "skills");
|
|
797
|
+
}
|
|
798
|
+
get skillDir() {
|
|
799
|
+
return join5(this.skillsBaseDir, "caik");
|
|
800
|
+
}
|
|
801
|
+
get skillLockPath() {
|
|
802
|
+
return join5(homedir4(), ".agents", ".skill-lock.json");
|
|
803
|
+
}
|
|
804
|
+
// --- JSON helpers --------------------------------------------------------
|
|
805
|
+
readJson(path) {
|
|
806
|
+
if (!existsSync5(path)) return {};
|
|
807
|
+
try {
|
|
808
|
+
const raw = readFileSync4(path, "utf-8");
|
|
809
|
+
const parsed = JSON.parse(raw);
|
|
810
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
811
|
+
return parsed;
|
|
491
812
|
}
|
|
813
|
+
return {};
|
|
492
814
|
} catch {
|
|
493
|
-
|
|
815
|
+
return {};
|
|
494
816
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
{
|
|
500
|
-
{ name: "Cursor", check: existsSync3(".cursor") },
|
|
501
|
-
{ name: "Node.js", check: existsSync3("node_modules") || existsSync3("package.json") }
|
|
502
|
-
];
|
|
503
|
-
for (const p of platforms) {
|
|
504
|
-
console.log(` ${p.name}: ${p.check ? success("detected") : dim("not found")}`);
|
|
817
|
+
}
|
|
818
|
+
writeJson(path, data) {
|
|
819
|
+
const dir = dirname(path);
|
|
820
|
+
if (!existsSync5(dir)) {
|
|
821
|
+
mkdirSync4(dir, { recursive: true });
|
|
505
822
|
}
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
823
|
+
writeFileSync4(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
824
|
+
}
|
|
825
|
+
readSkillLock() {
|
|
826
|
+
if (!existsSync5(this.skillLockPath)) {
|
|
827
|
+
return { version: 3, skills: {} };
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const raw = readFileSync4(this.skillLockPath, "utf-8");
|
|
831
|
+
const parsed = JSON.parse(raw);
|
|
832
|
+
return {
|
|
833
|
+
version: parsed.version ?? 3,
|
|
834
|
+
skills: parsed.skills ?? {}
|
|
835
|
+
};
|
|
836
|
+
} catch {
|
|
837
|
+
return { version: 3, skills: {} };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
writeSkillLock(lock) {
|
|
841
|
+
const dir = dirname(this.skillLockPath);
|
|
842
|
+
if (!existsSync5(dir)) {
|
|
843
|
+
mkdirSync4(dir, { recursive: true });
|
|
844
|
+
}
|
|
845
|
+
writeFileSync4(
|
|
846
|
+
this.skillLockPath,
|
|
847
|
+
JSON.stringify(lock, null, 2) + "\n",
|
|
848
|
+
"utf-8"
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
/** Check if `openclaw` binary is in PATH. */
|
|
852
|
+
isInPath() {
|
|
853
|
+
try {
|
|
854
|
+
execSync2("which openclaw", { stdio: "ignore" });
|
|
855
|
+
return true;
|
|
856
|
+
} catch {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// --- PlatformAdapter implementation --------------------------------------
|
|
861
|
+
async detect() {
|
|
862
|
+
const hasDir = existsSync5(this.openclawDir);
|
|
863
|
+
const inPath = this.isInPath();
|
|
864
|
+
const hasCwdConfig = existsSync5(join5(process.cwd(), "openclaw.json"));
|
|
865
|
+
if (!hasDir && !inPath && !hasCwdConfig) return null;
|
|
866
|
+
const configPaths = [];
|
|
867
|
+
if (hasCwdConfig) {
|
|
868
|
+
configPaths.push(join5(process.cwd(), "openclaw.json"));
|
|
869
|
+
}
|
|
870
|
+
if (existsSync5(this.openclawConfigPath)) {
|
|
871
|
+
configPaths.push(this.openclawConfigPath);
|
|
872
|
+
}
|
|
873
|
+
if (existsSync5(this.mcpConfigPath)) {
|
|
874
|
+
configPaths.push(this.mcpConfigPath);
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
name: this.name,
|
|
878
|
+
tier: this.tier,
|
|
879
|
+
configPaths,
|
|
880
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
async registerMcp(serverConfig) {
|
|
884
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
885
|
+
const servers = config.mcpServers ?? {};
|
|
886
|
+
servers[MCP_KEY2] = {
|
|
887
|
+
type: "stdio",
|
|
888
|
+
command: serverConfig.command,
|
|
889
|
+
args: serverConfig.args,
|
|
890
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
891
|
+
};
|
|
892
|
+
config.mcpServers = servers;
|
|
893
|
+
this.writeJson(this.mcpConfigPath, config);
|
|
894
|
+
}
|
|
895
|
+
async unregisterMcp() {
|
|
896
|
+
if (!existsSync5(this.mcpConfigPath)) return;
|
|
897
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
898
|
+
const servers = config.mcpServers;
|
|
899
|
+
if (!servers || !(MCP_KEY2 in servers)) return;
|
|
900
|
+
delete servers[MCP_KEY2];
|
|
901
|
+
config.mcpServers = servers;
|
|
902
|
+
this.writeJson(this.mcpConfigPath, config);
|
|
903
|
+
}
|
|
904
|
+
async registerHooks(_hookConfig) {
|
|
905
|
+
if (!existsSync5(this.caikHookDir)) {
|
|
906
|
+
mkdirSync4(this.caikHookDir, { recursive: true });
|
|
907
|
+
}
|
|
908
|
+
writeFileSync4(
|
|
909
|
+
join5(this.caikHookDir, "HOOK.md"),
|
|
910
|
+
HOOK_MD_CONTENT,
|
|
911
|
+
"utf-8"
|
|
912
|
+
);
|
|
913
|
+
writeFileSync4(
|
|
914
|
+
join5(this.caikHookDir, "handler.js"),
|
|
915
|
+
HANDLER_JS_CONTENT,
|
|
916
|
+
"utf-8"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
async unregisterHooks() {
|
|
920
|
+
if (existsSync5(this.caikHookDir)) {
|
|
921
|
+
try {
|
|
922
|
+
rmSync2(this.caikHookDir, { recursive: true, force: true });
|
|
923
|
+
} catch {
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async installSkill(_slug, content, files) {
|
|
928
|
+
if (!existsSync5(this.skillDir)) {
|
|
929
|
+
mkdirSync4(this.skillDir, { recursive: true });
|
|
930
|
+
}
|
|
931
|
+
writeFileSync4(join5(this.skillDir, "SKILL.md"), content, "utf-8");
|
|
932
|
+
if (files) {
|
|
933
|
+
for (const file of files) {
|
|
934
|
+
const filePath = join5(this.skillDir, file.path);
|
|
935
|
+
const fileDir = dirname(filePath);
|
|
936
|
+
if (!existsSync5(fileDir)) {
|
|
937
|
+
mkdirSync4(fileDir, { recursive: true });
|
|
938
|
+
}
|
|
939
|
+
writeFileSync4(filePath, file.content, "utf-8");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const lock = this.readSkillLock();
|
|
943
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
944
|
+
const existing = lock.skills["caik"];
|
|
945
|
+
lock.skills["caik"] = {
|
|
946
|
+
source: "@caik.dev/cli",
|
|
947
|
+
sourceType: "local",
|
|
948
|
+
sourceUrl: "",
|
|
949
|
+
skillPath: "skills/caik/SKILL.md",
|
|
950
|
+
skillFolderHash: "",
|
|
951
|
+
installedAt: existing?.installedAt ?? now,
|
|
952
|
+
updatedAt: now
|
|
953
|
+
};
|
|
954
|
+
this.writeSkillLock(lock);
|
|
955
|
+
}
|
|
956
|
+
async uninstallSkill(_slug) {
|
|
957
|
+
if (existsSync5(this.skillDir)) {
|
|
958
|
+
try {
|
|
959
|
+
rmSync2(this.skillDir, { recursive: true, force: true });
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const lock = this.readSkillLock();
|
|
964
|
+
if ("caik" in lock.skills) {
|
|
965
|
+
delete lock.skills["caik"];
|
|
966
|
+
this.writeSkillLock(lock);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
getConfigPath() {
|
|
970
|
+
return this.openclawConfigPath;
|
|
971
|
+
}
|
|
972
|
+
async readConfig() {
|
|
973
|
+
return this.readJson(this.openclawConfigPath);
|
|
974
|
+
}
|
|
975
|
+
async isRegistered() {
|
|
976
|
+
if (!existsSync5(this.mcpConfigPath)) return false;
|
|
977
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
978
|
+
const servers = config.mcpServers;
|
|
979
|
+
return !!servers && MCP_KEY2 in servers;
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
var HOOK_MD_CONTENT = `---
|
|
983
|
+
name: caik-contributions
|
|
984
|
+
description: "Track artifact usage to build your CAIK contribution level and community karma"
|
|
985
|
+
metadata:
|
|
986
|
+
{
|
|
987
|
+
"openclaw":
|
|
988
|
+
{
|
|
989
|
+
"emoji": "\u{1F4E6}",
|
|
990
|
+
"events": ["command:new", "command:reset", "command:stop", "command"],
|
|
991
|
+
"install": [{ "id": "local", "kind": "local", "label": "CAIK CLI hook pack" }],
|
|
992
|
+
},
|
|
993
|
+
}
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
# CAIK Contribution Tracking
|
|
997
|
+
|
|
998
|
+
Reports session lifecycle events to the CAIK API to build your contribution level and community karma.
|
|
999
|
+
|
|
1000
|
+
## What It Does
|
|
1001
|
+
|
|
1002
|
+
- **Session start** (\`command:new\`): Records that a new agent session began
|
|
1003
|
+
- **Session end** (\`command:stop\`, \`command:reset\`): Flushes buffered tool-use events and records session end
|
|
1004
|
+
- **Tool use** (\`command\`): Buffers tool execution events for batch reporting
|
|
1005
|
+
|
|
1006
|
+
## Privacy
|
|
1007
|
+
|
|
1008
|
+
- Only sends: event type, platform name, tool name, timestamp
|
|
1009
|
+
- No code, file contents, or conversation data is transmitted
|
|
1010
|
+
- Contribution tracking can be disabled: \`caik config set contributions false\`
|
|
1011
|
+
|
|
1012
|
+
## Configuration
|
|
1013
|
+
|
|
1014
|
+
Set \`CAIK_API_URL\` and \`CAIK_API_KEY\` environment variables, or configure via \`caik init --auth\`.
|
|
1015
|
+
`;
|
|
1016
|
+
var HANDLER_JS_CONTENT = `/**
|
|
1017
|
+
* CAIK contribution tracking hook for OpenClaw.
|
|
1018
|
+
* Fire-and-forget \u2014 never blocks the agent, never throws.
|
|
1019
|
+
*/
|
|
1020
|
+
import fs from "node:fs/promises";
|
|
1021
|
+
import os from "node:os";
|
|
1022
|
+
import path from "node:path";
|
|
1023
|
+
|
|
1024
|
+
const CAIK_DIR = path.join(os.homedir(), ".caik");
|
|
1025
|
+
const PENDING_PATH = path.join(CAIK_DIR, "pending-events.json");
|
|
1026
|
+
const CONFIG_PATH = path.join(CAIK_DIR, "config.json");
|
|
1027
|
+
const TIMEOUT_MS = 2000;
|
|
1028
|
+
|
|
1029
|
+
async function loadConfig() {
|
|
1030
|
+
try {
|
|
1031
|
+
const raw = await fs.readFile(CONFIG_PATH, "utf-8");
|
|
1032
|
+
return JSON.parse(raw);
|
|
1033
|
+
} catch { return {}; }
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function getApiUrl() {
|
|
1037
|
+
const cfg = await loadConfig();
|
|
1038
|
+
return process.env.CAIK_API_URL || cfg.apiUrl || "https://caik.dev";
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
async function getApiKey() {
|
|
1042
|
+
const cfg = await loadConfig();
|
|
1043
|
+
return process.env.CAIK_API_KEY || cfg.apiKey || null;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function readPending() {
|
|
1047
|
+
try {
|
|
1048
|
+
const raw = await fs.readFile(PENDING_PATH, "utf-8");
|
|
1049
|
+
const parsed = JSON.parse(raw);
|
|
1050
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1051
|
+
} catch { return []; }
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function writePending(events) {
|
|
1055
|
+
await fs.mkdir(CAIK_DIR, { recursive: true });
|
|
1056
|
+
await fs.writeFile(PENDING_PATH, JSON.stringify(events) + "\\n", "utf-8");
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async function appendEvent(event) {
|
|
1060
|
+
const events = await readPending();
|
|
1061
|
+
events.push(event);
|
|
1062
|
+
await writePending(events);
|
|
1063
|
+
return events;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function clearPending() { await writePending([]); }
|
|
1067
|
+
|
|
1068
|
+
async function postEvents(events) {
|
|
1069
|
+
if (events.length === 0) return;
|
|
1070
|
+
const apiUrl = await getApiUrl();
|
|
1071
|
+
const apiKey = await getApiKey();
|
|
1072
|
+
const headers = { "Content-Type": "application/json" };
|
|
1073
|
+
if (apiKey) headers["Authorization"] = "Bearer " + apiKey;
|
|
1074
|
+
const controller = new AbortController();
|
|
1075
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1076
|
+
try {
|
|
1077
|
+
await fetch(apiUrl + "/api/v1/events", {
|
|
1078
|
+
method: "POST", headers,
|
|
1079
|
+
body: JSON.stringify({ events }),
|
|
1080
|
+
signal: controller.signal,
|
|
1081
|
+
});
|
|
1082
|
+
await clearPending();
|
|
1083
|
+
} catch { /* fire-and-forget */ }
|
|
1084
|
+
finally { clearTimeout(timeout); }
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const handler = async (event) => {
|
|
1088
|
+
try {
|
|
1089
|
+
const cfg = await loadConfig();
|
|
1090
|
+
if (cfg.contributions === false || cfg.telemetry === false) return;
|
|
1091
|
+
const timestamp = (event.timestamp ?? new Date()).toISOString();
|
|
1092
|
+
if (event.type === "command") {
|
|
1093
|
+
const action = event.action;
|
|
1094
|
+
if (action === "new") {
|
|
1095
|
+
await postEvents([{ type: "session_start", platform: "openclaw", timestamp }]);
|
|
1096
|
+
} else if (action === "stop" || action === "reset") {
|
|
1097
|
+
const pending = await readPending();
|
|
1098
|
+
pending.push({ type: "session_end", platform: "openclaw", timestamp });
|
|
1099
|
+
await postEvents(pending);
|
|
1100
|
+
} else {
|
|
1101
|
+
await appendEvent({ type: "tool_use", platform: "openclaw", tool: action, timestamp });
|
|
1102
|
+
const pending = await readPending();
|
|
1103
|
+
if (pending.length >= 50) await postEvents(pending);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
} catch { /* never fail */ }
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
export default handler;
|
|
1110
|
+
`;
|
|
1111
|
+
|
|
1112
|
+
// src/platform/cursor.ts
|
|
1113
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, rmSync as rmSync3 } from "fs";
|
|
1114
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
1115
|
+
import { homedir as homedir5 } from "os";
|
|
1116
|
+
var MCP_KEY3 = "caik";
|
|
1117
|
+
function readJsonFile(path) {
|
|
1118
|
+
try {
|
|
1119
|
+
if (!existsSync6(path)) return null;
|
|
1120
|
+
const raw = readFileSync5(path, "utf-8");
|
|
1121
|
+
return JSON.parse(raw);
|
|
1122
|
+
} catch {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function writeJsonFile(path, data) {
|
|
1127
|
+
mkdirSync5(dirname2(path), { recursive: true });
|
|
1128
|
+
writeFileSync5(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1129
|
+
}
|
|
1130
|
+
var CursorAdapter = class {
|
|
1131
|
+
name = "cursor";
|
|
1132
|
+
tier = "hook-enabled";
|
|
1133
|
+
/** Project-level .cursor dir (preferred for MCP config). */
|
|
1134
|
+
get projectDir() {
|
|
1135
|
+
return join6(process.cwd(), ".cursor");
|
|
1136
|
+
}
|
|
1137
|
+
/** Global ~/.cursor dir. */
|
|
1138
|
+
get globalDir() {
|
|
1139
|
+
return join6(homedir5(), ".cursor");
|
|
1140
|
+
}
|
|
1141
|
+
/** Preferred MCP config path (project-level). */
|
|
1142
|
+
get mcpConfigPath() {
|
|
1143
|
+
return join6(this.projectDir, "mcp.json");
|
|
1144
|
+
}
|
|
1145
|
+
/** Global MCP config path (fallback). */
|
|
1146
|
+
get globalMcpConfigPath() {
|
|
1147
|
+
return join6(this.globalDir, "mcp.json");
|
|
1148
|
+
}
|
|
1149
|
+
/** Hooks config path (project-level). */
|
|
1150
|
+
get hooksConfigPath() {
|
|
1151
|
+
return join6(this.projectDir, "hooks.json");
|
|
1152
|
+
}
|
|
1153
|
+
/** Skills directory. */
|
|
1154
|
+
get skillsDir() {
|
|
1155
|
+
return join6(this.globalDir, "skills", "caik");
|
|
1156
|
+
}
|
|
1157
|
+
// ── Detection ─────────────────────────────────────────────
|
|
1158
|
+
async detect() {
|
|
1159
|
+
const hasGlobal = existsSync6(this.globalDir);
|
|
1160
|
+
const hasProject = existsSync6(this.projectDir);
|
|
1161
|
+
if (!hasGlobal && !hasProject) return null;
|
|
1162
|
+
const configPaths = [];
|
|
1163
|
+
if (existsSync6(this.mcpConfigPath)) configPaths.push(this.mcpConfigPath);
|
|
1164
|
+
if (existsSync6(this.globalMcpConfigPath)) configPaths.push(this.globalMcpConfigPath);
|
|
1165
|
+
return {
|
|
1166
|
+
name: "cursor",
|
|
1167
|
+
tier: "hook-enabled",
|
|
1168
|
+
configPaths,
|
|
1169
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
// ── MCP Registration ──────────────────────────────────────
|
|
1173
|
+
async registerMcp(serverConfig) {
|
|
1174
|
+
const configPath = existsSync6(this.projectDir) ? this.mcpConfigPath : this.globalMcpConfigPath;
|
|
1175
|
+
const config = readJsonFile(configPath) ?? {};
|
|
1176
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
1177
|
+
config.mcpServers[MCP_KEY3] = serverConfig;
|
|
1178
|
+
writeJsonFile(configPath, config);
|
|
1179
|
+
}
|
|
1180
|
+
async unregisterMcp() {
|
|
1181
|
+
for (const configPath of [this.mcpConfigPath, this.globalMcpConfigPath]) {
|
|
1182
|
+
const config = readJsonFile(configPath);
|
|
1183
|
+
if (!config?.mcpServers?.[MCP_KEY3]) continue;
|
|
1184
|
+
delete config.mcpServers[MCP_KEY3];
|
|
1185
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
1186
|
+
delete config.mcpServers;
|
|
1187
|
+
}
|
|
1188
|
+
writeJsonFile(configPath, config);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// ── Hooks ─────────────────────────────────────────────────
|
|
1192
|
+
async registerHooks(_hookConfig) {
|
|
1193
|
+
const defaultHooks = [
|
|
1194
|
+
{ event: "sessionStart", script: "npx -y @caik.dev/cli hook cursor-session-start", decision: "allow" },
|
|
1195
|
+
{ event: "beforeMCPExecution", script: "npx -y @caik.dev/cli hook cursor-mcp-exec --server=$SERVER --tool=$TOOL", decision: "allow" },
|
|
1196
|
+
{ event: "stop", script: "npx -y @caik.dev/cli hook cursor-session-end", decision: "allow" }
|
|
1197
|
+
];
|
|
1198
|
+
try {
|
|
1199
|
+
const config = readJsonFile(this.hooksConfigPath) ?? {};
|
|
1200
|
+
const existing = config.hooks ?? [];
|
|
1201
|
+
const filtered = existing.filter(
|
|
1202
|
+
(h) => !h.script?.includes("caik") || !h.script?.includes("hook cursor-")
|
|
1203
|
+
);
|
|
1204
|
+
config.hooks = [...filtered, ...defaultHooks];
|
|
1205
|
+
writeJsonFile(this.hooksConfigPath, config);
|
|
1206
|
+
} catch {
|
|
1207
|
+
process.stderr.write(
|
|
1208
|
+
"[caik] Warning: Could not register hooks \u2014 Cursor hooks require v1.7+. Skipping.\n"
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async unregisterHooks() {
|
|
1213
|
+
try {
|
|
1214
|
+
const config = readJsonFile(this.hooksConfigPath);
|
|
1215
|
+
if (!config?.hooks) return;
|
|
1216
|
+
config.hooks = config.hooks.filter(
|
|
1217
|
+
(h) => !h.script?.includes("caik") || !h.script?.includes("hook cursor-")
|
|
1218
|
+
);
|
|
1219
|
+
if (config.hooks.length === 0) {
|
|
1220
|
+
delete config.hooks;
|
|
1221
|
+
}
|
|
1222
|
+
writeJsonFile(this.hooksConfigPath, config);
|
|
1223
|
+
} catch {
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// ── Skills ────────────────────────────────────────────────
|
|
1227
|
+
async installSkill(slug, content, files) {
|
|
1228
|
+
const skillDir = join6(this.skillsDir, slug);
|
|
1229
|
+
mkdirSync5(skillDir, { recursive: true });
|
|
1230
|
+
writeFileSync5(join6(skillDir, "SKILL.md"), content, "utf-8");
|
|
1231
|
+
if (files) {
|
|
1232
|
+
for (const file of files) {
|
|
1233
|
+
const filePath = join6(skillDir, file.path);
|
|
1234
|
+
mkdirSync5(dirname2(filePath), { recursive: true });
|
|
1235
|
+
writeFileSync5(filePath, file.content, "utf-8");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
async uninstallSkill(slug) {
|
|
1240
|
+
const skillDir = join6(this.skillsDir, slug);
|
|
1241
|
+
try {
|
|
1242
|
+
if (existsSync6(skillDir)) {
|
|
1243
|
+
rmSync3(skillDir, { recursive: true, force: true });
|
|
1244
|
+
}
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
// ── Config Accessors ──────────────────────────────────────
|
|
1249
|
+
getConfigPath() {
|
|
1250
|
+
if (existsSync6(this.mcpConfigPath)) return this.mcpConfigPath;
|
|
1251
|
+
if (existsSync6(this.globalMcpConfigPath)) return this.globalMcpConfigPath;
|
|
1252
|
+
return this.mcpConfigPath;
|
|
1253
|
+
}
|
|
1254
|
+
async readConfig() {
|
|
1255
|
+
const configPath = this.getConfigPath();
|
|
1256
|
+
return readJsonFile(configPath) ?? {};
|
|
1257
|
+
}
|
|
1258
|
+
async isRegistered() {
|
|
1259
|
+
for (const configPath of [this.mcpConfigPath, this.globalMcpConfigPath]) {
|
|
1260
|
+
const config = readJsonFile(configPath);
|
|
1261
|
+
if (config?.mcpServers?.[MCP_KEY3]) return true;
|
|
1262
|
+
}
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// src/platform/generic.ts
|
|
1268
|
+
var GenericAdapter = class {
|
|
1269
|
+
name;
|
|
1270
|
+
tier = "cli-mcp";
|
|
1271
|
+
constructor(name) {
|
|
1272
|
+
this.name = name;
|
|
1273
|
+
}
|
|
1274
|
+
// ── Detection ─────────────────────────────────────────────
|
|
1275
|
+
async detect() {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
// ── MCP Registration ──────────────────────────────────────
|
|
1279
|
+
async registerMcp(serverConfig) {
|
|
1280
|
+
const snippet = {
|
|
1281
|
+
mcpServers: {
|
|
1282
|
+
caik: {
|
|
1283
|
+
command: serverConfig.command,
|
|
1284
|
+
args: serverConfig.args,
|
|
1285
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const json = JSON.stringify(snippet, null, 2);
|
|
1290
|
+
process.stdout.write(
|
|
1291
|
+
`
|
|
1292
|
+
CAIK MCP server configuration for ${this.name}:
|
|
1293
|
+
|
|
1294
|
+
Add the following to your MCP configuration file:
|
|
1295
|
+
|
|
1296
|
+
${json}
|
|
1297
|
+
|
|
1298
|
+
Refer to your platform's documentation for the correct config file location.
|
|
1299
|
+
|
|
1300
|
+
`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
async unregisterMcp() {
|
|
1304
|
+
process.stdout.write(
|
|
1305
|
+
`
|
|
1306
|
+
To unregister CAIK from ${this.name}, remove the "caik" entry
|
|
1307
|
+
from the "mcpServers" section in your MCP configuration file.
|
|
1308
|
+
|
|
1309
|
+
`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
// ── No hooks support ──────────────────────────────────────
|
|
1313
|
+
// registerHooks / unregisterHooks intentionally omitted (cli-mcp tier)
|
|
1314
|
+
// ── No skills support ─────────────────────────────────────
|
|
1315
|
+
// installSkill / uninstallSkill intentionally omitted (cli-mcp tier)
|
|
1316
|
+
// ── Config Accessors ──────────────────────────────────────
|
|
1317
|
+
getConfigPath() {
|
|
1318
|
+
return "";
|
|
1319
|
+
}
|
|
1320
|
+
async readConfig() {
|
|
1321
|
+
return {};
|
|
1322
|
+
}
|
|
1323
|
+
async isRegistered() {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
// src/platform/index.ts
|
|
1329
|
+
var adapters = {
|
|
1330
|
+
"claude-code": () => new ClaudeCodeAdapter(),
|
|
1331
|
+
"openclaw": () => new OpenClawAdapter(),
|
|
1332
|
+
"cursor": () => new CursorAdapter(),
|
|
1333
|
+
"codex": () => new GenericAdapter("codex"),
|
|
1334
|
+
"windsurf": () => new GenericAdapter("windsurf"),
|
|
1335
|
+
"generic": () => new GenericAdapter("generic")
|
|
1336
|
+
};
|
|
1337
|
+
function getPlatformAdapter(name) {
|
|
1338
|
+
const factory = adapters[name];
|
|
1339
|
+
if (!factory) {
|
|
1340
|
+
return new GenericAdapter(name);
|
|
1341
|
+
}
|
|
1342
|
+
return factory();
|
|
1343
|
+
}
|
|
1344
|
+
function getDefaultMcpConfig() {
|
|
1345
|
+
return {
|
|
1346
|
+
command: "npx",
|
|
1347
|
+
args: ["-y", "@caik.dev/mcp"]
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/commands/install.ts
|
|
1352
|
+
var legacyPlatformMap = {
|
|
1353
|
+
claude: "claude-code",
|
|
1354
|
+
cursor: "cursor",
|
|
1355
|
+
node: "generic"
|
|
1356
|
+
};
|
|
1357
|
+
function registerInstallCommand(program2) {
|
|
1358
|
+
program2.command("install <slug>").description("Install an artifact from the CAIK registry").option("--platform <platform>", "Target platform (auto-detected if not specified)").option("-y, --yes", "Skip confirmation prompts (for CI/scripting)").addHelpText("after", `
|
|
1359
|
+
Examples:
|
|
1360
|
+
caik install auth-middleware
|
|
1361
|
+
caik install my-skill --platform claude`).action(async (slug, opts) => {
|
|
1362
|
+
const globalOpts = program2.opts();
|
|
1363
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
1364
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
1365
|
+
let detectedPlatform;
|
|
1366
|
+
if (opts.platform) {
|
|
1367
|
+
const raw = opts.platform;
|
|
1368
|
+
detectedPlatform = legacyPlatformMap[raw] ?? raw;
|
|
1369
|
+
} else {
|
|
1370
|
+
const detected = detectPlatforms();
|
|
1371
|
+
if (detected.length > 1 && !opts.yes && !globalOpts.json) {
|
|
1372
|
+
console.log(info("Multiple platforms detected:"));
|
|
1373
|
+
for (let i = 0; i < detected.length; i++) {
|
|
1374
|
+
const d = detected[i];
|
|
1375
|
+
console.log(info(` ${i + 1}) ${d.name} (${d.tier})`));
|
|
1376
|
+
}
|
|
1377
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1378
|
+
const answer = await rl.question(`Select platform [1-${detected.length}]: `);
|
|
1379
|
+
rl.close();
|
|
1380
|
+
const idx = parseInt(answer, 10);
|
|
1381
|
+
if (isNaN(idx) || idx < 1 || idx > detected.length) {
|
|
1382
|
+
console.log(error("Invalid selection. Aborting."));
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
detectedPlatform = detected[idx - 1].name;
|
|
1386
|
+
} else if (detected.length > 0) {
|
|
1387
|
+
detectedPlatform = detected[0].name;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const platform2 = detectedPlatform;
|
|
1391
|
+
const spinner = createSpinner(`Fetching ${slug}...`);
|
|
1392
|
+
if (!globalOpts.json) spinner.start();
|
|
1393
|
+
const params = { platform: platform2 };
|
|
1394
|
+
const installInfo = await client.get(`/install/${encodeURIComponent(slug)}`, params);
|
|
1395
|
+
if (globalOpts.json) {
|
|
1396
|
+
spinner.stop();
|
|
1397
|
+
outputResult(installInfo, { json: true });
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
spinner.text = `Installing ${installInfo.artifact.name}...`;
|
|
1401
|
+
const skipConfirm = Boolean(opts.yes);
|
|
1402
|
+
const cwd = process.cwd();
|
|
1403
|
+
const safeFiles = [];
|
|
1404
|
+
if (installInfo.files && installInfo.files.length > 0) {
|
|
1405
|
+
for (const file of installInfo.files) {
|
|
1406
|
+
const resolvedPath = resolve(cwd, file.path);
|
|
1407
|
+
if (!resolvedPath.startsWith(cwd + "/") && resolvedPath !== cwd) {
|
|
1408
|
+
spinner.stop();
|
|
1409
|
+
console.log(error(`Rejected file path outside working directory: ${file.path}`));
|
|
1410
|
+
spinner.start();
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
safeFiles.push({ resolvedPath, content: file.content ?? "" });
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const hasFiles = safeFiles.length > 0;
|
|
1417
|
+
const hasInstallCmd = Boolean(installInfo.installCommand);
|
|
1418
|
+
const hasPostHook = Boolean(installInfo.postInstallHook);
|
|
1419
|
+
if (!skipConfirm && (hasFiles || hasInstallCmd || hasPostHook)) {
|
|
1420
|
+
spinner.stop();
|
|
1421
|
+
console.log(info("\n--- Install plan ---"));
|
|
1422
|
+
if (hasFiles) {
|
|
1423
|
+
console.log(info("Files to write:"));
|
|
1424
|
+
for (const f of safeFiles) {
|
|
1425
|
+
console.log(info(` ${f.resolvedPath}`));
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (hasInstallCmd) {
|
|
1429
|
+
console.log(info(`Install command: ${installInfo.installCommand}`));
|
|
1430
|
+
}
|
|
1431
|
+
if (hasPostHook) {
|
|
1432
|
+
console.log(info(`Post-install hook: ${installInfo.postInstallHook}`));
|
|
1433
|
+
}
|
|
1434
|
+
console.log();
|
|
1435
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1436
|
+
const answer = await rl.question("Continue? (y/N) ");
|
|
1437
|
+
rl.close();
|
|
1438
|
+
if (answer.toLowerCase() !== "y") {
|
|
1439
|
+
console.log(info("Installation cancelled."));
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
spinner.start();
|
|
1443
|
+
}
|
|
1444
|
+
const writtenFiles = [];
|
|
1445
|
+
const resolvedPlatform = detectedPlatform ?? "generic";
|
|
1446
|
+
let registryWritten = false;
|
|
1447
|
+
let mcpRegistered = false;
|
|
1448
|
+
let skillInstalled = false;
|
|
1449
|
+
const rollback = async () => {
|
|
1450
|
+
for (const filePath of writtenFiles) {
|
|
1451
|
+
try {
|
|
1452
|
+
if (existsSync7(filePath)) unlinkSync2(filePath);
|
|
1453
|
+
} catch {
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (registryWritten) {
|
|
1457
|
+
try {
|
|
1458
|
+
removeRegistryEntry(slug, resolvedPlatform);
|
|
1459
|
+
} catch {
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const adapter2 = getPlatformAdapter(resolvedPlatform);
|
|
1463
|
+
if (mcpRegistered) {
|
|
1464
|
+
try {
|
|
1465
|
+
await adapter2.unregisterMcp();
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (skillInstalled && adapter2.uninstallSkill) {
|
|
1470
|
+
try {
|
|
1471
|
+
await adapter2.uninstallSkill(slug);
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (writtenFiles.length > 0) {
|
|
1476
|
+
console.log(info(`Rolled back ${writtenFiles.length} file(s) written during install.`));
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
try {
|
|
1480
|
+
for (const file of safeFiles) {
|
|
1481
|
+
const dir = dirname3(file.resolvedPath);
|
|
1482
|
+
if (!existsSync7(dir)) {
|
|
1483
|
+
mkdirSync6(dir, { recursive: true });
|
|
1484
|
+
}
|
|
1485
|
+
writeFileSync6(file.resolvedPath, file.content, "utf-8");
|
|
1486
|
+
writtenFiles.push(file.resolvedPath);
|
|
1487
|
+
}
|
|
1488
|
+
if (installInfo.installCommand) {
|
|
1489
|
+
execSync3(installInfo.installCommand, { stdio: "pipe" });
|
|
1490
|
+
}
|
|
1491
|
+
if (installInfo.postInstallHook) {
|
|
1492
|
+
try {
|
|
1493
|
+
execSync3(installInfo.postInstallHook, { stdio: "pipe" });
|
|
1494
|
+
} catch {
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
spinner.stop();
|
|
1499
|
+
console.error(error(`Install failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1500
|
+
await rollback();
|
|
1501
|
+
client.post("/telemetry/install", {
|
|
1502
|
+
artifactSlug: slug,
|
|
1503
|
+
platform: platform2,
|
|
1504
|
+
success: false,
|
|
1505
|
+
errorMessage: err instanceof Error ? err.message : String(err)
|
|
1506
|
+
}).catch(() => {
|
|
1507
|
+
});
|
|
1508
|
+
process.exit(1);
|
|
1509
|
+
}
|
|
1510
|
+
spinner.stop();
|
|
1511
|
+
console.log(success(`Installed ${installInfo.artifact.name}`));
|
|
1512
|
+
if (installInfo.contract?.touchedPaths) {
|
|
1513
|
+
console.log(info(`Files: ${installInfo.contract.touchedPaths.join(", ")}`));
|
|
1514
|
+
}
|
|
1515
|
+
upsertRegistryEntry({
|
|
1516
|
+
slug,
|
|
1517
|
+
version: installInfo.artifact.version ?? "0.0.0",
|
|
1518
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1519
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1520
|
+
platform: resolvedPlatform,
|
|
1521
|
+
files: safeFiles.map((f) => f.resolvedPath),
|
|
1522
|
+
artifactType: installInfo.artifact.primitive ?? "unknown"
|
|
1523
|
+
});
|
|
1524
|
+
registryWritten = true;
|
|
1525
|
+
const adapter = getPlatformAdapter(resolvedPlatform);
|
|
1526
|
+
const primitive = installInfo.artifact.primitive;
|
|
1527
|
+
if (primitive === "connector") {
|
|
1528
|
+
try {
|
|
1529
|
+
await adapter.registerMcp(getDefaultMcpConfig());
|
|
1530
|
+
mcpRegistered = true;
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if ((primitive === "executable" || primitive === "composition") && adapter.installSkill) {
|
|
1535
|
+
try {
|
|
1536
|
+
const content = safeFiles.map((f) => f.content).join("\n");
|
|
1537
|
+
await adapter.installSkill(slug, content);
|
|
1538
|
+
skillInstalled = true;
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
client.post("/telemetry/install", {
|
|
1543
|
+
artifactSlug: slug,
|
|
1544
|
+
platform: platform2,
|
|
1545
|
+
success: true
|
|
1546
|
+
}).catch(() => {
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/commands/init.ts
|
|
1552
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
1553
|
+
import { stdin, stdout } from "process";
|
|
1554
|
+
|
|
1555
|
+
// src/auth.ts
|
|
1556
|
+
import { createServer } from "http";
|
|
1557
|
+
import { execFile } from "child_process";
|
|
1558
|
+
import { URL as URL2 } from "url";
|
|
1559
|
+
import { platform } from "os";
|
|
1560
|
+
import { randomBytes } from "crypto";
|
|
1561
|
+
var TIMEOUT_MS = 6e4;
|
|
1562
|
+
function openBrowser(url) {
|
|
1563
|
+
const os = platform();
|
|
1564
|
+
return new Promise((resolve3) => {
|
|
1565
|
+
if (os === "darwin") {
|
|
1566
|
+
execFile("open", [url], (err) => resolve3(!err));
|
|
1567
|
+
} else if (os === "win32") {
|
|
1568
|
+
execFile("cmd", ["/c", "start", "", url], (err) => resolve3(!err));
|
|
1569
|
+
} else {
|
|
1570
|
+
execFile("xdg-open", [url], (err) => resolve3(!err));
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
async function authenticate(apiUrl, port = 0) {
|
|
1575
|
+
const state = randomBytes(16).toString("hex");
|
|
1576
|
+
return new Promise((resolve3, reject) => {
|
|
1577
|
+
const server = createServer((req, res) => {
|
|
1578
|
+
const actualPort = server.address().port;
|
|
1579
|
+
const reqUrl = new URL2(req.url ?? "/", `http://localhost:${actualPort}`);
|
|
1580
|
+
if (reqUrl.pathname !== "/callback") {
|
|
1581
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1582
|
+
res.end("Not found");
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
1586
|
+
if (returnedState !== state) {
|
|
1587
|
+
res.writeHead(403, { "Content-Type": "text/html" });
|
|
1588
|
+
res.end(
|
|
1589
|
+
"<html><body><h2>Authentication failed</h2><p>Invalid state parameter (possible CSRF). You can close this tab.</p></body></html>"
|
|
1590
|
+
);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const apiKeyParam = reqUrl.searchParams.get("api_key");
|
|
1594
|
+
if (!apiKeyParam) {
|
|
1595
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1596
|
+
res.end(
|
|
1597
|
+
"<html><body><h2>Authentication failed</h2><p>No API key received. You can close this tab.</p></body></html>"
|
|
1598
|
+
);
|
|
1599
|
+
cleanup();
|
|
1600
|
+
reject(new Error("No API key received in callback"));
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
setApiKey(apiKeyParam);
|
|
1604
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1605
|
+
res.end(
|
|
1606
|
+
"<html><body><h2>Authenticated!</h2><p>You can close this tab and return to your terminal.</p></body></html>"
|
|
1607
|
+
);
|
|
1608
|
+
cleanup();
|
|
1609
|
+
resolve3(apiKeyParam);
|
|
1610
|
+
});
|
|
1611
|
+
const timeout = setTimeout(() => {
|
|
1612
|
+
cleanup();
|
|
1613
|
+
reject(new Error("Authentication timed out after 60 seconds"));
|
|
1614
|
+
}, TIMEOUT_MS);
|
|
1615
|
+
function cleanup() {
|
|
1616
|
+
clearTimeout(timeout);
|
|
1617
|
+
server.close();
|
|
1618
|
+
}
|
|
1619
|
+
server.listen(port, async () => {
|
|
1620
|
+
const actualPort = server.address().port;
|
|
1621
|
+
const callbackUrl = `http://localhost:${actualPort}/callback`;
|
|
1622
|
+
const loginUrl = `${apiUrl}/auth/signin?redirect=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
|
|
1623
|
+
console.log(info(`Listening on http://localhost:${actualPort} for callback...`));
|
|
1624
|
+
const opened = await openBrowser(loginUrl);
|
|
1625
|
+
if (opened) {
|
|
1626
|
+
console.log(info("Browser opened. Complete sign-in to continue."));
|
|
1627
|
+
} else {
|
|
1628
|
+
console.log(
|
|
1629
|
+
error("Could not open browser automatically.")
|
|
1630
|
+
);
|
|
1631
|
+
console.log(info(`Open this URL manually:
|
|
1632
|
+
${loginUrl}`));
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
server.on("error", (err) => {
|
|
1636
|
+
clearTimeout(timeout);
|
|
1637
|
+
reject(
|
|
1638
|
+
new Error(
|
|
1639
|
+
`Failed to start auth server: ${err.message}`
|
|
1640
|
+
)
|
|
1641
|
+
);
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/commands/init.ts
|
|
1647
|
+
function registerInitCommand(program2) {
|
|
1648
|
+
program2.command("init").description("Configure the CAIK CLI").option("--auth", "Set up authentication (opens browser)").addHelpText("after", `
|
|
1649
|
+
Examples:
|
|
1650
|
+
caik init
|
|
1651
|
+
caik init --auth`).action(async (opts) => {
|
|
1652
|
+
const config = readConfig();
|
|
1653
|
+
if (opts.auth) {
|
|
1654
|
+
console.log(info("Opening browser for authentication..."));
|
|
1655
|
+
try {
|
|
1656
|
+
const apiKey = await authenticate(config.apiUrl);
|
|
1657
|
+
config.apiKey = apiKey;
|
|
1658
|
+
writeConfig(config);
|
|
1659
|
+
console.log(success("Authenticated and saved API key"));
|
|
1660
|
+
console.log(info(`Config saved to ~/.caik/config.json`));
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
console.log(error(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1663
|
+
}
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const rl = createInterface2({ input: stdin, output: stdout });
|
|
1667
|
+
try {
|
|
1668
|
+
const apiUrl = await rl.question(`API URL (${config.apiUrl}): `);
|
|
1669
|
+
if (apiUrl.trim()) {
|
|
1670
|
+
config.apiUrl = apiUrl.trim();
|
|
1671
|
+
}
|
|
1672
|
+
const apiKey = await rl.question("API Key (leave blank to skip): ");
|
|
1673
|
+
if (apiKey.trim()) {
|
|
1674
|
+
config.apiKey = apiKey.trim();
|
|
1675
|
+
}
|
|
1676
|
+
writeConfig(config);
|
|
1677
|
+
console.log(success("CAIK CLI configured"));
|
|
1678
|
+
console.log(info(`Config saved to ~/.caik/config.json`));
|
|
1679
|
+
if (config.apiKey) {
|
|
1680
|
+
const client = new CaikApiClient({ apiUrl: config.apiUrl, apiKey: config.apiKey });
|
|
1681
|
+
try {
|
|
1682
|
+
await client.get("/me/karma");
|
|
1683
|
+
console.log(success("API connection verified \u2014 authenticated"));
|
|
1684
|
+
} catch {
|
|
1685
|
+
console.log(error("Could not verify API connection. Check your API key."));
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
const detected = detectPlatforms();
|
|
1689
|
+
if (detected.length > 0) {
|
|
1690
|
+
console.log("");
|
|
1691
|
+
console.log(heading("Platform Integration"));
|
|
1692
|
+
console.log("\u2500".repeat(40));
|
|
1693
|
+
for (const p of detected) {
|
|
1694
|
+
console.log(` ${success(p.name)} detected`);
|
|
1695
|
+
}
|
|
1696
|
+
console.log("");
|
|
1697
|
+
const hasClaude = detected.some((p) => p.name === "claude-code");
|
|
1698
|
+
const hasOpenClaw = detected.some((p) => p.name === "openclaw");
|
|
1699
|
+
if (hasClaude) {
|
|
1700
|
+
console.log(info("Claude Code: Install the CAIK plugin for the best experience:"));
|
|
1701
|
+
console.log(dim(" claude plugin marketplace add https://github.com/caik-dev/claude-code-plugin"));
|
|
1702
|
+
console.log(dim(" claude plugin install caik@caik-dev"));
|
|
1703
|
+
console.log("");
|
|
1704
|
+
}
|
|
1705
|
+
if (hasOpenClaw) {
|
|
1706
|
+
console.log(info("OpenClaw: Add the CAIK MCP server to your project's .mcp.json:"));
|
|
1707
|
+
console.log(dim(' { "mcpServers": { "caik": { "type": "stdio", "command": "npx", "args": ["-y", "@caik.dev/mcp"] } } }'));
|
|
1708
|
+
console.log("");
|
|
1709
|
+
}
|
|
1710
|
+
console.log(info("Or run `caik setup` for interactive platform configuration."));
|
|
1711
|
+
}
|
|
1712
|
+
} finally {
|
|
1713
|
+
rl.close();
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/commands/status.ts
|
|
1719
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1720
|
+
function registerStatusCommand(program2) {
|
|
1721
|
+
program2.command("status").description("Show CLI configuration and connectivity status").action(async () => {
|
|
1722
|
+
const globalOpts = program2.opts();
|
|
1723
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
1724
|
+
const detected = detectPlatforms();
|
|
1725
|
+
const platformResults = [];
|
|
1726
|
+
for (const dp of detected) {
|
|
1727
|
+
const adapter = getPlatformAdapter(dp.name);
|
|
1728
|
+
let registered = false;
|
|
1729
|
+
try {
|
|
1730
|
+
registered = await adapter.isRegistered();
|
|
1731
|
+
} catch {
|
|
1732
|
+
}
|
|
1733
|
+
platformResults.push({
|
|
1734
|
+
name: dp.name,
|
|
1735
|
+
tier: dp.tier,
|
|
1736
|
+
registered,
|
|
1737
|
+
driftWarnings: []
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
const registryEntries = listRegistryEntries();
|
|
1741
|
+
const driftEntries = [];
|
|
1742
|
+
for (const entry of registryEntries) {
|
|
1743
|
+
const missing = entry.files.filter((f) => !existsSync8(f));
|
|
1744
|
+
if (missing.length > 0) {
|
|
1745
|
+
driftEntries.push({ slug: entry.slug, missingFiles: missing });
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
for (const drift of driftEntries) {
|
|
1749
|
+
const entry = registryEntries.find((e) => e.slug === drift.slug);
|
|
1750
|
+
if (entry) {
|
|
1751
|
+
const pr = platformResults.find((p) => p.name === entry.platform);
|
|
1752
|
+
if (pr) {
|
|
1753
|
+
pr.driftWarnings.push(
|
|
1754
|
+
`${drift.slug}: ${drift.missingFiles.length} missing file(s)`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
if (globalOpts.json) {
|
|
1760
|
+
const data = {
|
|
1761
|
+
apiUrl,
|
|
1762
|
+
apiKeyConfigured: !!apiKey,
|
|
1763
|
+
platforms: platformResults,
|
|
1764
|
+
installedArtifacts: registryEntries.length
|
|
1765
|
+
};
|
|
1766
|
+
if (driftEntries.length > 0) {
|
|
1767
|
+
data.driftWarnings = driftEntries;
|
|
1768
|
+
}
|
|
1769
|
+
const client2 = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
1770
|
+
try {
|
|
1771
|
+
if (apiKey) {
|
|
1772
|
+
const karma = await client2.get("/me/karma");
|
|
1773
|
+
data.authenticated = true;
|
|
1774
|
+
data.user = karma;
|
|
1775
|
+
}
|
|
1776
|
+
data.apiReachable = true;
|
|
1777
|
+
} catch {
|
|
1778
|
+
data.apiReachable = false;
|
|
1779
|
+
data.authenticated = false;
|
|
1780
|
+
}
|
|
1781
|
+
outputResult(data, { json: true });
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
console.log(heading("CAIK CLI Status"));
|
|
1785
|
+
console.log("\u2550".repeat(40));
|
|
1786
|
+
console.log(`API URL: ${apiUrl}`);
|
|
1787
|
+
console.log(`API Key: ${apiKey ? `${dim("configured")} (${apiKey.slice(0, 12)}...)` : dim("not set")}`);
|
|
1788
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
1789
|
+
try {
|
|
1790
|
+
if (apiKey) {
|
|
1791
|
+
const karma = await client.get("/me/karma");
|
|
1792
|
+
console.log(`Authenticated: ${success("yes")}`);
|
|
1793
|
+
console.log(`User: ${karma.handle ?? karma.displayName ?? "unknown"} (${karma.karmaTier})`);
|
|
1794
|
+
} else {
|
|
1795
|
+
console.log(`Authenticated: ${dim("no (no API key)")}`);
|
|
1796
|
+
}
|
|
1797
|
+
} catch {
|
|
1798
|
+
console.log(`API Reachable: ${error("no")}`);
|
|
1799
|
+
}
|
|
1800
|
+
console.log("");
|
|
1801
|
+
console.log(heading("Platform Integration"));
|
|
1802
|
+
console.log("\u2500".repeat(40));
|
|
1803
|
+
if (platformResults.length === 0) {
|
|
1804
|
+
console.log(` ${dim("No platforms detected")}`);
|
|
1805
|
+
} else {
|
|
1806
|
+
const maxLen = Math.max(...platformResults.map((p) => p.name.length));
|
|
1807
|
+
for (const p of platformResults) {
|
|
1808
|
+
const label = `${p.name}:`.padEnd(maxLen + 2);
|
|
1809
|
+
const mcpStatus = p.registered ? `(MCP: ${success("registered")})` : `(MCP: ${dim("not registered")})`;
|
|
1810
|
+
console.log(` ${label}${success("detected")} ${mcpStatus}`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
console.log("");
|
|
1814
|
+
console.log(`Installed Artifacts: ${registryEntries.length}`);
|
|
1815
|
+
if (driftEntries.length > 0) {
|
|
1816
|
+
const count = driftEntries.length;
|
|
1817
|
+
const noun = count === 1 ? "artifact has" : "artifacts have";
|
|
1818
|
+
console.log(
|
|
1819
|
+
` ${warn(`${count} ${noun} missing files`)} \u2014 run \`caik setup\` to reconfigure`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/commands/stats.ts
|
|
1826
|
+
var PRIMITIVES = ["executable", "knowledge", "connector", "composition", "reference"];
|
|
1827
|
+
function registerStatsCommand(program2) {
|
|
1828
|
+
program2.command("stats").description("Show CAIK ecosystem statistics").addHelpText("after", `
|
|
1829
|
+
Examples:
|
|
514
1830
|
caik stats
|
|
515
1831
|
caik stats --json`).action(async () => {
|
|
516
1832
|
const globalOpts = program2.opts();
|
|
@@ -552,7 +1868,7 @@ Examples:
|
|
|
552
1868
|
}
|
|
553
1869
|
|
|
554
1870
|
// src/commands/publish.ts
|
|
555
|
-
import { readFileSync as
|
|
1871
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
|
|
556
1872
|
function registerPublishCommand(program2) {
|
|
557
1873
|
program2.command("publish [path]").description("Publish an artifact to the CAIK registry").requiredOption("--name <name>", "Artifact name").requiredOption("--description <desc>", "Artifact description (min 20 chars)").option("--slug <slug>", "Custom slug (auto-generated from name if omitted)").option("--primitive <type>", "Primitive type", "executable").option("--platform <platform>", "Target platform(s) (comma-separated)", "claude").option("--tag <tag>", "Add a tag (can be repeated)", (val, prev) => [...prev, val], []).option("--source-url <url>", "Source code URL").addHelpText("after", `
|
|
558
1874
|
Examples:
|
|
@@ -566,10 +1882,10 @@ Examples:
|
|
|
566
1882
|
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
567
1883
|
let content;
|
|
568
1884
|
if (path) {
|
|
569
|
-
if (!
|
|
1885
|
+
if (!existsSync9(path)) {
|
|
570
1886
|
throw new CaikError(`File not found: ${path}`);
|
|
571
1887
|
}
|
|
572
|
-
content =
|
|
1888
|
+
content = readFileSync6(path, "utf-8");
|
|
573
1889
|
}
|
|
574
1890
|
const tags = opts.tag.length > 0 ? opts.tag : ["general"];
|
|
575
1891
|
const platforms = opts.platform.split(",").map((p) => p.trim());
|
|
@@ -730,16 +2046,341 @@ Examples:
|
|
|
730
2046
|
}
|
|
731
2047
|
|
|
732
2048
|
// src/commands/update.ts
|
|
2049
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2050
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
2051
|
+
import { dirname as dirname4, resolve as resolve2 } from "path";
|
|
2052
|
+
import { execSync as execSync4 } from "child_process";
|
|
2053
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
733
2054
|
function registerUpdateCommand(program2) {
|
|
734
|
-
program2.command("update [slug]").description("Check for and apply artifact updates").option("--yes", "Skip confirmation
|
|
735
|
-
|
|
2055
|
+
program2.command("update [slug]").description("Check for and apply artifact updates").option("--platform <platform>", "Filter by platform").option("-y, --yes", "Skip confirmation prompts").addHelpText("after", `
|
|
2056
|
+
Examples:
|
|
2057
|
+
caik update # Check all installed artifacts for updates
|
|
2058
|
+
caik update auth-skill # Update a specific artifact
|
|
2059
|
+
caik update --yes # Update all without confirmation`).action(async (slug, opts) => {
|
|
2060
|
+
const globalOpts = program2.opts();
|
|
2061
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
2062
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
2063
|
+
const skipConfirm = Boolean(opts?.yes);
|
|
2064
|
+
const platformFilter = opts?.platform;
|
|
2065
|
+
if (slug) {
|
|
2066
|
+
const entry = findRegistryEntry(slug, platformFilter);
|
|
2067
|
+
if (!entry) {
|
|
2068
|
+
console.log(error(`Artifact "${slug}" is not in the install registry.`));
|
|
2069
|
+
console.log(info("Use 'caik install <slug>' to install it first."));
|
|
2070
|
+
process.exit(1);
|
|
2071
|
+
}
|
|
2072
|
+
const spinner = createSpinner(`Checking for updates to ${slug}...`);
|
|
2073
|
+
if (!globalOpts.json) spinner.start();
|
|
2074
|
+
let remote;
|
|
2075
|
+
try {
|
|
2076
|
+
remote = await client.get(`/artifacts/${encodeURIComponent(slug)}`);
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
spinner.stop();
|
|
2079
|
+
console.log(error(`Failed to fetch artifact info: ${err instanceof Error ? err.message : String(err)}`));
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
}
|
|
2082
|
+
if (globalOpts.json) {
|
|
2083
|
+
spinner.stop();
|
|
2084
|
+
outputResult({
|
|
2085
|
+
slug,
|
|
2086
|
+
installedVersion: entry.version,
|
|
2087
|
+
remoteUpdatedAt: remote.updatedAt,
|
|
2088
|
+
localUpdatedAt: entry.updatedAt,
|
|
2089
|
+
updateAvailable: remote.updatedAt > entry.updatedAt
|
|
2090
|
+
}, { json: true });
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
if (remote.updatedAt <= entry.updatedAt) {
|
|
2094
|
+
spinner.stop();
|
|
2095
|
+
console.log(success(`${slug} is already up to date.`));
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
spinner.stop();
|
|
2099
|
+
console.log(info(`Update available for ${slug} (installed: ${entry.updatedAt.slice(0, 10)}, latest: ${remote.updatedAt.slice(0, 10)})`));
|
|
2100
|
+
await performUpdate(client, entry, skipConfirm, globalOpts);
|
|
2101
|
+
} else {
|
|
2102
|
+
const entries = listRegistryEntries(platformFilter);
|
|
2103
|
+
if (entries.length === 0) {
|
|
2104
|
+
console.log(info("No installed artifacts found."));
|
|
2105
|
+
console.log(info("Install artifacts with 'caik install <slug>' first."));
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const spinner = createSpinner(`Checking ${entries.length} artifact(s) for updates...`);
|
|
2109
|
+
if (!globalOpts.json) spinner.start();
|
|
2110
|
+
const candidates = [];
|
|
2111
|
+
const errors = [];
|
|
2112
|
+
for (const entry of entries) {
|
|
2113
|
+
try {
|
|
2114
|
+
const remote = await client.get(`/artifacts/${encodeURIComponent(entry.slug)}`);
|
|
2115
|
+
if (remote.updatedAt > entry.updatedAt) {
|
|
2116
|
+
candidates.push({ entry, remote });
|
|
2117
|
+
}
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
errors.push({
|
|
2120
|
+
slug: entry.slug,
|
|
2121
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
spinner.stop();
|
|
2126
|
+
if (globalOpts.json) {
|
|
2127
|
+
outputResult({
|
|
2128
|
+
total: entries.length,
|
|
2129
|
+
updatesAvailable: candidates.length,
|
|
2130
|
+
updates: candidates.map((c) => ({
|
|
2131
|
+
slug: c.entry.slug,
|
|
2132
|
+
platform: c.entry.platform,
|
|
2133
|
+
installedAt: c.entry.updatedAt,
|
|
2134
|
+
remoteUpdatedAt: c.remote.updatedAt
|
|
2135
|
+
})),
|
|
2136
|
+
errors
|
|
2137
|
+
}, { json: true });
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
if (errors.length > 0) {
|
|
2141
|
+
for (const e of errors) {
|
|
2142
|
+
console.log(warn(`Could not check ${e.slug}: ${e.message}`));
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (candidates.length === 0) {
|
|
2146
|
+
console.log(success("All installed artifacts are up to date."));
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const rows = candidates.map((c) => [
|
|
2150
|
+
c.entry.slug,
|
|
2151
|
+
c.entry.platform,
|
|
2152
|
+
c.entry.updatedAt.slice(0, 10),
|
|
2153
|
+
c.remote.updatedAt.slice(0, 10)
|
|
2154
|
+
]);
|
|
2155
|
+
console.log(renderTable(["Artifact", "Platform", "Installed", "Latest"], rows));
|
|
2156
|
+
console.log(info(`${candidates.length} update(s) available.`));
|
|
2157
|
+
if (!skipConfirm) {
|
|
2158
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2159
|
+
const answer = await rl.question("\nUpdate all? (y/N) ");
|
|
2160
|
+
rl.close();
|
|
2161
|
+
if (answer.toLowerCase() !== "y") {
|
|
2162
|
+
console.log(info("Update cancelled."));
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
let updated = 0;
|
|
2167
|
+
for (const candidate of candidates) {
|
|
2168
|
+
try {
|
|
2169
|
+
await performUpdate(client, candidate.entry, true, globalOpts);
|
|
2170
|
+
updated++;
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
console.log(error(`Failed to update ${candidate.entry.slug}: ${err instanceof Error ? err.message : String(err)}`));
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
console.log(success(`Updated ${updated}/${candidates.length} artifact(s).`));
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
async function performUpdate(client, entry, skipConfirm, globalOpts) {
|
|
2180
|
+
const spinner = createSpinner(`Updating ${entry.slug}...`);
|
|
2181
|
+
if (!globalOpts.json) spinner.start();
|
|
2182
|
+
const params = { platform: entry.platform };
|
|
2183
|
+
let installInfo;
|
|
2184
|
+
try {
|
|
2185
|
+
installInfo = await client.get(`/install/${encodeURIComponent(entry.slug)}`, params);
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
spinner.stop();
|
|
2188
|
+
throw err;
|
|
2189
|
+
}
|
|
2190
|
+
const cwd = process.cwd();
|
|
2191
|
+
const safeFiles = [];
|
|
2192
|
+
if (installInfo.files && installInfo.files.length > 0) {
|
|
2193
|
+
for (const file of installInfo.files) {
|
|
2194
|
+
const resolvedPath = resolve2(cwd, file.path);
|
|
2195
|
+
if (!resolvedPath.startsWith(cwd + "/") && resolvedPath !== cwd) {
|
|
2196
|
+
spinner.stop();
|
|
2197
|
+
console.log(error(`Rejected file path outside working directory: ${file.path}`));
|
|
2198
|
+
spinner.start();
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
safeFiles.push({ resolvedPath, content: file.content });
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (!skipConfirm && safeFiles.length > 0) {
|
|
2205
|
+
spinner.stop();
|
|
2206
|
+
console.log(info("\n--- Update plan ---"));
|
|
2207
|
+
console.log(info("Files to write:"));
|
|
2208
|
+
for (const f of safeFiles) {
|
|
2209
|
+
console.log(info(` ${f.resolvedPath}`));
|
|
2210
|
+
}
|
|
2211
|
+
if (installInfo.installCommand) {
|
|
2212
|
+
console.log(info(`Install command: ${installInfo.installCommand}`));
|
|
2213
|
+
}
|
|
2214
|
+
console.log();
|
|
2215
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2216
|
+
const answer = await rl.question("Continue? (y/N) ");
|
|
2217
|
+
rl.close();
|
|
2218
|
+
if (answer.toLowerCase() !== "y") {
|
|
2219
|
+
console.log(info("Update cancelled."));
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
spinner.start();
|
|
2223
|
+
}
|
|
2224
|
+
const writtenFiles = [];
|
|
2225
|
+
for (const file of safeFiles) {
|
|
2226
|
+
const dir = dirname4(file.resolvedPath);
|
|
2227
|
+
if (!existsSync10(dir)) {
|
|
2228
|
+
mkdirSync7(dir, { recursive: true });
|
|
2229
|
+
}
|
|
2230
|
+
writeFileSync7(file.resolvedPath, file.content, "utf-8");
|
|
2231
|
+
writtenFiles.push(file.resolvedPath);
|
|
2232
|
+
}
|
|
2233
|
+
if (installInfo.installCommand) {
|
|
2234
|
+
try {
|
|
2235
|
+
execSync4(installInfo.installCommand, { stdio: "pipe" });
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
spinner.stop();
|
|
2238
|
+
console.log(error(`Install command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
2239
|
+
client.post("/telemetry/install", {
|
|
2240
|
+
artifactSlug: entry.slug,
|
|
2241
|
+
platform: entry.platform,
|
|
2242
|
+
success: false,
|
|
2243
|
+
errorMessage: err instanceof Error ? err.message : String(err)
|
|
2244
|
+
}).catch(() => {
|
|
2245
|
+
});
|
|
2246
|
+
throw err;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (installInfo.postInstallHook) {
|
|
2250
|
+
try {
|
|
2251
|
+
execSync4(installInfo.postInstallHook, { stdio: "pipe" });
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2256
|
+
upsertRegistryEntry({
|
|
2257
|
+
...entry,
|
|
2258
|
+
updatedAt: now,
|
|
2259
|
+
files: writtenFiles.length > 0 ? writtenFiles : entry.files
|
|
2260
|
+
});
|
|
2261
|
+
spinner.stop();
|
|
2262
|
+
console.log(success(`Updated ${installInfo.artifact.name}`));
|
|
2263
|
+
client.post("/telemetry/install", {
|
|
2264
|
+
artifactSlug: entry.slug,
|
|
2265
|
+
platform: entry.platform,
|
|
2266
|
+
success: true
|
|
2267
|
+
}).catch(() => {
|
|
736
2268
|
});
|
|
737
2269
|
}
|
|
738
2270
|
|
|
739
2271
|
// src/commands/uninstall.ts
|
|
2272
|
+
import { createInterface as createInterface4 } from "readline/promises";
|
|
740
2273
|
function registerUninstallCommand(program2) {
|
|
741
|
-
program2.command("uninstall <slug>").description("Remove an installed artifact").
|
|
742
|
-
|
|
2274
|
+
program2.command("uninstall <slug>").description("Remove an installed artifact").option("--platform <platform>", "Target platform (disambiguates if installed on multiple)").option("-y, --yes", "Skip confirmation prompts").addHelpText("after", `
|
|
2275
|
+
Examples:
|
|
2276
|
+
caik uninstall auth-skill
|
|
2277
|
+
caik uninstall my-mcp --platform claude-code
|
|
2278
|
+
caik uninstall old-rules --yes`).action(async (slug, opts) => {
|
|
2279
|
+
const globalOpts = program2.opts();
|
|
2280
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
2281
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
2282
|
+
const skipConfirm = Boolean(opts.yes);
|
|
2283
|
+
const platformOpt = opts.platform;
|
|
2284
|
+
const allEntries = listRegistryEntries();
|
|
2285
|
+
const matchingEntries = allEntries.filter(
|
|
2286
|
+
(e) => e.slug === slug && (!platformOpt || e.platform === platformOpt)
|
|
2287
|
+
);
|
|
2288
|
+
if (matchingEntries.length === 0) {
|
|
2289
|
+
if (globalOpts.json) {
|
|
2290
|
+
outputResult({ error: "not_found", slug }, { json: true });
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
console.log(error(`Artifact "${slug}" not found in install registry.`));
|
|
2294
|
+
console.log(info("It may have been installed manually or already uninstalled."));
|
|
2295
|
+
process.exit(1);
|
|
2296
|
+
}
|
|
2297
|
+
if (matchingEntries.length > 1 && !platformOpt) {
|
|
2298
|
+
if (globalOpts.json) {
|
|
2299
|
+
outputResult({
|
|
2300
|
+
error: "ambiguous",
|
|
2301
|
+
slug,
|
|
2302
|
+
platforms: matchingEntries.map((e) => e.platform)
|
|
2303
|
+
}, { json: true });
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
console.log(warn(`"${slug}" is installed on multiple platforms: ${matchingEntries.map((e) => e.platform).join(", ")}`));
|
|
2307
|
+
console.log(info("Use --platform <platform> to specify which to uninstall."));
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
const entry = matchingEntries[0];
|
|
2311
|
+
if (globalOpts.json) {
|
|
2312
|
+
} else {
|
|
2313
|
+
console.log(info(`
|
|
2314
|
+
Artifact: ${entry.slug} (${entry.artifactType})`));
|
|
2315
|
+
console.log(info(`Platform: ${entry.platform}`));
|
|
2316
|
+
console.log(info(`Installed: ${entry.installedAt.slice(0, 10)}`));
|
|
2317
|
+
if (entry.files.length > 0) {
|
|
2318
|
+
console.log(info("Files to remove:"));
|
|
2319
|
+
for (const f of entry.files) {
|
|
2320
|
+
console.log(info(` ${f}`));
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (!skipConfirm) {
|
|
2324
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2325
|
+
const answer = await rl.question("\nUninstall? (y/N) ");
|
|
2326
|
+
rl.close();
|
|
2327
|
+
if (answer.toLowerCase() !== "y") {
|
|
2328
|
+
console.log(info("Uninstall cancelled."));
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
const spinner = createSpinner(`Uninstalling ${slug}...`);
|
|
2334
|
+
if (!globalOpts.json) spinner.start();
|
|
2335
|
+
const failedFiles = cleanupFiles(entry);
|
|
2336
|
+
const adapter = getPlatformAdapter(entry.platform);
|
|
2337
|
+
if (entry.artifactType === "connector" || entry.artifactType === "mcp-server") {
|
|
2338
|
+
try {
|
|
2339
|
+
await adapter.unregisterMcp();
|
|
2340
|
+
} catch (err) {
|
|
2341
|
+
if (!globalOpts.json) {
|
|
2342
|
+
spinner.stop();
|
|
2343
|
+
console.log(warn(`Could not unregister MCP server: ${err instanceof Error ? err.message : String(err)}`));
|
|
2344
|
+
spinner.start();
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const skillTypes = /* @__PURE__ */ new Set(["executable", "composition", "skill", "subagent", "command"]);
|
|
2349
|
+
if (skillTypes.has(entry.artifactType) && adapter.uninstallSkill) {
|
|
2350
|
+
try {
|
|
2351
|
+
await adapter.uninstallSkill(slug);
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
if (!globalOpts.json) {
|
|
2354
|
+
spinner.stop();
|
|
2355
|
+
console.log(warn(`Could not uninstall skill: ${err instanceof Error ? err.message : String(err)}`));
|
|
2356
|
+
spinner.start();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
removeRegistryEntry(slug, entry.platform);
|
|
2361
|
+
spinner.stop();
|
|
2362
|
+
if (globalOpts.json) {
|
|
2363
|
+
outputResult({
|
|
2364
|
+
slug,
|
|
2365
|
+
platform: entry.platform,
|
|
2366
|
+
removed: true,
|
|
2367
|
+
filesRemoved: entry.files.length - failedFiles.length,
|
|
2368
|
+
filesFailed: failedFiles
|
|
2369
|
+
}, { json: true });
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
console.log(success(`Uninstalled ${slug} from ${entry.platform}`));
|
|
2373
|
+
if (failedFiles.length > 0) {
|
|
2374
|
+
console.log(warn(`Could not remove ${failedFiles.length} file(s):`));
|
|
2375
|
+
for (const f of failedFiles) {
|
|
2376
|
+
console.log(warn(` ${f}`));
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
client.post("/telemetry/uninstall", {
|
|
2380
|
+
artifactSlug: slug,
|
|
2381
|
+
platform: entry.platform
|
|
2382
|
+
}).catch(() => {
|
|
2383
|
+
});
|
|
743
2384
|
});
|
|
744
2385
|
}
|
|
745
2386
|
|
|
@@ -769,13 +2410,780 @@ function registerKarmaCommand(program2) {
|
|
|
769
2410
|
});
|
|
770
2411
|
}
|
|
771
2412
|
|
|
2413
|
+
// src/commands/hook.ts
|
|
2414
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync11 } from "fs";
|
|
2415
|
+
import { join as join7 } from "path";
|
|
2416
|
+
import { homedir as homedir6 } from "os";
|
|
2417
|
+
var PENDING_EVENTS_PATH = join7(homedir6(), ".caik", "pending-events.json");
|
|
2418
|
+
var FLUSH_THRESHOLD = 50;
|
|
2419
|
+
var API_TIMEOUT_MS = 2e3;
|
|
2420
|
+
function readPendingEvents() {
|
|
2421
|
+
try {
|
|
2422
|
+
if (!existsSync11(PENDING_EVENTS_PATH)) return [];
|
|
2423
|
+
const raw = readFileSync7(PENDING_EVENTS_PATH, "utf-8");
|
|
2424
|
+
const parsed = JSON.parse(raw);
|
|
2425
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2426
|
+
} catch {
|
|
2427
|
+
return [];
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
function writePendingEvents(events) {
|
|
2431
|
+
const dir = join7(homedir6(), ".caik");
|
|
2432
|
+
if (!existsSync11(dir)) {
|
|
2433
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
2434
|
+
}
|
|
2435
|
+
writeFileSync8(PENDING_EVENTS_PATH, JSON.stringify(events, null, 2) + "\n", "utf-8");
|
|
2436
|
+
}
|
|
2437
|
+
function bufferEvent(event) {
|
|
2438
|
+
const events = readPendingEvents();
|
|
2439
|
+
events.push(event);
|
|
2440
|
+
writePendingEvents(events);
|
|
2441
|
+
return events;
|
|
2442
|
+
}
|
|
2443
|
+
function clearPendingEvents() {
|
|
2444
|
+
writePendingEvents([]);
|
|
2445
|
+
}
|
|
2446
|
+
async function postEventsWithTimeout(client, events) {
|
|
2447
|
+
if (events.length === 0) return;
|
|
2448
|
+
const controller = new AbortController();
|
|
2449
|
+
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
2450
|
+
try {
|
|
2451
|
+
await Promise.race([
|
|
2452
|
+
client.post("/events", { events }),
|
|
2453
|
+
new Promise(
|
|
2454
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), API_TIMEOUT_MS)
|
|
2455
|
+
)
|
|
2456
|
+
]);
|
|
2457
|
+
clearPendingEvents();
|
|
2458
|
+
} catch {
|
|
2459
|
+
} finally {
|
|
2460
|
+
clearTimeout(timeout);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
async function postSingleEventWithTimeout(client, event) {
|
|
2464
|
+
try {
|
|
2465
|
+
await Promise.race([
|
|
2466
|
+
client.post("/events", { events: [event] }),
|
|
2467
|
+
new Promise(
|
|
2468
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), API_TIMEOUT_MS)
|
|
2469
|
+
)
|
|
2470
|
+
]);
|
|
2471
|
+
} catch {
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
function createClient(program2) {
|
|
2475
|
+
const globalOpts = program2.opts();
|
|
2476
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
2477
|
+
return new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
2478
|
+
}
|
|
2479
|
+
function registerHookCommand(program2) {
|
|
2480
|
+
const hook = program2.command("hook").description("Platform hook callbacks (observation only, never gates agent actions)");
|
|
2481
|
+
hook.command("session-start").description("Log session start event").option("--platform <name>", "Platform name", "claude-code").action(async (opts) => {
|
|
2482
|
+
try {
|
|
2483
|
+
const client = createClient(program2);
|
|
2484
|
+
const event = {
|
|
2485
|
+
type: "session_start",
|
|
2486
|
+
platform: opts.platform ?? "claude-code",
|
|
2487
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2488
|
+
};
|
|
2489
|
+
await postSingleEventWithTimeout(client, event);
|
|
2490
|
+
} catch {
|
|
2491
|
+
}
|
|
2492
|
+
process.exit(0);
|
|
2493
|
+
});
|
|
2494
|
+
hook.command("session-end").description("Flush pending events and log session end").option("--platform <name>", "Platform name", "claude-code").action(async (opts) => {
|
|
2495
|
+
try {
|
|
2496
|
+
const client = createClient(program2);
|
|
2497
|
+
const pending = readPendingEvents();
|
|
2498
|
+
const endEvent = {
|
|
2499
|
+
type: "session_end",
|
|
2500
|
+
platform: opts.platform ?? "claude-code",
|
|
2501
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2502
|
+
};
|
|
2503
|
+
pending.push(endEvent);
|
|
2504
|
+
await postEventsWithTimeout(client, pending);
|
|
2505
|
+
} catch {
|
|
2506
|
+
}
|
|
2507
|
+
process.exit(0);
|
|
2508
|
+
});
|
|
2509
|
+
hook.command("post-tool-use").description("Buffer a tool-use event").option("--platform <name>", "Platform name", "claude-code").option("--tool <name>", "Tool name").option("--success <bool>", "Whether the tool call succeeded").action(async (opts) => {
|
|
2510
|
+
try {
|
|
2511
|
+
const event = {
|
|
2512
|
+
type: "tool_use",
|
|
2513
|
+
platform: opts.platform ?? "claude-code",
|
|
2514
|
+
tool: opts.tool,
|
|
2515
|
+
success: opts.success === "true" || opts.success === true,
|
|
2516
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2517
|
+
};
|
|
2518
|
+
const events = bufferEvent(event);
|
|
2519
|
+
if (events.length >= FLUSH_THRESHOLD) {
|
|
2520
|
+
const client = createClient(program2);
|
|
2521
|
+
await postEventsWithTimeout(client, events);
|
|
2522
|
+
}
|
|
2523
|
+
} catch {
|
|
2524
|
+
}
|
|
2525
|
+
process.exit(0);
|
|
2526
|
+
});
|
|
2527
|
+
hook.command("cursor-session-start").description("Log Cursor session start event").action(async () => {
|
|
2528
|
+
try {
|
|
2529
|
+
const client = createClient(program2);
|
|
2530
|
+
const event = {
|
|
2531
|
+
type: "session_start",
|
|
2532
|
+
platform: "cursor",
|
|
2533
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2534
|
+
};
|
|
2535
|
+
await postSingleEventWithTimeout(client, event);
|
|
2536
|
+
} catch {
|
|
2537
|
+
}
|
|
2538
|
+
process.exit(0);
|
|
2539
|
+
});
|
|
2540
|
+
hook.command("cursor-mcp-exec").description("Buffer a Cursor MCP tool execution event").option("--server <name>", "MCP server name").option("--tool <name>", "Tool name").action(async (opts) => {
|
|
2541
|
+
try {
|
|
2542
|
+
const event = {
|
|
2543
|
+
type: "mcp_exec",
|
|
2544
|
+
platform: "cursor",
|
|
2545
|
+
server: opts.server,
|
|
2546
|
+
tool: opts.tool,
|
|
2547
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2548
|
+
};
|
|
2549
|
+
const events = bufferEvent(event);
|
|
2550
|
+
if (events.length >= FLUSH_THRESHOLD) {
|
|
2551
|
+
const client = createClient(program2);
|
|
2552
|
+
await postEventsWithTimeout(client, events);
|
|
2553
|
+
}
|
|
2554
|
+
} catch {
|
|
2555
|
+
}
|
|
2556
|
+
process.exit(0);
|
|
2557
|
+
});
|
|
2558
|
+
hook.command("cursor-session-end").description("Flush pending events and log Cursor session end").action(async () => {
|
|
2559
|
+
try {
|
|
2560
|
+
const client = createClient(program2);
|
|
2561
|
+
const pending = readPendingEvents();
|
|
2562
|
+
const endEvent = {
|
|
2563
|
+
type: "session_end",
|
|
2564
|
+
platform: "cursor",
|
|
2565
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2566
|
+
};
|
|
2567
|
+
pending.push(endEvent);
|
|
2568
|
+
await postEventsWithTimeout(client, pending);
|
|
2569
|
+
} catch {
|
|
2570
|
+
}
|
|
2571
|
+
process.exit(0);
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// src/commands/setup.ts
|
|
2576
|
+
import { createInterface as createInterface5 } from "readline/promises";
|
|
2577
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2578
|
+
|
|
2579
|
+
// src/platform/templates/claude-code-skill.ts
|
|
2580
|
+
function getClaudeCodeSkillContent() {
|
|
2581
|
+
return `---
|
|
2582
|
+
name: caik
|
|
2583
|
+
description: Search, install, and manage AI coding artifacts (skills, rules, prompts, MCP servers, knowledge packs) from the CAIK community registry. Use when the user wants to find reusable building blocks, install artifacts, or contribute back to the community.
|
|
2584
|
+
---
|
|
2585
|
+
|
|
2586
|
+
# CAIK \u2014 AI Artifact Registry
|
|
2587
|
+
|
|
2588
|
+
CAIK is a community-driven registry for AI coding artifacts \u2014 skills, rules, prompts, MCP servers, and knowledge packs. Use CAIK to discover, install, and share reusable building blocks across projects.
|
|
2589
|
+
|
|
2590
|
+
## Available MCP Tools
|
|
2591
|
+
|
|
2592
|
+
When the CAIK MCP server is running, these tools are available:
|
|
2593
|
+
|
|
2594
|
+
| Tool | Description |
|
|
2595
|
+
|------|-------------|
|
|
2596
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2597
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2598
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2599
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2600
|
+
| \`get_alternatives\` | Find similar artifacts |
|
|
2601
|
+
|
|
2602
|
+
## Quick Commands
|
|
2603
|
+
|
|
2604
|
+
If MCP is unavailable, use the CLI directly:
|
|
2605
|
+
|
|
2606
|
+
- **Search:** \`caik search <query>\`
|
|
2607
|
+
- **Install:** \`caik install <slug>\`
|
|
2608
|
+
- **Status:** \`caik status\`
|
|
2609
|
+
- **Update:** \`caik update [slug]\`
|
|
2610
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2611
|
+
|
|
2612
|
+
## Contributing Back
|
|
2613
|
+
|
|
2614
|
+
When an artifact works well for a task, use \`report_outcome\` (MCP) or \`caik flag --positive <slug>\` to help the community. Your contributions earn karma on the CAIK leaderboard.
|
|
2615
|
+
|
|
2616
|
+
## Discovery
|
|
2617
|
+
|
|
2618
|
+
Ask your agent: "Find CAIK artifacts for [your use case]" \u2014 the agent will use the \`search\` tool to find relevant artifacts from the registry.
|
|
2619
|
+
`;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// src/platform/templates/cursor-skill.ts
|
|
2623
|
+
function getCursorSkillContent() {
|
|
2624
|
+
return `---
|
|
2625
|
+
name: caik
|
|
2626
|
+
description: Search, install, and manage AI coding artifacts (skills, rules, prompts, MCP servers, knowledge packs) from the CAIK community registry. Use when the user wants to find reusable building blocks, install artifacts, or contribute back to the community.
|
|
2627
|
+
---
|
|
2628
|
+
|
|
2629
|
+
# CAIK \u2014 AI Artifact Registry
|
|
2630
|
+
|
|
2631
|
+
CAIK is a community-driven registry for AI coding artifacts. Use CAIK to discover and install rules, prompts, and knowledge packs into your Cursor workspace.
|
|
2632
|
+
|
|
2633
|
+
## Cursor Rules Integration
|
|
2634
|
+
|
|
2635
|
+
CAIK artifacts install directly into your \`.cursorrules\` file. Installed rules are automatically loaded by Cursor's AI features.
|
|
2636
|
+
|
|
2637
|
+
## MCP Tools
|
|
2638
|
+
|
|
2639
|
+
If the CAIK MCP server is configured, these tools are available:
|
|
2640
|
+
|
|
2641
|
+
| Tool | Description |
|
|
2642
|
+
|------|-------------|
|
|
2643
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2644
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2645
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2646
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2647
|
+
|
|
2648
|
+
## CLI Fallback
|
|
2649
|
+
|
|
2650
|
+
- **Search:** \`caik search <query>\`
|
|
2651
|
+
- **Install:** \`caik install <slug>\`
|
|
2652
|
+
- **Status:** \`caik status\`
|
|
2653
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2654
|
+
|
|
2655
|
+
## Discovery
|
|
2656
|
+
|
|
2657
|
+
Ask Cursor: "Find CAIK artifacts for [your use case]" to search the registry.
|
|
2658
|
+
`;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/platform/templates/openclaw-skill.ts
|
|
2662
|
+
function getOpenClawSkillContent() {
|
|
2663
|
+
return `---
|
|
2664
|
+
name: caik
|
|
2665
|
+
description: Search, install, and manage AI coding artifacts (skills, rules, prompts, MCP servers, knowledge packs) from the CAIK community registry. Use when the user wants to find reusable building blocks, install artifacts, or contribute back to the community.
|
|
2666
|
+
---
|
|
2667
|
+
|
|
2668
|
+
# CAIK \u2014 AI Artifact Registry
|
|
2669
|
+
|
|
2670
|
+
CAIK is a community-driven registry for AI coding artifacts \u2014 skills, rules, prompts, MCP servers, and knowledge packs. Use CAIK to discover and install reusable building blocks.
|
|
2671
|
+
|
|
2672
|
+
## Skill Capabilities
|
|
2673
|
+
|
|
2674
|
+
CAIK artifacts integrate with OpenClaw's skill system. Installed skills are available as callable capabilities within your OpenClaw agent.
|
|
2675
|
+
|
|
2676
|
+
## Contribution Tracking
|
|
2677
|
+
|
|
2678
|
+
CAIK uses OpenClaw lifecycle hooks to track artifact usage. When you invoke a CAIK-installed skill, outcome data is reported automatically to build your contribution level and community karma. You can disable it with \`caik config set contributions false\`.
|
|
2679
|
+
|
|
2680
|
+
## MCP Tools
|
|
2681
|
+
|
|
2682
|
+
| Tool | Description |
|
|
2683
|
+
|------|-------------|
|
|
2684
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2685
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2686
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2687
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2688
|
+
| \`get_alternatives\` | Find similar artifacts |
|
|
2689
|
+
|
|
2690
|
+
## CLI Fallback
|
|
2691
|
+
|
|
2692
|
+
- **Search:** \`caik search <query>\`
|
|
2693
|
+
- **Install:** \`caik install <slug>\`
|
|
2694
|
+
- **Status:** \`caik status\`
|
|
2695
|
+
- **Update:** \`caik update [slug]\`
|
|
2696
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2697
|
+
|
|
2698
|
+
## Discovery
|
|
2699
|
+
|
|
2700
|
+
Ask your agent: "Find CAIK artifacts for [your use case]" to search the registry.
|
|
2701
|
+
`;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// src/platform/templates/index.ts
|
|
2705
|
+
function getSkillContent(platform2) {
|
|
2706
|
+
switch (platform2) {
|
|
2707
|
+
case "claude-code":
|
|
2708
|
+
return getClaudeCodeSkillContent();
|
|
2709
|
+
case "cursor":
|
|
2710
|
+
return getCursorSkillContent();
|
|
2711
|
+
case "openclaw":
|
|
2712
|
+
return getOpenClawSkillContent();
|
|
2713
|
+
default:
|
|
2714
|
+
return getGenericSkillContent();
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function getGenericSkillContent() {
|
|
2718
|
+
return `# CAIK \u2014 AI Artifact Registry
|
|
2719
|
+
|
|
2720
|
+
CAIK is a community-driven registry for AI coding artifacts \u2014 skills, rules, prompts, MCP servers, and knowledge packs.
|
|
2721
|
+
|
|
2722
|
+
## CLI Commands
|
|
2723
|
+
|
|
2724
|
+
- **Search:** \`caik search <query>\`
|
|
2725
|
+
- **Install:** \`caik install <slug>\`
|
|
2726
|
+
- **Status:** \`caik status\`
|
|
2727
|
+
- **Update:** \`caik update [slug]\`
|
|
2728
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2729
|
+
|
|
2730
|
+
## MCP Tools
|
|
2731
|
+
|
|
2732
|
+
If the CAIK MCP server is configured, these tools are available:
|
|
2733
|
+
|
|
2734
|
+
| Tool | Description |
|
|
2735
|
+
|------|-------------|
|
|
2736
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2737
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2738
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2739
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2740
|
+
|
|
2741
|
+
## Discovery
|
|
2742
|
+
|
|
2743
|
+
Ask your agent: "Find CAIK artifacts for [your use case]" to search the registry.
|
|
2744
|
+
`;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// src/commands/setup.ts
|
|
2748
|
+
function getHookConfig(platform2) {
|
|
2749
|
+
if (platform2 === "claude-code") {
|
|
2750
|
+
return {
|
|
2751
|
+
hooks: {
|
|
2752
|
+
SessionStart: [{ type: "command", command: "npx -y @caik.dev/cli hook session-start" }],
|
|
2753
|
+
PostToolUse: [{ type: "command", command: "npx -y @caik.dev/cli hook post-tool-use" }],
|
|
2754
|
+
SessionEnd: [{ type: "command", command: "npx -y @caik.dev/cli hook session-end" }]
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
if (platform2 === "cursor") {
|
|
2759
|
+
return {
|
|
2760
|
+
hooks: {
|
|
2761
|
+
sessionStart: "npx -y @caik.dev/cli hook cursor-session-start",
|
|
2762
|
+
beforeMCPExecution: "npx -y @caik.dev/cli hook cursor-mcp-exec",
|
|
2763
|
+
stop: "npx -y @caik.dev/cli hook cursor-session-end"
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
return {
|
|
2768
|
+
hooks: {
|
|
2769
|
+
session_start: "npx -y @caik.dev/cli hook session-start --platform openclaw",
|
|
2770
|
+
session_end: "npx -y @caik.dev/cli hook session-end --platform openclaw",
|
|
2771
|
+
after_tool_call: "npx -y @caik.dev/cli hook post-tool-use --platform openclaw"
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
async function runRepair() {
|
|
2776
|
+
console.log(heading("\nCAIK Repair"));
|
|
2777
|
+
console.log("\u2550".repeat(40));
|
|
2778
|
+
console.log();
|
|
2779
|
+
const spinner = createSpinner("Detecting platforms...");
|
|
2780
|
+
spinner.start();
|
|
2781
|
+
const detected = detectPlatforms();
|
|
2782
|
+
spinner.stop();
|
|
2783
|
+
if (detected.length === 0) {
|
|
2784
|
+
console.log(warn("No supported agent platforms detected. Nothing to repair."));
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
console.log(info(`Detected ${detected.length} platform(s): ${detected.map((p) => p.name).join(", ")}`));
|
|
2788
|
+
console.log();
|
|
2789
|
+
let repairedMcp = 0;
|
|
2790
|
+
let repairedHooks = 0;
|
|
2791
|
+
let repairedSkills = 0;
|
|
2792
|
+
const registryWarnings = [];
|
|
2793
|
+
for (const platform2 of detected) {
|
|
2794
|
+
console.log(heading(`Checking ${platform2.name}...`));
|
|
2795
|
+
console.log("\u2500".repeat(40));
|
|
2796
|
+
const adapter = getPlatformAdapter(platform2.name);
|
|
2797
|
+
let mcpRegistered = false;
|
|
2798
|
+
try {
|
|
2799
|
+
mcpRegistered = await adapter.isRegistered();
|
|
2800
|
+
} catch {
|
|
2801
|
+
}
|
|
2802
|
+
if (mcpRegistered) {
|
|
2803
|
+
console.log(` MCP server: ${success("registered")}`);
|
|
2804
|
+
} else {
|
|
2805
|
+
const mcpSpinner = createSpinner(" Re-registering MCP server...");
|
|
2806
|
+
mcpSpinner.start();
|
|
2807
|
+
try {
|
|
2808
|
+
const mcpConfig = getDefaultMcpConfig();
|
|
2809
|
+
await adapter.registerMcp(mcpConfig);
|
|
2810
|
+
mcpSpinner.stop();
|
|
2811
|
+
console.log(` MCP server: ${success("repaired")} (re-registered)`);
|
|
2812
|
+
repairedMcp++;
|
|
2813
|
+
} catch (err) {
|
|
2814
|
+
mcpSpinner.stop();
|
|
2815
|
+
console.log(` MCP server: ${error(`repair failed: ${err instanceof Error ? err.message : String(err)}`)}`);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (adapter.registerHooks && platform2.capabilities.hooks) {
|
|
2819
|
+
const hookSpinner = createSpinner(" Ensuring hooks are registered...");
|
|
2820
|
+
hookSpinner.start();
|
|
2821
|
+
try {
|
|
2822
|
+
const hookConfig = getHookConfig(platform2.name);
|
|
2823
|
+
await adapter.registerHooks(hookConfig);
|
|
2824
|
+
hookSpinner.stop();
|
|
2825
|
+
console.log(` Hooks: ${success("ensured")}`);
|
|
2826
|
+
repairedHooks++;
|
|
2827
|
+
} catch (err) {
|
|
2828
|
+
hookSpinner.stop();
|
|
2829
|
+
console.log(` Hooks: ${error(`repair failed: ${err instanceof Error ? err.message : String(err)}`)}`);
|
|
2830
|
+
}
|
|
2831
|
+
} else if (!platform2.capabilities.hooks) {
|
|
2832
|
+
console.log(` Hooks: ${dim("not supported on this platform")}`);
|
|
2833
|
+
}
|
|
2834
|
+
if (adapter.installSkill && platform2.capabilities.skills) {
|
|
2835
|
+
const skillSpinner = createSpinner(" Ensuring SKILL.md is installed...");
|
|
2836
|
+
skillSpinner.start();
|
|
2837
|
+
try {
|
|
2838
|
+
const content = getSkillContent(platform2.name);
|
|
2839
|
+
await adapter.installSkill("caik", content);
|
|
2840
|
+
skillSpinner.stop();
|
|
2841
|
+
console.log(` SKILL.md: ${success("ensured")}`);
|
|
2842
|
+
repairedSkills++;
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
skillSpinner.stop();
|
|
2845
|
+
console.log(` SKILL.md: ${error(`repair failed: ${err instanceof Error ? err.message : String(err)}`)}`);
|
|
2846
|
+
}
|
|
2847
|
+
} else if (!platform2.capabilities.skills) {
|
|
2848
|
+
console.log(` SKILL.md: ${dim("not supported on this platform")}`);
|
|
2849
|
+
}
|
|
2850
|
+
console.log();
|
|
2851
|
+
}
|
|
2852
|
+
const registryEntries = listRegistryEntries();
|
|
2853
|
+
if (registryEntries.length > 0) {
|
|
2854
|
+
console.log(heading("Registry Integrity"));
|
|
2855
|
+
console.log("\u2500".repeat(40));
|
|
2856
|
+
let cleanEntries = 0;
|
|
2857
|
+
for (const entry of registryEntries) {
|
|
2858
|
+
const missing = entry.files.filter((f) => !existsSync12(f));
|
|
2859
|
+
if (missing.length > 0) {
|
|
2860
|
+
const msg = `${entry.slug} (${entry.platform}): ${missing.length} missing file(s)`;
|
|
2861
|
+
registryWarnings.push(msg);
|
|
2862
|
+
console.log(` ${warn(msg)}`);
|
|
2863
|
+
for (const f of missing) {
|
|
2864
|
+
console.log(` ${dim(f)}`);
|
|
2865
|
+
}
|
|
2866
|
+
} else {
|
|
2867
|
+
cleanEntries++;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (registryWarnings.length === 0) {
|
|
2871
|
+
console.log(` ${success("All")} ${registryEntries.length} registry entries have their files intact.`);
|
|
2872
|
+
} else if (cleanEntries > 0) {
|
|
2873
|
+
console.log(` ${success(`${cleanEntries}`)} entries OK, ${warn(`${registryWarnings.length}`)} with missing files.`);
|
|
2874
|
+
}
|
|
2875
|
+
if (registryWarnings.length > 0) {
|
|
2876
|
+
console.log();
|
|
2877
|
+
console.log(
|
|
2878
|
+
dim(" Artifact files cannot be auto-repaired. Run `caik install <slug>` to reinstall.")
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
console.log();
|
|
2882
|
+
}
|
|
2883
|
+
console.log(heading("Repair Summary"));
|
|
2884
|
+
console.log("\u2550".repeat(40));
|
|
2885
|
+
const actions = [];
|
|
2886
|
+
if (repairedMcp > 0) actions.push(`${repairedMcp} MCP registration(s) repaired`);
|
|
2887
|
+
if (repairedHooks > 0) actions.push(`${repairedHooks} hook registration(s) ensured`);
|
|
2888
|
+
if (repairedSkills > 0) actions.push(`${repairedSkills} skill installation(s) ensured`);
|
|
2889
|
+
if (registryWarnings.length > 0) actions.push(`${registryWarnings.length} registry warning(s)`);
|
|
2890
|
+
if (actions.length === 0) {
|
|
2891
|
+
console.log(success("Everything looks good. No repairs needed."));
|
|
2892
|
+
} else {
|
|
2893
|
+
for (const a of actions) {
|
|
2894
|
+
console.log(` ${info(a)}`);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
console.log();
|
|
2898
|
+
}
|
|
2899
|
+
function registerSetupCommand(program2) {
|
|
2900
|
+
program2.command("setup").description("Interactive setup wizard \u2014 configure CAIK for your agent platforms").option("--platform <name>", "Skip detection, configure a specific platform").option("--skip-hooks", "Register MCP only, skip hook registration").option("--skip-stacks", "Skip stack recommendations").option("--repair", "Detect and fix configuration drift (re-register MCP, hooks, skills)").option("-y, --yes", "Accept all defaults (non-interactive)").addHelpText("after", `
|
|
2901
|
+
Examples:
|
|
2902
|
+
caik setup
|
|
2903
|
+
caik setup --platform claude-code
|
|
2904
|
+
caik setup --yes --skip-hooks
|
|
2905
|
+
caik setup --repair`).action(async (opts) => {
|
|
2906
|
+
const globalOpts = program2.opts();
|
|
2907
|
+
const { apiKey } = resolveConfig(globalOpts);
|
|
2908
|
+
const skipHooks = Boolean(opts.skipHooks);
|
|
2909
|
+
const skipStacks = Boolean(opts.skipStacks);
|
|
2910
|
+
const autoYes = Boolean(opts.yes);
|
|
2911
|
+
const platformFlag = opts.platform;
|
|
2912
|
+
const repair = Boolean(opts.repair);
|
|
2913
|
+
if (repair) {
|
|
2914
|
+
await runRepair();
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
console.log(heading("\nCAIK Setup Wizard"));
|
|
2918
|
+
console.log("\u2550".repeat(40));
|
|
2919
|
+
console.log();
|
|
2920
|
+
let detected;
|
|
2921
|
+
if (platformFlag) {
|
|
2922
|
+
const adapter = getPlatformAdapter(platformFlag);
|
|
2923
|
+
const det = await adapter.detect();
|
|
2924
|
+
if (det) {
|
|
2925
|
+
detected = [det];
|
|
2926
|
+
} else {
|
|
2927
|
+
detected = [{
|
|
2928
|
+
name: platformFlag,
|
|
2929
|
+
tier: "cli-mcp",
|
|
2930
|
+
configPaths: [],
|
|
2931
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
2932
|
+
}];
|
|
2933
|
+
console.log(warn(`Platform "${platformFlag}" was not detected but will be configured anyway.`));
|
|
2934
|
+
}
|
|
2935
|
+
} else {
|
|
2936
|
+
const spinner = createSpinner("Detecting installed platforms...");
|
|
2937
|
+
spinner.start();
|
|
2938
|
+
detected = detectPlatforms();
|
|
2939
|
+
spinner.stop();
|
|
2940
|
+
if (detected.length === 0) {
|
|
2941
|
+
console.log(warn("No supported agent platforms detected."));
|
|
2942
|
+
console.log(info("Supported platforms: claude-code, cursor, openclaw, codex, windsurf"));
|
|
2943
|
+
console.log(info("Use --platform <name> to configure a specific platform."));
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
console.log(info(`Detected ${detected.length} platform(s):`));
|
|
2947
|
+
for (const p of detected) {
|
|
2948
|
+
console.log(` ${success(p.name)} ${dim(`(${p.tier})`)}`);
|
|
2949
|
+
}
|
|
2950
|
+
console.log();
|
|
2951
|
+
}
|
|
2952
|
+
let selected;
|
|
2953
|
+
if (autoYes || detected.length === 1 || platformFlag) {
|
|
2954
|
+
selected = detected;
|
|
2955
|
+
} else {
|
|
2956
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
2957
|
+
console.log(info("Which platforms would you like to configure?"));
|
|
2958
|
+
for (let i = 0; i < detected.length; i++) {
|
|
2959
|
+
console.log(` ${i + 1}. ${detected[i].name}`);
|
|
2960
|
+
}
|
|
2961
|
+
console.log(` a. All`);
|
|
2962
|
+
const answer = await rl.question("\nSelect (numbers separated by commas, or 'a' for all): ");
|
|
2963
|
+
rl.close();
|
|
2964
|
+
if (answer.toLowerCase() === "a" || answer.trim() === "") {
|
|
2965
|
+
selected = detected;
|
|
2966
|
+
} else {
|
|
2967
|
+
const indices = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1);
|
|
2968
|
+
selected = indices.filter((i) => i >= 0 && i < detected.length).map((i) => detected[i]);
|
|
2969
|
+
if (selected.length === 0) {
|
|
2970
|
+
console.log(warn("No valid platforms selected. Exiting."));
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
console.log();
|
|
2976
|
+
for (const platform2 of selected) {
|
|
2977
|
+
console.log(heading(`Configuring ${platform2.name}...`));
|
|
2978
|
+
console.log("\u2500".repeat(40));
|
|
2979
|
+
const adapter = getPlatformAdapter(platform2.name);
|
|
2980
|
+
const mcpConfig = getDefaultMcpConfig();
|
|
2981
|
+
const mcpSpinner = createSpinner("Registering MCP server...");
|
|
2982
|
+
mcpSpinner.start();
|
|
2983
|
+
try {
|
|
2984
|
+
await adapter.registerMcp(mcpConfig);
|
|
2985
|
+
mcpSpinner.stop();
|
|
2986
|
+
console.log(success("MCP server registered"));
|
|
2987
|
+
} catch (err) {
|
|
2988
|
+
mcpSpinner.stop();
|
|
2989
|
+
console.log(error(`Failed to register MCP server: ${err instanceof Error ? err.message : String(err)}`));
|
|
2990
|
+
}
|
|
2991
|
+
if (!skipHooks && adapter.registerHooks && platform2.capabilities.hooks) {
|
|
2992
|
+
const hookSpinner = createSpinner("Registering hooks...");
|
|
2993
|
+
hookSpinner.start();
|
|
2994
|
+
try {
|
|
2995
|
+
const hookConfig = getHookConfig(platform2.name);
|
|
2996
|
+
await adapter.registerHooks(hookConfig);
|
|
2997
|
+
hookSpinner.stop();
|
|
2998
|
+
console.log(success("Hooks registered"));
|
|
2999
|
+
if (platform2.name === "openclaw") {
|
|
3000
|
+
console.log(dim(" Restart the OpenClaw gateway to load hooks: openclaw daemon restart"));
|
|
3001
|
+
}
|
|
3002
|
+
} catch (err) {
|
|
3003
|
+
hookSpinner.stop();
|
|
3004
|
+
console.log(error(`Failed to register hooks: ${err instanceof Error ? err.message : String(err)}`));
|
|
3005
|
+
}
|
|
3006
|
+
} else if (!skipHooks && !platform2.capabilities.hooks) {
|
|
3007
|
+
console.log(dim(" Hooks not supported on this platform \u2014 skipped"));
|
|
3008
|
+
} else if (skipHooks) {
|
|
3009
|
+
console.log(dim(" Hooks skipped (--skip-hooks)"));
|
|
3010
|
+
}
|
|
3011
|
+
if (adapter.installSkill && platform2.capabilities.skills) {
|
|
3012
|
+
const skillSpinner = createSpinner("Installing SKILL.md...");
|
|
3013
|
+
skillSpinner.start();
|
|
3014
|
+
try {
|
|
3015
|
+
const content = getSkillContent(platform2.name);
|
|
3016
|
+
await adapter.installSkill("caik", content);
|
|
3017
|
+
skillSpinner.stop();
|
|
3018
|
+
console.log(success("SKILL.md installed"));
|
|
3019
|
+
} catch (err) {
|
|
3020
|
+
skillSpinner.stop();
|
|
3021
|
+
console.log(error(`Failed to install SKILL.md: ${err instanceof Error ? err.message : String(err)}`));
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
console.log();
|
|
3025
|
+
}
|
|
3026
|
+
if (!skipStacks) {
|
|
3027
|
+
console.log(dim("Stack recommendations: coming soon"));
|
|
3028
|
+
console.log();
|
|
3029
|
+
}
|
|
3030
|
+
if (!apiKey) {
|
|
3031
|
+
console.log(warn("Not authenticated. Run `caik init --auth` to connect your account."));
|
|
3032
|
+
console.log();
|
|
3033
|
+
}
|
|
3034
|
+
console.log(heading("Setup Complete"));
|
|
3035
|
+
console.log("\u2550".repeat(40));
|
|
3036
|
+
console.log(success(`Configured ${selected.length} platform(s): ${selected.map((p) => p.name).join(", ")}`));
|
|
3037
|
+
if (!apiKey) {
|
|
3038
|
+
console.log(info("Next step: run `caik init --auth` to authenticate"));
|
|
3039
|
+
} else {
|
|
3040
|
+
console.log(info("Next step: run `caik search <query>` to find artifacts"));
|
|
3041
|
+
}
|
|
3042
|
+
console.log();
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/commands/upgrade.ts
|
|
3047
|
+
import { execSync as execSync5 } from "child_process";
|
|
3048
|
+
|
|
3049
|
+
// src/update-check.ts
|
|
3050
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13 } from "fs";
|
|
3051
|
+
import { join as join8 } from "path";
|
|
3052
|
+
import { homedir as homedir7 } from "os";
|
|
3053
|
+
import chalk2 from "chalk";
|
|
3054
|
+
var PKG_NAME = "@caik.dev/cli";
|
|
3055
|
+
var CACHE_PATH = join8(homedir7(), ".caik", "update-check.json");
|
|
3056
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3057
|
+
function readCache() {
|
|
3058
|
+
try {
|
|
3059
|
+
const raw = readFileSync8(CACHE_PATH, "utf-8");
|
|
3060
|
+
return JSON.parse(raw);
|
|
3061
|
+
} catch {
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
function writeCache(entry) {
|
|
3066
|
+
const dir = join8(homedir7(), ".caik");
|
|
3067
|
+
if (!existsSync13(dir)) {
|
|
3068
|
+
mkdirSync9(dir, { recursive: true });
|
|
3069
|
+
}
|
|
3070
|
+
writeFileSync9(CACHE_PATH, JSON.stringify(entry) + "\n", "utf-8");
|
|
3071
|
+
}
|
|
3072
|
+
function getCurrentVersion() {
|
|
3073
|
+
try {
|
|
3074
|
+
const pkgPath = join8(
|
|
3075
|
+
new URL(".", import.meta.url).pathname,
|
|
3076
|
+
"..",
|
|
3077
|
+
"package.json"
|
|
3078
|
+
);
|
|
3079
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
3080
|
+
return pkg.version;
|
|
3081
|
+
} catch {
|
|
3082
|
+
return "0.0.0";
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
async function fetchLatestVersion() {
|
|
3086
|
+
try {
|
|
3087
|
+
const controller = new AbortController();
|
|
3088
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
3089
|
+
const res = await fetch(
|
|
3090
|
+
`https://registry.npmjs.org/${PKG_NAME}/latest`,
|
|
3091
|
+
{ signal: controller.signal }
|
|
3092
|
+
);
|
|
3093
|
+
clearTimeout(timeout);
|
|
3094
|
+
if (!res.ok) return null;
|
|
3095
|
+
const data = await res.json();
|
|
3096
|
+
return data.version ?? null;
|
|
3097
|
+
} catch {
|
|
3098
|
+
return null;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
function isNewer(latest, current) {
|
|
3102
|
+
const a = latest.split(".").map(Number);
|
|
3103
|
+
const b = current.split(".").map(Number);
|
|
3104
|
+
for (let i = 0; i < 3; i++) {
|
|
3105
|
+
if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
|
|
3106
|
+
if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
|
|
3107
|
+
}
|
|
3108
|
+
return false;
|
|
3109
|
+
}
|
|
3110
|
+
async function checkForUpdate(opts = {}) {
|
|
3111
|
+
const current = getCurrentVersion();
|
|
3112
|
+
if (!opts.force) {
|
|
3113
|
+
const cache = readCache();
|
|
3114
|
+
if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
|
|
3115
|
+
return {
|
|
3116
|
+
current,
|
|
3117
|
+
latest: cache.latest,
|
|
3118
|
+
updateAvailable: isNewer(cache.latest, current)
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
const latest = await fetchLatestVersion();
|
|
3123
|
+
if (latest) {
|
|
3124
|
+
writeCache({ latest, checkedAt: Date.now() });
|
|
3125
|
+
}
|
|
3126
|
+
return {
|
|
3127
|
+
current,
|
|
3128
|
+
latest,
|
|
3129
|
+
updateAvailable: latest ? isNewer(latest, current) : false
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
async function printUpdateHint() {
|
|
3133
|
+
try {
|
|
3134
|
+
const info2 = await checkForUpdate();
|
|
3135
|
+
if (info2.updateAvailable && info2.latest) {
|
|
3136
|
+
console.error(
|
|
3137
|
+
chalk2.yellow(
|
|
3138
|
+
`
|
|
3139
|
+
Update available: ${info2.current} \u2192 ${info2.latest} Run ${chalk2.bold("caik upgrade")} to update
|
|
3140
|
+
`
|
|
3141
|
+
)
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
} catch {
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// src/commands/upgrade.ts
|
|
3149
|
+
function registerUpgradeCommand(program2) {
|
|
3150
|
+
program2.command("upgrade").description("Upgrade the CAIK CLI to the latest version").action(async () => {
|
|
3151
|
+
const spinner = createSpinner("Checking for updates...");
|
|
3152
|
+
spinner.start();
|
|
3153
|
+
const update = await checkForUpdate({ force: true });
|
|
3154
|
+
if (!update.latest) {
|
|
3155
|
+
spinner.stop();
|
|
3156
|
+
console.log(error("Could not determine the latest version. Check your network connection."));
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (!update.updateAvailable) {
|
|
3160
|
+
spinner.stop();
|
|
3161
|
+
console.log(success(`Already on the latest version (${update.current})`));
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
spinner.text = `Upgrading ${update.current} \u2192 ${update.latest}...`;
|
|
3165
|
+
try {
|
|
3166
|
+
execSync5("npm install -g @caik.dev/cli@latest", {
|
|
3167
|
+
stdio: "pipe",
|
|
3168
|
+
timeout: 6e4
|
|
3169
|
+
});
|
|
3170
|
+
spinner.stop();
|
|
3171
|
+
console.log(success(`Upgraded to ${update.latest}`));
|
|
3172
|
+
} catch (err) {
|
|
3173
|
+
spinner.stop();
|
|
3174
|
+
console.log(error("Upgrade failed. Try manually:"));
|
|
3175
|
+
console.log(info("npm install -g @caik.dev/cli@latest"));
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
|
|
772
3180
|
// src/index.ts
|
|
773
3181
|
var __filename = fileURLToPath(import.meta.url);
|
|
774
|
-
var __dirname =
|
|
3182
|
+
var __dirname = dirname5(__filename);
|
|
775
3183
|
var version = "0.0.1";
|
|
776
3184
|
try {
|
|
777
|
-
const pkgPath =
|
|
778
|
-
const pkg = JSON.parse(
|
|
3185
|
+
const pkgPath = join9(__dirname, "..", "package.json");
|
|
3186
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
779
3187
|
version = pkg.version;
|
|
780
3188
|
} catch {
|
|
781
3189
|
}
|
|
@@ -793,15 +3201,19 @@ registerAlternativesCommand(program);
|
|
|
793
3201
|
registerUpdateCommand(program);
|
|
794
3202
|
registerUninstallCommand(program);
|
|
795
3203
|
registerKarmaCommand(program);
|
|
3204
|
+
registerHookCommand(program);
|
|
3205
|
+
registerSetupCommand(program);
|
|
3206
|
+
registerUpgradeCommand(program);
|
|
796
3207
|
program.exitOverride();
|
|
797
3208
|
async function main() {
|
|
3209
|
+
const updateHint = printUpdateHint();
|
|
798
3210
|
try {
|
|
799
3211
|
await program.parseAsync(process.argv);
|
|
800
3212
|
} catch (err) {
|
|
801
3213
|
if (err instanceof CaikError) {
|
|
802
|
-
console.error(
|
|
3214
|
+
console.error(chalk3.red(`\u2717 ${err.message}`));
|
|
803
3215
|
if (err.suggestion) {
|
|
804
|
-
console.error(
|
|
3216
|
+
console.error(chalk3.dim(` ${err.suggestion}`));
|
|
805
3217
|
}
|
|
806
3218
|
process.exit(err.exitCode);
|
|
807
3219
|
}
|
|
@@ -809,8 +3221,9 @@ async function main() {
|
|
|
809
3221
|
const exitCode = err.exitCode;
|
|
810
3222
|
process.exit(exitCode);
|
|
811
3223
|
}
|
|
812
|
-
console.error(
|
|
3224
|
+
console.error(chalk3.red(`\u2717 Unexpected error: ${err instanceof Error ? err.message : String(err)}`));
|
|
813
3225
|
process.exit(1);
|
|
814
3226
|
}
|
|
3227
|
+
await updateHint;
|
|
815
3228
|
}
|
|
816
3229
|
main();
|