@synap-core/cli 1.6.1 → 1.7.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/dist/commands/data.js +16 -9
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/discover.d.ts +18 -0
- package/dist/commands/discover.js +71 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/knowledge.js +9 -5
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/hub-client.d.ts +8 -0
- package/dist/lib/hub-client.js +14 -0
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/pod.d.ts +8 -0
- package/dist/lib/pod.js +9 -1
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/targets.js +31 -7
- package/dist/lib/targets.js.map +1 -1
- package/node_modules/@synap/hub-rest-client/dist/index.cjs +881 -0
- package/node_modules/@synap/hub-rest-client/dist/index.d.cts +907 -0
- package/node_modules/@synap/hub-rest-client/dist/index.d.ts +907 -0
- package/node_modules/@synap/hub-rest-client/dist/index.js +851 -0
- package/node_modules/@synap/hub-rest-client/package.json +45 -0
- package/node_modules/@synap/hub-rest-client/src/client.ts +1143 -0
- package/node_modules/@synap/hub-rest-client/src/errors.ts +30 -0
- package/node_modules/@synap/hub-rest-client/src/index.ts +111 -0
- package/node_modules/@synap/hub-rest-client/src/setup.ts +77 -0
- package/node_modules/@synap/hub-rest-client/src/types.ts +639 -0
- package/package.json +8 -2
- package/skills/synap/SKILL.md +19 -20
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var HubApiError = class extends Error {
|
|
3
|
+
constructor(message, statusCode, body) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.body = body;
|
|
7
|
+
this.name = "HubApiError";
|
|
8
|
+
}
|
|
9
|
+
get isUnauthorized() {
|
|
10
|
+
return this.statusCode === 401;
|
|
11
|
+
}
|
|
12
|
+
get isForbidden() {
|
|
13
|
+
return this.statusCode === 403;
|
|
14
|
+
}
|
|
15
|
+
get isNotFound() {
|
|
16
|
+
return this.statusCode === 404;
|
|
17
|
+
}
|
|
18
|
+
get isServerError() {
|
|
19
|
+
return this.statusCode >= 500;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/client.ts
|
|
24
|
+
function normalizeUrl(url) {
|
|
25
|
+
return url.replace(/\/$/, "");
|
|
26
|
+
}
|
|
27
|
+
function formatHubErrorMessage(status, statusText, errorBody) {
|
|
28
|
+
let detail = "";
|
|
29
|
+
if (errorBody && typeof errorBody === "object" && "error" in errorBody) {
|
|
30
|
+
detail = String(errorBody.error);
|
|
31
|
+
}
|
|
32
|
+
const base = `Hub API error: ${status} ${statusText}`;
|
|
33
|
+
let msg = detail ? `${base} \u2014 ${detail}` : base;
|
|
34
|
+
if (status === 403 && /hub-protocol\.write/i.test(detail)) {
|
|
35
|
+
msg += " Create or reconnect an API key that includes the hub-protocol.write scope (Settings \u2192 API keys on your pod).";
|
|
36
|
+
}
|
|
37
|
+
return msg;
|
|
38
|
+
}
|
|
39
|
+
function unwrapList(result) {
|
|
40
|
+
return Array.isArray(result) ? result : result.data ?? [];
|
|
41
|
+
}
|
|
42
|
+
function unwrapWorkspacesResponse(result) {
|
|
43
|
+
if (Array.isArray(result)) return result;
|
|
44
|
+
if (result && typeof result === "object" && "workspaces" in result) {
|
|
45
|
+
const w = result.workspaces;
|
|
46
|
+
return Array.isArray(w) ? w : [];
|
|
47
|
+
}
|
|
48
|
+
return unwrapList(result);
|
|
49
|
+
}
|
|
50
|
+
function unwrapSingle(result) {
|
|
51
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
52
|
+
return result.data;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
var HubRestClient = class {
|
|
57
|
+
base;
|
|
58
|
+
headers;
|
|
59
|
+
timeoutMs;
|
|
60
|
+
workspaceId;
|
|
61
|
+
/** Cached from GET /users/me — avoids repeated identity calls. */
|
|
62
|
+
resolvedUserId = null;
|
|
63
|
+
constructor(config) {
|
|
64
|
+
this.base = normalizeUrl(config.podUrl);
|
|
65
|
+
this.headers = {
|
|
66
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
67
|
+
"Content-Type": "application/json"
|
|
68
|
+
};
|
|
69
|
+
this.workspaceId = config.workspaceId;
|
|
70
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
71
|
+
}
|
|
72
|
+
/** User id for the current API key (Hub REST requires userId on several GETs). */
|
|
73
|
+
async resolveUserId() {
|
|
74
|
+
if (this.resolvedUserId) return this.resolvedUserId;
|
|
75
|
+
const me = await this.getMe();
|
|
76
|
+
this.resolvedUserId = me.id;
|
|
77
|
+
return me.id;
|
|
78
|
+
}
|
|
79
|
+
async request(method, path, body, signal) {
|
|
80
|
+
const url = `${this.base}${path}`;
|
|
81
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
82
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
83
|
+
const res = await fetch(url, {
|
|
84
|
+
method,
|
|
85
|
+
headers: this.headers,
|
|
86
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
87
|
+
signal: combined
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
91
|
+
throw new HubApiError(
|
|
92
|
+
formatHubErrorMessage(res.status, res.statusText, errorBody),
|
|
93
|
+
res.status,
|
|
94
|
+
errorBody
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
// ─── Identity ─────────────────────────────────────────────────────────────
|
|
100
|
+
async getMe() {
|
|
101
|
+
return this.request("GET", "/api/hub/users/me");
|
|
102
|
+
}
|
|
103
|
+
async getWorkspaces() {
|
|
104
|
+
const result = await this.request("GET", "/api/hub/workspaces");
|
|
105
|
+
return unwrapWorkspacesResponse(result);
|
|
106
|
+
}
|
|
107
|
+
async provisionAgentWorkspace(input) {
|
|
108
|
+
return this.request(
|
|
109
|
+
"POST",
|
|
110
|
+
"/api/hub/workspaces/provision-agent",
|
|
111
|
+
input
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get full activity context for a user — recent entities, active threads, workspace summary.
|
|
116
|
+
* Use at session start to orient the agent to the user's current state.
|
|
117
|
+
*/
|
|
118
|
+
async getUserContext(userId, options) {
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
121
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
122
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
123
|
+
return this.request(
|
|
124
|
+
"GET",
|
|
125
|
+
`/api/hub/users/${userId}/context${qs}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
// ─── Entities ─────────────────────────────────────────────────────────────
|
|
129
|
+
async searchEntities(query, options, signal) {
|
|
130
|
+
const params = new URLSearchParams({ q: query });
|
|
131
|
+
if (options?.profileSlug) params.set("profileSlug", options.profileSlug);
|
|
132
|
+
if (options?.scope !== "all") {
|
|
133
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
134
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
135
|
+
}
|
|
136
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
137
|
+
const result = await this.request(
|
|
138
|
+
"GET",
|
|
139
|
+
`/api/hub/entities?${params}`,
|
|
140
|
+
void 0,
|
|
141
|
+
signal
|
|
142
|
+
);
|
|
143
|
+
return unwrapList(result);
|
|
144
|
+
}
|
|
145
|
+
async getEntity(id) {
|
|
146
|
+
const result = await this.request(
|
|
147
|
+
"GET",
|
|
148
|
+
`/api/hub/entities/${id}`
|
|
149
|
+
);
|
|
150
|
+
return unwrapSingle(result);
|
|
151
|
+
}
|
|
152
|
+
async getRecentEntities(options) {
|
|
153
|
+
const params = new URLSearchParams({
|
|
154
|
+
sort: "updatedAt:desc",
|
|
155
|
+
limit: String(options?.limit ?? 20)
|
|
156
|
+
});
|
|
157
|
+
if (options?.profileSlug) params.set("profileSlug", options.profileSlug);
|
|
158
|
+
if (options?.scope !== "all") {
|
|
159
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
160
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
161
|
+
}
|
|
162
|
+
const result = await this.request(
|
|
163
|
+
"GET",
|
|
164
|
+
`/api/hub/entities?${params}`
|
|
165
|
+
);
|
|
166
|
+
return unwrapList(result);
|
|
167
|
+
}
|
|
168
|
+
async createEntity(input) {
|
|
169
|
+
return this.request("POST", "/api/hub/entities", {
|
|
170
|
+
...input,
|
|
171
|
+
workspaceId: input.workspaceId ?? this.workspaceId
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async updateEntity(id, input) {
|
|
175
|
+
return this.request(
|
|
176
|
+
"PATCH",
|
|
177
|
+
`/api/hub/entities/${id}`,
|
|
178
|
+
input
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
// ─── Unified Search ───────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Unified full-text search across entities, documents, and views.
|
|
184
|
+
* Use when you don't know the content type. For entity-only search use searchEntities().
|
|
185
|
+
*
|
|
186
|
+
* Note: The backend GET /search requires userId as a query param; this method resolves
|
|
187
|
+
* the current user automatically.
|
|
188
|
+
*/
|
|
189
|
+
async search(query, options, signal) {
|
|
190
|
+
const userId = await this.resolveUserId();
|
|
191
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
192
|
+
const params = new URLSearchParams({ userId, query });
|
|
193
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
194
|
+
if (options?.collections?.length)
|
|
195
|
+
params.set("collections", options.collections.join(","));
|
|
196
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
197
|
+
return this.request(
|
|
198
|
+
"GET",
|
|
199
|
+
`/api/hub/search?${params}`,
|
|
200
|
+
void 0,
|
|
201
|
+
signal
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
// ─── Relations & Graph ────────────────────────────────────────────────────
|
|
205
|
+
/**
|
|
206
|
+
* Get all relations for an entity — inbound and outbound.
|
|
207
|
+
* Use to discover connections before graph traversal.
|
|
208
|
+
*
|
|
209
|
+
* Note: The backend GET /relations requires both userId and workspaceId.
|
|
210
|
+
* This method resolves userId automatically; workspaceId falls back to client default.
|
|
211
|
+
*/
|
|
212
|
+
async getRelations(entityId, options) {
|
|
213
|
+
const userId = await this.resolveUserId();
|
|
214
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
215
|
+
if (!wsId) throw new Error("workspaceId is required for getRelations");
|
|
216
|
+
const params = new URLSearchParams({ userId, workspaceId: wsId, entityId });
|
|
217
|
+
const result = await this.request("GET", `/api/hub/relations?${params}`);
|
|
218
|
+
return unwrapList(result);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Create a typed relation between two entities.
|
|
222
|
+
* Type is a free string — conventions: "related_to", "parent_of", "child_of",
|
|
223
|
+
* "belongs_to", "authored_by", "depends_on", "references".
|
|
224
|
+
* Goes through governance — may return "proposed".
|
|
225
|
+
*/
|
|
226
|
+
async createRelation(input) {
|
|
227
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
228
|
+
const wsId = input.workspaceId ?? this.workspaceId;
|
|
229
|
+
if (!wsId) throw new Error("workspaceId is required for createRelation");
|
|
230
|
+
return this.request("POST", "/api/hub/relations", {
|
|
231
|
+
userId,
|
|
232
|
+
workspaceId: wsId,
|
|
233
|
+
sourceEntityId: input.sourceEntityId,
|
|
234
|
+
targetEntityId: input.targetEntityId,
|
|
235
|
+
type: input.type
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Delete a relation by ID (get from getRelations()).
|
|
240
|
+
*/
|
|
241
|
+
async deleteRelation(relationId) {
|
|
242
|
+
const userId = await this.resolveUserId();
|
|
243
|
+
await this.request("DELETE", `/api/hub/relations/${relationId}`, {
|
|
244
|
+
userId
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Traverse the knowledge graph from an entity using BFS.
|
|
249
|
+
* Returns nodes and edges up to maxDepth hops away.
|
|
250
|
+
* maxDepth: 1=direct neighbors, 2=neighborhood (recommended), 3=extended (expensive).
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* const graph = await client.traverseGraph(projectId, { maxDepth: 2 });
|
|
254
|
+
* const tasks = graph.nodes.filter(n => n.profileSlug === "task");
|
|
255
|
+
*/
|
|
256
|
+
async traverseGraph(entityId, options) {
|
|
257
|
+
const userId = await this.resolveUserId();
|
|
258
|
+
const params = new URLSearchParams({
|
|
259
|
+
userId,
|
|
260
|
+
startEntityId: entityId,
|
|
261
|
+
maxDepth: String(options?.maxDepth ?? 2)
|
|
262
|
+
});
|
|
263
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
264
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
265
|
+
return this.request(
|
|
266
|
+
"GET",
|
|
267
|
+
`/api/hub/graph/traverse?${params}`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Unified view of everything connected to an entity. Merges three sources:
|
|
272
|
+
* 1. Graph relations — explicit rows in the relations table (both directions)
|
|
273
|
+
* 2. Structural links — entities whose `entity_id` properties point to this entity
|
|
274
|
+
* 3. Thread connections — chat threads that touched this entity
|
|
275
|
+
*
|
|
276
|
+
* Prefer this over `getRelations()` / `traverseGraph()` when you want the complete
|
|
277
|
+
* picture — those only see the relations table and miss property-based links that
|
|
278
|
+
* haven't been synced (notably custom profiles without a `relationDefId` mapping).
|
|
279
|
+
*
|
|
280
|
+
* Each connection carries a `source` field so callers can filter by origin.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* const { connections } = await client.getConnections(entityId);
|
|
284
|
+
* const tasks = connections.filter(c => c.entity?.profileSlug === "task");
|
|
285
|
+
*/
|
|
286
|
+
async getConnections(entityId, options) {
|
|
287
|
+
const userId = await this.resolveUserId();
|
|
288
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
289
|
+
const params = new URLSearchParams({ userId });
|
|
290
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
291
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
292
|
+
return this.request(
|
|
293
|
+
"GET",
|
|
294
|
+
`/api/hub/entities/${entityId}/connections?${params}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
// ─── Profiles & Schema ────────────────────────────────────────────────────
|
|
298
|
+
/**
|
|
299
|
+
* List all entity profile types in the workspace.
|
|
300
|
+
* Always call before creating entities to discover what types are available.
|
|
301
|
+
* Returns system profiles (always present) + custom workspace profiles.
|
|
302
|
+
*/
|
|
303
|
+
async listProfiles(workspaceId) {
|
|
304
|
+
const userId = await this.resolveUserId();
|
|
305
|
+
const params = new URLSearchParams({ userId, workspaceId });
|
|
306
|
+
const result = await this.request("GET", `/api/hub/profiles?${params}`);
|
|
307
|
+
return unwrapList(result);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* List property definitions for a workspace, optionally filtered by profile.
|
|
311
|
+
*/
|
|
312
|
+
async listPropertyDefs(workspaceId, options) {
|
|
313
|
+
const userId = await this.resolveUserId();
|
|
314
|
+
const params = new URLSearchParams({ userId, workspaceId });
|
|
315
|
+
if (options?.profileSlug) params.set("profileId", options.profileSlug);
|
|
316
|
+
const result = await this.request("GET", `/api/hub/property-defs?${params}`);
|
|
317
|
+
return unwrapList(result);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Runtime discovery — profiles with property schemas + command tree.
|
|
321
|
+
*
|
|
322
|
+
* Call once per session at session start. Returns ground-truth profile
|
|
323
|
+
* schemas (including custom workspace profiles) and the canonical CLI
|
|
324
|
+
* command map. Replaces static skill file profile descriptions.
|
|
325
|
+
*/
|
|
326
|
+
async discover(workspaceId) {
|
|
327
|
+
const userId = await this.resolveUserId();
|
|
328
|
+
const wsId = workspaceId ?? this.workspaceId ?? "";
|
|
329
|
+
const params = new URLSearchParams({ userId, workspaceId: wsId });
|
|
330
|
+
return this.request(
|
|
331
|
+
"GET",
|
|
332
|
+
`/api/hub/discover?${params}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
// ─── Threads & Channels ───────────────────────────────────────────────────
|
|
336
|
+
/**
|
|
337
|
+
* List threads (channels) accessible to a user.
|
|
338
|
+
*/
|
|
339
|
+
async listThreads(userId, options) {
|
|
340
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
341
|
+
const params = new URLSearchParams({ userId });
|
|
342
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
343
|
+
if (options?.type) params.set("type", options.type);
|
|
344
|
+
const result = await this.request(
|
|
345
|
+
"GET",
|
|
346
|
+
`/api/hub/threads?${params}`
|
|
347
|
+
);
|
|
348
|
+
return unwrapList(result);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Get the user's personal channel — their private AI conversation thread.
|
|
352
|
+
* Use as default destination for messages and proactive posts.
|
|
353
|
+
*
|
|
354
|
+
* Note: The backend GET /channels/personal requires both userId and workspaceId.
|
|
355
|
+
*/
|
|
356
|
+
async getPersonalChannel(userId, workspaceId) {
|
|
357
|
+
const wsId = workspaceId ?? this.workspaceId;
|
|
358
|
+
if (!wsId)
|
|
359
|
+
throw new Error("workspaceId is required for getPersonalChannel");
|
|
360
|
+
const params = new URLSearchParams({ userId, workspaceId: wsId });
|
|
361
|
+
return this.request(
|
|
362
|
+
"GET",
|
|
363
|
+
`/api/hub/channels/personal?${params}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Create a new thread. Pass entityId to auto-link on creation.
|
|
368
|
+
*
|
|
369
|
+
* Note: The backend POST /threads requires workspaceId in the body.
|
|
370
|
+
*/
|
|
371
|
+
async createThread(input) {
|
|
372
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
373
|
+
const wsId = input.workspaceId ?? this.workspaceId;
|
|
374
|
+
if (!wsId) throw new Error("workspaceId is required for createThread");
|
|
375
|
+
return this.request("POST", "/api/hub/threads", {
|
|
376
|
+
userId,
|
|
377
|
+
workspaceId: wsId,
|
|
378
|
+
title: input.name,
|
|
379
|
+
agentType: input.agentType,
|
|
380
|
+
contextObjectType: input.entityId ? "entity" : input.documentId ? "document" : void 0,
|
|
381
|
+
contextObjectId: input.entityId ?? input.documentId
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get full thread context: messages + all linked entities and documents.
|
|
386
|
+
* Call before sending a message to orient the AI with conversation history.
|
|
387
|
+
*/
|
|
388
|
+
async getThreadContext(threadId) {
|
|
389
|
+
return this.request(
|
|
390
|
+
"GET",
|
|
391
|
+
`/api/hub/threads/${threadId}/context`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get messages in a thread.
|
|
396
|
+
*/
|
|
397
|
+
async getMessages(threadId, _options) {
|
|
398
|
+
const result = await this.request("GET", `/api/hub/threads/${threadId}/messages`);
|
|
399
|
+
return unwrapList(result);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Link an entity to a thread so it appears in thread context for AI.
|
|
403
|
+
*/
|
|
404
|
+
async linkEntityToThread(threadId, entityId) {
|
|
405
|
+
const userId = await this.resolveUserId();
|
|
406
|
+
await this.request(
|
|
407
|
+
"POST",
|
|
408
|
+
`/api/hub/threads/${threadId}/link-entity`,
|
|
409
|
+
{ userId, entityId }
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Link a document to a thread.
|
|
414
|
+
*/
|
|
415
|
+
async linkDocumentToThread(threadId, documentId) {
|
|
416
|
+
const userId = await this.resolveUserId();
|
|
417
|
+
await this.request(
|
|
418
|
+
"POST",
|
|
419
|
+
`/api/hub/threads/${threadId}/link-document`,
|
|
420
|
+
{ userId, documentId }
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get research branches of a thread — parallel AI investigations.
|
|
425
|
+
*/
|
|
426
|
+
async getThreadBranches(threadId) {
|
|
427
|
+
const result = await this.request("GET", `/api/hub/threads/${threadId}/branches`);
|
|
428
|
+
return result.branches ?? [];
|
|
429
|
+
}
|
|
430
|
+
// ─── Memory ───────────────────────────────────────────────────────────────
|
|
431
|
+
async storeMemory(input) {
|
|
432
|
+
const userId = await this.resolveUserId();
|
|
433
|
+
const fact = input.context && String(input.context).trim().length > 0 ? `[${String(input.context).trim()}] ${input.fact}` : input.fact;
|
|
434
|
+
return this.request("POST", "/api/hub/memory", {
|
|
435
|
+
userId,
|
|
436
|
+
fact
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async recallMemory(query, options) {
|
|
440
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
441
|
+
const userId = await this.resolveUserId();
|
|
442
|
+
const params = new URLSearchParams({
|
|
443
|
+
userId,
|
|
444
|
+
query,
|
|
445
|
+
limit: String(options?.limit ?? 10)
|
|
446
|
+
});
|
|
447
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
448
|
+
const result = await this.request("GET", `/api/hub/memory?${params}`);
|
|
449
|
+
return unwrapList(result);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Delete a stored memory fact by ID.
|
|
453
|
+
*/
|
|
454
|
+
async deleteMemory(memoryId) {
|
|
455
|
+
const userId = await this.resolveUserId();
|
|
456
|
+
const params = new URLSearchParams({ userId });
|
|
457
|
+
await this.request(
|
|
458
|
+
"DELETE",
|
|
459
|
+
`/api/hub/memory/${memoryId}?${params}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
// ─── Channels (Hub REST: channels are listed via GET /threads) ────────────
|
|
463
|
+
async getChannels(options) {
|
|
464
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
465
|
+
const userId = await this.resolveUserId();
|
|
466
|
+
const params = new URLSearchParams({ userId });
|
|
467
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
468
|
+
const result = await this.request("GET", `/api/hub/threads?${params}`);
|
|
469
|
+
return unwrapList(result);
|
|
470
|
+
}
|
|
471
|
+
async sendToChannel(input) {
|
|
472
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
473
|
+
return this.request(
|
|
474
|
+
"POST",
|
|
475
|
+
`/api/hub/threads/${input.channelId}/messages`,
|
|
476
|
+
{
|
|
477
|
+
role: input.role ?? "user",
|
|
478
|
+
content: input.content,
|
|
479
|
+
userId,
|
|
480
|
+
...input.autoRespond !== void 0 ? { autoRespond: input.autoRespond } : {}
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
// ─── Proposals ────────────────────────────────────────────────────────────
|
|
485
|
+
/**
|
|
486
|
+
* List proposals — pending AI writes awaiting human review.
|
|
487
|
+
* Filter by status: "pending" (needs review), "approved", "rejected".
|
|
488
|
+
*/
|
|
489
|
+
async listProposals(options) {
|
|
490
|
+
const userId = await this.resolveUserId();
|
|
491
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
492
|
+
const params = new URLSearchParams({ userId });
|
|
493
|
+
if (options?.status) params.set("status", options.status);
|
|
494
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
495
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
496
|
+
const result = await this.request("GET", `/api/hub/proposals?${params}`);
|
|
497
|
+
return unwrapList(result);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Approve or reject a proposal.
|
|
501
|
+
*
|
|
502
|
+
* Note: The backend PATCH /proposals/:id is an AI-revision endpoint that updates
|
|
503
|
+
* the proposal data/summary, not a review (approve/reject) endpoint.
|
|
504
|
+
* Use this to update proposal data before human review.
|
|
505
|
+
*/
|
|
506
|
+
async reviewProposal(proposalId, decision, reason) {
|
|
507
|
+
return this.request(
|
|
508
|
+
"PATCH",
|
|
509
|
+
`/api/hub/proposals/${proposalId}`,
|
|
510
|
+
{
|
|
511
|
+
data: { status: decision },
|
|
512
|
+
summary: reason
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
// ─── Views ────────────────────────────────────────────────────────────────
|
|
517
|
+
/**
|
|
518
|
+
* List data views in a workspace.
|
|
519
|
+
*/
|
|
520
|
+
async listViews(workspaceId, options) {
|
|
521
|
+
const userId = await this.resolveUserId();
|
|
522
|
+
const params = new URLSearchParams({ userId, workspaceId });
|
|
523
|
+
if (options?.profileSlug) params.set("profileId", options.profileSlug);
|
|
524
|
+
const result = await this.request(
|
|
525
|
+
"GET",
|
|
526
|
+
`/api/hub/views?${params}`
|
|
527
|
+
);
|
|
528
|
+
return unwrapList(result);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Create a new view. Goes through governance.
|
|
532
|
+
*/
|
|
533
|
+
async createView(input) {
|
|
534
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
535
|
+
return this.request("POST", "/api/hub/views", {
|
|
536
|
+
userId,
|
|
537
|
+
workspaceId: input.workspaceId ?? this.workspaceId,
|
|
538
|
+
name: input.name,
|
|
539
|
+
type: input.type,
|
|
540
|
+
profileId: input.profileSlug,
|
|
541
|
+
config: input.config
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
// ─── Documents ────────────────────────────────────────────────────────────
|
|
545
|
+
/**
|
|
546
|
+
* Get a document by ID with full markdown content.
|
|
547
|
+
*/
|
|
548
|
+
async getDocument(documentId) {
|
|
549
|
+
const userId = await this.resolveUserId();
|
|
550
|
+
const params = new URLSearchParams({ userId });
|
|
551
|
+
return this.request(
|
|
552
|
+
"GET",
|
|
553
|
+
`/api/hub/documents/${documentId}?${params}`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Create a document. Use for long-form content: meeting notes, research, writeups.
|
|
558
|
+
* Goes through governance.
|
|
559
|
+
*/
|
|
560
|
+
async createDocument(input) {
|
|
561
|
+
const userId = await this.resolveUserId();
|
|
562
|
+
return this.request("POST", "/api/hub/documents", {
|
|
563
|
+
userId,
|
|
564
|
+
workspaceId: input.workspaceId ?? this.workspaceId,
|
|
565
|
+
title: input.title,
|
|
566
|
+
content: input.content ?? ""
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
// ─── Commands & Agents ────────────────────────────────────────────────────
|
|
570
|
+
/**
|
|
571
|
+
* List available commands (automation shortcuts) in the workspace.
|
|
572
|
+
*/
|
|
573
|
+
async listCommands(workspaceId) {
|
|
574
|
+
const wsId = workspaceId ?? this.workspaceId;
|
|
575
|
+
const params = new URLSearchParams();
|
|
576
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
577
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
578
|
+
const result = await this.request("GET", `/api/hub/commands${qs}`);
|
|
579
|
+
return unwrapList(result);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Execute a command by slug.
|
|
583
|
+
*
|
|
584
|
+
* Note: The backend POST /commands/execute uses a `command` field (the shell command
|
|
585
|
+
* string) and `userId`, not a `slug`. This maps ExecuteCommandInput.slug to `command`.
|
|
586
|
+
*/
|
|
587
|
+
async executeCommand(input) {
|
|
588
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
589
|
+
return this.request(
|
|
590
|
+
"POST",
|
|
591
|
+
"/api/hub/commands/execute",
|
|
592
|
+
{
|
|
593
|
+
command: input.slug,
|
|
594
|
+
userId,
|
|
595
|
+
workspaceId: input.workspaceId ?? this.workspaceId,
|
|
596
|
+
...input.parameters ? { parameters: input.parameters } : {}
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* List agent users provisioned in the workspace.
|
|
602
|
+
*/
|
|
603
|
+
async listAgentUsers(workspaceId) {
|
|
604
|
+
const wsId = workspaceId ?? this.workspaceId;
|
|
605
|
+
const params = new URLSearchParams();
|
|
606
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
607
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
608
|
+
const result = await this.request("GET", `/api/hub/agent-users${qs}`);
|
|
609
|
+
return unwrapList(result);
|
|
610
|
+
}
|
|
611
|
+
// ─── Proactive posting ────────────────────────────────────────────────────
|
|
612
|
+
/**
|
|
613
|
+
* Post a proactive message to the user's personal channel.
|
|
614
|
+
* For AI-initiated insights and summaries. Rate-limited: 3/hour, 10/day.
|
|
615
|
+
* proactiveType must be one of: insight, suggestion, alert, nudge,
|
|
616
|
+
* morning_briefing, weekly_digest, health_check.
|
|
617
|
+
*/
|
|
618
|
+
async postProactive(userId, content, options) {
|
|
619
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
620
|
+
if (!wsId) throw new Error("workspaceId is required for postProactive");
|
|
621
|
+
return this.request("POST", "/api/hub/proactive/post", {
|
|
622
|
+
userId,
|
|
623
|
+
workspaceId: wsId,
|
|
624
|
+
content,
|
|
625
|
+
proactiveType: options?.type ?? "insight"
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
// ─── Capture pipeline ─────────────────────────────────────────────────────
|
|
629
|
+
async captureStructure(input) {
|
|
630
|
+
const userId = await this.resolveUserId();
|
|
631
|
+
return this.request(
|
|
632
|
+
"POST",
|
|
633
|
+
"/api/hub/capture/structure",
|
|
634
|
+
{
|
|
635
|
+
userId,
|
|
636
|
+
text: input.text,
|
|
637
|
+
url: input.url,
|
|
638
|
+
workspaceId: input.workspaceId ?? this.workspaceId,
|
|
639
|
+
previousEntities: input.previousEntities
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
async captureExecute(input) {
|
|
644
|
+
const userId = await this.resolveUserId();
|
|
645
|
+
return this.request(
|
|
646
|
+
"POST",
|
|
647
|
+
"/api/hub/capture/execute",
|
|
648
|
+
{
|
|
649
|
+
userId,
|
|
650
|
+
entities: input.entities,
|
|
651
|
+
relations: input.relations ?? [],
|
|
652
|
+
workspaceId: input.workspaceId ?? this.workspaceId
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
// ─── Automations ───────────────────────────────────────────────────────────
|
|
657
|
+
/**
|
|
658
|
+
* List automations for the current user, optionally filtered by workspace and status.
|
|
659
|
+
*/
|
|
660
|
+
async listAutomations(options) {
|
|
661
|
+
const userId = await this.resolveUserId();
|
|
662
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
663
|
+
const params = new URLSearchParams({ userId });
|
|
664
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
665
|
+
if (options?.status) params.set("status", options.status);
|
|
666
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
667
|
+
const result = await this.request("GET", `/api/hub/automations?${params}`);
|
|
668
|
+
return unwrapList(result);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Get a single automation by ID.
|
|
672
|
+
*/
|
|
673
|
+
async getAutomation(automationId, options) {
|
|
674
|
+
const userId = await this.resolveUserId();
|
|
675
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
676
|
+
const params = new URLSearchParams({ userId });
|
|
677
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
678
|
+
return this.request(
|
|
679
|
+
"GET",
|
|
680
|
+
`/api/hub/automations/${automationId}?${params}`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Create an automation. Defaults to status=draft.
|
|
685
|
+
* Use activateAutomation() to enable it.
|
|
686
|
+
*/
|
|
687
|
+
async createAutomation(input) {
|
|
688
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
689
|
+
const wsId = input.workspaceId ?? this.workspaceId;
|
|
690
|
+
return this.request("POST", "/api/hub/automations/create", {
|
|
691
|
+
...input,
|
|
692
|
+
userId,
|
|
693
|
+
workspaceId: wsId ?? null
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Update an automation's definition or metadata.
|
|
698
|
+
*/
|
|
699
|
+
async updateAutomation(automationId, input) {
|
|
700
|
+
const userId = input.userId ?? await this.resolveUserId();
|
|
701
|
+
const wsId = input.workspaceId ?? this.workspaceId;
|
|
702
|
+
if (!wsId) throw new Error("workspaceId is required for updateAutomation");
|
|
703
|
+
return this.request(
|
|
704
|
+
"PATCH",
|
|
705
|
+
`/api/hub/automations/${automationId}`,
|
|
706
|
+
{ ...input, userId, workspaceId: wsId }
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Manually trigger an automation once with an optional payload.
|
|
711
|
+
* Bypasses the automation's normal trigger config.
|
|
712
|
+
*/
|
|
713
|
+
async triggerAutomation(automationId, options) {
|
|
714
|
+
const userId = await this.resolveUserId();
|
|
715
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
716
|
+
return this.request(
|
|
717
|
+
"POST",
|
|
718
|
+
`/api/hub/automations/${automationId}/trigger`,
|
|
719
|
+
{ userId, workspaceId: wsId ?? null, payload: options?.payload }
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Activate a draft or paused automation (sets status=active).
|
|
724
|
+
*/
|
|
725
|
+
async activateAutomation(automationId, options) {
|
|
726
|
+
const userId = await this.resolveUserId();
|
|
727
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
728
|
+
if (!wsId)
|
|
729
|
+
throw new Error("workspaceId is required for activateAutomation");
|
|
730
|
+
return this.request(
|
|
731
|
+
"POST",
|
|
732
|
+
`/api/hub/automations/${automationId}/activate`,
|
|
733
|
+
{ userId, workspaceId: wsId }
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Pause an active automation (sets status=paused).
|
|
738
|
+
*/
|
|
739
|
+
async pauseAutomation(automationId, options) {
|
|
740
|
+
const userId = await this.resolveUserId();
|
|
741
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
742
|
+
if (!wsId) throw new Error("workspaceId is required for pauseAutomation");
|
|
743
|
+
return this.request(
|
|
744
|
+
"POST",
|
|
745
|
+
`/api/hub/automations/${automationId}/pause`,
|
|
746
|
+
{ userId, workspaceId: wsId }
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
// ─── Subscriptions / Reactions (Pulse) ────────────────────────────────────
|
|
750
|
+
/**
|
|
751
|
+
* List the user-wide Pulse feed — the timestamp-sorted union of reactive events.
|
|
752
|
+
* Call getSubscriptionFanout() on an individual event for its dense reactions[].
|
|
753
|
+
*/
|
|
754
|
+
async listSubscriptions(options) {
|
|
755
|
+
const wsId = options?.workspaceId ?? this.workspaceId;
|
|
756
|
+
const params = new URLSearchParams();
|
|
757
|
+
if (wsId) params.set("workspaceId", wsId);
|
|
758
|
+
if (options?.kind) params.set("kind", options.kind);
|
|
759
|
+
if (options?.eventType) params.set("eventType", options.eventType);
|
|
760
|
+
if (options?.lens) params.set("lens", options.lens);
|
|
761
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
762
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
763
|
+
const result = await this.request("GET", `/api/hub/subscriptions${qs}`);
|
|
764
|
+
return unwrapList(result);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get the reaction fan-out for a single event — full reactions[] populated.
|
|
768
|
+
*/
|
|
769
|
+
async getSubscriptionFanout(eventId, options) {
|
|
770
|
+
const params = new URLSearchParams();
|
|
771
|
+
if (options?.lens) params.set("lens", options.lens);
|
|
772
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
773
|
+
return this.request(
|
|
774
|
+
"GET",
|
|
775
|
+
`/api/hub/subscriptions/${eventId}/fanout${qs}`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
// ─── Notifications ─────────────────────────────────────────────────────────
|
|
779
|
+
/**
|
|
780
|
+
* Persist a notification and emit notification:new to the frontend.
|
|
781
|
+
* Use for IS-originated events (skill.triggered, agent actions, etc.).
|
|
782
|
+
* Backend-originated notifications (vault, proposals) use NotificationService directly.
|
|
783
|
+
*/
|
|
784
|
+
async createNotification(input) {
|
|
785
|
+
return this.request(
|
|
786
|
+
"POST",
|
|
787
|
+
"/api/hub/notifications",
|
|
788
|
+
input
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
// ─── Webhooks ──────────────────────────────────────────────────────────────
|
|
792
|
+
/**
|
|
793
|
+
* List delivery log for a webhook subscription.
|
|
794
|
+
* Powers the Reactions Health tab and replay flows.
|
|
795
|
+
*/
|
|
796
|
+
async getWebhookDeliveries(subscriptionId, options) {
|
|
797
|
+
const params = new URLSearchParams();
|
|
798
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
799
|
+
const qs = params.toString() ? `?${params}` : "";
|
|
800
|
+
const result = await this.request("GET", `/api/hub/webhooks/${subscriptionId}/deliveries${qs}`);
|
|
801
|
+
return unwrapList(result);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/setup.ts
|
|
806
|
+
function normalizeUrl2(url) {
|
|
807
|
+
return url.replace(/\/$/, "");
|
|
808
|
+
}
|
|
809
|
+
async function checkPodHealth(podUrl) {
|
|
810
|
+
const url = normalizeUrl2(podUrl);
|
|
811
|
+
const status = { url, healthy: false };
|
|
812
|
+
try {
|
|
813
|
+
const res = await fetch(`${url}/health`, {
|
|
814
|
+
signal: AbortSignal.timeout(5e3)
|
|
815
|
+
});
|
|
816
|
+
if (res.ok) {
|
|
817
|
+
status.healthy = true;
|
|
818
|
+
const data = await res.json();
|
|
819
|
+
status.version = data.version;
|
|
820
|
+
}
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
return status;
|
|
824
|
+
}
|
|
825
|
+
async function setupAgent(podUrl, provisioningToken, agentType = "openclaw") {
|
|
826
|
+
const url = normalizeUrl2(podUrl);
|
|
827
|
+
const res = await fetch(`${url}/api/hub/setup/agent`, {
|
|
828
|
+
method: "POST",
|
|
829
|
+
headers: {
|
|
830
|
+
"Content-Type": "application/json",
|
|
831
|
+
Authorization: `Bearer ${provisioningToken}`
|
|
832
|
+
},
|
|
833
|
+
body: JSON.stringify({ agentType }),
|
|
834
|
+
signal: AbortSignal.timeout(15e3)
|
|
835
|
+
});
|
|
836
|
+
if (!res.ok) {
|
|
837
|
+
const body = await res.json().catch(() => ({}));
|
|
838
|
+
throw new HubApiError(
|
|
839
|
+
`Agent setup failed (HTTP ${res.status})`,
|
|
840
|
+
res.status,
|
|
841
|
+
body
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
return res.json();
|
|
845
|
+
}
|
|
846
|
+
export {
|
|
847
|
+
HubApiError,
|
|
848
|
+
HubRestClient,
|
|
849
|
+
checkPodHealth,
|
|
850
|
+
setupAgent
|
|
851
|
+
};
|