hyperstack-core 1.2.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,544 +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
- ingest <file|text> Auto-extract cards from text (zero LLM cost)
46
- list List all cards
47
-
48
- Templates:
49
- openclaw-multiagent Multi-agent coordination for OpenClaw
50
-
51
- Options:
52
- --workspace <slug> Workspace (default: "default")
53
- --agent <id> Agent ID for multi-agent setups
54
-
55
- Environment:
56
- HYPERSTACK_API_KEY Your API key (or use 'login' command)
57
- HYPERSTACK_WORKSPACE Default workspace
58
-
59
- Examples:
60
- npx hyperstack-core login
61
- npx hyperstack-core init openclaw-multiagent
62
- npx hyperstack-core store --slug "use-clerk" --title "Use Clerk for auth" --type decision
63
- npx hyperstack-core blockers deploy-prod
64
- npx hyperstack-core graph auth-api --depth 2
65
- npx hyperstack-core ingest project.txt
66
- echo "We use Next.js 14 and PostgreSQL" | npx hyperstack-core ingest -
67
- `);
68
- }
69
-
70
- async function init(template) {
71
- const templatePath = resolve(__dirname, "templates", `${template}.json`);
72
- if (!existsSync(templatePath)) {
73
- console.error(`Template "${template}" not found.`);
74
- console.error("Available: openclaw-multiagent");
75
- process.exit(1);
76
- }
77
-
78
- const tmpl = JSON.parse(readFileSync(templatePath, "utf-8"));
79
- console.log(`\n🃏 HyperStack — ${tmpl.name}\n`);
80
- console.log(` ${tmpl.description}\n`);
81
-
82
- // Check for API key (env var or saved credentials)
83
- const apiKey = getApiKey();
84
- if (!apiKey) {
85
- console.log("⚠️ Not authenticated.");
86
- console.log(" Run: npx hyperstack-core login");
87
- console.log(" Or: export HYPERSTACK_API_KEY=hs_your_key\n");
88
-
89
- // Still create the config file
90
- const configDir = ".hyperstack";
91
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
92
- writeFileSync(
93
- resolve(configDir, "config.json"),
94
- JSON.stringify({
95
- workspace: "default",
96
- template: template,
97
- agents: tmpl.agentSetup?.agents || {},
98
- }, null, 2)
99
- );
100
- console.log(`✅ Created .hyperstack/config.json`);
101
- console.log(` Set HYPERSTACK_API_KEY and run again to seed starter cards.\n`);
102
- return;
103
- }
104
-
105
- const client = new HyperStackClient({
106
- apiKey: apiKey,
107
- workspace: getFlag("workspace", "default"),
108
- });
109
-
110
- // Create starter cards
111
- console.log("Creating starter cards...\n");
112
- for (const card of tmpl.starterCards || []) {
113
- try {
114
- const result = await client.store(card);
115
- console.log(` ✅ [${card.slug}] ${card.title} — ${result.updated ? "updated" : "created"}`);
116
- } catch (err) {
117
- console.log(` [${card.slug}] ${err.message}`);
118
- }
119
- }
120
-
121
- // Register agents if template has them
122
- if (tmpl.agentSetup?.agents) {
123
- console.log("\nRegistering agents...\n");
124
- for (const [id, agent] of Object.entries(tmpl.agentSetup.agents)) {
125
- try {
126
- await client.registerAgent({
127
- id,
128
- name: id,
129
- role: agent.role,
130
- });
131
- console.log(` ✅ Agent "${id}" registered (${agent.role.slice(0, 60)})`);
132
- } catch (err) {
133
- console.log(` ❌ Agent "${id}": ${err.message}`);
134
- }
135
- }
136
- }
137
-
138
- // Save config
139
- const configDir = ".hyperstack";
140
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
141
- writeFileSync(
142
- resolve(configDir, "config.json"),
143
- JSON.stringify({
144
- workspace: getFlag("workspace", "default"),
145
- template: template,
146
- agents: tmpl.agentSetup?.agents || {},
147
- cardTypes: tmpl.cardTypes,
148
- relationTypes: tmpl.relationTypes,
149
- }, null, 2)
150
- );
151
-
152
- console.log(`\n✅ HyperStack initialized with "${template}" template`);
153
- console.log(` Config: .hyperstack/config.json`);
154
- console.log(` Cards: ${(tmpl.starterCards || []).length} starter cards created`);
155
- console.log(` Agents: ${Object.keys(tmpl.agentSetup?.agents || {}).length} registered`);
156
-
157
- // Show next steps
158
- console.log(`
159
- Next steps:
160
-
161
- 1. In your OpenClaw config, add HyperStack tools:
162
-
163
- import { createOpenClawAdapter } from "hyperstack-core/adapters/openclaw";
164
- const adapter = createOpenClawAdapter({ agentId: "researcher" });
165
-
166
- 2. Use typed graph instead of DECISIONS.md:
167
-
168
- // Old: append to DECISIONS.md
169
- // New:
170
- await adapter.tools.hs_decide({
171
- slug: "use-clerk",
172
- title: "Use Clerk for auth",
173
- rationale: "Better DX, lower cost, native Next.js support",
174
- affects: "auth-api",
175
- });
176
-
177
- 3. Query the graph:
178
-
179
- await adapter.tools.hs_blockers({ slug: "deploy-prod" });
180
- // "2 blockers: [migration-23] needs approval, [auth-api] not deployed"
181
-
182
- Docs: https://cascadeai.dev/hyperstack
183
- Discord: Share your setup in #multi-agent
184
- `);
185
- }
186
-
187
- // ─── Credentials file ─────────────────────────────────
188
-
189
- const CRED_DIR = join(homedir(), ".hyperstack");
190
- const CRED_FILE = join(CRED_DIR, "credentials.json");
191
- const BASE_URL = process.env.HYPERSTACK_BASE_URL || "https://hyperstack-cloud.vercel.app";
192
-
193
- function loadCredentials() {
194
- try {
195
- if (existsSync(CRED_FILE)) {
196
- const creds = JSON.parse(readFileSync(CRED_FILE, "utf-8"));
197
- return creds;
198
- }
199
- } catch {}
200
- return null;
201
- }
202
-
203
- function saveCredentials(creds) {
204
- if (!existsSync(CRED_DIR)) mkdirSync(CRED_DIR, { recursive: true });
205
- writeFileSync(CRED_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
206
- }
207
-
208
- function deleteCredentials() {
209
- try {
210
- if (existsSync(CRED_FILE)) {
211
- writeFileSync(CRED_FILE, "{}", { mode: 0o600 });
212
- }
213
- } catch {}
214
- }
215
-
216
- function getApiKey() {
217
- // Priority: env var > credentials file
218
- if (process.env.HYPERSTACK_API_KEY) return process.env.HYPERSTACK_API_KEY;
219
- const creds = loadCredentials();
220
- if (creds?.api_key) return creds.api_key;
221
- return null;
222
- }
223
-
224
- // ─── Device flow login ────────────────────────────────
225
-
226
- async function login() {
227
- console.log("\n🃏 HyperStack Login\n");
228
-
229
- // Check if already logged in
230
- const existing = loadCredentials();
231
- if (existing?.api_key) {
232
- console.log(`Already logged in as ${existing.user?.email || "unknown"}`);
233
- console.log(`API key: ${existing.api_key.slice(0, 8)}...`);
234
- console.log(`Run 'hyperstack-core logout' to sign out.\n`);
235
- return;
236
- }
237
-
238
- // Step 1: Request device code
239
- console.log("Requesting device code...\n");
240
- let deviceRes;
241
- try {
242
- const r = await fetch(BASE_URL + "/api/auth?action=device-code", { method: "POST" });
243
- deviceRes = await r.json();
244
- if (!r.ok) {
245
- console.error("Error:", deviceRes.error || "Failed to get device code");
246
- console.error("You can also set HYPERSTACK_API_KEY manually.");
247
- console.error("Get a key at: https://cascadeai.dev/hyperstack\n");
248
- process.exit(1);
249
- }
250
- } catch (err) {
251
- console.error("Connection error:", err.message);
252
- console.error("\nFallback: set HYPERSTACK_API_KEY manually.");
253
- console.error("Get a key at: https://cascadeai.dev/hyperstack\n");
254
- process.exit(1);
255
- }
256
-
257
- // Step 2: Show user the code and URL
258
- console.log(" ┌─────────────────────────────────────┐");
259
- console.log(" │ │");
260
- console.log(` │ Code: ${deviceRes.user_code} │`);
261
- console.log(" │ │");
262
- console.log(" └─────────────────────────────────────┘\n");
263
- console.log(" Open this URL in your browser:\n");
264
- console.log(` ${deviceRes.verification_uri_complete}\n`);
265
- console.log(" Waiting for approval...\n");
266
-
267
- // Try to open browser automatically
268
- try {
269
- const { exec } = await import("child_process");
270
- const url = deviceRes.verification_uri_complete;
271
- const platform = process.platform;
272
- if (platform === "darwin") exec(`open "${url}"`);
273
- else if (platform === "linux") exec(`xdg-open "${url}" 2>/dev/null || echo ""`);
274
- else if (platform === "win32") exec(`start "${url}"`);
275
- } catch {}
276
-
277
- // Step 3: Poll for approval
278
- const pollInterval = (deviceRes.interval || 5) * 1000;
279
- const maxAttempts = Math.ceil((deviceRes.expires_in || 600) / (deviceRes.interval || 5));
280
- let attempt = 0;
281
-
282
- while (attempt < maxAttempts) {
283
- attempt++;
284
- await new Promise(r => setTimeout(r, pollInterval));
285
-
286
- try {
287
- const r = await fetch(BASE_URL + "/api/auth?action=device-token", {
288
- method: "POST",
289
- headers: { "Content-Type": "application/json" },
290
- body: JSON.stringify({ device_code: deviceRes.device_code }),
291
- });
292
- const data = await r.json();
293
-
294
- if (r.status === 428) {
295
- // Still pending
296
- process.stdout.write(".");
297
- continue;
298
- }
299
-
300
- if (r.status === 403) {
301
- console.log("\n\n❌ Device denied. Try again with 'hyperstack-core login'.\n");
302
- process.exit(1);
303
- }
304
-
305
- if (r.status === 410) {
306
- console.log("\n\n⏰ Code expired. Run 'hyperstack-core login' again.\n");
307
- process.exit(1);
308
- }
309
-
310
- if (r.ok && data.api_key) {
311
- // Success!
312
- saveCredentials({
313
- api_key: data.api_key,
314
- user: data.user,
315
- workspaces: data.workspaces,
316
- authenticated_at: new Date().toISOString(),
317
- });
318
-
319
- console.log("\n");
320
- console.log(" ✅ Logged in as " + data.user.email);
321
- console.log(" Plan: " + data.user.plan);
322
- console.log(" Workspaces: " + data.workspaces.map(w => w.slug).join(", "));
323
- console.log(" Credentials saved to: ~/.hyperstack/credentials.json\n");
324
- console.log(" You're ready! Try:");
325
- console.log(" npx hyperstack-core init openclaw-multiagent");
326
- console.log(" npx hyperstack-core list\n");
327
- return;
328
- }
329
-
330
- // Unknown error
331
- console.error("\n\nUnexpected response:", data);
332
- process.exit(1);
333
-
334
- } catch (err) {
335
- // Network error, keep trying
336
- process.stdout.write("x");
337
- }
338
- }
339
-
340
- console.log("\n\n⏰ Timed out. Run 'hyperstack-core login' again.\n");
341
- process.exit(1);
342
- }
343
-
344
- async function logout() {
345
- const creds = loadCredentials();
346
- if (!creds) {
347
- console.log("Not logged in.\n");
348
- return;
349
- }
350
- try {
351
- writeFileSync(CRED_FILE, "{}", { mode: 0o600 });
352
- } catch {}
353
- console.log(`Logged out. Removed ~/.hyperstack/credentials.json\n`);
354
- }
355
-
356
- async function run() {
357
- if (!command || command === "help" || command === "--help" || command === "-h") {
358
- help();
359
- return;
360
- }
361
-
362
- if (command === "login") {
363
- await login();
364
- return;
365
- }
366
-
367
- if (command === "logout") {
368
- await logout();
369
- return;
370
- }
371
-
372
- if (command === "init") {
373
- const template = args[1];
374
- if (!template) {
375
- console.error("Usage: npx hyperstack-core init <template>");
376
- console.error("Available: openclaw-multiagent");
377
- process.exit(1);
378
- }
379
- await init(template);
380
- return;
381
- }
382
-
383
- // All other commands need API key (from env or credentials file)
384
- const apiKey = getApiKey();
385
- let client;
386
- try {
387
- client = new HyperStackClient({
388
- apiKey: apiKey,
389
- workspace: getFlag("workspace", "default"),
390
- agentId: getFlag("agent", undefined),
391
- });
392
- } catch (err) {
393
- console.error(err.message);
394
- console.error("\nRun 'npx hyperstack-core login' to authenticate.\n");
395
- process.exit(1);
396
- }
397
-
398
- if (command === "search") {
399
- const query = args.slice(1).filter(a => !a.startsWith("--")).join(" ");
400
- if (!query) { console.error("Usage: hyperstack-core search <query>"); process.exit(1); }
401
- const result = await client.search(query);
402
- const cards = result.results || [];
403
- if (!cards.length) { console.log("No results."); return; }
404
- for (const c of cards.slice(0, 10)) {
405
- console.log(`[${c.slug}] ${c.title} (${c.cardType || "general"})`);
406
- if (c.body) console.log(` ${c.body.slice(0, 150)}`);
407
- if (c.links?.length) console.log(` Links: ${c.links.map(l => `${l.relation}→${l.target}`).join(", ")}`);
408
- console.log();
409
- }
410
- return;
411
- }
412
-
413
- if (command === "store") {
414
- const slug = getFlag("slug");
415
- const title = getFlag("title");
416
- if (!slug || !title) { console.error("Required: --slug and --title"); process.exit(1); }
417
- const result = await client.store({
418
- slug,
419
- title,
420
- body: getFlag("body"),
421
- cardType: getFlag("type", "general"),
422
- keywords: getFlag("keywords") ? getFlag("keywords").split(",").map(k => k.trim()) : [],
423
- links: getFlag("links") ? getFlag("links").split(",").map(l => {
424
- const [target, relation] = l.trim().split(":");
425
- return { target, relation: relation || "related" };
426
- }) : [],
427
- });
428
- console.log(`${result.updated ? "Updated" : "Created"} [${slug}]: ${title}`);
429
- return;
430
- }
431
-
432
- if (command === "decide") {
433
- const slug = getFlag("slug");
434
- const title = getFlag("title");
435
- if (!slug || !title) { console.error("Required: --slug and --title"); process.exit(1); }
436
- await client.decide({
437
- slug,
438
- title,
439
- body: getFlag("rationale", getFlag("body", "")),
440
- affects: getFlag("affects") ? getFlag("affects").split(",").map(s => s.trim()) : [],
441
- blocks: getFlag("blocks") ? getFlag("blocks").split(",").map(s => s.trim()) : [],
442
- });
443
- console.log(`Decision recorded: [${slug}] ${title}`);
444
- return;
445
- }
446
-
447
- if (command === "blockers") {
448
- const slug = args[1];
449
- if (!slug) { console.error("Usage: hyperstack-core blockers <slug>"); process.exit(1); }
450
- try {
451
- const result = await client.blockers(slug);
452
- const blockers = result.blockers || [];
453
- if (!blockers.length) { console.log(`Nothing blocks [${slug}].`); return; }
454
- console.log(`${blockers.length} blocker(s) for [${slug}]:`);
455
- for (const b of blockers) {
456
- console.log(` [${b.slug}] ${b.title || "?"}`);
457
- }
458
- } catch (err) {
459
- console.error(`Error: ${err.message}`);
460
- }
461
- return;
462
- }
463
-
464
- if (command === "graph") {
465
- const from = args[1];
466
- if (!from) { console.error("Usage: hyperstack-core graph <slug>"); process.exit(1); }
467
- try {
468
- const result = await client.graph(from, {
469
- depth: parseInt(getFlag("depth", "2")),
470
- relation: getFlag("relation") || undefined,
471
- });
472
- console.log(`Graph from [${from}]: ${result.nodes?.length || 0} nodes, ${result.edges?.length || 0} edges\n`);
473
- for (const n of result.nodes || []) {
474
- console.log(` [${n.slug}] ${n.title || "?"} (${n.cardType || "?"})`);
475
- }
476
- console.log();
477
- for (const e of result.edges || []) {
478
- console.log(` ${e.from} --${e.relation}--> ${e.to}`);
479
- }
480
- } catch (err) {
481
- console.error(`Error: ${err.message}`);
482
- }
483
- return;
484
- }
485
-
486
- if (command === "ingest") {
487
- let text = "";
488
- const input = args[1];
489
-
490
- if (!input) {
491
- console.error("Usage: npx hyperstack-core ingest <file|text|->");
492
- console.error(" file Read from file");
493
- console.error(" - Read from stdin");
494
- console.error(" text Treat argument as raw text");
495
- process.exit(1);
496
- }
497
-
498
- if (input === "-") {
499
- // Read from stdin
500
- const chunks = [];
501
- process.stdin.on("data", chunk => chunks.push(chunk));
502
- await new Promise(resolve => process.stdin.on("end", resolve));
503
- text = Buffer.concat(chunks).toString("utf-8");
504
- } else if (existsSync(input)) {
505
- // Read from file
506
- text = readFileSync(input, "utf-8");
507
- } else {
508
- // Treat as raw text
509
- text = input;
510
- }
511
-
512
- if (!text || text.trim().length === 0) {
513
- console.error("Error: No text provided");
514
- process.exit(1);
515
- }
516
-
517
- console.log(`Ingesting ${text.length} chars...`);
518
- const result = await client.ingest(text);
519
- const cards = result.cards || [];
520
- console.log(`\n✅ Created ${cards.length} cards:\n`);
521
- for (const c of cards) {
522
- console.log(` [${c.slug}] ${c.title} (${c.cardType || "general"})`);
523
- }
524
- return;
525
- }
526
-
527
- if (command === "list") {
528
- const result = await client.list();
529
- console.log(`HyperStack: ${result.count ?? 0}/${result.limit ?? "?"} cards (plan: ${result.plan || "?"})\n`);
530
- for (const c of result.cards || []) {
531
- console.log(` [${c.slug}] ${c.title} (${c.cardType || "general"})`);
532
- }
533
- return;
534
- }
535
-
536
- console.error(`Unknown command: ${command}`);
537
- help();
538
- process.exit(1);
539
- }
540
-
541
- run().catch(err => {
542
- console.error(`Error: ${err.message}`);
543
- process.exit(1);
544
- });
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
+ });