hyperstack-core 1.3.0 → 1.5.0

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/cli.js CHANGED
@@ -1,500 +1,631 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * hyperstack-core CLI
5
- *
6
- * Usage:
7
- * npx hyperstack-core login ← NEW: OAuth device flow
8
- * npx hyperstack-core init openclaw-multiagent
9
- * npx hyperstack-core search "what blocks deploy"
10
- * npx hyperstack-core store --slug task-1 --title "Deploy API" --type task
11
- * npx hyperstack-core blockers task-1
12
- * npx hyperstack-core graph task-1 --depth 2
13
- */
14
-
15
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
16
- import { resolve, dirname, join } from "path";
17
- import { fileURLToPath } from "url";
18
- import { homedir } from "os";
19
- import { HyperStackClient } from "./src/client.js";
20
-
21
- const __dirname = dirname(fileURLToPath(import.meta.url));
22
-
23
- const args = process.argv.slice(2);
24
- const command = args[0];
25
-
26
- function getFlag(name, fallback = "") {
27
- const idx = args.indexOf(`--${name}`);
28
- if (idx === -1 || idx + 1 >= args.length) return fallback;
29
- return args[idx + 1];
30
- }
31
-
32
- function help() {
33
- console.log(`
34
- hyperstack-core — Typed graph memory for AI agents
35
-
36
- Commands:
37
- login Authenticate via browser (OAuth device flow)
38
- logout Remove saved credentials
39
- init <template> Initialize a project with a template
40
- search <query> Search the knowledge graph
41
- store Store a card (use --slug, --title, --body, --type, --links)
42
- decide Record a decision (use --slug, --title, --rationale)
43
- blockers <slug> Show what blocks a card
44
- graph <slug> Traverse graph from a card
45
- list List all cards
46
-
47
- Templates:
48
- openclaw-multiagent Multi-agent coordination for OpenClaw
49
-
50
- Options:
51
- --workspace <slug> Workspace (default: "default")
52
- --agent <id> Agent ID for multi-agent setups
53
-
54
- Environment:
55
- HYPERSTACK_API_KEY Your API key (or use 'login' command)
56
- HYPERSTACK_WORKSPACE Default workspace
57
-
58
- Examples:
59
- npx hyperstack-core login
60
- npx hyperstack-core init openclaw-multiagent
61
- npx hyperstack-core store --slug "use-clerk" --title "Use Clerk for auth" --type decision
62
- npx hyperstack-core blockers deploy-prod
63
- npx hyperstack-core graph auth-api --depth 2
64
- `);
65
- }
66
-
67
- async function init(template) {
68
- const templatePath = resolve(__dirname, "templates", `${template}.json`);
69
- if (!existsSync(templatePath)) {
70
- console.error(`Template "${template}" not found.`);
71
- console.error("Available: openclaw-multiagent");
72
- process.exit(1);
73
- }
74
-
75
- const tmpl = JSON.parse(readFileSync(templatePath, "utf-8"));
76
- console.log(`\n🃏 HyperStack — ${tmpl.name}\n`);
77
- console.log(` ${tmpl.description}\n`);
78
-
79
- // Check for API key (env var or saved credentials)
80
- const apiKey = getApiKey();
81
- if (!apiKey) {
82
- console.log("⚠️ Not authenticated.");
83
- console.log(" Run: npx hyperstack-core login");
84
- console.log(" Or: export HYPERSTACK_API_KEY=hs_your_key\n");
85
-
86
- // Still create the config file
87
- const configDir = ".hyperstack";
88
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
89
- writeFileSync(
90
- resolve(configDir, "config.json"),
91
- JSON.stringify({
92
- workspace: "default",
93
- template: template,
94
- agents: tmpl.agentSetup?.agents || {},
95
- }, null, 2)
96
- );
97
- console.log(`✅ Created .hyperstack/config.json`);
98
- console.log(` Set HYPERSTACK_API_KEY and run again to seed starter cards.\n`);
99
- return;
100
- }
101
-
102
- const client = new HyperStackClient({
103
- apiKey: apiKey,
104
- workspace: getFlag("workspace", "default"),
105
- });
106
-
107
- // Create starter cards
108
- console.log("Creating starter cards...\n");
109
- for (const card of tmpl.starterCards || []) {
110
- try {
111
- const result = await client.store(card);
112
- console.log(` [${card.slug}] ${card.title} ${result.updated ? "updated" : "created"}`);
113
- } catch (err) {
114
- console.log(` ❌ [${card.slug}] ${err.message}`);
115
- }
116
- }
117
-
118
- // Register agents if template has them
119
- if (tmpl.agentSetup?.agents) {
120
- console.log("\nRegistering agents...\n");
121
- for (const [id, agent] of Object.entries(tmpl.agentSetup.agents)) {
122
- try {
123
- await client.registerAgent({
124
- id,
125
- name: id,
126
- role: agent.role,
127
- });
128
- console.log(` Agent "${id}" registered (${agent.role.slice(0, 60)})`);
129
- } catch (err) {
130
- console.log(` ❌ Agent "${id}": ${err.message}`);
131
- }
132
- }
133
- }
134
-
135
- // Save config
136
- const configDir = ".hyperstack";
137
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
138
- writeFileSync(
139
- resolve(configDir, "config.json"),
140
- JSON.stringify({
141
- workspace: getFlag("workspace", "default"),
142
- template: template,
143
- agents: tmpl.agentSetup?.agents || {},
144
- cardTypes: tmpl.cardTypes,
145
- relationTypes: tmpl.relationTypes,
146
- }, null, 2)
147
- );
148
-
149
- console.log(`\n✅ HyperStack initialized with "${template}" template`);
150
- console.log(` Config: .hyperstack/config.json`);
151
- console.log(` Cards: ${(tmpl.starterCards || []).length} starter cards created`);
152
- console.log(` Agents: ${Object.keys(tmpl.agentSetup?.agents || {}).length} registered`);
153
-
154
- // Show next steps
155
- console.log(`
156
- Next steps:
157
-
158
- 1. In your OpenClaw config, add HyperStack tools:
159
-
160
- import { createOpenClawAdapter } from "hyperstack-core/adapters/openclaw";
161
- const adapter = createOpenClawAdapter({ agentId: "researcher" });
162
-
163
- 2. Use typed graph instead of DECISIONS.md:
164
-
165
- // Old: append to DECISIONS.md
166
- // New:
167
- await adapter.tools.hs_decide({
168
- slug: "use-clerk",
169
- title: "Use Clerk for auth",
170
- rationale: "Better DX, lower cost, native Next.js support",
171
- affects: "auth-api",
172
- });
173
-
174
- 3. Query the graph:
175
-
176
- await adapter.tools.hs_blockers({ slug: "deploy-prod" });
177
- // → "2 blockers: [migration-23] needs approval, [auth-api] not deployed"
178
-
179
- Docs: https://cascadeai.dev/hyperstack
180
- Discord: Share your setup in #multi-agent
181
- `);
182
- }
183
-
184
- // ─── Credentials file ─────────────────────────────────
185
-
186
- const CRED_DIR = join(homedir(), ".hyperstack");
187
- const CRED_FILE = join(CRED_DIR, "credentials.json");
188
- const BASE_URL = process.env.HYPERSTACK_BASE_URL || "https://hyperstack-cloud.vercel.app";
189
-
190
- function loadCredentials() {
191
- try {
192
- if (existsSync(CRED_FILE)) {
193
- const creds = JSON.parse(readFileSync(CRED_FILE, "utf-8"));
194
- return creds;
195
- }
196
- } catch {}
197
- return null;
198
- }
199
-
200
- function saveCredentials(creds) {
201
- if (!existsSync(CRED_DIR)) mkdirSync(CRED_DIR, { recursive: true });
202
- writeFileSync(CRED_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
203
- }
204
-
205
- function deleteCredentials() {
206
- try {
207
- if (existsSync(CRED_FILE)) {
208
- writeFileSync(CRED_FILE, "{}", { mode: 0o600 });
209
- }
210
- } catch {}
211
- }
212
-
213
- function getApiKey() {
214
- // Priority: env var > credentials file
215
- if (process.env.HYPERSTACK_API_KEY) return process.env.HYPERSTACK_API_KEY;
216
- const creds = loadCredentials();
217
- if (creds?.api_key) return creds.api_key;
218
- return null;
219
- }
220
-
221
- // ─── Device flow login ────────────────────────────────
222
-
223
- async function login() {
224
- console.log("\n🃏 HyperStack Login\n");
225
-
226
- // Check if already logged in
227
- const existing = loadCredentials();
228
- if (existing?.api_key) {
229
- console.log(`Already logged in as ${existing.user?.email || "unknown"}`);
230
- console.log(`API key: ${existing.api_key.slice(0, 8)}...`);
231
- console.log(`Run 'hyperstack-core logout' to sign out.\n`);
232
- return;
233
- }
234
-
235
- // Step 1: Request device code
236
- console.log("Requesting device code...\n");
237
- let deviceRes;
238
- try {
239
- const r = await fetch(BASE_URL + "/api/auth?action=device-code", { method: "POST" });
240
- deviceRes = await r.json();
241
- if (!r.ok) {
242
- console.error("Error:", deviceRes.error || "Failed to get device code");
243
- console.error("You can also set HYPERSTACK_API_KEY manually.");
244
- console.error("Get a key at: https://cascadeai.dev/hyperstack\n");
245
- process.exit(1);
246
- }
247
- } catch (err) {
248
- console.error("Connection error:", err.message);
249
- console.error("\nFallback: set HYPERSTACK_API_KEY manually.");
250
- console.error("Get a key at: https://cascadeai.dev/hyperstack\n");
251
- process.exit(1);
252
- }
253
-
254
- // Step 2: Show user the code and URL
255
- console.log(" ┌─────────────────────────────────────┐");
256
- console.log(" │ │");
257
- console.log(` │ Code: ${deviceRes.user_code} │`);
258
- console.log(" │ │");
259
- console.log(" └─────────────────────────────────────┘\n");
260
- console.log(" Open this URL in your browser:\n");
261
- console.log(` ${deviceRes.verification_uri_complete}\n`);
262
- console.log(" Waiting for approval...\n");
263
-
264
- // Try to open browser automatically
265
- try {
266
- const { exec } = await import("child_process");
267
- const url = deviceRes.verification_uri_complete;
268
- const platform = process.platform;
269
- if (platform === "darwin") exec(`open "${url}"`);
270
- else if (platform === "linux") exec(`xdg-open "${url}" 2>/dev/null || echo ""`);
271
- else if (platform === "win32") exec(`start "${url}"`);
272
- } catch {}
273
-
274
- // Step 3: Poll for approval
275
- const pollInterval = (deviceRes.interval || 5) * 1000;
276
- const maxAttempts = Math.ceil((deviceRes.expires_in || 600) / (deviceRes.interval || 5));
277
- let attempt = 0;
278
-
279
- while (attempt < maxAttempts) {
280
- attempt++;
281
- await new Promise(r => setTimeout(r, pollInterval));
282
-
283
- try {
284
- const r = await fetch(BASE_URL + "/api/auth?action=device-token", {
285
- method: "POST",
286
- headers: { "Content-Type": "application/json" },
287
- body: JSON.stringify({ device_code: deviceRes.device_code }),
288
- });
289
- const data = await r.json();
290
-
291
- if (r.status === 428) {
292
- // Still pending
293
- process.stdout.write(".");
294
- continue;
295
- }
296
-
297
- if (r.status === 403) {
298
- console.log("\n\n❌ Device denied. Try again with 'hyperstack-core login'.\n");
299
- process.exit(1);
300
- }
301
-
302
- if (r.status === 410) {
303
- console.log("\n\n⏰ Code expired. Run 'hyperstack-core login' again.\n");
304
- process.exit(1);
305
- }
306
-
307
- if (r.ok && data.api_key) {
308
- // Success!
309
- saveCredentials({
310
- api_key: data.api_key,
311
- user: data.user,
312
- workspaces: data.workspaces,
313
- authenticated_at: new Date().toISOString(),
314
- });
315
-
316
- console.log("\n");
317
- console.log(" ✅ Logged in as " + data.user.email);
318
- console.log(" Plan: " + data.user.plan);
319
- console.log(" Workspaces: " + data.workspaces.map(w => w.slug).join(", "));
320
- console.log(" Credentials saved to: ~/.hyperstack/credentials.json\n");
321
- console.log(" You're ready! Try:");
322
- console.log(" npx hyperstack-core init openclaw-multiagent");
323
- console.log(" npx hyperstack-core list\n");
324
- return;
325
- }
326
-
327
- // Unknown error
328
- console.error("\n\nUnexpected response:", data);
329
- process.exit(1);
330
-
331
- } catch (err) {
332
- // Network error, keep trying
333
- process.stdout.write("x");
334
- }
335
- }
336
-
337
- console.log("\n\n⏰ Timed out. Run 'hyperstack-core login' again.\n");
338
- process.exit(1);
339
- }
340
-
341
- async function logout() {
342
- const creds = loadCredentials();
343
- if (!creds) {
344
- console.log("Not logged in.\n");
345
- return;
346
- }
347
- try {
348
- writeFileSync(CRED_FILE, "{}", { mode: 0o600 });
349
- } catch {}
350
- console.log(`Logged out. Removed ~/.hyperstack/credentials.json\n`);
351
- }
352
-
353
- async function run() {
354
- if (!command || command === "help" || command === "--help" || command === "-h") {
355
- help();
356
- return;
357
- }
358
-
359
- if (command === "login") {
360
- await login();
361
- return;
362
- }
363
-
364
- if (command === "logout") {
365
- await logout();
366
- return;
367
- }
368
-
369
- if (command === "init") {
370
- const template = args[1];
371
- if (!template) {
372
- console.error("Usage: npx hyperstack-core init <template>");
373
- console.error("Available: openclaw-multiagent");
374
- process.exit(1);
375
- }
376
- await init(template);
377
- return;
378
- }
379
-
380
- // All other commands need API key (from env or credentials file)
381
- const apiKey = getApiKey();
382
- let client;
383
- try {
384
- client = new HyperStackClient({
385
- apiKey: apiKey,
386
- workspace: getFlag("workspace", "default"),
387
- agentId: getFlag("agent", undefined),
388
- });
389
- } catch (err) {
390
- console.error(err.message);
391
- console.error("\nRun 'npx hyperstack-core login' to authenticate.\n");
392
- process.exit(1);
393
- }
394
-
395
- if (command === "search") {
396
- const query = args.slice(1).filter(a => !a.startsWith("--")).join(" ");
397
- if (!query) { console.error("Usage: hyperstack-core search <query>"); process.exit(1); }
398
- const result = await client.search(query);
399
- const cards = result.results || [];
400
- if (!cards.length) { console.log("No results."); return; }
401
- for (const c of cards.slice(0, 10)) {
402
- console.log(`[${c.slug}] ${c.title} (${c.cardType || "general"})`);
403
- if (c.body) console.log(` ${c.body.slice(0, 150)}`);
404
- if (c.links?.length) console.log(` Links: ${c.links.map(l => `${l.relation}→${l.target}`).join(", ")}`);
405
- console.log();
406
- }
407
- return;
408
- }
409
-
410
- if (command === "store") {
411
- const slug = getFlag("slug");
412
- const title = getFlag("title");
413
- if (!slug || !title) { console.error("Required: --slug and --title"); process.exit(1); }
414
- const result = await client.store({
415
- slug,
416
- title,
417
- body: getFlag("body"),
418
- cardType: getFlag("type", "general"),
419
- keywords: getFlag("keywords") ? getFlag("keywords").split(",").map(k => k.trim()) : [],
420
- links: getFlag("links") ? getFlag("links").split(",").map(l => {
421
- const [target, relation] = l.trim().split(":");
422
- return { target, relation: relation || "related" };
423
- }) : [],
424
- });
425
- console.log(`${result.updated ? "Updated" : "Created"} [${slug}]: ${title}`);
426
- return;
427
- }
428
-
429
- if (command === "decide") {
430
- const slug = getFlag("slug");
431
- const title = getFlag("title");
432
- if (!slug || !title) { console.error("Required: --slug and --title"); process.exit(1); }
433
- await client.decide({
434
- slug,
435
- title,
436
- body: getFlag("rationale", getFlag("body", "")),
437
- affects: getFlag("affects") ? getFlag("affects").split(",").map(s => s.trim()) : [],
438
- blocks: getFlag("blocks") ? getFlag("blocks").split(",").map(s => s.trim()) : [],
439
- });
440
- console.log(`Decision recorded: [${slug}] ${title}`);
441
- return;
442
- }
443
-
444
- if (command === "blockers") {
445
- const slug = args[1];
446
- if (!slug) { console.error("Usage: hyperstack-core blockers <slug>"); process.exit(1); }
447
- try {
448
- const result = await client.blockers(slug);
449
- const blockers = result.blockers || [];
450
- if (!blockers.length) { console.log(`Nothing blocks [${slug}].`); return; }
451
- console.log(`${blockers.length} blocker(s) for [${slug}]:`);
452
- for (const b of blockers) {
453
- console.log(` [${b.slug}] ${b.title || "?"}`);
454
- }
455
- } catch (err) {
456
- console.error(`Error: ${err.message}`);
457
- }
458
- return;
459
- }
460
-
461
- if (command === "graph") {
462
- const from = args[1];
463
- if (!from) { console.error("Usage: hyperstack-core graph <slug>"); process.exit(1); }
464
- try {
465
- const result = await client.graph(from, {
466
- depth: parseInt(getFlag("depth", "2")),
467
- relation: getFlag("relation") || undefined,
468
- });
469
- console.log(`Graph from [${from}]: ${result.nodes?.length || 0} nodes, ${result.edges?.length || 0} edges\n`);
470
- for (const n of result.nodes || []) {
471
- console.log(` [${n.slug}] ${n.title || "?"} (${n.cardType || "?"})`);
472
- }
473
- console.log();
474
- for (const e of result.edges || []) {
475
- console.log(` ${e.from} --${e.relation}--> ${e.to}`);
476
- }
477
- } catch (err) {
478
- console.error(`Error: ${err.message}`);
479
- }
480
- return;
481
- }
482
-
483
- if (command === "list") {
484
- const result = await client.list();
485
- console.log(`HyperStack: ${result.count ?? 0}/${result.limit ?? "?"} cards (plan: ${result.plan || "?"})\n`);
486
- for (const c of result.cards || []) {
487
- console.log(` [${c.slug}] ${c.title} (${c.cardType || "general"})`);
488
- }
489
- return;
490
- }
491
-
492
- console.error(`Unknown command: ${command}`);
493
- help();
494
- process.exit(1);
495
- }
496
-
497
- run().catch(err => {
498
- console.error(`Error: ${err.message}`);
499
- process.exit(1);
500
- });
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * hyperstack-core CLI
5
+ *
6
+ * Usage:
7
+ * npx hyperstack-core login ← NEW: OAuth device flow
8
+ * npx hyperstack-core init openclaw-multiagent
9
+ * npx hyperstack-core search "what blocks deploy"
10
+ * npx hyperstack-core store --slug task-1 --title "Deploy API" --type task
11
+ * npx hyperstack-core blockers task-1
12
+ * npx hyperstack-core graph task-1 --depth 2
13
+ */
14
+
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
16
+ import { resolve, dirname, join, basename } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { homedir } from "os";
19
+ import { HyperStackClient } from "./src/client.js";
20
+ import { parse, slugify } from "./src/parser.js";
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+
24
+ const args = process.argv.slice(2);
25
+ const command = args[0];
26
+
27
+ function getFlag(name, fallback = "") {
28
+ const idx = args.indexOf(`--${name}`);
29
+ if (idx === -1 || idx + 1 >= args.length) return fallback;
30
+ return args[idx + 1];
31
+ }
32
+
33
+ function help() {
34
+ console.log(`
35
+ hyperstack-core — Typed graph memory for AI agents
36
+
37
+ Commands:
38
+ login Authenticate via browser (OAuth device flow)
39
+ logout Remove saved credentials
40
+ init <template> Initialize a project with a template
41
+ ingest <path> Auto-parse files into cards + edges (markdown/text/logs)
42
+ search <query> Search the knowledge graph
43
+ store Store a card (use --slug, --title, --body, --type, --links)
44
+ decide Record a decision (use --slug, --title, --rationale)
45
+ blockers <slug> Show what blocks a card
46
+ graph <slug> Traverse graph from a card
47
+ list List all cards
48
+
49
+ Templates:
50
+ openclaw-multiagent Multi-agent coordination for OpenClaw
51
+
52
+ Options:
53
+ --workspace <slug> Workspace (default: "default")
54
+ --agent <id> Agent ID for multi-agent setups
55
+
56
+ Ingest Options:
57
+ --source <prefix> Slug prefix for ingested cards (default: filename)
58
+ --type <cardType> Override default card type inference
59
+ --dry Preview cards without storing
60
+
61
+ Environment:
62
+ HYPERSTACK_API_KEY Your API key (or use 'login' command)
63
+ HYPERSTACK_WORKSPACE Default workspace
64
+
65
+ Examples:
66
+ npx hyperstack-core login
67
+ npx hyperstack-core ingest ./README.md
68
+ npx hyperstack-core ingest ./docs/
69
+ cat DECISIONS.md | npx hyperstack-core ingest --source decisions
70
+ npx hyperstack-core ingest ./README.md --dry
71
+ npx hyperstack-core store --slug "use-clerk" --title "Use Clerk for auth" --type decision
72
+ npx hyperstack-core blockers deploy-prod
73
+ npx hyperstack-core graph auth-api --depth 2
74
+ `);
75
+ }
76
+
77
+ async function init(template) {
78
+ const templatePath = resolve(__dirname, "templates", `${template}.json`);
79
+ if (!existsSync(templatePath)) {
80
+ console.error(`Template "${template}" not found.`);
81
+ console.error("Available: openclaw-multiagent");
82
+ process.exit(1);
83
+ }
84
+
85
+ const tmpl = JSON.parse(readFileSync(templatePath, "utf-8"));
86
+ console.log(`\n🃏 HyperStack ${tmpl.name}\n`);
87
+ console.log(` ${tmpl.description}\n`);
88
+
89
+ // Check for API key (env var or saved credentials)
90
+ const apiKey = getApiKey();
91
+ if (!apiKey) {
92
+ console.log("⚠️ Not authenticated.");
93
+ console.log(" Run: npx hyperstack-core login");
94
+ console.log(" Or: export HYPERSTACK_API_KEY=hs_your_key\n");
95
+
96
+ // Still create the config file
97
+ const configDir = ".hyperstack";
98
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
99
+ writeFileSync(
100
+ resolve(configDir, "config.json"),
101
+ JSON.stringify({
102
+ workspace: "default",
103
+ template: template,
104
+ agents: tmpl.agentSetup?.agents || {},
105
+ }, null, 2)
106
+ );
107
+ console.log(`✅ Created .hyperstack/config.json`);
108
+ console.log(` Set HYPERSTACK_API_KEY and run again to seed starter cards.\n`);
109
+ return;
110
+ }
111
+
112
+ const client = new HyperStackClient({
113
+ apiKey: apiKey,
114
+ workspace: getFlag("workspace", "default"),
115
+ });
116
+
117
+ // Create starter cards
118
+ console.log("Creating starter cards...\n");
119
+ for (const card of tmpl.starterCards || []) {
120
+ try {
121
+ const result = await client.store(card);
122
+ console.log(` ✅ [${card.slug}] ${card.title} — ${result.updated ? "updated" : "created"}`);
123
+ } catch (err) {
124
+ console.log(` ❌ [${card.slug}] ${err.message}`);
125
+ }
126
+ }
127
+
128
+ // Register agents if template has them
129
+ if (tmpl.agentSetup?.agents) {
130
+ console.log("\nRegistering agents...\n");
131
+ for (const [id, agent] of Object.entries(tmpl.agentSetup.agents)) {
132
+ try {
133
+ await client.registerAgent({
134
+ id,
135
+ name: id,
136
+ role: agent.role,
137
+ });
138
+ console.log(` ✅ Agent "${id}" registered (${agent.role.slice(0, 60)})`);
139
+ } catch (err) {
140
+ console.log(` ❌ Agent "${id}": ${err.message}`);
141
+ }
142
+ }
143
+ }
144
+
145
+ // Save config
146
+ const configDir = ".hyperstack";
147
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
148
+ writeFileSync(
149
+ resolve(configDir, "config.json"),
150
+ JSON.stringify({
151
+ workspace: getFlag("workspace", "default"),
152
+ template: template,
153
+ agents: tmpl.agentSetup?.agents || {},
154
+ cardTypes: tmpl.cardTypes,
155
+ relationTypes: tmpl.relationTypes,
156
+ }, null, 2)
157
+ );
158
+
159
+ console.log(`\n✅ HyperStack initialized with "${template}" template`);
160
+ console.log(` Config: .hyperstack/config.json`);
161
+ console.log(` Cards: ${(tmpl.starterCards || []).length} starter cards created`);
162
+ console.log(` Agents: ${Object.keys(tmpl.agentSetup?.agents || {}).length} registered`);
163
+
164
+ // Show next steps
165
+ console.log(`
166
+ Next steps:
167
+
168
+ 1. In your OpenClaw config, add HyperStack tools:
169
+
170
+ import { createOpenClawAdapter } from "hyperstack-core/adapters/openclaw";
171
+ const adapter = createOpenClawAdapter({ agentId: "researcher" });
172
+
173
+ 2. Use typed graph instead of DECISIONS.md:
174
+
175
+ // Old: append to DECISIONS.md
176
+ // New:
177
+ await adapter.tools.hs_decide({
178
+ slug: "use-clerk",
179
+ title: "Use Clerk for auth",
180
+ rationale: "Better DX, lower cost, native Next.js support",
181
+ affects: "auth-api",
182
+ });
183
+
184
+ 3. Query the graph:
185
+
186
+ await adapter.tools.hs_blockers({ slug: "deploy-prod" });
187
+ // "2 blockers: [migration-23] needs approval, [auth-api] not deployed"
188
+
189
+ Docs: https://cascadeai.dev/hyperstack
190
+ Discord: Share your setup in #multi-agent
191
+ `);
192
+ }
193
+
194
+ // ─── Credentials file ─────────────────────────────────
195
+
196
+ const CRED_DIR = join(homedir(), ".hyperstack");
197
+ const CRED_FILE = join(CRED_DIR, "credentials.json");
198
+ const BASE_URL = process.env.HYPERSTACK_BASE_URL || "https://hyperstack-cloud.vercel.app";
199
+
200
+ function loadCredentials() {
201
+ try {
202
+ if (existsSync(CRED_FILE)) {
203
+ const creds = JSON.parse(readFileSync(CRED_FILE, "utf-8"));
204
+ return creds;
205
+ }
206
+ } catch {}
207
+ return null;
208
+ }
209
+
210
+ function saveCredentials(creds) {
211
+ if (!existsSync(CRED_DIR)) mkdirSync(CRED_DIR, { recursive: true });
212
+ writeFileSync(CRED_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
213
+ }
214
+
215
+ function deleteCredentials() {
216
+ try {
217
+ if (existsSync(CRED_FILE)) {
218
+ writeFileSync(CRED_FILE, "{}", { mode: 0o600 });
219
+ }
220
+ } catch {}
221
+ }
222
+
223
+ function getApiKey() {
224
+ // Priority: env var > credentials file
225
+ if (process.env.HYPERSTACK_API_KEY) return process.env.HYPERSTACK_API_KEY;
226
+ const creds = loadCredentials();
227
+ if (creds?.api_key) return creds.api_key;
228
+ return null;
229
+ }
230
+
231
+ // ─── Device flow login ────────────────────────────────
232
+
233
+ async function login() {
234
+ console.log("\n🃏 HyperStack Login\n");
235
+
236
+ // Check if already logged in
237
+ const existing = loadCredentials();
238
+ if (existing?.api_key) {
239
+ console.log(`Already logged in as ${existing.user?.email || "unknown"}`);
240
+ console.log(`API key: ${existing.api_key.slice(0, 8)}...`);
241
+ console.log(`Run 'hyperstack-core logout' to sign out.\n`);
242
+ return;
243
+ }
244
+
245
+ // Step 1: Request device code
246
+ console.log("Requesting device code...\n");
247
+ let deviceRes;
248
+ try {
249
+ const r = await fetch(BASE_URL + "/api/auth?action=device-code", { method: "POST" });
250
+ deviceRes = await r.json();
251
+ if (!r.ok) {
252
+ console.error("Error:", deviceRes.error || "Failed to get device code");
253
+ console.error("You can also set HYPERSTACK_API_KEY manually.");
254
+ console.error("Get a key at: https://cascadeai.dev/hyperstack\n");
255
+ process.exit(1);
256
+ }
257
+ } catch (err) {
258
+ console.error("Connection error:", err.message);
259
+ console.error("\nFallback: set HYPERSTACK_API_KEY manually.");
260
+ console.error("Get a key at: https://cascadeai.dev/hyperstack\n");
261
+ process.exit(1);
262
+ }
263
+
264
+ // Step 2: Show user the code and URL
265
+ console.log(" ┌─────────────────────────────────────┐");
266
+ console.log(" │ │");
267
+ console.log(` │ Code: ${deviceRes.user_code} │`);
268
+ console.log(" │ │");
269
+ console.log(" └─────────────────────────────────────┘\n");
270
+ console.log(" Open this URL in your browser:\n");
271
+ console.log(` ${deviceRes.verification_uri_complete}\n`);
272
+ console.log(" Waiting for approval...\n");
273
+
274
+ // Try to open browser automatically
275
+ try {
276
+ const { exec } = await import("child_process");
277
+ const url = deviceRes.verification_uri_complete;
278
+ const platform = process.platform;
279
+ if (platform === "darwin") exec(`open "${url}"`);
280
+ else if (platform === "linux") exec(`xdg-open "${url}" 2>/dev/null || echo ""`);
281
+ else if (platform === "win32") exec(`start "${url}"`);
282
+ } catch {}
283
+
284
+ // Step 3: Poll for approval
285
+ const pollInterval = (deviceRes.interval || 5) * 1000;
286
+ const maxAttempts = Math.ceil((deviceRes.expires_in || 600) / (deviceRes.interval || 5));
287
+ let attempt = 0;
288
+
289
+ while (attempt < maxAttempts) {
290
+ attempt++;
291
+ await new Promise(r => setTimeout(r, pollInterval));
292
+
293
+ try {
294
+ const r = await fetch(BASE_URL + "/api/auth?action=device-token", {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json" },
297
+ body: JSON.stringify({ device_code: deviceRes.device_code }),
298
+ });
299
+ const data = await r.json();
300
+
301
+ if (r.status === 428) {
302
+ // Still pending
303
+ process.stdout.write(".");
304
+ continue;
305
+ }
306
+
307
+ if (r.status === 403) {
308
+ console.log("\n\n❌ Device denied. Try again with 'hyperstack-core login'.\n");
309
+ process.exit(1);
310
+ }
311
+
312
+ if (r.status === 410) {
313
+ console.log("\n\n⏰ Code expired. Run 'hyperstack-core login' again.\n");
314
+ process.exit(1);
315
+ }
316
+
317
+ if (r.ok && data.api_key) {
318
+ // Success!
319
+ saveCredentials({
320
+ api_key: data.api_key,
321
+ user: data.user,
322
+ workspaces: data.workspaces,
323
+ authenticated_at: new Date().toISOString(),
324
+ });
325
+
326
+ console.log("\n");
327
+ console.log(" ✅ Logged in as " + data.user.email);
328
+ console.log(" Plan: " + data.user.plan);
329
+ console.log(" Workspaces: " + data.workspaces.map(w => w.slug).join(", "));
330
+ console.log(" Credentials saved to: ~/.hyperstack/credentials.json\n");
331
+ console.log(" You're ready! Try:");
332
+ console.log(" npx hyperstack-core init openclaw-multiagent");
333
+ console.log(" npx hyperstack-core list\n");
334
+ return;
335
+ }
336
+
337
+ // Unknown error
338
+ console.error("\n\nUnexpected response:", data);
339
+ process.exit(1);
340
+
341
+ } catch (err) {
342
+ // Network error, keep trying
343
+ process.stdout.write("x");
344
+ }
345
+ }
346
+
347
+ console.log("\n\n⏰ Timed out. Run 'hyperstack-core login' again.\n");
348
+ process.exit(1);
349
+ }
350
+
351
+ async function logout() {
352
+ const creds = loadCredentials();
353
+ if (!creds) {
354
+ console.log("Not logged in.\n");
355
+ return;
356
+ }
357
+ try {
358
+ writeFileSync(CRED_FILE, "{}", { mode: 0o600 });
359
+ } catch {}
360
+ console.log(`Logged out. Removed ~/.hyperstack/credentials.json\n`);
361
+ }
362
+
363
+ async function run() {
364
+ if (!command || command === "help" || command === "--help" || command === "-h") {
365
+ help();
366
+ return;
367
+ }
368
+
369
+ if (command === "login") {
370
+ await login();
371
+ return;
372
+ }
373
+
374
+ if (command === "logout") {
375
+ await logout();
376
+ return;
377
+ }
378
+
379
+ if (command === "init") {
380
+ const template = args[1];
381
+ if (!template) {
382
+ console.error("Usage: npx hyperstack-core init <template>");
383
+ console.error("Available: openclaw-multiagent");
384
+ process.exit(1);
385
+ }
386
+ await init(template);
387
+ return;
388
+ }
389
+
390
+ // ─── Ingest command ─────────────────────────────────
391
+
392
+ if (command === "ingest") {
393
+ const target = args[1] && !args[1].startsWith("--") ? args[1] : null;
394
+ const sourceFlag = getFlag("source", "");
395
+ const typeFlag = getFlag("type", "");
396
+ const dryRun = args.includes("--dry");
397
+
398
+ let allCards = [];
399
+ let totalEdges = 0;
400
+ const fileSummaries = [];
401
+
402
+ if (target) {
403
+ const resolved = resolve(target);
404
+ if (!existsSync(resolved)) {
405
+ console.error(`Not found: ${target}`);
406
+ process.exit(1);
407
+ }
408
+
409
+ const stat = statSync(resolved);
410
+ if (stat.isDirectory()) {
411
+ const files = readdirSync(resolved)
412
+ .filter((f) => /\.(md|txt|log)$/i.test(f))
413
+ .sort();
414
+ if (!files.length) {
415
+ console.error(`No .md/.txt/.log files in ${target}`);
416
+ process.exit(1);
417
+ }
418
+ for (const f of files) {
419
+ const content = readFileSync(join(resolved, f), "utf-8");
420
+ const prefix = sourceFlag || slugify(f.replace(/\.[^.]+$/, ""));
421
+ const result = parse(content, { prefix, defaultType: typeFlag || undefined });
422
+ allCards.push(...result.cards);
423
+ totalEdges += result.edgeCount;
424
+ fileSummaries.push({ name: f, cards: result.cards.length, edges: result.edgeCount, format: result.format });
425
+ }
426
+ } else {
427
+ const content = readFileSync(resolved, "utf-8");
428
+ const name = basename(resolved).replace(/\.[^.]+$/, "");
429
+ const prefix = sourceFlag || slugify(name);
430
+ const result = parse(content, { prefix, defaultType: typeFlag || undefined });
431
+ allCards = result.cards;
432
+ totalEdges = result.edgeCount;
433
+ fileSummaries.push({ name: basename(resolved), cards: result.cards.length, edges: result.edgeCount, format: result.format });
434
+ }
435
+ } else if (!process.stdin.isTTY) {
436
+ const chunks = [];
437
+ for await (const chunk of process.stdin) chunks.push(chunk);
438
+ const content = Buffer.concat(chunks).toString("utf-8");
439
+ const prefix = sourceFlag || "import";
440
+ const result = parse(content, { prefix, defaultType: typeFlag || undefined });
441
+ allCards = result.cards;
442
+ totalEdges = result.edgeCount;
443
+ fileSummaries.push({ name: "stdin", cards: result.cards.length, edges: result.edgeCount, format: result.format });
444
+ } else {
445
+ console.error("Usage: hyperstack-core ingest <file|directory>");
446
+ console.error(" cat file.md | hyperstack-core ingest --source my-doc");
447
+ process.exit(1);
448
+ }
449
+
450
+ if (!allCards.length) {
451
+ console.log("No cards extracted from input.");
452
+ return;
453
+ }
454
+
455
+ // Print parse summary
456
+ console.log(`\n🧠 Parsed: ${allCards.length} cards, ${totalEdges} edges\n`);
457
+ for (const s of fileSummaries) {
458
+ console.log(` 📄 ${s.name} → ${s.cards} cards, ${s.edges} edges (${s.format})`);
459
+ }
460
+ console.log();
461
+
462
+ // Print card preview
463
+ for (const card of allCards) {
464
+ const edgeInfo = card.links?.length
465
+ ? ` ${card.links.map((l) => `${l.relation}:${l.target}`).join(", ")}`
466
+ : "";
467
+ console.log(` [${card.slug}] ${card.title} (${card.cardType})${edgeInfo}`);
468
+ }
469
+ console.log();
470
+
471
+ if (dryRun) {
472
+ console.log("--dry: No cards stored. Remove --dry to store.\n");
473
+ return;
474
+ }
475
+
476
+ // Store cards
477
+ const apiKey = getApiKey();
478
+ if (!apiKey) {
479
+ console.error("Not authenticated. Run: npx hyperstack-core login\n");
480
+ process.exit(1);
481
+ }
482
+
483
+ const client = new HyperStackClient({
484
+ apiKey,
485
+ workspace: getFlag("workspace", "default"),
486
+ agentId: getFlag("agent", undefined),
487
+ });
488
+
489
+ const result = await client.ingest(allCards, {
490
+ onProgress: (i, total, card, res) => {
491
+ const ok = !(res instanceof Error);
492
+ process.stdout.write(ok ? "." : "x");
493
+ },
494
+ });
495
+
496
+ console.log("\n");
497
+ console.log(`✅ ${result.stored} cards stored${result.failed ? `, ${result.failed} failed` : ""}`);
498
+ if (result.errors.length) {
499
+ for (const e of result.errors) console.log(` ❌ [${e.slug}] ${e.error}`);
500
+ }
501
+
502
+ // Show next steps with first card slug
503
+ const firstSlug = allCards[0]?.slug || "";
504
+ console.log(`\nTry:`);
505
+ console.log(` npx hyperstack-core search "${allCards[0]?.title?.split(" ").slice(0, 3).join(" ") || "getting started"}"`);
506
+ if (firstSlug) console.log(` npx hyperstack-core graph ${firstSlug} --depth 2`);
507
+ console.log();
508
+ return;
509
+ }
510
+
511
+ // All other commands need API key (from env or credentials file)
512
+ const apiKey = getApiKey();
513
+ let client;
514
+ try {
515
+ client = new HyperStackClient({
516
+ apiKey: apiKey,
517
+ workspace: getFlag("workspace", "default"),
518
+ agentId: getFlag("agent", undefined),
519
+ });
520
+ } catch (err) {
521
+ console.error(err.message);
522
+ console.error("\nRun 'npx hyperstack-core login' to authenticate.\n");
523
+ process.exit(1);
524
+ }
525
+
526
+ if (command === "search") {
527
+ const query = args.slice(1).filter(a => !a.startsWith("--")).join(" ");
528
+ if (!query) { console.error("Usage: hyperstack-core search <query>"); process.exit(1); }
529
+ const result = await client.search(query);
530
+ const cards = result.results || [];
531
+ if (!cards.length) { console.log("No results."); return; }
532
+ for (const c of cards.slice(0, 10)) {
533
+ console.log(`[${c.slug}] ${c.title} (${c.cardType || "general"})`);
534
+ if (c.body) console.log(` ${c.body.slice(0, 150)}`);
535
+ if (c.links?.length) console.log(` Links: ${c.links.map(l => `${l.relation}→${l.target}`).join(", ")}`);
536
+ console.log();
537
+ }
538
+ return;
539
+ }
540
+
541
+ if (command === "store") {
542
+ const slug = getFlag("slug");
543
+ const title = getFlag("title");
544
+ if (!slug || !title) { console.error("Required: --slug and --title"); process.exit(1); }
545
+ const result = await client.store({
546
+ slug,
547
+ title,
548
+ body: getFlag("body"),
549
+ cardType: getFlag("type", "general"),
550
+ keywords: getFlag("keywords") ? getFlag("keywords").split(",").map(k => k.trim()) : [],
551
+ links: getFlag("links") ? getFlag("links").split(",").map(l => {
552
+ const [target, relation] = l.trim().split(":");
553
+ return { target, relation: relation || "related" };
554
+ }) : [],
555
+ });
556
+ console.log(`${result.updated ? "Updated" : "Created"} [${slug}]: ${title}`);
557
+ return;
558
+ }
559
+
560
+ if (command === "decide") {
561
+ const slug = getFlag("slug");
562
+ const title = getFlag("title");
563
+ if (!slug || !title) { console.error("Required: --slug and --title"); process.exit(1); }
564
+ await client.decide({
565
+ slug,
566
+ title,
567
+ body: getFlag("rationale", getFlag("body", "")),
568
+ affects: getFlag("affects") ? getFlag("affects").split(",").map(s => s.trim()) : [],
569
+ blocks: getFlag("blocks") ? getFlag("blocks").split(",").map(s => s.trim()) : [],
570
+ });
571
+ console.log(`Decision recorded: [${slug}] ${title}`);
572
+ return;
573
+ }
574
+
575
+ if (command === "blockers") {
576
+ const slug = args[1];
577
+ if (!slug) { console.error("Usage: hyperstack-core blockers <slug>"); process.exit(1); }
578
+ try {
579
+ const result = await client.blockers(slug);
580
+ const blockers = result.blockers || [];
581
+ if (!blockers.length) { console.log(`Nothing blocks [${slug}].`); return; }
582
+ console.log(`${blockers.length} blocker(s) for [${slug}]:`);
583
+ for (const b of blockers) {
584
+ console.log(` [${b.slug}] ${b.title || "?"}`);
585
+ }
586
+ } catch (err) {
587
+ console.error(`Error: ${err.message}`);
588
+ }
589
+ return;
590
+ }
591
+
592
+ if (command === "graph") {
593
+ const from = args[1];
594
+ if (!from) { console.error("Usage: hyperstack-core graph <slug>"); process.exit(1); }
595
+ try {
596
+ const result = await client.graph(from, {
597
+ depth: parseInt(getFlag("depth", "2")),
598
+ relation: getFlag("relation") || undefined,
599
+ });
600
+ console.log(`Graph from [${from}]: ${result.nodes?.length || 0} nodes, ${result.edges?.length || 0} edges\n`);
601
+ for (const n of result.nodes || []) {
602
+ console.log(` [${n.slug}] ${n.title || "?"} (${n.cardType || "?"})`);
603
+ }
604
+ console.log();
605
+ for (const e of result.edges || []) {
606
+ console.log(` ${e.from} --${e.relation}--> ${e.to}`);
607
+ }
608
+ } catch (err) {
609
+ console.error(`Error: ${err.message}`);
610
+ }
611
+ return;
612
+ }
613
+
614
+ if (command === "list") {
615
+ const result = await client.list();
616
+ console.log(`HyperStack: ${result.count ?? 0}/${result.limit ?? "?"} cards (plan: ${result.plan || "?"})\n`);
617
+ for (const c of result.cards || []) {
618
+ console.log(` [${c.slug}] ${c.title} (${c.cardType || "general"})`);
619
+ }
620
+ return;
621
+ }
622
+
623
+ console.error(`Unknown command: ${command}`);
624
+ help();
625
+ process.exit(1);
626
+ }
627
+
628
+ run().catch(err => {
629
+ console.error(`Error: ${err.message}`);
630
+ process.exit(1);
631
+ });