hyperstack-core 1.0.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 ADDED
@@ -0,0 +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 } 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
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * example-before-after.js
3
+ *
4
+ * Side-by-side: markdown files vs HyperStack
5
+ *
6
+ * THE PROBLEM:
7
+ * Your 3 OpenClaw agents coordinate via shared markdown files.
8
+ * Agent A appends to DECISIONS.md. Agent B reads it. Agent C greps for blockers.
9
+ *
10
+ * Question: "What blocks the production deploy?"
11
+ * Old way: grep -r "blocks.*deploy" *.md → fragile, returns text blobs
12
+ * New way: hs.blockers("deploy-prod") → exact typed cards
13
+ */
14
+
15
+ import { HyperStackClient } from "hyperstack-core";
16
+
17
+ const hs = new HyperStackClient({ workspace: "demo" });
18
+
19
+ async function main() {
20
+ // ─── What agents used to write in DECISIONS.md ────────
21
+ //
22
+ // # DECISIONS.md
23
+ // - 2026-02-15: Use Clerk for auth (coder-agent)
24
+ // Rationale: Better DX, lower cost, native Next.js support
25
+ //
26
+ // - 2026-02-15: Deploy needs auth migration first (deploy-agent)
27
+ // Migration to Clerk must complete before we can deploy v2
28
+ //
29
+ // - 2026-02-16: Staging tests failing, blocks deploy (ops-agent)
30
+ // 3 integration tests broken after Clerk middleware change
31
+
32
+ // ─── Same info, structured as typed cards ─────────────
33
+
34
+ await hs.store({
35
+ slug: "decision-use-clerk",
36
+ title: "Use Clerk for auth",
37
+ body: "Better DX, lower cost, native Next.js support. Chose over Auth0 and NextAuth.",
38
+ cardType: "decision",
39
+ keywords: ["clerk", "auth", "auth0"],
40
+ links: [
41
+ { target: "agent-coder", relation: "decided" },
42
+ { target: "auth-api", relation: "triggers" },
43
+ ],
44
+ });
45
+
46
+ await hs.store({
47
+ slug: "migration-clerk",
48
+ title: "Auth migration to Clerk",
49
+ body: "Migration to Clerk must complete before v2 deploy. Estimated 2 days.",
50
+ cardType: "workflow",
51
+ keywords: ["migration", "clerk", "deploy"],
52
+ links: [
53
+ { target: "deploy-prod", relation: "blocks" },
54
+ { target: "decision-use-clerk", relation: "depends_on" },
55
+ ],
56
+ });
57
+
58
+ await hs.store({
59
+ slug: "staging-tests-broken",
60
+ title: "3 integration tests failing after Clerk middleware",
61
+ body: "Tests auth-flow-1, auth-flow-2, session-persist broken. Clerk middleware changed req.auth shape.",
62
+ cardType: "event",
63
+ keywords: ["tests", "staging", "broken", "clerk"],
64
+ links: [
65
+ { target: "deploy-prod", relation: "blocks" },
66
+ { target: "migration-clerk", relation: "related" },
67
+ ],
68
+ });
69
+
70
+ await hs.store({
71
+ slug: "deploy-prod",
72
+ title: "Deploy v2 to production",
73
+ body: "Production deploy of v2 with new auth system.",
74
+ cardType: "workflow",
75
+ keywords: ["deploy", "production", "v2"],
76
+ });
77
+
78
+ // ─── Now ask: "What blocks the production deploy?" ────
79
+
80
+ console.log('Question: "What blocks the production deploy?"\n');
81
+
82
+ console.log("Old way (grep DECISIONS.md):");
83
+ console.log(' $ grep -r "blocks.*deploy" *.md');
84
+ console.log(' DECISIONS.md:- Deploy needs auth migration first');
85
+ console.log(' DECISIONS.md:- Staging tests failing, blocks deploy');
86
+ console.log(" → Text blobs. No structure. Which is resolved? Who owns it?\n");
87
+
88
+ console.log("New way (HyperStack):");
89
+ const result = await hs.blockers("deploy-prod");
90
+ console.log(` ${result.blockers?.length || 0} typed blockers:`);
91
+ for (const b of result.blockers || []) {
92
+ console.log(` [${b.slug}] ${b.title} (${b.cardType})`);
93
+ }
94
+ console.log(" → Exact cards. Typed relations. Queryable.\n");
95
+
96
+ // ─── Bonus: follow the decision trail ─────────────────
97
+
98
+ console.log('Bonus: "Why did we choose Clerk?"');
99
+ const search = await hs.search("clerk decision rationale");
100
+ const decision = search.results?.[0];
101
+ if (decision) {
102
+ console.log(` [${decision.slug}] ${decision.title}`);
103
+ console.log(` ${decision.body}`);
104
+ if (decision.links?.length) {
105
+ console.log(` Links: ${decision.links.map(l => `${l.relation}→${l.target}`).join(", ")}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ main().catch(err => console.error(err.message));