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/LICENSE +21 -0
- package/README.md +154 -0
- package/SKILL.md +118 -0
- package/adapters/openclaw.js +221 -0
- package/cli.js +500 -0
- package/examples/before-after.js +110 -0
- package/examples/openclaw-multiagent.js +214 -0
- package/index.js +18 -0
- package/package.json +50 -0
- package/src/client.js +267 -0
- package/templates/openclaw-multiagent.json +98 -0
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));
|