@caik.dev/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/dist/index.js +1857 -49
- package/package.json +21 -2
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
8
8
|
import chalk2 from "chalk";
|
|
9
9
|
|
|
10
10
|
// src/errors.ts
|
|
@@ -186,6 +186,9 @@ function error(msg) {
|
|
|
186
186
|
function info(msg) {
|
|
187
187
|
return chalk.cyan(`\u2192 ${msg}`);
|
|
188
188
|
}
|
|
189
|
+
function warn(msg) {
|
|
190
|
+
return chalk.yellow(`\u26A0 ${msg}`);
|
|
191
|
+
}
|
|
189
192
|
function dim(msg) {
|
|
190
193
|
return chalk.dim(msg);
|
|
191
194
|
}
|
|
@@ -291,17 +294,908 @@ ${result.total} result${result.total !== 1 ? "s" : ""} found.`);
|
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
// src/commands/install.ts
|
|
297
|
+
import { existsSync as existsSync7 } from "fs";
|
|
298
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
|
|
299
|
+
import { dirname as dirname3, resolve } from "path";
|
|
300
|
+
import { execSync as execSync3 } from "child_process";
|
|
301
|
+
import { createInterface } from "readline/promises";
|
|
302
|
+
|
|
303
|
+
// src/platform/detect.ts
|
|
294
304
|
import { existsSync as existsSync2 } from "fs";
|
|
295
|
-
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
296
|
-
import { dirname, resolve } from "path";
|
|
297
305
|
import { execSync } from "child_process";
|
|
298
|
-
import {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
import { join as join2 } from "path";
|
|
307
|
+
import { homedir as homedir2 } from "os";
|
|
308
|
+
function commandExists(cmd) {
|
|
309
|
+
try {
|
|
310
|
+
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function detectClaudeCode() {
|
|
317
|
+
const home = homedir2();
|
|
318
|
+
const claudeDir = join2(home, ".claude");
|
|
319
|
+
if (!existsSync2(claudeDir)) return null;
|
|
320
|
+
const hasSettings = existsSync2(join2(claudeDir, "settings.json"));
|
|
321
|
+
const hasMcpConfig = existsSync2(join2(claudeDir, "mcp.json"));
|
|
322
|
+
const hasAnyMarker = hasSettings || hasMcpConfig || existsSync2(join2(claudeDir, "statsig"));
|
|
323
|
+
if (!hasAnyMarker) return null;
|
|
324
|
+
const configPaths = [];
|
|
325
|
+
if (hasSettings) configPaths.push(join2(claudeDir, "settings.json"));
|
|
326
|
+
if (hasMcpConfig) configPaths.push(join2(claudeDir, "mcp.json"));
|
|
327
|
+
return {
|
|
328
|
+
name: "claude-code",
|
|
329
|
+
tier: "full-plugin",
|
|
330
|
+
configPaths,
|
|
331
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function detectOpenClaw() {
|
|
335
|
+
const home = homedir2();
|
|
336
|
+
const openclawDir = join2(home, ".openclaw");
|
|
337
|
+
const cwdConfig = join2(process.cwd(), "openclaw.json");
|
|
338
|
+
if (!existsSync2(openclawDir) && !existsSync2(cwdConfig)) return null;
|
|
339
|
+
const configPaths = [];
|
|
340
|
+
if (existsSync2(cwdConfig)) configPaths.push(cwdConfig);
|
|
341
|
+
if (existsSync2(join2(openclawDir, "openclaw.json"))) {
|
|
342
|
+
configPaths.push(join2(openclawDir, "openclaw.json"));
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
name: "openclaw",
|
|
346
|
+
tier: "hook-enabled",
|
|
347
|
+
configPaths,
|
|
348
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function detectCursor() {
|
|
352
|
+
const home = homedir2();
|
|
353
|
+
const cursorDir = join2(home, ".cursor");
|
|
354
|
+
const cwdCursor = join2(process.cwd(), ".cursor");
|
|
355
|
+
if (!existsSync2(cursorDir) && !existsSync2(cwdCursor)) return null;
|
|
356
|
+
const configPaths = [];
|
|
357
|
+
if (existsSync2(join2(cwdCursor, "mcp.json"))) {
|
|
358
|
+
configPaths.push(join2(cwdCursor, "mcp.json"));
|
|
359
|
+
}
|
|
360
|
+
if (existsSync2(join2(cursorDir, "mcp.json"))) {
|
|
361
|
+
configPaths.push(join2(cursorDir, "mcp.json"));
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
name: "cursor",
|
|
365
|
+
tier: "hook-enabled",
|
|
366
|
+
configPaths,
|
|
367
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function detectCodex() {
|
|
371
|
+
if (!commandExists("codex")) return null;
|
|
372
|
+
return {
|
|
373
|
+
name: "codex",
|
|
374
|
+
tier: "cli-mcp",
|
|
375
|
+
configPaths: [],
|
|
376
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function detectWindsurf() {
|
|
380
|
+
const home = homedir2();
|
|
381
|
+
const windsurfDir = join2(home, ".windsurf");
|
|
382
|
+
const cwdWindsurf = join2(process.cwd(), ".windsurf");
|
|
383
|
+
if (!existsSync2(windsurfDir) && !existsSync2(cwdWindsurf)) return null;
|
|
384
|
+
const configPaths = [];
|
|
385
|
+
if (existsSync2(join2(cwdWindsurf, "mcp.json"))) {
|
|
386
|
+
configPaths.push(join2(cwdWindsurf, "mcp.json"));
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
name: "windsurf",
|
|
390
|
+
tier: "cli-mcp",
|
|
391
|
+
configPaths,
|
|
392
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
var detectors = [
|
|
396
|
+
detectClaudeCode,
|
|
397
|
+
detectOpenClaw,
|
|
398
|
+
detectCursor,
|
|
399
|
+
detectCodex,
|
|
400
|
+
detectWindsurf
|
|
401
|
+
];
|
|
402
|
+
function detectPlatforms() {
|
|
403
|
+
const detected = [];
|
|
404
|
+
for (const detect of detectors) {
|
|
405
|
+
try {
|
|
406
|
+
const result = detect();
|
|
407
|
+
if (result) detected.push(result);
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return detected;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/platform/registry.ts
|
|
415
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
416
|
+
import { join as join3 } from "path";
|
|
417
|
+
var REGISTRY_VERSION = 1;
|
|
418
|
+
function getRegistryPath() {
|
|
419
|
+
return join3(getConfigDir(), "registry.json");
|
|
420
|
+
}
|
|
421
|
+
function emptyRegistry() {
|
|
422
|
+
return { version: REGISTRY_VERSION, entries: [] };
|
|
423
|
+
}
|
|
424
|
+
function readRegistry() {
|
|
425
|
+
const path = getRegistryPath();
|
|
426
|
+
if (!existsSync3(path)) return emptyRegistry();
|
|
427
|
+
try {
|
|
428
|
+
const raw = readFileSync2(path, "utf-8");
|
|
429
|
+
const parsed = JSON.parse(raw);
|
|
430
|
+
if (!parsed.entries || !Array.isArray(parsed.entries)) return emptyRegistry();
|
|
431
|
+
return parsed;
|
|
432
|
+
} catch {
|
|
433
|
+
return emptyRegistry();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function writeRegistry(registry) {
|
|
437
|
+
const dir = getConfigDir();
|
|
438
|
+
if (!existsSync3(dir)) {
|
|
439
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
440
|
+
}
|
|
441
|
+
const path = getRegistryPath();
|
|
442
|
+
writeFileSync2(path, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
443
|
+
}
|
|
444
|
+
function upsertRegistryEntry(entry) {
|
|
445
|
+
const registry = readRegistry();
|
|
446
|
+
const idx = registry.entries.findIndex(
|
|
447
|
+
(e) => e.slug === entry.slug && e.platform === entry.platform
|
|
448
|
+
);
|
|
449
|
+
if (idx >= 0) {
|
|
450
|
+
registry.entries[idx] = entry;
|
|
451
|
+
} else {
|
|
452
|
+
registry.entries.push(entry);
|
|
453
|
+
}
|
|
454
|
+
writeRegistry(registry);
|
|
455
|
+
}
|
|
456
|
+
function removeRegistryEntry(slug, platform) {
|
|
457
|
+
const registry = readRegistry();
|
|
458
|
+
const idx = registry.entries.findIndex(
|
|
459
|
+
(e) => e.slug === slug && e.platform === platform
|
|
460
|
+
);
|
|
461
|
+
if (idx < 0) return null;
|
|
462
|
+
const [removed] = registry.entries.splice(idx, 1);
|
|
463
|
+
writeRegistry(registry);
|
|
464
|
+
return removed;
|
|
465
|
+
}
|
|
466
|
+
function findRegistryEntry(slug, platform) {
|
|
467
|
+
const registry = readRegistry();
|
|
468
|
+
return registry.entries.find(
|
|
469
|
+
(e) => e.slug === slug && (!platform || e.platform === platform)
|
|
470
|
+
) ?? null;
|
|
471
|
+
}
|
|
472
|
+
function listRegistryEntries(platform) {
|
|
473
|
+
const registry = readRegistry();
|
|
474
|
+
if (!platform) return registry.entries;
|
|
475
|
+
return registry.entries.filter((e) => e.platform === platform);
|
|
476
|
+
}
|
|
477
|
+
function cleanupFiles(entry) {
|
|
478
|
+
const failed = [];
|
|
479
|
+
for (const file of entry.files) {
|
|
480
|
+
try {
|
|
481
|
+
if (existsSync3(file)) {
|
|
482
|
+
unlinkSync(file);
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
failed.push(file);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return failed;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/platform/claude-code.ts
|
|
492
|
+
import {
|
|
493
|
+
existsSync as existsSync4,
|
|
494
|
+
readFileSync as readFileSync3,
|
|
495
|
+
writeFileSync as writeFileSync3,
|
|
496
|
+
mkdirSync as mkdirSync3,
|
|
497
|
+
readdirSync,
|
|
498
|
+
rmSync
|
|
499
|
+
} from "fs";
|
|
500
|
+
import { join as join4 } from "path";
|
|
501
|
+
import { homedir as homedir3 } from "os";
|
|
502
|
+
var MCP_KEY = "caik";
|
|
503
|
+
var ClaudeCodeAdapter = class {
|
|
504
|
+
name = "claude-code";
|
|
505
|
+
tier = "full-plugin";
|
|
506
|
+
get claudeDir() {
|
|
507
|
+
return join4(homedir3(), ".claude");
|
|
508
|
+
}
|
|
509
|
+
get mcpPath() {
|
|
510
|
+
return join4(this.claudeDir, "mcp.json");
|
|
511
|
+
}
|
|
512
|
+
get settingsPath() {
|
|
513
|
+
return join4(this.claudeDir, "settings.json");
|
|
514
|
+
}
|
|
515
|
+
get skillsDir() {
|
|
516
|
+
return join4(this.claudeDir, "skills", "caik");
|
|
517
|
+
}
|
|
518
|
+
get pluginsCacheDir() {
|
|
519
|
+
return join4(this.claudeDir, "plugins", "cache");
|
|
520
|
+
}
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// JSON helpers
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
readJson(path) {
|
|
525
|
+
if (!existsSync4(path)) return {};
|
|
526
|
+
try {
|
|
527
|
+
const raw = readFileSync3(path, "utf-8");
|
|
528
|
+
const parsed = JSON.parse(raw);
|
|
529
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
530
|
+
return parsed;
|
|
531
|
+
}
|
|
532
|
+
return {};
|
|
533
|
+
} catch {
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
writeJson(path, data) {
|
|
538
|
+
const dir = join4(path, "..");
|
|
539
|
+
if (!existsSync4(dir)) {
|
|
540
|
+
mkdirSync3(dir, { recursive: true });
|
|
541
|
+
}
|
|
542
|
+
writeFileSync3(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
543
|
+
}
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Plugin detection
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
/**
|
|
548
|
+
* Check if CAIK is installed as a Claude Code plugin.
|
|
549
|
+
* Looks for "caik" in enabledPlugins in settings.json, or for a caik
|
|
550
|
+
* directory in the plugins cache.
|
|
551
|
+
*/
|
|
552
|
+
isPluginInstalled() {
|
|
553
|
+
const settings = this.readJson(this.settingsPath);
|
|
554
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
555
|
+
if (enabledPlugins) {
|
|
556
|
+
for (const key of Object.keys(enabledPlugins)) {
|
|
557
|
+
if (key.toLowerCase().includes("caik") && enabledPlugins[key] !== false) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (existsSync4(this.pluginsCacheDir)) {
|
|
563
|
+
try {
|
|
564
|
+
const publishers = readdirSync(this.pluginsCacheDir);
|
|
565
|
+
for (const publisher of publishers) {
|
|
566
|
+
const publisherDir = join4(this.pluginsCacheDir, publisher);
|
|
567
|
+
const caikDir = join4(publisherDir, "caik");
|
|
568
|
+
if (existsSync4(caikDir)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Returns the current install method:
|
|
579
|
+
* - "plugin" if CAIK plugin is installed via marketplace
|
|
580
|
+
* - "manual" if CAIK is registered in mcp.json manually
|
|
581
|
+
* - "none" if not installed
|
|
582
|
+
*/
|
|
583
|
+
getInstallMethod() {
|
|
584
|
+
if (this.isPluginInstalled()) return "plugin";
|
|
585
|
+
if (existsSync4(this.mcpPath)) {
|
|
586
|
+
const config = this.readJson(this.mcpPath);
|
|
587
|
+
const servers = config.mcpServers;
|
|
588
|
+
if (servers && MCP_KEY in servers) return "manual";
|
|
589
|
+
}
|
|
590
|
+
return "none";
|
|
591
|
+
}
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
// PlatformAdapter implementation
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
async detect() {
|
|
596
|
+
if (!existsSync4(this.claudeDir)) return null;
|
|
597
|
+
const hasSettings = existsSync4(this.settingsPath);
|
|
598
|
+
const hasMcp = existsSync4(this.mcpPath);
|
|
599
|
+
const hasStatsig = existsSync4(join4(this.claudeDir, "statsig"));
|
|
600
|
+
if (!hasSettings && !hasMcp && !hasStatsig) return null;
|
|
601
|
+
const configPaths = [];
|
|
602
|
+
if (hasSettings) configPaths.push(this.settingsPath);
|
|
603
|
+
if (hasMcp) configPaths.push(this.mcpPath);
|
|
604
|
+
return {
|
|
605
|
+
name: this.name,
|
|
606
|
+
tier: this.tier,
|
|
607
|
+
configPaths,
|
|
608
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
async registerMcp(serverConfig) {
|
|
612
|
+
if (this.isPluginInstalled()) {
|
|
613
|
+
console.log(
|
|
614
|
+
"CAIK plugin is installed \u2014 MCP server is managed by the plugin."
|
|
615
|
+
);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const config = this.readJson(this.mcpPath);
|
|
619
|
+
const servers = config.mcpServers ?? {};
|
|
620
|
+
servers[MCP_KEY] = {
|
|
621
|
+
command: serverConfig.command,
|
|
622
|
+
args: serverConfig.args,
|
|
623
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
624
|
+
};
|
|
625
|
+
config.mcpServers = servers;
|
|
626
|
+
this.writeJson(this.mcpPath, config);
|
|
627
|
+
}
|
|
628
|
+
async unregisterMcp() {
|
|
629
|
+
if (!existsSync4(this.mcpPath)) return;
|
|
630
|
+
const config = this.readJson(this.mcpPath);
|
|
631
|
+
const servers = config.mcpServers;
|
|
632
|
+
if (!servers || !(MCP_KEY in servers)) return;
|
|
633
|
+
delete servers[MCP_KEY];
|
|
634
|
+
config.mcpServers = servers;
|
|
635
|
+
this.writeJson(this.mcpPath, config);
|
|
636
|
+
}
|
|
637
|
+
async registerHooks(hookConfig) {
|
|
638
|
+
if (this.isPluginInstalled()) {
|
|
639
|
+
console.log(
|
|
640
|
+
"CAIK plugin is installed \u2014 hooks are managed by the plugin."
|
|
641
|
+
);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const settings = this.readJson(this.settingsPath);
|
|
645
|
+
const existingHooks = settings.hooks ?? {};
|
|
646
|
+
for (const [event, entries] of Object.entries(hookConfig.hooks)) {
|
|
647
|
+
const newEntries = Array.isArray(entries) ? entries : [entries];
|
|
648
|
+
const existing = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
|
|
649
|
+
const filtered = existing.filter((group) => {
|
|
650
|
+
if (typeof group === "object" && group !== null) {
|
|
651
|
+
const g = group;
|
|
652
|
+
if (Array.isArray(g.hooks)) {
|
|
653
|
+
const hasCAIK = g.hooks.some(
|
|
654
|
+
(h) => typeof h.command === "string" && h.command.includes("caik")
|
|
655
|
+
);
|
|
656
|
+
if (hasCAIK) return false;
|
|
657
|
+
}
|
|
658
|
+
if ("command" in g) {
|
|
659
|
+
return !g.command.includes("caik");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return true;
|
|
663
|
+
});
|
|
664
|
+
const wrappedEntries = newEntries.map((entry) => {
|
|
665
|
+
if (typeof entry === "object" && entry !== null) {
|
|
666
|
+
const e = entry;
|
|
667
|
+
const hookEntry = {
|
|
668
|
+
type: "command",
|
|
669
|
+
timeout: 10,
|
|
670
|
+
...e
|
|
671
|
+
};
|
|
672
|
+
return { hooks: [hookEntry] };
|
|
673
|
+
}
|
|
674
|
+
return entry;
|
|
675
|
+
});
|
|
676
|
+
existingHooks[event] = [...filtered, ...wrappedEntries];
|
|
677
|
+
}
|
|
678
|
+
settings.hooks = existingHooks;
|
|
679
|
+
this.writeJson(this.settingsPath, settings);
|
|
680
|
+
}
|
|
681
|
+
async unregisterHooks() {
|
|
682
|
+
if (!existsSync4(this.settingsPath)) return;
|
|
683
|
+
const settings = this.readJson(this.settingsPath);
|
|
684
|
+
const hooks = settings.hooks;
|
|
685
|
+
if (!hooks) return;
|
|
686
|
+
for (const event of Object.keys(hooks)) {
|
|
687
|
+
const entries = hooks[event];
|
|
688
|
+
if (!Array.isArray(entries)) continue;
|
|
689
|
+
hooks[event] = entries.filter((group) => {
|
|
690
|
+
if (typeof group === "object" && group !== null) {
|
|
691
|
+
const g = group;
|
|
692
|
+
if (Array.isArray(g.hooks)) {
|
|
693
|
+
const hasCAIK = g.hooks.some(
|
|
694
|
+
(h) => typeof h.command === "string" && h.command.includes("caik")
|
|
695
|
+
);
|
|
696
|
+
if (hasCAIK) return false;
|
|
697
|
+
}
|
|
698
|
+
if ("command" in g) {
|
|
699
|
+
return !g.command.includes("caik");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return true;
|
|
703
|
+
});
|
|
704
|
+
if (hooks[event].length === 0) {
|
|
705
|
+
delete hooks[event];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (Object.keys(hooks).length === 0) {
|
|
709
|
+
delete settings.hooks;
|
|
710
|
+
} else {
|
|
711
|
+
settings.hooks = hooks;
|
|
712
|
+
}
|
|
713
|
+
this.writeJson(this.settingsPath, settings);
|
|
714
|
+
}
|
|
715
|
+
async installSkill(slug, content, files) {
|
|
716
|
+
const skillDir = join4(this.skillsDir, slug);
|
|
717
|
+
if (!existsSync4(skillDir)) {
|
|
718
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
719
|
+
}
|
|
720
|
+
writeFileSync3(join4(skillDir, "SKILL.md"), content, "utf-8");
|
|
721
|
+
if (files) {
|
|
722
|
+
for (const file of files) {
|
|
723
|
+
const filePath = join4(skillDir, file.path);
|
|
724
|
+
const fileDir = join4(filePath, "..");
|
|
725
|
+
if (!existsSync4(fileDir)) {
|
|
726
|
+
mkdirSync3(fileDir, { recursive: true });
|
|
727
|
+
}
|
|
728
|
+
writeFileSync3(filePath, file.content, "utf-8");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async uninstallSkill(slug) {
|
|
733
|
+
const skillDir = join4(this.skillsDir, slug);
|
|
734
|
+
if (!existsSync4(skillDir)) return;
|
|
735
|
+
try {
|
|
736
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
getConfigPath() {
|
|
741
|
+
return this.mcpPath;
|
|
742
|
+
}
|
|
743
|
+
async readConfig() {
|
|
744
|
+
return this.readJson(this.mcpPath);
|
|
745
|
+
}
|
|
746
|
+
async isRegistered() {
|
|
747
|
+
if (this.isPluginInstalled()) return true;
|
|
748
|
+
if (!existsSync4(this.mcpPath)) return false;
|
|
749
|
+
const config = this.readJson(this.mcpPath);
|
|
750
|
+
const servers = config.mcpServers;
|
|
751
|
+
return !!servers && MCP_KEY in servers;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/platform/openclaw.ts
|
|
756
|
+
import {
|
|
757
|
+
existsSync as existsSync5,
|
|
758
|
+
readFileSync as readFileSync4,
|
|
759
|
+
writeFileSync as writeFileSync4,
|
|
760
|
+
mkdirSync as mkdirSync4,
|
|
761
|
+
rmSync as rmSync2
|
|
762
|
+
} from "fs";
|
|
763
|
+
import { execSync as execSync2 } from "child_process";
|
|
764
|
+
import { join as join5, dirname } from "path";
|
|
765
|
+
import { homedir as homedir4 } from "os";
|
|
766
|
+
var MCP_KEY2 = "caik";
|
|
767
|
+
var OpenClawAdapter = class {
|
|
768
|
+
name = "openclaw";
|
|
769
|
+
tier = "hook-enabled";
|
|
770
|
+
// --- Path helpers --------------------------------------------------------
|
|
771
|
+
get openclawDir() {
|
|
772
|
+
return join5(homedir4(), ".openclaw");
|
|
773
|
+
}
|
|
774
|
+
/** OpenClaw's own config — NOT where MCP servers go. */
|
|
775
|
+
get openclawConfigPath() {
|
|
776
|
+
return join5(this.openclawDir, "openclaw.json");
|
|
777
|
+
}
|
|
778
|
+
/** Project-level `.mcp.json` — where MCP servers are registered. */
|
|
779
|
+
get mcpConfigPath() {
|
|
780
|
+
return join5(process.cwd(), ".mcp.json");
|
|
781
|
+
}
|
|
782
|
+
/** Skills directory following OpenClaw convention. */
|
|
783
|
+
get skillsBaseDir() {
|
|
784
|
+
return join5(homedir4(), ".agents", "skills");
|
|
785
|
+
}
|
|
786
|
+
get skillDir() {
|
|
787
|
+
return join5(this.skillsBaseDir, "caik");
|
|
788
|
+
}
|
|
789
|
+
get skillLockPath() {
|
|
790
|
+
return join5(homedir4(), ".agents", ".skill-lock.json");
|
|
791
|
+
}
|
|
792
|
+
// --- JSON helpers --------------------------------------------------------
|
|
793
|
+
readJson(path) {
|
|
794
|
+
if (!existsSync5(path)) return {};
|
|
795
|
+
try {
|
|
796
|
+
const raw = readFileSync4(path, "utf-8");
|
|
797
|
+
const parsed = JSON.parse(raw);
|
|
798
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
799
|
+
return parsed;
|
|
800
|
+
}
|
|
801
|
+
return {};
|
|
802
|
+
} catch {
|
|
803
|
+
return {};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
writeJson(path, data) {
|
|
807
|
+
const dir = dirname(path);
|
|
808
|
+
if (!existsSync5(dir)) {
|
|
809
|
+
mkdirSync4(dir, { recursive: true });
|
|
810
|
+
}
|
|
811
|
+
writeFileSync4(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
812
|
+
}
|
|
813
|
+
readSkillLock() {
|
|
814
|
+
if (!existsSync5(this.skillLockPath)) {
|
|
815
|
+
return { version: 3, skills: {} };
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const raw = readFileSync4(this.skillLockPath, "utf-8");
|
|
819
|
+
const parsed = JSON.parse(raw);
|
|
820
|
+
return {
|
|
821
|
+
version: parsed.version ?? 3,
|
|
822
|
+
skills: parsed.skills ?? {}
|
|
823
|
+
};
|
|
824
|
+
} catch {
|
|
825
|
+
return { version: 3, skills: {} };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
writeSkillLock(lock) {
|
|
829
|
+
const dir = dirname(this.skillLockPath);
|
|
830
|
+
if (!existsSync5(dir)) {
|
|
831
|
+
mkdirSync4(dir, { recursive: true });
|
|
832
|
+
}
|
|
833
|
+
writeFileSync4(
|
|
834
|
+
this.skillLockPath,
|
|
835
|
+
JSON.stringify(lock, null, 2) + "\n",
|
|
836
|
+
"utf-8"
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
/** Check if `openclaw` binary is in PATH. */
|
|
840
|
+
isInPath() {
|
|
841
|
+
try {
|
|
842
|
+
execSync2("which openclaw", { stdio: "ignore" });
|
|
843
|
+
return true;
|
|
844
|
+
} catch {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// --- PlatformAdapter implementation --------------------------------------
|
|
849
|
+
async detect() {
|
|
850
|
+
const hasDir = existsSync5(this.openclawDir);
|
|
851
|
+
const inPath = this.isInPath();
|
|
852
|
+
if (!hasDir && !inPath) return null;
|
|
853
|
+
const configPaths = [];
|
|
854
|
+
if (existsSync5(this.openclawConfigPath)) {
|
|
855
|
+
configPaths.push(this.openclawConfigPath);
|
|
856
|
+
}
|
|
857
|
+
if (existsSync5(this.mcpConfigPath)) {
|
|
858
|
+
configPaths.push(this.mcpConfigPath);
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
name: this.name,
|
|
862
|
+
tier: this.tier,
|
|
863
|
+
configPaths,
|
|
864
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
async registerMcp(serverConfig) {
|
|
868
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
869
|
+
const servers = config.mcpServers ?? {};
|
|
870
|
+
servers[MCP_KEY2] = {
|
|
871
|
+
type: "stdio",
|
|
872
|
+
command: serverConfig.command,
|
|
873
|
+
args: serverConfig.args,
|
|
874
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
875
|
+
};
|
|
876
|
+
config.mcpServers = servers;
|
|
877
|
+
this.writeJson(this.mcpConfigPath, config);
|
|
878
|
+
}
|
|
879
|
+
async unregisterMcp() {
|
|
880
|
+
if (!existsSync5(this.mcpConfigPath)) return;
|
|
881
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
882
|
+
const servers = config.mcpServers;
|
|
883
|
+
if (!servers || !(MCP_KEY2 in servers)) return;
|
|
884
|
+
delete servers[MCP_KEY2];
|
|
885
|
+
config.mcpServers = servers;
|
|
886
|
+
this.writeJson(this.mcpConfigPath, config);
|
|
887
|
+
}
|
|
888
|
+
async registerHooks(_hookConfig) {
|
|
889
|
+
console.error(
|
|
890
|
+
"[caik] To enable CAIK telemetry hooks in OpenClaw, install the CAIK hook pack:\n openclaw hooks install @caik.dev/openclaw-hooks\n (Hook pack not yet available \u2014 hooks are optional, CAIK works without them.)"
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
async unregisterHooks() {
|
|
894
|
+
console.error(
|
|
895
|
+
"[caik] To remove CAIK hooks from OpenClaw, run:\n openclaw hooks uninstall @caik.dev/openclaw-hooks"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
async installSkill(_slug, content, files) {
|
|
899
|
+
if (!existsSync5(this.skillDir)) {
|
|
900
|
+
mkdirSync4(this.skillDir, { recursive: true });
|
|
901
|
+
}
|
|
902
|
+
writeFileSync4(join5(this.skillDir, "SKILL.md"), content, "utf-8");
|
|
903
|
+
if (files) {
|
|
904
|
+
for (const file of files) {
|
|
905
|
+
const filePath = join5(this.skillDir, file.path);
|
|
906
|
+
const fileDir = dirname(filePath);
|
|
907
|
+
if (!existsSync5(fileDir)) {
|
|
908
|
+
mkdirSync4(fileDir, { recursive: true });
|
|
909
|
+
}
|
|
910
|
+
writeFileSync4(filePath, file.content, "utf-8");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const lock = this.readSkillLock();
|
|
914
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
915
|
+
const existing = lock.skills["caik"];
|
|
916
|
+
lock.skills["caik"] = {
|
|
917
|
+
source: "@caik.dev/cli",
|
|
918
|
+
sourceType: "local",
|
|
919
|
+
sourceUrl: "",
|
|
920
|
+
skillPath: "skills/caik/SKILL.md",
|
|
921
|
+
skillFolderHash: "",
|
|
922
|
+
installedAt: existing?.installedAt ?? now,
|
|
923
|
+
updatedAt: now
|
|
924
|
+
};
|
|
925
|
+
this.writeSkillLock(lock);
|
|
926
|
+
}
|
|
927
|
+
async uninstallSkill(_slug) {
|
|
928
|
+
if (existsSync5(this.skillDir)) {
|
|
929
|
+
try {
|
|
930
|
+
rmSync2(this.skillDir, { recursive: true, force: true });
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const lock = this.readSkillLock();
|
|
935
|
+
if ("caik" in lock.skills) {
|
|
936
|
+
delete lock.skills["caik"];
|
|
937
|
+
this.writeSkillLock(lock);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
getConfigPath() {
|
|
941
|
+
return this.openclawConfigPath;
|
|
942
|
+
}
|
|
943
|
+
async readConfig() {
|
|
944
|
+
return this.readJson(this.openclawConfigPath);
|
|
945
|
+
}
|
|
946
|
+
async isRegistered() {
|
|
947
|
+
if (!existsSync5(this.mcpConfigPath)) return false;
|
|
948
|
+
const config = this.readJson(this.mcpConfigPath);
|
|
949
|
+
const servers = config.mcpServers;
|
|
950
|
+
return !!servers && MCP_KEY2 in servers;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/platform/cursor.ts
|
|
955
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, rmSync as rmSync3 } from "fs";
|
|
956
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
957
|
+
import { homedir as homedir5 } from "os";
|
|
958
|
+
var MCP_KEY3 = "caik";
|
|
959
|
+
function readJsonFile(path) {
|
|
960
|
+
try {
|
|
961
|
+
if (!existsSync6(path)) return null;
|
|
962
|
+
const raw = readFileSync5(path, "utf-8");
|
|
963
|
+
return JSON.parse(raw);
|
|
964
|
+
} catch {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function writeJsonFile(path, data) {
|
|
969
|
+
mkdirSync5(dirname2(path), { recursive: true });
|
|
970
|
+
writeFileSync5(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
971
|
+
}
|
|
972
|
+
var CursorAdapter = class {
|
|
973
|
+
name = "cursor";
|
|
974
|
+
tier = "hook-enabled";
|
|
975
|
+
/** Project-level .cursor dir (preferred for MCP config). */
|
|
976
|
+
get projectDir() {
|
|
977
|
+
return join6(process.cwd(), ".cursor");
|
|
978
|
+
}
|
|
979
|
+
/** Global ~/.cursor dir. */
|
|
980
|
+
get globalDir() {
|
|
981
|
+
return join6(homedir5(), ".cursor");
|
|
982
|
+
}
|
|
983
|
+
/** Preferred MCP config path (project-level). */
|
|
984
|
+
get mcpConfigPath() {
|
|
985
|
+
return join6(this.projectDir, "mcp.json");
|
|
986
|
+
}
|
|
987
|
+
/** Global MCP config path (fallback). */
|
|
988
|
+
get globalMcpConfigPath() {
|
|
989
|
+
return join6(this.globalDir, "mcp.json");
|
|
990
|
+
}
|
|
991
|
+
/** Hooks config path (project-level). */
|
|
992
|
+
get hooksConfigPath() {
|
|
993
|
+
return join6(this.projectDir, "hooks.json");
|
|
994
|
+
}
|
|
995
|
+
/** Skills directory. */
|
|
996
|
+
get skillsDir() {
|
|
997
|
+
return join6(this.globalDir, "skills", "caik");
|
|
998
|
+
}
|
|
999
|
+
// ── Detection ─────────────────────────────────────────────
|
|
1000
|
+
async detect() {
|
|
1001
|
+
const hasGlobal = existsSync6(this.globalDir);
|
|
1002
|
+
const hasProject = existsSync6(this.projectDir);
|
|
1003
|
+
if (!hasGlobal && !hasProject) return null;
|
|
1004
|
+
const configPaths = [];
|
|
1005
|
+
if (existsSync6(this.mcpConfigPath)) configPaths.push(this.mcpConfigPath);
|
|
1006
|
+
if (existsSync6(this.globalMcpConfigPath)) configPaths.push(this.globalMcpConfigPath);
|
|
1007
|
+
return {
|
|
1008
|
+
name: "cursor",
|
|
1009
|
+
tier: "hook-enabled",
|
|
1010
|
+
configPaths,
|
|
1011
|
+
capabilities: { hooks: true, mcp: true, skills: true }
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
// ── MCP Registration ──────────────────────────────────────
|
|
1015
|
+
async registerMcp(serverConfig) {
|
|
1016
|
+
const configPath = existsSync6(this.projectDir) ? this.mcpConfigPath : this.globalMcpConfigPath;
|
|
1017
|
+
const config = readJsonFile(configPath) ?? {};
|
|
1018
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
1019
|
+
config.mcpServers[MCP_KEY3] = serverConfig;
|
|
1020
|
+
writeJsonFile(configPath, config);
|
|
1021
|
+
}
|
|
1022
|
+
async unregisterMcp() {
|
|
1023
|
+
for (const configPath of [this.mcpConfigPath, this.globalMcpConfigPath]) {
|
|
1024
|
+
const config = readJsonFile(configPath);
|
|
1025
|
+
if (!config?.mcpServers?.[MCP_KEY3]) continue;
|
|
1026
|
+
delete config.mcpServers[MCP_KEY3];
|
|
1027
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
1028
|
+
delete config.mcpServers;
|
|
1029
|
+
}
|
|
1030
|
+
writeJsonFile(configPath, config);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// ── Hooks ─────────────────────────────────────────────────
|
|
1034
|
+
async registerHooks(_hookConfig) {
|
|
1035
|
+
const defaultHooks = [
|
|
1036
|
+
{ event: "sessionStart", script: "npx -y @caik.dev/cli hook cursor-session-start", decision: "allow" },
|
|
1037
|
+
{ event: "beforeMCPExecution", script: "npx -y @caik.dev/cli hook cursor-mcp-exec --server=$SERVER --tool=$TOOL", decision: "allow" },
|
|
1038
|
+
{ event: "stop", script: "npx -y @caik.dev/cli hook cursor-session-end", decision: "allow" }
|
|
1039
|
+
];
|
|
1040
|
+
try {
|
|
1041
|
+
const config = readJsonFile(this.hooksConfigPath) ?? {};
|
|
1042
|
+
const existing = config.hooks ?? [];
|
|
1043
|
+
const filtered = existing.filter(
|
|
1044
|
+
(h) => !h.script?.includes("caik") || !h.script?.includes("hook cursor-")
|
|
1045
|
+
);
|
|
1046
|
+
config.hooks = [...filtered, ...defaultHooks];
|
|
1047
|
+
writeJsonFile(this.hooksConfigPath, config);
|
|
1048
|
+
} catch {
|
|
1049
|
+
process.stderr.write(
|
|
1050
|
+
"[caik] Warning: Could not register hooks \u2014 Cursor hooks require v1.7+. Skipping.\n"
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async unregisterHooks() {
|
|
1055
|
+
try {
|
|
1056
|
+
const config = readJsonFile(this.hooksConfigPath);
|
|
1057
|
+
if (!config?.hooks) return;
|
|
1058
|
+
config.hooks = config.hooks.filter(
|
|
1059
|
+
(h) => !h.script?.includes("caik") || !h.script?.includes("hook cursor-")
|
|
1060
|
+
);
|
|
1061
|
+
if (config.hooks.length === 0) {
|
|
1062
|
+
delete config.hooks;
|
|
1063
|
+
}
|
|
1064
|
+
writeJsonFile(this.hooksConfigPath, config);
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
// ── Skills ────────────────────────────────────────────────
|
|
1069
|
+
async installSkill(slug, content, files) {
|
|
1070
|
+
const skillDir = join6(this.skillsDir, slug);
|
|
1071
|
+
mkdirSync5(skillDir, { recursive: true });
|
|
1072
|
+
writeFileSync5(join6(skillDir, "SKILL.md"), content, "utf-8");
|
|
1073
|
+
if (files) {
|
|
1074
|
+
for (const file of files) {
|
|
1075
|
+
const filePath = join6(skillDir, file.path);
|
|
1076
|
+
mkdirSync5(dirname2(filePath), { recursive: true });
|
|
1077
|
+
writeFileSync5(filePath, file.content, "utf-8");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async uninstallSkill(slug) {
|
|
1082
|
+
const skillDir = join6(this.skillsDir, slug);
|
|
1083
|
+
try {
|
|
1084
|
+
if (existsSync6(skillDir)) {
|
|
1085
|
+
rmSync3(skillDir, { recursive: true, force: true });
|
|
1086
|
+
}
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// ── Config Accessors ──────────────────────────────────────
|
|
1091
|
+
getConfigPath() {
|
|
1092
|
+
if (existsSync6(this.mcpConfigPath)) return this.mcpConfigPath;
|
|
1093
|
+
if (existsSync6(this.globalMcpConfigPath)) return this.globalMcpConfigPath;
|
|
1094
|
+
return this.mcpConfigPath;
|
|
1095
|
+
}
|
|
1096
|
+
async readConfig() {
|
|
1097
|
+
const configPath = this.getConfigPath();
|
|
1098
|
+
return readJsonFile(configPath) ?? {};
|
|
1099
|
+
}
|
|
1100
|
+
async isRegistered() {
|
|
1101
|
+
for (const configPath of [this.mcpConfigPath, this.globalMcpConfigPath]) {
|
|
1102
|
+
const config = readJsonFile(configPath);
|
|
1103
|
+
if (config?.mcpServers?.[MCP_KEY3]) return true;
|
|
1104
|
+
}
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
// src/platform/generic.ts
|
|
1110
|
+
var GenericAdapter = class {
|
|
1111
|
+
name;
|
|
1112
|
+
tier = "cli-mcp";
|
|
1113
|
+
constructor(name) {
|
|
1114
|
+
this.name = name;
|
|
1115
|
+
}
|
|
1116
|
+
// ── Detection ─────────────────────────────────────────────
|
|
1117
|
+
async detect() {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
// ── MCP Registration ──────────────────────────────────────
|
|
1121
|
+
async registerMcp(serverConfig) {
|
|
1122
|
+
const snippet = {
|
|
1123
|
+
mcpServers: {
|
|
1124
|
+
caik: {
|
|
1125
|
+
command: serverConfig.command,
|
|
1126
|
+
args: serverConfig.args,
|
|
1127
|
+
...serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
const json = JSON.stringify(snippet, null, 2);
|
|
1132
|
+
process.stdout.write(
|
|
1133
|
+
`
|
|
1134
|
+
CAIK MCP server configuration for ${this.name}:
|
|
1135
|
+
|
|
1136
|
+
Add the following to your MCP configuration file:
|
|
1137
|
+
|
|
1138
|
+
${json}
|
|
1139
|
+
|
|
1140
|
+
Refer to your platform's documentation for the correct config file location.
|
|
1141
|
+
|
|
1142
|
+
`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
async unregisterMcp() {
|
|
1146
|
+
process.stdout.write(
|
|
1147
|
+
`
|
|
1148
|
+
To unregister CAIK from ${this.name}, remove the "caik" entry
|
|
1149
|
+
from the "mcpServers" section in your MCP configuration file.
|
|
1150
|
+
|
|
1151
|
+
`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
// ── No hooks support ──────────────────────────────────────
|
|
1155
|
+
// registerHooks / unregisterHooks intentionally omitted (cli-mcp tier)
|
|
1156
|
+
// ── No skills support ─────────────────────────────────────
|
|
1157
|
+
// installSkill / uninstallSkill intentionally omitted (cli-mcp tier)
|
|
1158
|
+
// ── Config Accessors ──────────────────────────────────────
|
|
1159
|
+
getConfigPath() {
|
|
1160
|
+
return "";
|
|
1161
|
+
}
|
|
1162
|
+
async readConfig() {
|
|
1163
|
+
return {};
|
|
1164
|
+
}
|
|
1165
|
+
async isRegistered() {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// src/platform/index.ts
|
|
1171
|
+
var adapters = {
|
|
1172
|
+
"claude-code": () => new ClaudeCodeAdapter(),
|
|
1173
|
+
"openclaw": () => new OpenClawAdapter(),
|
|
1174
|
+
"cursor": () => new CursorAdapter(),
|
|
1175
|
+
"codex": () => new GenericAdapter("codex"),
|
|
1176
|
+
"windsurf": () => new GenericAdapter("windsurf"),
|
|
1177
|
+
"generic": () => new GenericAdapter("generic")
|
|
1178
|
+
};
|
|
1179
|
+
function getPlatformAdapter(name) {
|
|
1180
|
+
const factory = adapters[name];
|
|
1181
|
+
if (!factory) {
|
|
1182
|
+
return new GenericAdapter(name);
|
|
1183
|
+
}
|
|
1184
|
+
return factory();
|
|
1185
|
+
}
|
|
1186
|
+
function getDefaultMcpConfig() {
|
|
1187
|
+
return {
|
|
1188
|
+
command: "npx",
|
|
1189
|
+
args: ["-y", "@caik.dev/mcp"]
|
|
1190
|
+
};
|
|
304
1191
|
}
|
|
1192
|
+
|
|
1193
|
+
// src/commands/install.ts
|
|
1194
|
+
var legacyPlatformMap = {
|
|
1195
|
+
claude: "claude-code",
|
|
1196
|
+
cursor: "cursor",
|
|
1197
|
+
node: "generic"
|
|
1198
|
+
};
|
|
305
1199
|
function registerInstallCommand(program2) {
|
|
306
1200
|
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", `
|
|
307
1201
|
Examples:
|
|
@@ -310,7 +1204,17 @@ Examples:
|
|
|
310
1204
|
const globalOpts = program2.opts();
|
|
311
1205
|
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
312
1206
|
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
313
|
-
|
|
1207
|
+
let detectedPlatform;
|
|
1208
|
+
if (opts.platform) {
|
|
1209
|
+
const raw = opts.platform;
|
|
1210
|
+
detectedPlatform = legacyPlatformMap[raw] ?? raw;
|
|
1211
|
+
} else {
|
|
1212
|
+
const detected = detectPlatforms();
|
|
1213
|
+
if (detected.length > 0) {
|
|
1214
|
+
detectedPlatform = detected[0].name;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const platform = detectedPlatform;
|
|
314
1218
|
const spinner = createSpinner(`Fetching ${slug}...`);
|
|
315
1219
|
if (!globalOpts.json) spinner.start();
|
|
316
1220
|
const params = { platform };
|
|
@@ -333,7 +1237,7 @@ Examples:
|
|
|
333
1237
|
spinner.start();
|
|
334
1238
|
continue;
|
|
335
1239
|
}
|
|
336
|
-
safeFiles.push({ resolvedPath, content: file.content });
|
|
1240
|
+
safeFiles.push({ resolvedPath, content: file.content ?? "" });
|
|
337
1241
|
}
|
|
338
1242
|
}
|
|
339
1243
|
const hasFiles = safeFiles.length > 0;
|
|
@@ -365,15 +1269,15 @@ Examples:
|
|
|
365
1269
|
spinner.start();
|
|
366
1270
|
}
|
|
367
1271
|
for (const file of safeFiles) {
|
|
368
|
-
const dir =
|
|
369
|
-
if (!
|
|
370
|
-
|
|
1272
|
+
const dir = dirname3(file.resolvedPath);
|
|
1273
|
+
if (!existsSync7(dir)) {
|
|
1274
|
+
mkdirSync6(dir, { recursive: true });
|
|
371
1275
|
}
|
|
372
|
-
|
|
1276
|
+
writeFileSync6(file.resolvedPath, file.content, "utf-8");
|
|
373
1277
|
}
|
|
374
1278
|
if (installInfo.installCommand) {
|
|
375
1279
|
try {
|
|
376
|
-
|
|
1280
|
+
execSync3(installInfo.installCommand, { stdio: "pipe" });
|
|
377
1281
|
} catch (err) {
|
|
378
1282
|
spinner.stop();
|
|
379
1283
|
console.error(error(`Install command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
@@ -389,7 +1293,7 @@ Examples:
|
|
|
389
1293
|
}
|
|
390
1294
|
if (installInfo.postInstallHook) {
|
|
391
1295
|
try {
|
|
392
|
-
|
|
1296
|
+
execSync3(installInfo.postInstallHook, { stdio: "pipe" });
|
|
393
1297
|
} catch {
|
|
394
1298
|
}
|
|
395
1299
|
}
|
|
@@ -398,6 +1302,31 @@ Examples:
|
|
|
398
1302
|
if (installInfo.contract?.touchedPaths) {
|
|
399
1303
|
console.log(info(`Files: ${installInfo.contract.touchedPaths.join(", ")}`));
|
|
400
1304
|
}
|
|
1305
|
+
const resolvedPlatform = detectedPlatform ?? "generic";
|
|
1306
|
+
upsertRegistryEntry({
|
|
1307
|
+
slug,
|
|
1308
|
+
version: installInfo.artifact.version ?? "0.0.0",
|
|
1309
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1310
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1311
|
+
platform: resolvedPlatform,
|
|
1312
|
+
files: safeFiles.map((f) => f.resolvedPath),
|
|
1313
|
+
artifactType: installInfo.artifact.primitive ?? "unknown"
|
|
1314
|
+
});
|
|
1315
|
+
const adapter = getPlatformAdapter(resolvedPlatform);
|
|
1316
|
+
const primitive = installInfo.artifact.primitive;
|
|
1317
|
+
if (primitive === "connector") {
|
|
1318
|
+
try {
|
|
1319
|
+
await adapter.registerMcp(getDefaultMcpConfig());
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if ((primitive === "executable" || primitive === "composition") && adapter.installSkill) {
|
|
1324
|
+
try {
|
|
1325
|
+
const content = safeFiles.map((f) => f.content).join("\n");
|
|
1326
|
+
await adapter.installSkill(slug, content);
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
401
1330
|
client.post("/telemetry/install", {
|
|
402
1331
|
artifactSlug: slug,
|
|
403
1332
|
platform,
|
|
@@ -414,7 +1343,7 @@ function registerInitCommand(program2) {
|
|
|
414
1343
|
program2.command("init").description("Configure the CAIK CLI").option("--auth", "Set up authentication (opens browser)").addHelpText("after", `
|
|
415
1344
|
Examples:
|
|
416
1345
|
caik init
|
|
417
|
-
caik init --auth`).action(async (
|
|
1346
|
+
caik init --auth`).action(async (_opts) => {
|
|
418
1347
|
const rl = createInterface2({ input: stdin, output: stdout });
|
|
419
1348
|
try {
|
|
420
1349
|
const config = readConfig();
|
|
@@ -432,12 +1361,36 @@ Examples:
|
|
|
432
1361
|
if (config.apiKey) {
|
|
433
1362
|
const client = new CaikApiClient({ apiUrl: config.apiUrl, apiKey: config.apiKey });
|
|
434
1363
|
try {
|
|
435
|
-
|
|
1364
|
+
await client.get("/me/karma");
|
|
436
1365
|
console.log(success("API connection verified \u2014 authenticated"));
|
|
437
1366
|
} catch {
|
|
438
1367
|
console.log(error("Could not verify API connection. Check your API key."));
|
|
439
1368
|
}
|
|
440
1369
|
}
|
|
1370
|
+
const detected = detectPlatforms();
|
|
1371
|
+
if (detected.length > 0) {
|
|
1372
|
+
console.log("");
|
|
1373
|
+
console.log(heading("Platform Integration"));
|
|
1374
|
+
console.log("\u2500".repeat(40));
|
|
1375
|
+
for (const p of detected) {
|
|
1376
|
+
console.log(` ${success(p.name)} detected`);
|
|
1377
|
+
}
|
|
1378
|
+
console.log("");
|
|
1379
|
+
const hasClaude = detected.some((p) => p.name === "claude-code");
|
|
1380
|
+
const hasOpenClaw = detected.some((p) => p.name === "openclaw");
|
|
1381
|
+
if (hasClaude) {
|
|
1382
|
+
console.log(info("Claude Code: Install the CAIK plugin for the best experience:"));
|
|
1383
|
+
console.log(dim(" claude plugin marketplace add https://github.com/caik-dev/claude-code-plugin"));
|
|
1384
|
+
console.log(dim(" claude plugin install caik@caik-dev"));
|
|
1385
|
+
console.log("");
|
|
1386
|
+
}
|
|
1387
|
+
if (hasOpenClaw) {
|
|
1388
|
+
console.log(info("OpenClaw: Add the CAIK MCP server to your project's .mcp.json:"));
|
|
1389
|
+
console.log(dim(' { "mcpServers": { "caik": { "type": "stdio", "command": "npx", "args": ["-y", "@caik.dev/mcp"] } } }'));
|
|
1390
|
+
console.log("");
|
|
1391
|
+
}
|
|
1392
|
+
console.log(info("Or run `caik setup` for interactive platform configuration."));
|
|
1393
|
+
}
|
|
441
1394
|
} finally {
|
|
442
1395
|
rl.close();
|
|
443
1396
|
}
|
|
@@ -445,22 +1398,56 @@ Examples:
|
|
|
445
1398
|
}
|
|
446
1399
|
|
|
447
1400
|
// src/commands/status.ts
|
|
448
|
-
import { existsSync as
|
|
1401
|
+
import { existsSync as existsSync8 } from "fs";
|
|
449
1402
|
function registerStatusCommand(program2) {
|
|
450
1403
|
program2.command("status").description("Show CLI configuration and connectivity status").action(async () => {
|
|
451
1404
|
const globalOpts = program2.opts();
|
|
452
|
-
const config = readConfig();
|
|
453
1405
|
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1406
|
+
const detected = detectPlatforms();
|
|
1407
|
+
const platformResults = [];
|
|
1408
|
+
for (const dp of detected) {
|
|
1409
|
+
const adapter = getPlatformAdapter(dp.name);
|
|
1410
|
+
let registered = false;
|
|
1411
|
+
try {
|
|
1412
|
+
registered = await adapter.isRegistered();
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
platformResults.push({
|
|
1416
|
+
name: dp.name,
|
|
1417
|
+
tier: dp.tier,
|
|
1418
|
+
registered,
|
|
1419
|
+
driftWarnings: []
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
const registryEntries = listRegistryEntries();
|
|
1423
|
+
const driftEntries = [];
|
|
1424
|
+
for (const entry of registryEntries) {
|
|
1425
|
+
const missing = entry.files.filter((f) => !existsSync8(f));
|
|
1426
|
+
if (missing.length > 0) {
|
|
1427
|
+
driftEntries.push({ slug: entry.slug, missingFiles: missing });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
for (const drift of driftEntries) {
|
|
1431
|
+
const entry = registryEntries.find((e) => e.slug === drift.slug);
|
|
1432
|
+
if (entry) {
|
|
1433
|
+
const pr = platformResults.find((p) => p.name === entry.platform);
|
|
1434
|
+
if (pr) {
|
|
1435
|
+
pr.driftWarnings.push(
|
|
1436
|
+
`${drift.slug}: ${drift.missingFiles.length} missing file(s)`
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (globalOpts.json) {
|
|
1442
|
+
const data = {
|
|
1443
|
+
apiUrl,
|
|
1444
|
+
apiKeyConfigured: !!apiKey,
|
|
1445
|
+
platforms: platformResults,
|
|
1446
|
+
installedArtifacts: registryEntries.length
|
|
463
1447
|
};
|
|
1448
|
+
if (driftEntries.length > 0) {
|
|
1449
|
+
data.driftWarnings = driftEntries;
|
|
1450
|
+
}
|
|
464
1451
|
const client2 = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
465
1452
|
try {
|
|
466
1453
|
if (apiKey) {
|
|
@@ -493,15 +1480,26 @@ function registerStatusCommand(program2) {
|
|
|
493
1480
|
console.log(`API Reachable: ${error("no")}`);
|
|
494
1481
|
}
|
|
495
1482
|
console.log("");
|
|
496
|
-
console.log(heading("
|
|
1483
|
+
console.log(heading("Platform Integration"));
|
|
497
1484
|
console.log("\u2500".repeat(40));
|
|
498
|
-
|
|
499
|
-
{
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1485
|
+
if (platformResults.length === 0) {
|
|
1486
|
+
console.log(` ${dim("No platforms detected")}`);
|
|
1487
|
+
} else {
|
|
1488
|
+
const maxLen = Math.max(...platformResults.map((p) => p.name.length));
|
|
1489
|
+
for (const p of platformResults) {
|
|
1490
|
+
const label = `${p.name}:`.padEnd(maxLen + 2);
|
|
1491
|
+
const mcpStatus = p.registered ? `(MCP: ${success("registered")})` : `(MCP: ${dim("not registered")})`;
|
|
1492
|
+
console.log(` ${label}${success("detected")} ${mcpStatus}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
console.log("");
|
|
1496
|
+
console.log(`Installed Artifacts: ${registryEntries.length}`);
|
|
1497
|
+
if (driftEntries.length > 0) {
|
|
1498
|
+
const count = driftEntries.length;
|
|
1499
|
+
const noun = count === 1 ? "artifact has" : "artifacts have";
|
|
1500
|
+
console.log(
|
|
1501
|
+
` ${warn(`${count} ${noun} missing files`)} \u2014 run \`caik setup\` to reconfigure`
|
|
1502
|
+
);
|
|
505
1503
|
}
|
|
506
1504
|
});
|
|
507
1505
|
}
|
|
@@ -552,7 +1550,7 @@ Examples:
|
|
|
552
1550
|
}
|
|
553
1551
|
|
|
554
1552
|
// src/commands/publish.ts
|
|
555
|
-
import { readFileSync as
|
|
1553
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
|
|
556
1554
|
function registerPublishCommand(program2) {
|
|
557
1555
|
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
1556
|
Examples:
|
|
@@ -566,10 +1564,10 @@ Examples:
|
|
|
566
1564
|
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
567
1565
|
let content;
|
|
568
1566
|
if (path) {
|
|
569
|
-
if (!
|
|
1567
|
+
if (!existsSync9(path)) {
|
|
570
1568
|
throw new CaikError(`File not found: ${path}`);
|
|
571
1569
|
}
|
|
572
|
-
content =
|
|
1570
|
+
content = readFileSync6(path, "utf-8");
|
|
573
1571
|
}
|
|
574
1572
|
const tags = opts.tag.length > 0 ? opts.tag : ["general"];
|
|
575
1573
|
const platforms = opts.platform.split(",").map((p) => p.trim());
|
|
@@ -730,16 +1728,340 @@ Examples:
|
|
|
730
1728
|
}
|
|
731
1729
|
|
|
732
1730
|
// src/commands/update.ts
|
|
1731
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1732
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
1733
|
+
import { dirname as dirname4, resolve as resolve2 } from "path";
|
|
1734
|
+
import { execSync as execSync4 } from "child_process";
|
|
1735
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
733
1736
|
function registerUpdateCommand(program2) {
|
|
734
|
-
program2.command("update [slug]").description("Check for and apply artifact updates").option("--yes", "Skip confirmation
|
|
735
|
-
|
|
1737
|
+
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", `
|
|
1738
|
+
Examples:
|
|
1739
|
+
caik update # Check all installed artifacts for updates
|
|
1740
|
+
caik update auth-skill # Update a specific artifact
|
|
1741
|
+
caik update --yes # Update all without confirmation`).action(async (slug, opts) => {
|
|
1742
|
+
const globalOpts = program2.opts();
|
|
1743
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
1744
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
1745
|
+
const skipConfirm = Boolean(opts?.yes);
|
|
1746
|
+
const platformFilter = opts?.platform;
|
|
1747
|
+
if (slug) {
|
|
1748
|
+
const entry = findRegistryEntry(slug, platformFilter);
|
|
1749
|
+
if (!entry) {
|
|
1750
|
+
console.log(error(`Artifact "${slug}" is not in the install registry.`));
|
|
1751
|
+
console.log(info("Use 'caik install <slug>' to install it first."));
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1754
|
+
const spinner = createSpinner(`Checking for updates to ${slug}...`);
|
|
1755
|
+
if (!globalOpts.json) spinner.start();
|
|
1756
|
+
let remote;
|
|
1757
|
+
try {
|
|
1758
|
+
remote = await client.get(`/artifacts/${encodeURIComponent(slug)}`);
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
spinner.stop();
|
|
1761
|
+
console.log(error(`Failed to fetch artifact info: ${err instanceof Error ? err.message : String(err)}`));
|
|
1762
|
+
process.exit(1);
|
|
1763
|
+
}
|
|
1764
|
+
if (globalOpts.json) {
|
|
1765
|
+
spinner.stop();
|
|
1766
|
+
outputResult({
|
|
1767
|
+
slug,
|
|
1768
|
+
installedVersion: entry.version,
|
|
1769
|
+
remoteUpdatedAt: remote.updatedAt,
|
|
1770
|
+
localUpdatedAt: entry.updatedAt,
|
|
1771
|
+
updateAvailable: remote.updatedAt > entry.updatedAt
|
|
1772
|
+
}, { json: true });
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (remote.updatedAt <= entry.updatedAt) {
|
|
1776
|
+
spinner.stop();
|
|
1777
|
+
console.log(success(`${slug} is already up to date.`));
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
spinner.stop();
|
|
1781
|
+
console.log(info(`Update available for ${slug} (installed: ${entry.updatedAt.slice(0, 10)}, latest: ${remote.updatedAt.slice(0, 10)})`));
|
|
1782
|
+
await performUpdate(client, entry, skipConfirm, globalOpts);
|
|
1783
|
+
} else {
|
|
1784
|
+
const entries = listRegistryEntries(platformFilter);
|
|
1785
|
+
if (entries.length === 0) {
|
|
1786
|
+
console.log(info("No installed artifacts found."));
|
|
1787
|
+
console.log(info("Install artifacts with 'caik install <slug>' first."));
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
const spinner = createSpinner(`Checking ${entries.length} artifact(s) for updates...`);
|
|
1791
|
+
if (!globalOpts.json) spinner.start();
|
|
1792
|
+
const candidates = [];
|
|
1793
|
+
const errors = [];
|
|
1794
|
+
for (const entry of entries) {
|
|
1795
|
+
try {
|
|
1796
|
+
const remote = await client.get(`/artifacts/${encodeURIComponent(entry.slug)}`);
|
|
1797
|
+
if (remote.updatedAt > entry.updatedAt) {
|
|
1798
|
+
candidates.push({ entry, remote });
|
|
1799
|
+
}
|
|
1800
|
+
} catch (err) {
|
|
1801
|
+
errors.push({
|
|
1802
|
+
slug: entry.slug,
|
|
1803
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
spinner.stop();
|
|
1808
|
+
if (globalOpts.json) {
|
|
1809
|
+
outputResult({
|
|
1810
|
+
total: entries.length,
|
|
1811
|
+
updatesAvailable: candidates.length,
|
|
1812
|
+
updates: candidates.map((c) => ({
|
|
1813
|
+
slug: c.entry.slug,
|
|
1814
|
+
platform: c.entry.platform,
|
|
1815
|
+
installedAt: c.entry.updatedAt,
|
|
1816
|
+
remoteUpdatedAt: c.remote.updatedAt
|
|
1817
|
+
})),
|
|
1818
|
+
errors
|
|
1819
|
+
}, { json: true });
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (errors.length > 0) {
|
|
1823
|
+
for (const e of errors) {
|
|
1824
|
+
console.log(warn(`Could not check ${e.slug}: ${e.message}`));
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (candidates.length === 0) {
|
|
1828
|
+
console.log(success("All installed artifacts are up to date."));
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const rows = candidates.map((c) => [
|
|
1832
|
+
c.entry.slug,
|
|
1833
|
+
c.entry.platform,
|
|
1834
|
+
c.entry.updatedAt.slice(0, 10),
|
|
1835
|
+
c.remote.updatedAt.slice(0, 10)
|
|
1836
|
+
]);
|
|
1837
|
+
console.log(renderTable(["Artifact", "Platform", "Installed", "Latest"], rows));
|
|
1838
|
+
console.log(info(`${candidates.length} update(s) available.`));
|
|
1839
|
+
if (!skipConfirm) {
|
|
1840
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1841
|
+
const answer = await rl.question("\nUpdate all? (y/N) ");
|
|
1842
|
+
rl.close();
|
|
1843
|
+
if (answer.toLowerCase() !== "y") {
|
|
1844
|
+
console.log(info("Update cancelled."));
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
let updated = 0;
|
|
1849
|
+
for (const candidate of candidates) {
|
|
1850
|
+
try {
|
|
1851
|
+
await performUpdate(client, candidate.entry, true, globalOpts);
|
|
1852
|
+
updated++;
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
console.log(error(`Failed to update ${candidate.entry.slug}: ${err instanceof Error ? err.message : String(err)}`));
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
console.log(success(`Updated ${updated}/${candidates.length} artifact(s).`));
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
async function performUpdate(client, entry, skipConfirm, globalOpts) {
|
|
1862
|
+
const spinner = createSpinner(`Updating ${entry.slug}...`);
|
|
1863
|
+
if (!globalOpts.json) spinner.start();
|
|
1864
|
+
const params = { platform: entry.platform };
|
|
1865
|
+
let installInfo;
|
|
1866
|
+
try {
|
|
1867
|
+
installInfo = await client.get(`/install/${encodeURIComponent(entry.slug)}`, params);
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
spinner.stop();
|
|
1870
|
+
throw err;
|
|
1871
|
+
}
|
|
1872
|
+
const cwd = process.cwd();
|
|
1873
|
+
const safeFiles = [];
|
|
1874
|
+
if (installInfo.files && installInfo.files.length > 0) {
|
|
1875
|
+
for (const file of installInfo.files) {
|
|
1876
|
+
const resolvedPath = resolve2(cwd, file.path);
|
|
1877
|
+
if (!resolvedPath.startsWith(cwd + "/") && resolvedPath !== cwd) {
|
|
1878
|
+
spinner.stop();
|
|
1879
|
+
console.log(error(`Rejected file path outside working directory: ${file.path}`));
|
|
1880
|
+
spinner.start();
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
safeFiles.push({ resolvedPath, content: file.content });
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
if (!skipConfirm && safeFiles.length > 0) {
|
|
1887
|
+
spinner.stop();
|
|
1888
|
+
console.log(info("\n--- Update plan ---"));
|
|
1889
|
+
console.log(info("Files to write:"));
|
|
1890
|
+
for (const f of safeFiles) {
|
|
1891
|
+
console.log(info(` ${f.resolvedPath}`));
|
|
1892
|
+
}
|
|
1893
|
+
if (installInfo.installCommand) {
|
|
1894
|
+
console.log(info(`Install command: ${installInfo.installCommand}`));
|
|
1895
|
+
}
|
|
1896
|
+
console.log();
|
|
1897
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1898
|
+
const answer = await rl.question("Continue? (y/N) ");
|
|
1899
|
+
rl.close();
|
|
1900
|
+
if (answer.toLowerCase() !== "y") {
|
|
1901
|
+
console.log(info("Update cancelled."));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
spinner.start();
|
|
1905
|
+
}
|
|
1906
|
+
const writtenFiles = [];
|
|
1907
|
+
for (const file of safeFiles) {
|
|
1908
|
+
const dir = dirname4(file.resolvedPath);
|
|
1909
|
+
if (!existsSync10(dir)) {
|
|
1910
|
+
mkdirSync7(dir, { recursive: true });
|
|
1911
|
+
}
|
|
1912
|
+
writeFileSync7(file.resolvedPath, file.content, "utf-8");
|
|
1913
|
+
writtenFiles.push(file.resolvedPath);
|
|
1914
|
+
}
|
|
1915
|
+
if (installInfo.installCommand) {
|
|
1916
|
+
try {
|
|
1917
|
+
execSync4(installInfo.installCommand, { stdio: "pipe" });
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
spinner.stop();
|
|
1920
|
+
console.log(error(`Install command failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
1921
|
+
client.post("/telemetry/install", {
|
|
1922
|
+
artifactSlug: entry.slug,
|
|
1923
|
+
platform: entry.platform,
|
|
1924
|
+
success: false,
|
|
1925
|
+
errorMessage: err instanceof Error ? err.message : String(err)
|
|
1926
|
+
}).catch(() => {
|
|
1927
|
+
});
|
|
1928
|
+
throw err;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (installInfo.postInstallHook) {
|
|
1932
|
+
try {
|
|
1933
|
+
execSync4(installInfo.postInstallHook, { stdio: "pipe" });
|
|
1934
|
+
} catch {
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1938
|
+
upsertRegistryEntry({
|
|
1939
|
+
...entry,
|
|
1940
|
+
updatedAt: now,
|
|
1941
|
+
files: writtenFiles.length > 0 ? writtenFiles : entry.files
|
|
1942
|
+
});
|
|
1943
|
+
spinner.stop();
|
|
1944
|
+
console.log(success(`Updated ${installInfo.artifact.name}`));
|
|
1945
|
+
client.post("/telemetry/install", {
|
|
1946
|
+
artifactSlug: entry.slug,
|
|
1947
|
+
platform: entry.platform,
|
|
1948
|
+
success: true
|
|
1949
|
+
}).catch(() => {
|
|
736
1950
|
});
|
|
737
1951
|
}
|
|
738
1952
|
|
|
739
1953
|
// src/commands/uninstall.ts
|
|
1954
|
+
import { createInterface as createInterface4 } from "readline/promises";
|
|
740
1955
|
function registerUninstallCommand(program2) {
|
|
741
|
-
program2.command("uninstall <slug>").description("Remove an installed artifact").
|
|
742
|
-
|
|
1956
|
+
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", `
|
|
1957
|
+
Examples:
|
|
1958
|
+
caik uninstall auth-skill
|
|
1959
|
+
caik uninstall my-mcp --platform claude-code
|
|
1960
|
+
caik uninstall old-rules --yes`).action(async (slug, opts) => {
|
|
1961
|
+
const globalOpts = program2.opts();
|
|
1962
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
1963
|
+
const client = new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
1964
|
+
const skipConfirm = Boolean(opts.yes);
|
|
1965
|
+
const platformOpt = opts.platform;
|
|
1966
|
+
const allEntries = listRegistryEntries();
|
|
1967
|
+
const matchingEntries = allEntries.filter(
|
|
1968
|
+
(e) => e.slug === slug && (!platformOpt || e.platform === platformOpt)
|
|
1969
|
+
);
|
|
1970
|
+
if (matchingEntries.length === 0) {
|
|
1971
|
+
if (globalOpts.json) {
|
|
1972
|
+
outputResult({ error: "not_found", slug }, { json: true });
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
console.log(error(`Artifact "${slug}" not found in install registry.`));
|
|
1976
|
+
console.log(info("It may have been installed manually or already uninstalled."));
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
if (matchingEntries.length > 1 && !platformOpt) {
|
|
1980
|
+
if (globalOpts.json) {
|
|
1981
|
+
outputResult({
|
|
1982
|
+
error: "ambiguous",
|
|
1983
|
+
slug,
|
|
1984
|
+
platforms: matchingEntries.map((e) => e.platform)
|
|
1985
|
+
}, { json: true });
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
console.log(warn(`"${slug}" is installed on multiple platforms: ${matchingEntries.map((e) => e.platform).join(", ")}`));
|
|
1989
|
+
console.log(info("Use --platform <platform> to specify which to uninstall."));
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1992
|
+
const entry = matchingEntries[0];
|
|
1993
|
+
if (globalOpts.json) {
|
|
1994
|
+
} else {
|
|
1995
|
+
console.log(info(`
|
|
1996
|
+
Artifact: ${entry.slug} (${entry.artifactType})`));
|
|
1997
|
+
console.log(info(`Platform: ${entry.platform}`));
|
|
1998
|
+
console.log(info(`Installed: ${entry.installedAt.slice(0, 10)}`));
|
|
1999
|
+
if (entry.files.length > 0) {
|
|
2000
|
+
console.log(info("Files to remove:"));
|
|
2001
|
+
for (const f of entry.files) {
|
|
2002
|
+
console.log(info(` ${f}`));
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (!skipConfirm) {
|
|
2006
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2007
|
+
const answer = await rl.question("\nUninstall? (y/N) ");
|
|
2008
|
+
rl.close();
|
|
2009
|
+
if (answer.toLowerCase() !== "y") {
|
|
2010
|
+
console.log(info("Uninstall cancelled."));
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
const spinner = createSpinner(`Uninstalling ${slug}...`);
|
|
2016
|
+
if (!globalOpts.json) spinner.start();
|
|
2017
|
+
const failedFiles = cleanupFiles(entry);
|
|
2018
|
+
const adapter = getPlatformAdapter(entry.platform);
|
|
2019
|
+
if (entry.artifactType === "mcp-server") {
|
|
2020
|
+
try {
|
|
2021
|
+
await adapter.unregisterMcp();
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
if (!globalOpts.json) {
|
|
2024
|
+
spinner.stop();
|
|
2025
|
+
console.log(warn(`Could not unregister MCP server: ${err instanceof Error ? err.message : String(err)}`));
|
|
2026
|
+
spinner.start();
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
if ((entry.artifactType === "skill" || entry.artifactType === "subagent" || entry.artifactType === "command") && adapter.uninstallSkill) {
|
|
2031
|
+
try {
|
|
2032
|
+
await adapter.uninstallSkill(slug);
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
if (!globalOpts.json) {
|
|
2035
|
+
spinner.stop();
|
|
2036
|
+
console.log(warn(`Could not uninstall skill: ${err instanceof Error ? err.message : String(err)}`));
|
|
2037
|
+
spinner.start();
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
removeRegistryEntry(slug, entry.platform);
|
|
2042
|
+
spinner.stop();
|
|
2043
|
+
if (globalOpts.json) {
|
|
2044
|
+
outputResult({
|
|
2045
|
+
slug,
|
|
2046
|
+
platform: entry.platform,
|
|
2047
|
+
removed: true,
|
|
2048
|
+
filesRemoved: entry.files.length - failedFiles.length,
|
|
2049
|
+
filesFailed: failedFiles
|
|
2050
|
+
}, { json: true });
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
console.log(success(`Uninstalled ${slug} from ${entry.platform}`));
|
|
2054
|
+
if (failedFiles.length > 0) {
|
|
2055
|
+
console.log(warn(`Could not remove ${failedFiles.length} file(s):`));
|
|
2056
|
+
for (const f of failedFiles) {
|
|
2057
|
+
console.log(warn(` ${f}`));
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
client.post("/telemetry/uninstall", {
|
|
2061
|
+
artifactSlug: slug,
|
|
2062
|
+
platform: entry.platform
|
|
2063
|
+
}).catch(() => {
|
|
2064
|
+
});
|
|
743
2065
|
});
|
|
744
2066
|
}
|
|
745
2067
|
|
|
@@ -769,13 +2091,497 @@ function registerKarmaCommand(program2) {
|
|
|
769
2091
|
});
|
|
770
2092
|
}
|
|
771
2093
|
|
|
2094
|
+
// src/commands/hook.ts
|
|
2095
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync11 } from "fs";
|
|
2096
|
+
import { join as join7 } from "path";
|
|
2097
|
+
import { homedir as homedir6 } from "os";
|
|
2098
|
+
var PENDING_EVENTS_PATH = join7(homedir6(), ".caik", "pending-events.json");
|
|
2099
|
+
var FLUSH_THRESHOLD = 50;
|
|
2100
|
+
var API_TIMEOUT_MS = 2e3;
|
|
2101
|
+
function readPendingEvents() {
|
|
2102
|
+
try {
|
|
2103
|
+
if (!existsSync11(PENDING_EVENTS_PATH)) return [];
|
|
2104
|
+
const raw = readFileSync7(PENDING_EVENTS_PATH, "utf-8");
|
|
2105
|
+
const parsed = JSON.parse(raw);
|
|
2106
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2107
|
+
} catch {
|
|
2108
|
+
return [];
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
function writePendingEvents(events) {
|
|
2112
|
+
const dir = join7(homedir6(), ".caik");
|
|
2113
|
+
if (!existsSync11(dir)) {
|
|
2114
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
2115
|
+
}
|
|
2116
|
+
writeFileSync8(PENDING_EVENTS_PATH, JSON.stringify(events, null, 2) + "\n", "utf-8");
|
|
2117
|
+
}
|
|
2118
|
+
function bufferEvent(event) {
|
|
2119
|
+
const events = readPendingEvents();
|
|
2120
|
+
events.push(event);
|
|
2121
|
+
writePendingEvents(events);
|
|
2122
|
+
return events;
|
|
2123
|
+
}
|
|
2124
|
+
function clearPendingEvents() {
|
|
2125
|
+
writePendingEvents([]);
|
|
2126
|
+
}
|
|
2127
|
+
async function postEventsWithTimeout(client, events) {
|
|
2128
|
+
if (events.length === 0) return;
|
|
2129
|
+
const controller = new AbortController();
|
|
2130
|
+
const timeout = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
2131
|
+
try {
|
|
2132
|
+
await Promise.race([
|
|
2133
|
+
client.post("/events", { events }),
|
|
2134
|
+
new Promise(
|
|
2135
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), API_TIMEOUT_MS)
|
|
2136
|
+
)
|
|
2137
|
+
]);
|
|
2138
|
+
clearPendingEvents();
|
|
2139
|
+
} catch {
|
|
2140
|
+
} finally {
|
|
2141
|
+
clearTimeout(timeout);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
async function postSingleEventWithTimeout(client, event) {
|
|
2145
|
+
try {
|
|
2146
|
+
await Promise.race([
|
|
2147
|
+
client.post("/events", { events: [event] }),
|
|
2148
|
+
new Promise(
|
|
2149
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), API_TIMEOUT_MS)
|
|
2150
|
+
)
|
|
2151
|
+
]);
|
|
2152
|
+
} catch {
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
function createClient(program2) {
|
|
2156
|
+
const globalOpts = program2.opts();
|
|
2157
|
+
const { apiUrl, apiKey } = resolveConfig(globalOpts);
|
|
2158
|
+
return new CaikApiClient({ apiUrl, apiKey, verbose: globalOpts.verbose });
|
|
2159
|
+
}
|
|
2160
|
+
function registerHookCommand(program2) {
|
|
2161
|
+
const hook = program2.command("hook").description("Platform hook callbacks (observation only, never gates agent actions)");
|
|
2162
|
+
hook.command("session-start").description("Log session start event").action(async () => {
|
|
2163
|
+
try {
|
|
2164
|
+
const client = createClient(program2);
|
|
2165
|
+
const event = {
|
|
2166
|
+
type: "session_start",
|
|
2167
|
+
platform: "claude-code",
|
|
2168
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2169
|
+
};
|
|
2170
|
+
await postSingleEventWithTimeout(client, event);
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
process.exit(0);
|
|
2174
|
+
});
|
|
2175
|
+
hook.command("session-end").description("Flush pending events and log session end").action(async () => {
|
|
2176
|
+
try {
|
|
2177
|
+
const client = createClient(program2);
|
|
2178
|
+
const pending = readPendingEvents();
|
|
2179
|
+
const endEvent = {
|
|
2180
|
+
type: "session_end",
|
|
2181
|
+
platform: "claude-code",
|
|
2182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2183
|
+
};
|
|
2184
|
+
pending.push(endEvent);
|
|
2185
|
+
await postEventsWithTimeout(client, pending);
|
|
2186
|
+
} catch {
|
|
2187
|
+
}
|
|
2188
|
+
process.exit(0);
|
|
2189
|
+
});
|
|
2190
|
+
hook.command("post-tool-use").description("Buffer a tool-use event").option("--tool <name>", "Tool name").option("--success <bool>", "Whether the tool call succeeded").action(async (opts) => {
|
|
2191
|
+
try {
|
|
2192
|
+
const event = {
|
|
2193
|
+
type: "tool_use",
|
|
2194
|
+
platform: "claude-code",
|
|
2195
|
+
tool: opts.tool,
|
|
2196
|
+
success: opts.success === "true" || opts.success === true,
|
|
2197
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2198
|
+
};
|
|
2199
|
+
const events = bufferEvent(event);
|
|
2200
|
+
if (events.length >= FLUSH_THRESHOLD) {
|
|
2201
|
+
const client = createClient(program2);
|
|
2202
|
+
await postEventsWithTimeout(client, events);
|
|
2203
|
+
}
|
|
2204
|
+
} catch {
|
|
2205
|
+
}
|
|
2206
|
+
process.exit(0);
|
|
2207
|
+
});
|
|
2208
|
+
hook.command("cursor-session-start").description("Log Cursor session start event").action(async () => {
|
|
2209
|
+
try {
|
|
2210
|
+
const client = createClient(program2);
|
|
2211
|
+
const event = {
|
|
2212
|
+
type: "session_start",
|
|
2213
|
+
platform: "cursor",
|
|
2214
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2215
|
+
};
|
|
2216
|
+
await postSingleEventWithTimeout(client, event);
|
|
2217
|
+
} catch {
|
|
2218
|
+
}
|
|
2219
|
+
process.exit(0);
|
|
2220
|
+
});
|
|
2221
|
+
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) => {
|
|
2222
|
+
try {
|
|
2223
|
+
const event = {
|
|
2224
|
+
type: "mcp_exec",
|
|
2225
|
+
platform: "cursor",
|
|
2226
|
+
server: opts.server,
|
|
2227
|
+
tool: opts.tool,
|
|
2228
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2229
|
+
};
|
|
2230
|
+
const events = bufferEvent(event);
|
|
2231
|
+
if (events.length >= FLUSH_THRESHOLD) {
|
|
2232
|
+
const client = createClient(program2);
|
|
2233
|
+
await postEventsWithTimeout(client, events);
|
|
2234
|
+
}
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
process.exit(0);
|
|
2238
|
+
});
|
|
2239
|
+
hook.command("cursor-session-end").description("Flush pending events and log Cursor session end").action(async () => {
|
|
2240
|
+
try {
|
|
2241
|
+
const client = createClient(program2);
|
|
2242
|
+
const pending = readPendingEvents();
|
|
2243
|
+
const endEvent = {
|
|
2244
|
+
type: "session_end",
|
|
2245
|
+
platform: "cursor",
|
|
2246
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2247
|
+
};
|
|
2248
|
+
pending.push(endEvent);
|
|
2249
|
+
await postEventsWithTimeout(client, pending);
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
process.exit(0);
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// src/commands/setup.ts
|
|
2257
|
+
import { createInterface as createInterface5 } from "readline/promises";
|
|
2258
|
+
|
|
2259
|
+
// src/platform/templates/claude-code-skill.ts
|
|
2260
|
+
function getClaudeCodeSkillContent() {
|
|
2261
|
+
return `# CAIK \u2014 AI Artifact Registry
|
|
2262
|
+
|
|
2263
|
+
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.
|
|
2264
|
+
|
|
2265
|
+
## Available MCP Tools
|
|
2266
|
+
|
|
2267
|
+
When the CAIK MCP server is running, these tools are available:
|
|
2268
|
+
|
|
2269
|
+
| Tool | Description |
|
|
2270
|
+
|------|-------------|
|
|
2271
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2272
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2273
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2274
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2275
|
+
| \`get_alternatives\` | Find similar artifacts |
|
|
2276
|
+
|
|
2277
|
+
## Quick Commands
|
|
2278
|
+
|
|
2279
|
+
If MCP is unavailable, use the CLI directly:
|
|
2280
|
+
|
|
2281
|
+
- **Search:** \`caik search <query>\`
|
|
2282
|
+
- **Install:** \`caik install <slug>\`
|
|
2283
|
+
- **Status:** \`caik status\`
|
|
2284
|
+
- **Update:** \`caik update [slug]\`
|
|
2285
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2286
|
+
|
|
2287
|
+
## Contributing Back
|
|
2288
|
+
|
|
2289
|
+
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.
|
|
2290
|
+
|
|
2291
|
+
## Discovery
|
|
2292
|
+
|
|
2293
|
+
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.
|
|
2294
|
+
`;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/platform/templates/cursor-skill.ts
|
|
2298
|
+
function getCursorSkillContent() {
|
|
2299
|
+
return `# CAIK \u2014 AI Artifact Registry
|
|
2300
|
+
|
|
2301
|
+
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.
|
|
2302
|
+
|
|
2303
|
+
## Cursor Rules Integration
|
|
2304
|
+
|
|
2305
|
+
CAIK artifacts install directly into your \`.cursorrules\` file. Installed rules are automatically loaded by Cursor's AI features.
|
|
2306
|
+
|
|
2307
|
+
## MCP Tools
|
|
2308
|
+
|
|
2309
|
+
If the CAIK MCP server is configured, these tools are available:
|
|
2310
|
+
|
|
2311
|
+
| Tool | Description |
|
|
2312
|
+
|------|-------------|
|
|
2313
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2314
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2315
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2316
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2317
|
+
|
|
2318
|
+
## CLI Fallback
|
|
2319
|
+
|
|
2320
|
+
- **Search:** \`caik search <query>\`
|
|
2321
|
+
- **Install:** \`caik install <slug>\`
|
|
2322
|
+
- **Status:** \`caik status\`
|
|
2323
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2324
|
+
|
|
2325
|
+
## Discovery
|
|
2326
|
+
|
|
2327
|
+
Ask Cursor: "Find CAIK artifacts for [your use case]" to search the registry.
|
|
2328
|
+
`;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/platform/templates/openclaw-skill.ts
|
|
2332
|
+
function getOpenClawSkillContent() {
|
|
2333
|
+
return `# CAIK \u2014 AI Artifact Registry
|
|
2334
|
+
|
|
2335
|
+
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.
|
|
2336
|
+
|
|
2337
|
+
## Skill Capabilities
|
|
2338
|
+
|
|
2339
|
+
CAIK artifacts integrate with OpenClaw's skill system. Installed skills are available as callable capabilities within your OpenClaw agent.
|
|
2340
|
+
|
|
2341
|
+
## Hook Telemetry
|
|
2342
|
+
|
|
2343
|
+
CAIK uses OpenClaw lifecycle hooks to track artifact usage. When you invoke a CAIK-installed skill, outcome data is reported automatically to improve community rankings. You can disable telemetry with \`caik config set telemetry false\`.
|
|
2344
|
+
|
|
2345
|
+
## MCP Tools
|
|
2346
|
+
|
|
2347
|
+
| Tool | Description |
|
|
2348
|
+
|------|-------------|
|
|
2349
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2350
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2351
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2352
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2353
|
+
| \`get_alternatives\` | Find similar artifacts |
|
|
2354
|
+
|
|
2355
|
+
## CLI Fallback
|
|
2356
|
+
|
|
2357
|
+
- **Search:** \`caik search <query>\`
|
|
2358
|
+
- **Install:** \`caik install <slug>\`
|
|
2359
|
+
- **Status:** \`caik status\`
|
|
2360
|
+
- **Update:** \`caik update [slug]\`
|
|
2361
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2362
|
+
|
|
2363
|
+
## Discovery
|
|
2364
|
+
|
|
2365
|
+
Ask your agent: "Find CAIK artifacts for [your use case]" to search the registry.
|
|
2366
|
+
`;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/platform/templates/index.ts
|
|
2370
|
+
function getSkillContent(platform) {
|
|
2371
|
+
switch (platform) {
|
|
2372
|
+
case "claude-code":
|
|
2373
|
+
return getClaudeCodeSkillContent();
|
|
2374
|
+
case "cursor":
|
|
2375
|
+
return getCursorSkillContent();
|
|
2376
|
+
case "openclaw":
|
|
2377
|
+
return getOpenClawSkillContent();
|
|
2378
|
+
default:
|
|
2379
|
+
return getGenericSkillContent();
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function getGenericSkillContent() {
|
|
2383
|
+
return `# CAIK \u2014 AI Artifact Registry
|
|
2384
|
+
|
|
2385
|
+
CAIK is a community-driven registry for AI coding artifacts \u2014 skills, rules, prompts, MCP servers, and knowledge packs.
|
|
2386
|
+
|
|
2387
|
+
## CLI Commands
|
|
2388
|
+
|
|
2389
|
+
- **Search:** \`caik search <query>\`
|
|
2390
|
+
- **Install:** \`caik install <slug>\`
|
|
2391
|
+
- **Status:** \`caik status\`
|
|
2392
|
+
- **Update:** \`caik update [slug]\`
|
|
2393
|
+
- **Uninstall:** \`caik uninstall <slug>\`
|
|
2394
|
+
|
|
2395
|
+
## MCP Tools
|
|
2396
|
+
|
|
2397
|
+
If the CAIK MCP server is configured, these tools are available:
|
|
2398
|
+
|
|
2399
|
+
| Tool | Description |
|
|
2400
|
+
|------|-------------|
|
|
2401
|
+
| \`search\` | Search the CAIK registry for artifacts |
|
|
2402
|
+
| \`get_artifact\` | Get full details for an artifact by slug |
|
|
2403
|
+
| \`install_artifact\` | Install an artifact into the current project |
|
|
2404
|
+
| \`report_outcome\` | Report whether an artifact worked well or poorly |
|
|
2405
|
+
|
|
2406
|
+
## Discovery
|
|
2407
|
+
|
|
2408
|
+
Ask your agent: "Find CAIK artifacts for [your use case]" to search the registry.
|
|
2409
|
+
`;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// src/commands/setup.ts
|
|
2413
|
+
function getHookConfig(platform) {
|
|
2414
|
+
if (platform === "claude-code") {
|
|
2415
|
+
return {
|
|
2416
|
+
hooks: {
|
|
2417
|
+
SessionStart: [{ type: "command", command: "npx -y @caik.dev/cli hook session-start" }],
|
|
2418
|
+
PostToolUse: [{ type: "command", command: "npx -y @caik.dev/cli hook post-tool-use" }],
|
|
2419
|
+
SessionEnd: [{ type: "command", command: "npx -y @caik.dev/cli hook session-end" }]
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
if (platform === "cursor") {
|
|
2424
|
+
return {
|
|
2425
|
+
hooks: {
|
|
2426
|
+
sessionStart: "npx -y @caik.dev/cli hook cursor-session-start",
|
|
2427
|
+
beforeMCPExecution: "npx -y @caik.dev/cli hook cursor-mcp-exec",
|
|
2428
|
+
stop: "npx -y @caik.dev/cli hook cursor-session-end"
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
hooks: {
|
|
2434
|
+
session_start: "npx -y @caik.dev/cli hook session-start",
|
|
2435
|
+
session_end: "npx -y @caik.dev/cli hook session-end",
|
|
2436
|
+
after_tool_call: "npx -y @caik.dev/cli hook post-tool-use"
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
function registerSetupCommand(program2) {
|
|
2441
|
+
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("-y, --yes", "Accept all defaults (non-interactive)").addHelpText("after", `
|
|
2442
|
+
Examples:
|
|
2443
|
+
caik setup
|
|
2444
|
+
caik setup --platform claude-code
|
|
2445
|
+
caik setup --yes --skip-hooks`).action(async (opts) => {
|
|
2446
|
+
const globalOpts = program2.opts();
|
|
2447
|
+
const { apiKey } = resolveConfig(globalOpts);
|
|
2448
|
+
const skipHooks = Boolean(opts.skipHooks);
|
|
2449
|
+
const skipStacks = Boolean(opts.skipStacks);
|
|
2450
|
+
const autoYes = Boolean(opts.yes);
|
|
2451
|
+
const platformFlag = opts.platform;
|
|
2452
|
+
console.log(heading("\nCAIK Setup Wizard"));
|
|
2453
|
+
console.log("\u2550".repeat(40));
|
|
2454
|
+
console.log();
|
|
2455
|
+
let detected;
|
|
2456
|
+
if (platformFlag) {
|
|
2457
|
+
const adapter = getPlatformAdapter(platformFlag);
|
|
2458
|
+
const det = await adapter.detect();
|
|
2459
|
+
if (det) {
|
|
2460
|
+
detected = [det];
|
|
2461
|
+
} else {
|
|
2462
|
+
detected = [{
|
|
2463
|
+
name: platformFlag,
|
|
2464
|
+
tier: "cli-mcp",
|
|
2465
|
+
configPaths: [],
|
|
2466
|
+
capabilities: { hooks: false, mcp: true, skills: false }
|
|
2467
|
+
}];
|
|
2468
|
+
console.log(warn(`Platform "${platformFlag}" was not detected but will be configured anyway.`));
|
|
2469
|
+
}
|
|
2470
|
+
} else {
|
|
2471
|
+
const spinner = createSpinner("Detecting installed platforms...");
|
|
2472
|
+
spinner.start();
|
|
2473
|
+
detected = detectPlatforms();
|
|
2474
|
+
spinner.stop();
|
|
2475
|
+
if (detected.length === 0) {
|
|
2476
|
+
console.log(warn("No supported agent platforms detected."));
|
|
2477
|
+
console.log(info("Supported platforms: claude-code, cursor, openclaw, codex, windsurf"));
|
|
2478
|
+
console.log(info("Use --platform <name> to configure a specific platform."));
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
console.log(info(`Detected ${detected.length} platform(s):`));
|
|
2482
|
+
for (const p of detected) {
|
|
2483
|
+
console.log(` ${success(p.name)} ${dim(`(${p.tier})`)}`);
|
|
2484
|
+
}
|
|
2485
|
+
console.log();
|
|
2486
|
+
}
|
|
2487
|
+
let selected;
|
|
2488
|
+
if (autoYes || detected.length === 1 || platformFlag) {
|
|
2489
|
+
selected = detected;
|
|
2490
|
+
} else {
|
|
2491
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
2492
|
+
console.log(info("Which platforms would you like to configure?"));
|
|
2493
|
+
for (let i = 0; i < detected.length; i++) {
|
|
2494
|
+
console.log(` ${i + 1}. ${detected[i].name}`);
|
|
2495
|
+
}
|
|
2496
|
+
console.log(` a. All`);
|
|
2497
|
+
const answer = await rl.question("\nSelect (numbers separated by commas, or 'a' for all): ");
|
|
2498
|
+
rl.close();
|
|
2499
|
+
if (answer.toLowerCase() === "a" || answer.trim() === "") {
|
|
2500
|
+
selected = detected;
|
|
2501
|
+
} else {
|
|
2502
|
+
const indices = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1);
|
|
2503
|
+
selected = indices.filter((i) => i >= 0 && i < detected.length).map((i) => detected[i]);
|
|
2504
|
+
if (selected.length === 0) {
|
|
2505
|
+
console.log(warn("No valid platforms selected. Exiting."));
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
console.log();
|
|
2511
|
+
for (const platform of selected) {
|
|
2512
|
+
console.log(heading(`Configuring ${platform.name}...`));
|
|
2513
|
+
console.log("\u2500".repeat(40));
|
|
2514
|
+
const adapter = getPlatformAdapter(platform.name);
|
|
2515
|
+
const mcpConfig = getDefaultMcpConfig();
|
|
2516
|
+
const mcpSpinner = createSpinner("Registering MCP server...");
|
|
2517
|
+
mcpSpinner.start();
|
|
2518
|
+
try {
|
|
2519
|
+
await adapter.registerMcp(mcpConfig);
|
|
2520
|
+
mcpSpinner.stop();
|
|
2521
|
+
console.log(success("MCP server registered"));
|
|
2522
|
+
} catch (err) {
|
|
2523
|
+
mcpSpinner.stop();
|
|
2524
|
+
console.log(error(`Failed to register MCP server: ${err instanceof Error ? err.message : String(err)}`));
|
|
2525
|
+
}
|
|
2526
|
+
if (!skipHooks && adapter.registerHooks && platform.capabilities.hooks) {
|
|
2527
|
+
const hookSpinner = createSpinner("Registering hooks...");
|
|
2528
|
+
hookSpinner.start();
|
|
2529
|
+
try {
|
|
2530
|
+
const hookConfig = getHookConfig(platform.name);
|
|
2531
|
+
await adapter.registerHooks(hookConfig);
|
|
2532
|
+
hookSpinner.stop();
|
|
2533
|
+
console.log(success("Hooks registered"));
|
|
2534
|
+
} catch (err) {
|
|
2535
|
+
hookSpinner.stop();
|
|
2536
|
+
console.log(error(`Failed to register hooks: ${err instanceof Error ? err.message : String(err)}`));
|
|
2537
|
+
}
|
|
2538
|
+
} else if (!skipHooks && !platform.capabilities.hooks) {
|
|
2539
|
+
console.log(dim(" Hooks not supported on this platform \u2014 skipped"));
|
|
2540
|
+
} else if (skipHooks) {
|
|
2541
|
+
console.log(dim(" Hooks skipped (--skip-hooks)"));
|
|
2542
|
+
}
|
|
2543
|
+
if (adapter.installSkill && platform.capabilities.skills) {
|
|
2544
|
+
const skillSpinner = createSpinner("Installing SKILL.md...");
|
|
2545
|
+
skillSpinner.start();
|
|
2546
|
+
try {
|
|
2547
|
+
const content = getSkillContent(platform.name);
|
|
2548
|
+
await adapter.installSkill("caik", content);
|
|
2549
|
+
skillSpinner.stop();
|
|
2550
|
+
console.log(success("SKILL.md installed"));
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
skillSpinner.stop();
|
|
2553
|
+
console.log(error(`Failed to install SKILL.md: ${err instanceof Error ? err.message : String(err)}`));
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
console.log();
|
|
2557
|
+
}
|
|
2558
|
+
if (!skipStacks) {
|
|
2559
|
+
console.log(dim("Stack recommendations: coming soon"));
|
|
2560
|
+
console.log();
|
|
2561
|
+
}
|
|
2562
|
+
if (!apiKey) {
|
|
2563
|
+
console.log(warn("Not authenticated. Run `caik init --auth` to connect your account."));
|
|
2564
|
+
console.log();
|
|
2565
|
+
}
|
|
2566
|
+
console.log(heading("Setup Complete"));
|
|
2567
|
+
console.log("\u2550".repeat(40));
|
|
2568
|
+
console.log(success(`Configured ${selected.length} platform(s): ${selected.map((p) => p.name).join(", ")}`));
|
|
2569
|
+
if (!apiKey) {
|
|
2570
|
+
console.log(info("Next step: run `caik init --auth` to authenticate"));
|
|
2571
|
+
} else {
|
|
2572
|
+
console.log(info("Next step: run `caik search <query>` to find artifacts"));
|
|
2573
|
+
}
|
|
2574
|
+
console.log();
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
|
|
772
2578
|
// src/index.ts
|
|
773
2579
|
var __filename = fileURLToPath(import.meta.url);
|
|
774
|
-
var __dirname =
|
|
2580
|
+
var __dirname = dirname5(__filename);
|
|
775
2581
|
var version = "0.0.1";
|
|
776
2582
|
try {
|
|
777
|
-
const pkgPath =
|
|
778
|
-
const pkg = JSON.parse(
|
|
2583
|
+
const pkgPath = join8(__dirname, "..", "package.json");
|
|
2584
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
779
2585
|
version = pkg.version;
|
|
780
2586
|
} catch {
|
|
781
2587
|
}
|
|
@@ -793,6 +2599,8 @@ registerAlternativesCommand(program);
|
|
|
793
2599
|
registerUpdateCommand(program);
|
|
794
2600
|
registerUninstallCommand(program);
|
|
795
2601
|
registerKarmaCommand(program);
|
|
2602
|
+
registerHookCommand(program);
|
|
2603
|
+
registerSetupCommand(program);
|
|
796
2604
|
program.exitOverride();
|
|
797
2605
|
async function main() {
|
|
798
2606
|
try {
|