@vectorize-io/self-driving-agents 0.0.6 → 0.0.8

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/cli.js CHANGED
@@ -280,6 +280,370 @@ async function ensurePlugin() {
280
280
  }
281
281
  }
282
282
  }
283
+ // ── NemoClaw plugin management ─────────────────────────
284
+ function listNemoClawSandboxes() {
285
+ try {
286
+ const out = execSync("nemoclaw list", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
287
+ return out
288
+ .split("\n")
289
+ .filter((l) => /^\s{4}\S/.test(l) && !l.includes("model:") && !l.includes("dashboard:"))
290
+ .map((l) => l.trim().replace(/\s*\*$/, ""));
291
+ }
292
+ catch {
293
+ return [];
294
+ }
295
+ }
296
+ async function detectNemoClawSandbox() {
297
+ const sandboxes = listNemoClawSandboxes();
298
+ if (sandboxes.length === 0) {
299
+ p.cancel("No NemoClaw sandboxes found. Create one with: nemoclaw onboard");
300
+ process.exit(1);
301
+ }
302
+ if (sandboxes.length === 1) {
303
+ p.log.info(`Using sandbox: ${color.cyan(sandboxes[0])}`);
304
+ return sandboxes[0];
305
+ }
306
+ const selected = await p.select({
307
+ message: "Select a NemoClaw sandbox:",
308
+ options: sandboxes.map((s) => ({ value: s, label: s })),
309
+ });
310
+ if (p.isCancel(selected)) {
311
+ p.cancel("Cancelled.");
312
+ process.exit(0);
313
+ }
314
+ return selected;
315
+ }
316
+ async function ensureNemoClawPlugin(sandboxName, agentId) {
317
+ // Check nemoclaw is installed
318
+ try {
319
+ execSync("which nemoclaw", { stdio: "pipe" });
320
+ }
321
+ catch {
322
+ p.cancel("nemoclaw not found. Install it: curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash");
323
+ process.exit(1);
324
+ }
325
+ // Check sandbox exists
326
+ try {
327
+ execSync(`nemoclaw ${sandboxName} status`, { stdio: "pipe" });
328
+ }
329
+ catch {
330
+ p.cancel(`Sandbox '${sandboxName}' not found. Create one with: nemoclaw onboard`);
331
+ process.exit(1);
332
+ }
333
+ // NemoClaw runs OpenClaw inside a sandbox with read-only config (Landlock).
334
+ // hindsight-nemoclaw setup handles everything:
335
+ // 1. Installs the openclaw plugin
336
+ // 2. Writes plugin config to host ~/.openclaw/openclaw.json
337
+ // 3. Adds the Hindsight network policy to the sandbox
338
+ // 4. Restarts the gateway
339
+ // We always run it — it's idempotent and ensures the sandbox has the
340
+ // network policy even if the host already has the plugin configured.
341
+ const config = readOpenClawConfig();
342
+ const pc = config?.plugins?.entries?.["hindsight-openclaw"]?.config || {};
343
+ if (!pc.hindsightApiUrl || !pc.hindsightApiToken) {
344
+ // No Hindsight config at all — run interactive setup
345
+ p.log.warn("Hindsight plugin needs configuration for NemoClaw.");
346
+ try {
347
+ execSync(`npx --yes --package @vectorize-io/hindsight-nemoclaw hindsight-nemoclaw setup --sandbox ${sandboxName}`, { stdio: "inherit" });
348
+ }
349
+ catch {
350
+ p.cancel("Plugin setup failed. Run manually:\n npx --yes --package @vectorize-io/hindsight-nemoclaw hindsight-nemoclaw setup --sandbox " +
351
+ sandboxName);
352
+ process.exit(1);
353
+ }
354
+ }
355
+ else {
356
+ // Config exists — run non-interactive setup to ensure network policy + plugin are in place
357
+ const apiUrl = pc.hindsightApiUrl;
358
+ const apiToken = pc.hindsightApiToken;
359
+ const bankPrefix = pc.bankIdPrefix || "nemoclaw";
360
+ p.log.info(`Hindsight: ${color.cyan(`External: ${apiUrl}`)}`);
361
+ try {
362
+ execSync(`npx --yes --package @vectorize-io/hindsight-nemoclaw hindsight-nemoclaw setup` +
363
+ ` --sandbox ${sandboxName}` +
364
+ ` --api-url ${apiUrl}` +
365
+ ` --api-token ${apiToken}` +
366
+ ` --bank-prefix ${bankPrefix}` +
367
+ ` --skip-plugin-install`, { stdio: "inherit" });
368
+ }
369
+ catch {
370
+ p.cancel(`Failed to apply sandbox network policy.\n` +
371
+ ` The sandbox may have been destroyed. Check with: nemoclaw list\n` +
372
+ ` Recreate with: nemoclaw onboard`);
373
+ process.exit(1);
374
+ }
375
+ }
376
+ enableKnowledgeTools();
377
+ // Rebuild sandbox so it picks up the latest host config
378
+ p.log.info("Rebuilding sandbox to apply config...");
379
+ try {
380
+ execSync(`nemoclaw ${sandboxName} rebuild --yes`, { stdio: "inherit" });
381
+ p.log.success("Sandbox rebuilt");
382
+ }
383
+ catch {
384
+ p.cancel(`Failed to rebuild sandbox '${sandboxName}'.\n` +
385
+ ` Check with: nemoclaw list\n` +
386
+ ` Recreate with: nemoclaw onboard`);
387
+ process.exit(1);
388
+ }
389
+ }
390
+ // ── Hermes plugin management ───────────────────────────
391
+ function ensureHermesPlugin(agentId, apiUrl, bankId, apiToken) {
392
+ // Check hermes is installed
393
+ try {
394
+ execSync("which hermes", { stdio: "pipe" });
395
+ }
396
+ catch {
397
+ p.cancel("hermes not found. Install it: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash");
398
+ process.exit(1);
399
+ }
400
+ // Create a Hermes profile for this agent (or reuse if exists)
401
+ let profileHome;
402
+ try {
403
+ const showOut = execSync(`hermes profile show ${agentId}`, {
404
+ encoding: "utf-8",
405
+ stdio: ["pipe", "pipe", "pipe"],
406
+ });
407
+ const pathMatch = showOut.match(/Path:\s+(\S+)/);
408
+ profileHome = pathMatch ? pathMatch[1] : join(homedir(), ".hermes", "profiles", agentId);
409
+ p.log.info(`Hermes profile '${agentId}' already exists`);
410
+ }
411
+ catch {
412
+ // Profile doesn't exist — create it (clone config from default)
413
+ try {
414
+ execSync(`hermes profile create ${agentId} --clone`, {
415
+ encoding: "utf-8",
416
+ stdio: ["pipe", "pipe", "pipe"],
417
+ });
418
+ const showOut = execSync(`hermes profile show ${agentId}`, {
419
+ encoding: "utf-8",
420
+ stdio: ["pipe", "pipe", "pipe"],
421
+ });
422
+ const pathMatch = showOut.match(/Path:\s+(\S+)/);
423
+ profileHome = pathMatch ? pathMatch[1] : join(homedir(), ".hermes", "profiles", agentId);
424
+ p.log.success(`Hermes profile '${agentId}' created`);
425
+ }
426
+ catch (err) {
427
+ const msg = err?.stderr?.toString?.()?.trim() || err?.message || String(err);
428
+ p.cancel(`Failed to create Hermes profile: ${msg}`);
429
+ process.exit(1);
430
+ }
431
+ }
432
+ // Install plugin into the profile
433
+ const pluginDir = join(profileHome, "plugins", "hindsight-sda");
434
+ const sdaPluginSrc = join(__dirname, "..", "hermes-plugin");
435
+ mkdirSync(pluginDir, { recursive: true });
436
+ for (const file of ["plugin.yaml", "__init__.py"]) {
437
+ const src = join(sdaPluginSrc, file);
438
+ if (existsSync(src)) {
439
+ writeFileSync(join(pluginDir, file), readFileSync(src, "utf-8"));
440
+ }
441
+ }
442
+ p.log.success("Hermes plugin installed");
443
+ // Write hindsight/config.json in the profile — single source of truth for
444
+ // both the bundled hindsight provider (auto-retain) and our tool plugin.
445
+ // Static bank_id, no template — both read the same bank.
446
+ const hindsightCfgDir = join(profileHome, "hindsight");
447
+ mkdirSync(hindsightCfgDir, { recursive: true });
448
+ writeFileSync(join(hindsightCfgDir, "config.json"), JSON.stringify({
449
+ mode: "cloud",
450
+ api_url: apiUrl,
451
+ api_key: apiToken,
452
+ bank_id: bankId,
453
+ bank_id_template: "",
454
+ recall_budget: "mid",
455
+ memory_mode: "hybrid",
456
+ }, null, 2) + "\n");
457
+ // Set the bundled hindsight as memory provider + enable our plugin in config.yaml
458
+ const hermesConfigPath = join(profileHome, "config.yaml");
459
+ if (existsSync(hermesConfigPath)) {
460
+ let config = readFileSync(hermesConfigPath, "utf-8");
461
+ // Set memory.provider to hindsight (bundled provider for auto-retain)
462
+ const lines = config.split("\n");
463
+ let inMemory = false;
464
+ for (let i = 0; i < lines.length; i++) {
465
+ if (/^memory:/.test(lines[i]))
466
+ inMemory = true;
467
+ else if (/^\S/.test(lines[i]) && inMemory)
468
+ inMemory = false;
469
+ if (inMemory && /^\s+provider:\s/.test(lines[i])) {
470
+ lines[i] = lines[i].replace(/provider:\s*\S+/, "provider: hindsight");
471
+ break;
472
+ }
473
+ }
474
+ config = lines.join("\n");
475
+ // Enable our tool plugin in plugins.enabled
476
+ if (!config.includes("hindsight-sda")) {
477
+ if (/plugins:\s*\n\s+enabled:/.test(config)) {
478
+ config = config.replace(/(plugins:\s*\n\s+enabled:\s*\n)/, "$1 - hindsight-sda\n");
479
+ }
480
+ else if (/plugins:/.test(config)) {
481
+ config = config.replace(/(plugins:)/, "$1\n enabled:\n - hindsight-sda");
482
+ }
483
+ else {
484
+ config += "\nplugins:\n enabled:\n - hindsight-sda\n";
485
+ }
486
+ }
487
+ writeFileSync(hermesConfigPath, config);
488
+ }
489
+ p.log.success("Hindsight memory + knowledge tools configured");
490
+ // Install skill into the profile
491
+ const skillDir = join(profileHome, "skills", "agent-knowledge");
492
+ mkdirSync(skillDir, { recursive: true });
493
+ writeFileSync(join(skillDir, "SKILL.md"), SKILL_MD);
494
+ p.log.success("Knowledge skill installed");
495
+ }
496
+ // ── Claude skill generation ────────────────────────────
497
+ const CLAUDE_SKILLS_DIR = join(homedir(), "self-driving-agents", "claude");
498
+ const HINDSIGHT_CLOUD_API_URL = "https://api.hindsight.vectorize.io";
499
+ async function generateClaudeSkill(agentId, apiUrl, bankId, apiToken) {
500
+ const authHeader = apiToken ? `-H "Authorization: Bearer ${apiToken}" \\\n ` : "";
501
+ const skillMd = `# Hindsight Memory — ${agentId}
502
+
503
+ ## Mandatory Startup Sequence
504
+
505
+ On every new conversation, run these commands **before doing anything else**:
506
+
507
+ ### 1. List all knowledge pages
508
+ \`\`\`bash
509
+ curl -s ${apiUrl}/v1/default/banks/${bankId}/knowledge/pages \\
510
+ ${authHeader}-H "Content-Type: application/json"
511
+ \`\`\`
512
+
513
+ ### 2. Read each page (replace PAGE_ID)
514
+ \`\`\`bash
515
+ curl -s ${apiUrl}/v1/default/banks/${bankId}/knowledge/pages/PAGE_ID \\
516
+ ${authHeader}-H "Content-Type: application/json"
517
+ \`\`\`
518
+
519
+ Read **every** page listed in step 1 to load the agent's full knowledge base.
520
+
521
+ ## Creating Knowledge Pages
522
+
523
+ \`\`\`bash
524
+ curl -s -X POST ${apiUrl}/v1/default/banks/${bankId}/knowledge/pages \\
525
+ ${authHeader}-H "Content-Type: application/json" \\
526
+ -d '{"title": "Page Title", "content": "Markdown content here"}'
527
+ \`\`\`
528
+
529
+ ## Searching Memories
530
+
531
+ \`\`\`bash
532
+ curl -s -X POST ${apiUrl}/v1/default/banks/${bankId}/memories/recall \\
533
+ ${authHeader}-H "Content-Type: application/json" \\
534
+ -d '{"query": "your search query"}'
535
+ \`\`\`
536
+
537
+ ## Ingesting Documents
538
+
539
+ \`\`\`bash
540
+ curl -s -X POST ${apiUrl}/v1/default/banks/${bankId}/memories/retain \\
541
+ ${authHeader}-H "Content-Type: application/json" \\
542
+ -d '{"content": "Document content to remember"}'
543
+ \`\`\`
544
+
545
+ ## Updating Knowledge Pages
546
+
547
+ \`\`\`bash
548
+ curl -s -X PUT ${apiUrl}/v1/default/banks/${bankId}/knowledge/pages/PAGE_ID \\
549
+ ${authHeader}-H "Content-Type: application/json" \\
550
+ -d '{"title": "Updated Title", "content": "Updated content"}'
551
+ \`\`\`
552
+
553
+ ## Deleting Knowledge Pages
554
+
555
+ \`\`\`bash
556
+ curl -s -X DELETE ${apiUrl}/v1/default/banks/${bankId}/knowledge/pages/PAGE_ID \\
557
+ ${authHeader}-H "Content-Type: application/json"
558
+ \`\`\`
559
+
560
+ ## Retaining Conversations
561
+
562
+ Claude Chat and Cowork do not have automatic hooks. You must **self-retain** important conversation content.
563
+
564
+ After any significant exchange (decisions, new information, task outcomes), retain it:
565
+
566
+ \`\`\`bash
567
+ curl -s -X POST ${apiUrl}/v1/default/banks/${bankId}/memories/retain \\
568
+ ${authHeader}-H "Content-Type: application/json" \\
569
+ -d '{"content": "Summary of the conversation or key information learned"}'
570
+ \`\`\`
571
+
572
+ **When to self-retain:**
573
+ - After learning user preferences or project context
574
+ - After completing a task (retain the outcome)
575
+ - After receiving corrections or feedback
576
+ - After discovering important facts during research
577
+
578
+ ## Important Notes
579
+
580
+ - Always run the startup sequence at the beginning of every conversation
581
+ - Knowledge pages are persistent — they survive across conversations
582
+ - Use recall to search existing memories before creating duplicates
583
+ - Self-retain is essential since there are no automatic hooks in Claude Chat/Cowork
584
+ `;
585
+ const outDir = join(CLAUDE_SKILLS_DIR, agentId);
586
+ mkdirSync(outDir, { recursive: true });
587
+ const skillPath = join(outDir, "SKILL.md");
588
+ writeFileSync(skillPath, skillMd);
589
+ // Create a zip of the skill directory
590
+ const zipPath = join(outDir, `${agentId}-skill.zip`);
591
+ execSync(`cd "${outDir}" && zip -j "${zipPath}" SKILL.md`, { stdio: "pipe" });
592
+ return zipPath;
593
+ }
594
+ async function promptClaudeConfig(agentId) {
595
+ const deploymentType = await p.select({
596
+ message: "Hindsight deployment:",
597
+ options: [
598
+ { value: "cloud", label: "Cloud (api.hindsight.vectorize.io)" },
599
+ { value: "self-hosted", label: "Self-hosted" },
600
+ ],
601
+ });
602
+ if (p.isCancel(deploymentType)) {
603
+ p.cancel("Cancelled.");
604
+ process.exit(0);
605
+ }
606
+ let apiUrl;
607
+ if (deploymentType === "cloud") {
608
+ apiUrl = HINDSIGHT_CLOUD_API_URL;
609
+ }
610
+ else {
611
+ const urlInput = await p.text({
612
+ message: "Hindsight API URL:",
613
+ placeholder: "https://your-hindsight.example.com",
614
+ validate: (val) => {
615
+ if (!val)
616
+ return "URL is required";
617
+ if (val.startsWith("http://localhost") || val.startsWith("http://127.0.0.1")) {
618
+ return "Claude cannot reach localhost. Use a publicly accessible URL.";
619
+ }
620
+ return undefined;
621
+ },
622
+ });
623
+ if (p.isCancel(urlInput)) {
624
+ p.cancel("Cancelled.");
625
+ process.exit(0);
626
+ }
627
+ apiUrl = urlInput;
628
+ p.log.warn("Make sure your Hindsight instance is publicly accessible from Claude's servers.");
629
+ }
630
+ const tokenInput = await p.text({ message: "Hindsight API token:" });
631
+ if (p.isCancel(tokenInput)) {
632
+ p.cancel("Cancelled.");
633
+ process.exit(0);
634
+ }
635
+ const apiToken = tokenInput || undefined;
636
+ const bankInput = await p.text({
637
+ message: "Bank ID:",
638
+ initialValue: agentId,
639
+ });
640
+ if (p.isCancel(bankInput)) {
641
+ p.cancel("Cancelled.");
642
+ process.exit(0);
643
+ }
644
+ const bankId = bankInput.trim() || agentId;
645
+ return { apiUrl, bankId, apiToken };
646
+ }
283
647
  // ── Main ────────────────────────────────────────────────
284
648
  async function main() {
285
649
  const args = process.argv.slice(2);
@@ -296,8 +660,9 @@ async function main() {
296
660
  ${color.cyan("./local-dir")} → local directory
297
661
 
298
662
  ${color.dim("Options:")}
299
- ${color.cyan("--harness <h>")} Required. openclaw | hermes | claude-code
663
+ ${color.cyan("--harness <h>")} Required. openclaw | nemoclaw | hermes | claude | claude-code
300
664
  ${color.cyan("--agent <name>")} Agent name (defaults to directory name)
665
+ ${color.cyan("--sandbox <name>")} NemoClaw sandbox (auto-detected if only one exists)
301
666
  `);
302
667
  process.exit(0);
303
668
  }
@@ -309,30 +674,204 @@ async function main() {
309
674
  }
310
675
  let harness;
311
676
  let agentName;
677
+ let sandbox;
312
678
  for (let i = 0; i < restArgs.length; i++) {
313
679
  if (restArgs[i] === "--harness" && restArgs[i + 1])
314
680
  harness = restArgs[++i];
315
681
  else if (restArgs[i] === "--agent" && restArgs[i + 1])
316
682
  agentName = restArgs[++i];
683
+ else if (restArgs[i] === "--sandbox" && restArgs[i + 1])
684
+ sandbox = restArgs[++i];
317
685
  }
318
686
  if (!harness) {
319
- p.cancel("--harness required (openclaw | hermes | claude-code)");
687
+ p.cancel("--harness required (openclaw | nemoclaw | hermes | claude | claude-code)");
320
688
  process.exit(1);
321
689
  }
690
+ if (harness === "nemoclaw" && !sandbox) {
691
+ sandbox = await detectNemoClawSandbox();
692
+ }
322
693
  p.intro(color.bgCyan(color.black(` self-driving-agents `)));
323
694
  // Step 0: Resolve agent directory (local or GitHub)
324
695
  const spin = p.spinner();
325
696
  const { dir, source, defaultName, cleanup } = await resolveAgentDir(dirArg, spin);
326
697
  try {
327
- const agentId = agentName || defaultName;
698
+ let agentId;
699
+ if (agentName) {
700
+ agentId = agentName;
701
+ }
702
+ else if (process.stdin.isTTY) {
703
+ const nameInput = await p.text({
704
+ message: "Agent name:",
705
+ initialValue: defaultName,
706
+ });
707
+ if (p.isCancel(nameInput)) {
708
+ p.cancel("Cancelled.");
709
+ process.exit(0);
710
+ }
711
+ agentId = nameInput.trim() || defaultName;
712
+ }
713
+ else {
714
+ agentId = defaultName;
715
+ }
328
716
  // Step 1: Ensure plugin
717
+ let apiUrl;
718
+ let bankId;
719
+ let apiToken;
720
+ let claudeSkillZip;
329
721
  if (harness === "openclaw") {
330
722
  await ensurePlugin();
331
723
  enableKnowledgeTools();
724
+ ({ apiUrl, bankId, apiToken } = resolveFromPlugin(agentId));
332
725
  }
333
- // Step 2: Resolve bank + API from plugin config
334
- const { apiUrl, bankId, apiToken } = resolveFromPlugin(agentId);
335
- const workspaceDir = join(homedir(), ".self-driving-agents", "openclaw", agentId);
726
+ else if (harness === "nemoclaw") {
727
+ await ensureNemoClawPlugin(sandbox, agentId);
728
+ ({ apiUrl, bankId, apiToken } = resolveFromPlugin(agentId));
729
+ }
730
+ else if (harness === "hermes") {
731
+ // Resolve Hindsight connection: hermes config > openclaw config > prompt
732
+ const hermesHsCfgPath = join(homedir(), ".hermes", "hindsight", "config.json");
733
+ let defaultUrl = "https://api.hindsight.vectorize.io";
734
+ let defaultToken = "";
735
+ // Check existing hermes hindsight config first
736
+ if (existsSync(hermesHsCfgPath)) {
737
+ try {
738
+ const hsCfg = JSON.parse(readFileSync(hermesHsCfgPath, "utf-8"));
739
+ if (hsCfg.api_url)
740
+ defaultUrl = hsCfg.api_url;
741
+ if (hsCfg.api_key)
742
+ defaultToken = hsCfg.api_key;
743
+ }
744
+ catch {
745
+ /* ignore */
746
+ }
747
+ }
748
+ // Fall back to openclaw config
749
+ if (!defaultToken) {
750
+ const config = readOpenClawConfig();
751
+ const pc = config?.plugins?.entries?.["hindsight-openclaw"]?.config;
752
+ if (pc?.hindsightApiUrl)
753
+ defaultUrl = pc.hindsightApiUrl;
754
+ if (pc?.hindsightApiToken)
755
+ defaultToken = pc.hindsightApiToken;
756
+ }
757
+ // If we have credentials, confirm; otherwise prompt
758
+ if (defaultUrl && defaultToken) {
759
+ const ok = await p.confirm({
760
+ message: `Hindsight: ${color.cyan(defaultUrl)}. Use this?`,
761
+ });
762
+ if (p.isCancel(ok)) {
763
+ p.cancel("Cancelled.");
764
+ process.exit(0);
765
+ }
766
+ if (ok) {
767
+ apiUrl = defaultUrl;
768
+ apiToken = defaultToken;
769
+ }
770
+ else {
771
+ const urlInput = await p.text({
772
+ message: "Hindsight API URL:",
773
+ initialValue: defaultUrl,
774
+ });
775
+ if (p.isCancel(urlInput)) {
776
+ p.cancel("Cancelled.");
777
+ process.exit(0);
778
+ }
779
+ const tokenInput = await p.text({ message: "Hindsight API token:" });
780
+ if (p.isCancel(tokenInput)) {
781
+ p.cancel("Cancelled.");
782
+ process.exit(0);
783
+ }
784
+ apiUrl = urlInput;
785
+ apiToken = tokenInput;
786
+ }
787
+ }
788
+ else {
789
+ const urlInput = await p.text({ message: "Hindsight API URL:", initialValue: defaultUrl });
790
+ if (p.isCancel(urlInput)) {
791
+ p.cancel("Cancelled.");
792
+ process.exit(0);
793
+ }
794
+ const tokenInput = await p.text({ message: "Hindsight API token:" });
795
+ if (p.isCancel(tokenInput)) {
796
+ p.cancel("Cancelled.");
797
+ process.exit(0);
798
+ }
799
+ apiUrl = urlInput;
800
+ apiToken = tokenInput;
801
+ }
802
+ bankId = agentId;
803
+ ensureHermesPlugin(agentId, apiUrl, bankId, apiToken);
804
+ }
805
+ else if (harness === "claude") {
806
+ const claudeConfig = await promptClaudeConfig(agentId);
807
+ apiUrl = claudeConfig.apiUrl;
808
+ bankId = claudeConfig.bankId;
809
+ apiToken = claudeConfig.apiToken;
810
+ }
811
+ else if (harness === "claude-code") {
812
+ // Claude Code: just save content locally, Claude handles the rest via skill
813
+ const contentDir = join(homedir(), ".self-driving-agents", "claude-code", agentId);
814
+ mkdirSync(contentDir, { recursive: true });
815
+ // Copy content files to the local dir
816
+ const contentFiles = findContentFiles(dir);
817
+ for (const relPath of contentFiles) {
818
+ const destPath = join(contentDir, relPath);
819
+ mkdirSync(join(destPath, ".."), { recursive: true });
820
+ writeFileSync(destPath, readFileSync(join(dir, relPath), "utf-8"));
821
+ }
822
+ // Copy bank-template.json if present (has mental model definitions)
823
+ const templateSrc = join(dir, "bank-template.json");
824
+ if (existsSync(templateSrc)) {
825
+ writeFileSync(join(contentDir, "bank-template.json"), readFileSync(templateSrc, "utf-8"));
826
+ }
827
+ p.log.success(`Content saved to ${color.dim(contentDir)} (${contentFiles.length} files)`);
828
+ // Auto-approve hindsight MCP tools and skills in user settings
829
+ const userSettingsPath = join(homedir(), ".claude", "settings.json");
830
+ let userSettings = {};
831
+ if (existsSync(userSettingsPath)) {
832
+ try {
833
+ userSettings = JSON.parse(readFileSync(userSettingsPath, "utf-8"));
834
+ }
835
+ catch {
836
+ /* ignore */
837
+ }
838
+ }
839
+ const allowedTools = userSettings.allowedTools || [];
840
+ const toolsToAllow = [
841
+ "mcp__hindsight__*",
842
+ "Skill(hindsight-memory:create-agent)",
843
+ `Bash(ls ~/.self-driving-agents/*)`,
844
+ `Bash(cat ~/.self-driving-agents/*)`,
845
+ ];
846
+ let updated = false;
847
+ for (const tool of toolsToAllow) {
848
+ if (!allowedTools.includes(tool)) {
849
+ allowedTools.push(tool);
850
+ updated = true;
851
+ }
852
+ }
853
+ if (updated) {
854
+ userSettings.allowedTools = allowedTools;
855
+ writeFileSync(userSettingsPath, JSON.stringify(userSettings, null, 2) + "\n");
856
+ p.log.success("Auto-approved hindsight tools in Claude Code");
857
+ }
858
+ const hasBankTemplate = existsSync(join(contentDir, "bank-template.json"));
859
+ const prompt = hasBankTemplate
860
+ ? `Use /hindsight-memory:create-agent to create a "${agentId}" agent. Then ingest all files from ${contentDir}/ (skip bank-template.json). Read ${contentDir}/bank-template.json and create the exact mental models (knowledge pages) defined in its "mental_models" array using agent_knowledge_create_page for each one.`
861
+ : `Use /hindsight-memory:create-agent to create a "${agentId}" agent. Then ingest all files from ${contentDir}/ and create 3 knowledge pages that make sense based on the content.`;
862
+ p.note([
863
+ `${color.dim("1.")} Start Claude Code`,
864
+ `${color.dim("2.")} Say: ${color.cyan(prompt)}`,
865
+ ].join("\n"), "Next steps");
866
+ p.outro(color.green(`'${agentId}' content ready`));
867
+ cleanup?.();
868
+ return;
869
+ }
870
+ else {
871
+ p.cancel(`Unknown harness: ${harness}`);
872
+ process.exit(1);
873
+ }
874
+ const workspaceDir = join(homedir(), ".self-driving-agents", harness, agentId);
336
875
  p.log.info([
337
876
  `Agent: ${color.bold(agentId)}`,
338
877
  `Source: ${color.dim(source)}`,
@@ -390,13 +929,39 @@ async function main() {
390
929
  }
391
930
  spin.stop(`Ingested ${contentFiles.length} file(s)`);
392
931
  }
393
- // Step 6: Create agent + install skill
394
- mkdirSync(workspaceDir, { recursive: true });
395
- const skillDir = join(workspaceDir, "skills", "agent-knowledge");
396
- mkdirSync(skillDir, { recursive: true });
397
- writeFileSync(join(skillDir, "SKILL.md"), SKILL_MD);
398
- p.log.success("Knowledge skill installed");
399
- if (harness === "openclaw") {
932
+ // Step 6: Create agent + install skill (hermes handled in ensureHermesPlugin)
933
+ if (harness === "claude") {
934
+ claudeSkillZip = await generateClaudeSkill(agentId, apiUrl, bankId, apiToken);
935
+ }
936
+ else if (harness === "hermes") {
937
+ // Skill + plugin already installed by ensureHermesPlugin
938
+ }
939
+ else if (harness === "nemoclaw") {
940
+ // NemoClaw: install skill into the sandbox via nemoclaw CLI
941
+ const tmpSkillDir = join(tmpdir(), `sda-skill-${Date.now()}`);
942
+ const tmpSkill = join(tmpSkillDir, "agent-knowledge");
943
+ mkdirSync(tmpSkill, { recursive: true });
944
+ writeFileSync(join(tmpSkill, "SKILL.md"), SKILL_MD);
945
+ try {
946
+ execSync(`nemoclaw ${sandbox} skill install ${tmpSkill}`, { stdio: "inherit" });
947
+ p.log.success("Knowledge skill installed in sandbox");
948
+ }
949
+ catch (err) {
950
+ const stderr = err?.stderr?.toString?.()?.trim() || "";
951
+ const msg = stderr || err?.message || String(err);
952
+ p.log.warn(`Failed to install skill: ${msg}\n Install manually:\n nemoclaw ${sandbox} skill install <skill-dir>`);
953
+ }
954
+ finally {
955
+ rmSync(tmpSkillDir, { recursive: true, force: true });
956
+ }
957
+ }
958
+ else {
959
+ // OpenClaw: install skill locally + create agent
960
+ mkdirSync(workspaceDir, { recursive: true });
961
+ const skillDir = join(workspaceDir, "skills", "agent-knowledge");
962
+ mkdirSync(skillDir, { recursive: true });
963
+ writeFileSync(join(skillDir, "SKILL.md"), SKILL_MD);
964
+ p.log.success("Knowledge skill installed");
400
965
  try {
401
966
  const listOut = execSync("openclaw agents list --json", {
402
967
  encoding: "utf-8",
@@ -419,21 +984,44 @@ async function main() {
419
984
  const msg = stderr || err?.message || String(err);
420
985
  p.log.warn(`Failed to manage agent: ${msg}\n Create manually:\n openclaw agents add ${agentId} --workspace ${workspaceDir} --non-interactive`);
421
986
  }
422
- }
423
- // Step 7: Patch startup
424
- const startupFile = join(workspaceDir, "AGENTS.md");
425
- if (existsSync(startupFile)) {
426
- let text = readFileSync(startupFile, "utf-8");
427
- if (!text.includes("agent-knowledge")) {
428
- text = text.replace("Don't ask permission. Just do it.", "5. Read `skills/agent-knowledge/SKILL.md` and **execute its mandatory startup sequence**\n\nDon't ask permission. Just do it.");
429
- writeFileSync(startupFile, text);
430
- p.log.success("Startup patched");
987
+ // Patch startup
988
+ const startupFile = join(workspaceDir, "AGENTS.md");
989
+ if (existsSync(startupFile)) {
990
+ let text = readFileSync(startupFile, "utf-8");
991
+ if (!text.includes("agent-knowledge")) {
992
+ text = text.replace("Don't ask permission. Just do it.", "5. Read `skills/agent-knowledge/SKILL.md` and **execute its mandatory startup sequence**\n\nDon't ask permission. Just do it.");
993
+ writeFileSync(startupFile, text);
994
+ p.log.success("Startup patched");
995
+ }
431
996
  }
432
997
  }
433
- p.note([
434
- `${color.dim("1.")} openclaw gateway restart`,
435
- `${color.dim("2.")} openclaw tui --session agent:${agentId}:main:session1`,
436
- ].join("\n"), "Next steps");
998
+ // Next steps
999
+ let nextSteps;
1000
+ if (harness === "claude") {
1001
+ const apiHost = new URL(apiUrl).hostname;
1002
+ nextSteps = [
1003
+ `${color.dim("1.")} Open Claude → Customize → Skills → Upload skill`,
1004
+ `${color.dim("2.")} Select: ${color.cyan(claudeSkillZip)}`,
1005
+ `${color.dim("3.")} Allowlist the API host: Settings → Capabilities → add ${color.cyan(apiHost)}`,
1006
+ `${color.dim("4.")} Start a conversation and type ${color.cyan(`/${agentId}`)} to activate the agent`,
1007
+ ];
1008
+ }
1009
+ else if (harness === "hermes") {
1010
+ nextSteps = [`${color.dim("1.")} hermes -p ${agentId} chat`];
1011
+ }
1012
+ else if (harness === "nemoclaw") {
1013
+ nextSteps = [
1014
+ `${color.dim("1.")} nemoclaw ${sandbox} connect`,
1015
+ `${color.dim("2.")} openclaw tui --session agent:main:main:session1`,
1016
+ ];
1017
+ }
1018
+ else {
1019
+ nextSteps = [
1020
+ `${color.dim("1.")} openclaw gateway restart`,
1021
+ `${color.dim("2.")} openclaw tui --session agent:${agentId}:main:session1`,
1022
+ ];
1023
+ }
1024
+ p.note(nextSteps.join("\n"), "Next steps");
437
1025
  p.outro(color.green(`'${agentId}' is ready`));
438
1026
  }
439
1027
  finally {