@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/README.md +106 -32
- package/dist/cli.js +646 -34
- package/dist/tests/cli.test.d.ts +1 -0
- package/dist/tests/cli.test.js +529 -0
- package/package.json +6 -7
- /package/{skill → dist/skill}/SKILL.md +0 -0
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, "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 {
|