hyperstack-core 1.3.0 → 1.5.1
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 -21
- package/README.md +256 -63
- package/SKILL.md +679 -40
- package/adapters/openclaw.js +221 -221
- package/cli.js +633 -499
- package/examples/before-after.js +110 -110
- package/examples/openclaw-multiagent.js +214 -214
- package/index.js +19 -18
- package/package.json +2 -1
- package/src/client.js +322 -267
- package/src/parser.js +305 -0
- package/templates/openclaw-multiagent.json +98 -98
package/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hyperstack-core
|
|
3
|
-
*
|
|
4
|
-
* Typed graph memory for AI agents.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* import { HyperStackClient } from "hyperstack-core";
|
|
8
|
-
* const hs = new HyperStackClient({ apiKey: "hs_..." });
|
|
9
|
-
* await hs.store({ slug: "use-clerk", title: "Use Clerk for auth", cardType: "decision" });
|
|
10
|
-
* await hs.blockers("deploy-prod"); // → typed blockers
|
|
11
|
-
*
|
|
12
|
-
* OpenClaw:
|
|
13
|
-
* import { createOpenClawAdapter } from "hyperstack-core/adapters/openclaw";
|
|
14
|
-
* const adapter = createOpenClawAdapter({ agentId: "researcher" });
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export { HyperStackClient } from "./src/client.js";
|
|
18
|
-
export { createOpenClawAdapter } from "./adapters/openclaw.js";
|
|
1
|
+
/**
|
|
2
|
+
* hyperstack-core
|
|
3
|
+
*
|
|
4
|
+
* Typed graph memory for AI agents.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { HyperStackClient } from "hyperstack-core";
|
|
8
|
+
* const hs = new HyperStackClient({ apiKey: "hs_..." });
|
|
9
|
+
* await hs.store({ slug: "use-clerk", title: "Use Clerk for auth", cardType: "decision" });
|
|
10
|
+
* await hs.blockers("deploy-prod"); // → typed blockers
|
|
11
|
+
*
|
|
12
|
+
* OpenClaw:
|
|
13
|
+
* import { createOpenClawAdapter } from "hyperstack-core/adapters/openclaw";
|
|
14
|
+
* const adapter = createOpenClawAdapter({ agentId: "researcher" });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { HyperStackClient } from "./src/client.js";
|
|
18
|
+
export { createOpenClawAdapter } from "./adapters/openclaw.js";
|
|
19
|
+
export { parse, parseMarkdown, parseConversation, parsePlainText, slugify } from "./src/parser.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyperstack-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Typed graph memory for AI agents. Replace GOALS.md with queryable cards + relations. Works with OpenClaw, Claude Desktop, Cursor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./index.js",
|
|
12
12
|
"./client": "./src/client.js",
|
|
13
|
+
"./parser": "./src/parser.js",
|
|
13
14
|
"./adapters/openclaw": "./adapters/openclaw.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
package/src/client.js
CHANGED
|
@@ -1,267 +1,322 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hyperstack-core — HyperStack SDK for typed graph memory
|
|
3
|
-
*
|
|
4
|
-
* Lightweight client for the HyperStack API. Works in Node.js 18+.
|
|
5
|
-
* No dependencies. Used by the OpenClaw adapter and CLI.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const DEFAULT_BASE = "https://hyperstack-cloud.vercel.app";
|
|
9
|
-
|
|
10
|
-
// ESM-compatible sync file reading
|
|
11
|
-
import { readFileSync, existsSync } from "fs";
|
|
12
|
-
import { join } from "path";
|
|
13
|
-
import { homedir } from "os";
|
|
14
|
-
|
|
15
|
-
function loadCredApiKey() {
|
|
16
|
-
try {
|
|
17
|
-
const credFile = join(homedir(), ".hyperstack", "credentials.json");
|
|
18
|
-
if (existsSync(credFile)) {
|
|
19
|
-
const creds = JSON.parse(readFileSync(credFile, "utf-8"));
|
|
20
|
-
if (creds.api_key) return creds.api_key;
|
|
21
|
-
}
|
|
22
|
-
} catch {}
|
|
23
|
-
return "";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class HyperStackClient {
|
|
27
|
-
/**
|
|
28
|
-
* @param {object} opts
|
|
29
|
-
* @param {string} opts.apiKey — HyperStack API key (hs_...)
|
|
30
|
-
* @param {string} [opts.workspace="default"] — workspace slug
|
|
31
|
-
* @param {string} [opts.baseUrl] — API base URL
|
|
32
|
-
* @param {string} [opts.agentId] — agent identifier for multi-agent setups
|
|
33
|
-
*/
|
|
34
|
-
constructor(opts = {}) {
|
|
35
|
-
this.apiKey = opts.apiKey || process.env.HYPERSTACK_API_KEY || loadCredApiKey();
|
|
36
|
-
this.workspace = opts.workspace || process.env.HYPERSTACK_WORKSPACE || "default";
|
|
37
|
-
this.baseUrl = opts.baseUrl || process.env.HYPERSTACK_BASE_URL || DEFAULT_BASE;
|
|
38
|
-
this.agentId = opts.agentId || null;
|
|
39
|
-
|
|
40
|
-
if (!this.apiKey) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
"HYPERSTACK_API_KEY required.\n" +
|
|
43
|
-
"Run: npx hyperstack-core login\n" +
|
|
44
|
-
"Or: export HYPERSTACK_API_KEY=hs_your_key\n" +
|
|
45
|
-
"Get a free account: https://cascadeai.dev/hyperstack"
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** @private */
|
|
51
|
-
async _request(method, path, body = null) {
|
|
52
|
-
const url = `${this.baseUrl}${path}`;
|
|
53
|
-
const opts = {
|
|
54
|
-
method,
|
|
55
|
-
headers: {
|
|
56
|
-
"X-API-Key": this.apiKey,
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
"User-Agent": "hyperstack-core/1.0.0",
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
if (body) opts.body = JSON.stringify(body);
|
|
62
|
-
|
|
63
|
-
const res = await fetch(url, opts);
|
|
64
|
-
const data = await res.json();
|
|
65
|
-
if (!res.ok) {
|
|
66
|
-
const err = new Error(data.error || `HTTP ${res.status}`);
|
|
67
|
-
err.status = res.status;
|
|
68
|
-
err.body = data;
|
|
69
|
-
throw err;
|
|
70
|
-
}
|
|
71
|
-
return data;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── Cards ───────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Create or update a card (upsert by slug).
|
|
78
|
-
* @param {object} card
|
|
79
|
-
* @param {string} card.slug — unique identifier
|
|
80
|
-
* @param {string} card.title — short title
|
|
81
|
-
* @param {string} [card.body] — description (2-5 sentences)
|
|
82
|
-
* @param {string} [card.cardType] — person|project|decision|preference|workflow|event|general
|
|
83
|
-
* @param {string} [card.stack] — projects|people|decisions|preferences|workflows|general
|
|
84
|
-
* @param {string[]} [card.keywords] — search terms
|
|
85
|
-
* @param {Array<{target: string, relation: string}>} [card.links] — typed relations
|
|
86
|
-
* @param {object} [card.meta] — freeform metadata
|
|
87
|
-
* @returns {Promise<{slug: string, updated: boolean}>}
|
|
88
|
-
*/
|
|
89
|
-
async store(card) {
|
|
90
|
-
if (!card.slug) throw new Error("card.slug required");
|
|
91
|
-
if (!card.title) throw new Error("card.title required");
|
|
92
|
-
|
|
93
|
-
// Auto-tag with agentId if set
|
|
94
|
-
if (this.agentId) {
|
|
95
|
-
card.meta = card.meta || {};
|
|
96
|
-
card.meta.agentId = this.agentId;
|
|
97
|
-
card.keywords = card.keywords || [];
|
|
98
|
-
if (!card.keywords.includes(`agent:${this.agentId}`)) {
|
|
99
|
-
card.keywords.push(`agent:${this.agentId}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return this._request("POST", `/api/cards?workspace=${this.workspace}`, card);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Search cards by query (hybrid semantic + keyword).
|
|
108
|
-
* @param {string} query
|
|
109
|
-
* @returns {Promise<{results: Array}>}
|
|
110
|
-
*/
|
|
111
|
-
async search(query) {
|
|
112
|
-
return this._request("GET", `/api/search?workspace=${this.workspace}&q=${encodeURIComponent(query)}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* List all cards in workspace.
|
|
117
|
-
* @returns {Promise<{cards: Array, count: number, plan: string}>}
|
|
118
|
-
*/
|
|
119
|
-
async list() {
|
|
120
|
-
return this._request("GET", `/api/cards?workspace=${this.workspace}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Delete a card by slug.
|
|
125
|
-
* @param {string} slug
|
|
126
|
-
* @returns {Promise<{deleted: boolean}>}
|
|
127
|
-
*/
|
|
128
|
-
async delete(slug) {
|
|
129
|
-
return this._request("DELETE", `/api/cards?workspace=${this.workspace}&id=${slug}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*
|
|
136
|
-
* @param {
|
|
137
|
-
* @
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
188
|
-
* @param {
|
|
189
|
-
* @
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
*
|
|
243
|
-
* @param {string
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
1
|
+
/**
|
|
2
|
+
* hyperstack-core — HyperStack SDK for typed graph memory
|
|
3
|
+
*
|
|
4
|
+
* Lightweight client for the HyperStack API. Works in Node.js 18+.
|
|
5
|
+
* No dependencies. Used by the OpenClaw adapter and CLI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE = "https://hyperstack-cloud.vercel.app";
|
|
9
|
+
|
|
10
|
+
// ESM-compatible sync file reading
|
|
11
|
+
import { readFileSync, existsSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
|
|
15
|
+
function loadCredApiKey() {
|
|
16
|
+
try {
|
|
17
|
+
const credFile = join(homedir(), ".hyperstack", "credentials.json");
|
|
18
|
+
if (existsSync(credFile)) {
|
|
19
|
+
const creds = JSON.parse(readFileSync(credFile, "utf-8"));
|
|
20
|
+
if (creds.api_key) return creds.api_key;
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class HyperStackClient {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.apiKey — HyperStack API key (hs_...)
|
|
30
|
+
* @param {string} [opts.workspace="default"] — workspace slug
|
|
31
|
+
* @param {string} [opts.baseUrl] — API base URL
|
|
32
|
+
* @param {string} [opts.agentId] — agent identifier for multi-agent setups
|
|
33
|
+
*/
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
this.apiKey = opts.apiKey || process.env.HYPERSTACK_API_KEY || loadCredApiKey();
|
|
36
|
+
this.workspace = opts.workspace || process.env.HYPERSTACK_WORKSPACE || "default";
|
|
37
|
+
this.baseUrl = opts.baseUrl || process.env.HYPERSTACK_BASE_URL || DEFAULT_BASE;
|
|
38
|
+
this.agentId = opts.agentId || null;
|
|
39
|
+
|
|
40
|
+
if (!this.apiKey) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"HYPERSTACK_API_KEY required.\n" +
|
|
43
|
+
"Run: npx hyperstack-core login\n" +
|
|
44
|
+
"Or: export HYPERSTACK_API_KEY=hs_your_key\n" +
|
|
45
|
+
"Get a free account: https://cascadeai.dev/hyperstack"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @private */
|
|
51
|
+
async _request(method, path, body = null) {
|
|
52
|
+
const url = `${this.baseUrl}${path}`;
|
|
53
|
+
const opts = {
|
|
54
|
+
method,
|
|
55
|
+
headers: {
|
|
56
|
+
"X-API-Key": this.apiKey,
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"User-Agent": "hyperstack-core/1.0.0",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
if (body) opts.body = JSON.stringify(body);
|
|
62
|
+
|
|
63
|
+
const res = await fetch(url, opts);
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const err = new Error(data.error || `HTTP ${res.status}`);
|
|
67
|
+
err.status = res.status;
|
|
68
|
+
err.body = data;
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Cards ───────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create or update a card (upsert by slug).
|
|
78
|
+
* @param {object} card
|
|
79
|
+
* @param {string} card.slug — unique identifier
|
|
80
|
+
* @param {string} card.title — short title
|
|
81
|
+
* @param {string} [card.body] — description (2-5 sentences)
|
|
82
|
+
* @param {string} [card.cardType] — person|project|decision|preference|workflow|event|general
|
|
83
|
+
* @param {string} [card.stack] — projects|people|decisions|preferences|workflows|general
|
|
84
|
+
* @param {string[]} [card.keywords] — search terms
|
|
85
|
+
* @param {Array<{target: string, relation: string}>} [card.links] — typed relations
|
|
86
|
+
* @param {object} [card.meta] — freeform metadata
|
|
87
|
+
* @returns {Promise<{slug: string, updated: boolean}>}
|
|
88
|
+
*/
|
|
89
|
+
async store(card) {
|
|
90
|
+
if (!card.slug) throw new Error("card.slug required");
|
|
91
|
+
if (!card.title) throw new Error("card.title required");
|
|
92
|
+
|
|
93
|
+
// Auto-tag with agentId if set
|
|
94
|
+
if (this.agentId) {
|
|
95
|
+
card.meta = card.meta || {};
|
|
96
|
+
card.meta.agentId = this.agentId;
|
|
97
|
+
card.keywords = card.keywords || [];
|
|
98
|
+
if (!card.keywords.includes(`agent:${this.agentId}`)) {
|
|
99
|
+
card.keywords.push(`agent:${this.agentId}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this._request("POST", `/api/cards?workspace=${this.workspace}`, card);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Search cards by query (hybrid semantic + keyword).
|
|
108
|
+
* @param {string} query
|
|
109
|
+
* @returns {Promise<{results: Array}>}
|
|
110
|
+
*/
|
|
111
|
+
async search(query) {
|
|
112
|
+
return this._request("GET", `/api/search?workspace=${this.workspace}&q=${encodeURIComponent(query)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* List all cards in workspace.
|
|
117
|
+
* @returns {Promise<{cards: Array, count: number, plan: string}>}
|
|
118
|
+
*/
|
|
119
|
+
async list() {
|
|
120
|
+
return this._request("GET", `/api/cards?workspace=${this.workspace}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Delete a card by slug.
|
|
125
|
+
* @param {string} slug
|
|
126
|
+
* @returns {Promise<{deleted: boolean}>}
|
|
127
|
+
*/
|
|
128
|
+
async delete(slug) {
|
|
129
|
+
return this._request("DELETE", `/api/cards?workspace=${this.workspace}&id=${slug}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Batch-store an array of cards. Stores sequentially, collects errors.
|
|
134
|
+
* @param {Array<object>} cards — array of card objects (slug + title required)
|
|
135
|
+
* @param {object} [opts]
|
|
136
|
+
* @param {function} [opts.onProgress] — called with (index, total, card, result|error)
|
|
137
|
+
* @returns {Promise<{stored: number, failed: number, errors: Array}>}
|
|
138
|
+
*/
|
|
139
|
+
async ingest(cards, opts = {}) {
|
|
140
|
+
let stored = 0;
|
|
141
|
+
let failed = 0;
|
|
142
|
+
const errors = [];
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < cards.length; i++) {
|
|
145
|
+
try {
|
|
146
|
+
const result = await this.store(cards[i]);
|
|
147
|
+
stored++;
|
|
148
|
+
if (opts.onProgress) opts.onProgress(i, cards.length, cards[i], result);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
failed++;
|
|
151
|
+
errors.push({ slug: cards[i].slug, error: err.message });
|
|
152
|
+
if (opts.onProgress) opts.onProgress(i, cards.length, cards[i], err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { stored, failed, errors };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Graph ───────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Traverse the knowledge graph from a starting card.
|
|
163
|
+
* @param {string} from — starting card slug
|
|
164
|
+
* @param {object} [opts]
|
|
165
|
+
* @param {number} [opts.depth=1] — hops to traverse (1-3)
|
|
166
|
+
* @param {string} [opts.relation] — filter by relation type
|
|
167
|
+
* @param {string} [opts.type] — filter by card type
|
|
168
|
+
* @param {string} [opts.mode] — "impact" for reverse traversal, "replay" for decision replay
|
|
169
|
+
* @param {string} [opts.at] — ISO timestamp for time-travel
|
|
170
|
+
* @returns {Promise<{nodes: Array, edges: Array}>}
|
|
171
|
+
*/
|
|
172
|
+
async graph(from, opts = {}) {
|
|
173
|
+
let url = `/api/graph?workspace=${this.workspace}&from=${from}`;
|
|
174
|
+
if (opts.depth) url += `&depth=${opts.depth}`;
|
|
175
|
+
if (opts.relation) url += `&relation=${opts.relation}`;
|
|
176
|
+
if (opts.type) url += `&type=${opts.type}`;
|
|
177
|
+
if (opts.mode) url += `&mode=${opts.mode}`;
|
|
178
|
+
if (opts.at) url += `&at=${encodeURIComponent(opts.at)}`;
|
|
179
|
+
return this._request("GET", url);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Deterministic impact analysis — reverse traversal.
|
|
184
|
+
* Answers: "what depends on X?" / "what would break if X changed?"
|
|
185
|
+
*
|
|
186
|
+
* @param {string} slug — the card to analyse
|
|
187
|
+
* @param {object} [opts]
|
|
188
|
+
* @param {number} [opts.depth=2] — hops to traverse upstream (1-3)
|
|
189
|
+
* @param {string} [opts.relation] — filter by relation type (e.g. "blocks", "depends_on")
|
|
190
|
+
* @returns {Promise<{root: string, mode: "impact", nodes: Array, edges: Array}>}
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* // What depends on the billing API?
|
|
194
|
+
* const result = await hs.impact("billing-api", { depth: 2 });
|
|
195
|
+
* // Returns all cards that link TO billing-api, up to 2 hops upstream
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* // What specifically blocks the deploy?
|
|
199
|
+
* const result = await hs.impact("prod-deploy", { relation: "blocks" });
|
|
200
|
+
*/
|
|
201
|
+
async impact(slug, opts = {}) {
|
|
202
|
+
const { depth = 2, relation } = opts;
|
|
203
|
+
let url = `/api/graph?workspace=${this.workspace}&from=${slug}&mode=impact&depth=${depth}`;
|
|
204
|
+
if (relation) url += `&relation=${encodeURIComponent(relation)}`;
|
|
205
|
+
return this._request("GET", url);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── Multi-Agent Helpers ──────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Query cards that block a specific card/task.
|
|
212
|
+
* Shorthand for graph traversal with relation="blocks".
|
|
213
|
+
* @param {string} slug — card being blocked
|
|
214
|
+
* @returns {Promise<{blockers: Array}>}
|
|
215
|
+
*/
|
|
216
|
+
async blockers(slug) {
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.graph(slug, { depth: 2, relation: "blocks" });
|
|
219
|
+
const blockers = (result.edges || [])
|
|
220
|
+
.filter(e => e.relation === "blocks" && e.to === slug)
|
|
221
|
+
.map(e => {
|
|
222
|
+
const node = (result.nodes || []).find(n => n.slug === e.from);
|
|
223
|
+
return node || { slug: e.from };
|
|
224
|
+
});
|
|
225
|
+
return { blockers, graph: result };
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// If graph API not available (free tier), fallback to search
|
|
228
|
+
if (err.status === 403) {
|
|
229
|
+
const searchResult = await this.search(`blocks ${slug}`);
|
|
230
|
+
return {
|
|
231
|
+
blockers: (searchResult.results || []).filter(c =>
|
|
232
|
+
c.links?.some(l => l.relation === "blocks" && l.target === slug)
|
|
233
|
+
),
|
|
234
|
+
fallback: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find cards owned by a specific agent.
|
|
243
|
+
* @param {string} [agentId] — defaults to this.agentId
|
|
244
|
+
* @returns {Promise<{cards: Array}>}
|
|
245
|
+
*/
|
|
246
|
+
async agentCards(agentId) {
|
|
247
|
+
const id = agentId || this.agentId;
|
|
248
|
+
if (!id) throw new Error("agentId required");
|
|
249
|
+
return this.search(`agent:${id}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Record a decision with full provenance.
|
|
254
|
+
* Creates a decision card + links to who decided and what it affects.
|
|
255
|
+
* @param {object} decision
|
|
256
|
+
* @param {string} decision.slug
|
|
257
|
+
* @param {string} decision.title
|
|
258
|
+
* @param {string} decision.body — rationale
|
|
259
|
+
* @param {string} [decision.decidedBy] — agent/person slug
|
|
260
|
+
* @param {string[]} [decision.affects] — slugs of affected cards
|
|
261
|
+
* @param {string[]} [decision.blocks] — slugs of things this blocks
|
|
262
|
+
* @param {object} [decision.meta]
|
|
263
|
+
*/
|
|
264
|
+
async decide(decision) {
|
|
265
|
+
const links = [];
|
|
266
|
+
if (decision.decidedBy) {
|
|
267
|
+
links.push({ target: decision.decidedBy, relation: "decided" });
|
|
268
|
+
}
|
|
269
|
+
if (decision.affects) {
|
|
270
|
+
for (const a of decision.affects) {
|
|
271
|
+
links.push({ target: a, relation: "triggers" });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (decision.blocks) {
|
|
275
|
+
for (const b of decision.blocks) {
|
|
276
|
+
links.push({ target: b, relation: "blocks" });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return this.store({
|
|
281
|
+
slug: decision.slug,
|
|
282
|
+
title: decision.title,
|
|
283
|
+
body: decision.body,
|
|
284
|
+
cardType: "decision",
|
|
285
|
+
stack: "decisions",
|
|
286
|
+
links,
|
|
287
|
+
meta: { ...decision.meta, decidedAt: new Date().toISOString() },
|
|
288
|
+
keywords: decision.keywords || [],
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Register an agent as a card in the graph.
|
|
294
|
+
* @param {object} agent
|
|
295
|
+
* @param {string} agent.id — unique agent ID
|
|
296
|
+
* @param {string} agent.name — display name
|
|
297
|
+
* @param {string} agent.role — what this agent does
|
|
298
|
+
* @param {string[]} [agent.owns] — slugs this agent owns
|
|
299
|
+
*/
|
|
300
|
+
async registerAgent(agent) {
|
|
301
|
+
const links = [];
|
|
302
|
+
if (agent.owns) {
|
|
303
|
+
for (const o of agent.owns) {
|
|
304
|
+
links.push({ target: o, relation: "owns" });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return this.store({
|
|
309
|
+
slug: `agent-${agent.id}`,
|
|
310
|
+
title: `Agent: ${agent.name}`,
|
|
311
|
+
body: agent.role,
|
|
312
|
+
cardType: "person",
|
|
313
|
+
stack: "people",
|
|
314
|
+
links,
|
|
315
|
+
keywords: ["agent", agent.id, agent.name],
|
|
316
|
+
meta: { agentId: agent.id, registeredAt: new Date().toISOString() },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export { HyperStackClient };
|
|
322
|
+
export default HyperStackClient;
|