@vectorize-io/self-driving-agents 0.0.5 → 0.0.7

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
@@ -104,7 +104,7 @@ async function resolveAgentDir(input, spinner) {
104
104
  }
105
105
  // ── Skill ───────────────────────────────────────────────
106
106
  const __dirname = dirname(fileURLToPath(import.meta.url));
107
- const SKILL_PATH = join(__dirname, "..", "skill", "SKILL.md");
107
+ const SKILL_PATH = join(__dirname, "skill", "SKILL.md");
108
108
  const SKILL_MD = readFileSync(SKILL_PATH, "utf-8");
109
109
  // ── Plugin management ───────────────────────────────────
110
110
  const OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
@@ -125,7 +125,7 @@ function enableKnowledgeTools() {
125
125
  pc.enableKnowledgeTools = true;
126
126
  writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
127
127
  }
128
- const MIN_PLUGIN_VERSION = "0.7.0";
128
+ const MIN_PLUGIN_VERSION = "0.7.2";
129
129
  function getInstalledPluginVersion() {
130
130
  try {
131
131
  // Check the installed plugin's package.json
@@ -153,8 +153,11 @@ function isPluginInstalled() {
153
153
  const config = readOpenClawConfig();
154
154
  if (!config)
155
155
  return false;
156
- return (config.plugins?.entries?.["hindsight-openclaw"]?.enabled !== false &&
157
- config.plugins?.entries?.["hindsight-openclaw"] !== undefined);
156
+ const hasConfig = config.plugins?.entries?.["hindsight-openclaw"]?.enabled !== false &&
157
+ config.plugins?.entries?.["hindsight-openclaw"] !== undefined;
158
+ // Also check the extension dir actually exists (may have been deleted during a failed upgrade)
159
+ const extDir = join(homedir(), ".openclaw", "extensions", "hindsight-openclaw");
160
+ return hasConfig && existsSync(extDir);
158
161
  }
159
162
  function isPluginConfigured() {
160
163
  const config = readOpenClawConfig();
@@ -217,13 +220,28 @@ async function ensurePlugin() {
217
220
  p.log.warn("Hindsight plugin not found. Installing...");
218
221
  }
219
222
  try {
223
+ // Remove old extension if present — openclaw doesn't support in-place upgrade
224
+ const extDir = join(homedir(), ".openclaw", "extensions", "hindsight-openclaw");
225
+ rmSync(extDir, { recursive: true, force: true });
226
+ // Temporarily clear plugins.slots.memory so openclaw doesn't reject
227
+ // the config while the extension is missing
228
+ const cfg = readOpenClawConfig();
229
+ if (cfg?.plugins?.slots?.memory === "hindsight-openclaw") {
230
+ delete cfg.plugins.slots.memory;
231
+ writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
232
+ }
220
233
  execSync("openclaw plugins install @vectorize-io/hindsight-openclaw", { stdio: "inherit" });
234
+ const newVersion = getInstalledPluginVersion();
235
+ p.log.success(`Hindsight plugin v${newVersion} installed`);
221
236
  }
222
237
  catch {
223
238
  p.cancel("Failed to install plugin. Run manually:\n openclaw plugins install @vectorize-io/hindsight-openclaw");
224
239
  process.exit(1);
225
240
  }
226
241
  }
242
+ else if (currentVersion) {
243
+ p.log.info(`Hindsight plugin v${currentVersion}`);
244
+ }
227
245
  if (!isPluginConfigured()) {
228
246
  p.log.warn("Hindsight plugin needs configuration.");
229
247
  try {
@@ -262,6 +280,370 @@ async function ensurePlugin() {
262
280
  }
263
281
  }
264
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
+ }
265
647
  // ── Main ────────────────────────────────────────────────
266
648
  async function main() {
267
649
  const args = process.argv.slice(2);
@@ -278,8 +660,9 @@ async function main() {
278
660
  ${color.cyan("./local-dir")} → local directory
279
661
 
280
662
  ${color.dim("Options:")}
281
- ${color.cyan("--harness <h>")} Required. openclaw | hermes | claude-code
663
+ ${color.cyan("--harness <h>")} Required. openclaw | nemoclaw | hermes | claude | claude-code
282
664
  ${color.cyan("--agent <name>")} Agent name (defaults to directory name)
665
+ ${color.cyan("--sandbox <name>")} NemoClaw sandbox (auto-detected if only one exists)
283
666
  `);
284
667
  process.exit(0);
285
668
  }
@@ -291,30 +674,204 @@ async function main() {
291
674
  }
292
675
  let harness;
293
676
  let agentName;
677
+ let sandbox;
294
678
  for (let i = 0; i < restArgs.length; i++) {
295
679
  if (restArgs[i] === "--harness" && restArgs[i + 1])
296
680
  harness = restArgs[++i];
297
681
  else if (restArgs[i] === "--agent" && restArgs[i + 1])
298
682
  agentName = restArgs[++i];
683
+ else if (restArgs[i] === "--sandbox" && restArgs[i + 1])
684
+ sandbox = restArgs[++i];
299
685
  }
300
686
  if (!harness) {
301
- p.cancel("--harness required (openclaw | hermes | claude-code)");
687
+ p.cancel("--harness required (openclaw | nemoclaw | hermes | claude | claude-code)");
302
688
  process.exit(1);
303
689
  }
690
+ if (harness === "nemoclaw" && !sandbox) {
691
+ sandbox = await detectNemoClawSandbox();
692
+ }
304
693
  p.intro(color.bgCyan(color.black(` self-driving-agents `)));
305
694
  // Step 0: Resolve agent directory (local or GitHub)
306
695
  const spin = p.spinner();
307
696
  const { dir, source, defaultName, cleanup } = await resolveAgentDir(dirArg, spin);
308
697
  try {
309
- 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
+ }
310
716
  // Step 1: Ensure plugin
717
+ let apiUrl;
718
+ let bankId;
719
+ let apiToken;
720
+ let claudeSkillZip;
311
721
  if (harness === "openclaw") {
312
722
  await ensurePlugin();
313
723
  enableKnowledgeTools();
724
+ ({ apiUrl, bankId, apiToken } = resolveFromPlugin(agentId));
725
+ }
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);
314
804
  }
315
- // Step 2: Resolve bank + API from plugin config
316
- const { apiUrl, bankId, apiToken } = resolveFromPlugin(agentId);
317
- const workspaceDir = join(homedir(), ".self-driving-agents", "openclaw", agentId);
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);
318
875
  p.log.info([
319
876
  `Agent: ${color.bold(agentId)}`,
320
877
  `Source: ${color.dim(source)}`,
@@ -372,19 +929,49 @@ async function main() {
372
929
  }
373
930
  spin.stop(`Ingested ${contentFiles.length} file(s)`);
374
931
  }
375
- // Step 6: Create agent + install skill
376
- mkdirSync(workspaceDir, { recursive: true });
377
- const skillDir = join(workspaceDir, "skills", "agent-knowledge");
378
- mkdirSync(skillDir, { recursive: true });
379
- writeFileSync(join(skillDir, "SKILL.md"), SKILL_MD);
380
- p.log.success("Knowledge skill installed");
381
- 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");
382
965
  try {
383
- const listOut = execSync("openclaw agents list --json 2>/dev/null", { encoding: "utf-8" });
966
+ const listOut = execSync("openclaw agents list --json", {
967
+ encoding: "utf-8",
968
+ stdio: ["pipe", "pipe", "pipe"],
969
+ });
384
970
  const agents = parseAgentsJson(listOut);
385
971
  if (!agents.some((a) => a.name === agentId || a.id === agentId)) {
386
972
  execSync(`openclaw agents add ${agentId} --workspace ${workspaceDir} --non-interactive`, {
387
- stdio: "pipe",
973
+ encoding: "utf-8",
974
+ stdio: ["pipe", "pipe", "pipe"],
388
975
  });
389
976
  p.log.success(`Agent '${agentId}' created`);
390
977
  }
@@ -392,24 +979,49 @@ async function main() {
392
979
  p.log.info(`Agent '${agentId}' already exists`);
393
980
  }
394
981
  }
395
- catch {
396
- p.log.warn(`Create agent manually:\n openclaw agents add ${agentId} --workspace ${workspaceDir} --non-interactive`);
982
+ catch (err) {
983
+ const stderr = err?.stderr?.toString?.()?.trim() || "";
984
+ const msg = stderr || err?.message || String(err);
985
+ p.log.warn(`Failed to manage agent: ${msg}\n Create manually:\n openclaw agents add ${agentId} --workspace ${workspaceDir} --non-interactive`);
397
986
  }
398
- }
399
- // Step 7: Patch startup
400
- const startupFile = join(workspaceDir, "AGENTS.md");
401
- if (existsSync(startupFile)) {
402
- let text = readFileSync(startupFile, "utf-8");
403
- if (!text.includes("agent-knowledge")) {
404
- 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.");
405
- writeFileSync(startupFile, text);
406
- 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
+ }
407
996
  }
408
997
  }
409
- p.note([
410
- `${color.dim("1.")} openclaw gateway restart`,
411
- `${color.dim("2.")} openclaw tui --session agent:${agentId}:main:session1`,
412
- ].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");
413
1025
  p.outro(color.green(`'${agentId}' is ready`));
414
1026
  }
415
1027
  finally {