@wakeshift/sdk 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +165 -0
- package/README.md +76 -50
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +91 -51
- package/dist/events.js +178 -72
- package/dist/events.js.map +1 -1
- package/dist/harness.js +1 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +0 -5
- package/dist/key.js +9 -5
- package/dist/key.js.map +1 -1
- package/dist/retry.js +1 -1
- package/dist/retry.js.map +1 -1
- package/dist/spec.d.ts +2 -2
- package/dist/spec.js +2 -2
- package/dist/spec.js.map +1 -1
- package/dist/toolcall.d.ts +154 -0
- package/dist/toolcall.js +558 -0
- package/dist/toolcall.js.map +1 -0
- package/dist/wakeshift.d.ts +81 -256
- package/dist/wakeshift.js +167 -244
- package/dist/wakeshift.js.map +1 -1
- package/package.json +16 -2
package/dist/wakeshift.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { WakeshiftError } from "./errors.js";
|
|
3
2
|
import { WakeEvent, attachEvents } from "./events.js";
|
|
4
|
-
import { parseKey, signAssertion } from "./key.js";
|
|
3
|
+
import { parseKey, signAssertion, isTestKey } from "./key.js";
|
|
5
4
|
import { retryRefused } from "./retry.js";
|
|
6
5
|
/** Wakeshift's production core, the default for every key but an older wst_test_ one. Override with `core` or WAKESHIFT_CORE. */
|
|
7
6
|
export const DEFAULT_CORE = "https://app.wakeshift.ai";
|
|
@@ -14,19 +13,25 @@ export const HARNESS_ARG = "harness";
|
|
|
14
13
|
* when its program gives it one (Cursor and Grok Bot: CURSOR_CONVERSATION_ID).
|
|
15
14
|
*/
|
|
16
15
|
export const CONVERSATION_ARG = "conversation_id";
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
// recipientsOf reads an emit's unmatched or ended list: "user",
|
|
17
|
+
// { native: user } or { user, subscription }.
|
|
18
|
+
function recipientsOf(raw) {
|
|
19
|
+
if (!Array.isArray(raw))
|
|
20
|
+
return [];
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const r of raw) {
|
|
23
|
+
if (typeof r === "string")
|
|
24
|
+
out.push(r);
|
|
25
|
+
else if (r && typeof r === "object") {
|
|
26
|
+
const m = r;
|
|
27
|
+
if (typeof m.native === "string" && m.native)
|
|
28
|
+
out.push({ native: m.native });
|
|
29
|
+
else if (typeof m.user === "string")
|
|
30
|
+
out.push({ user: m.user, subscription: typeof m.subscription === "string" ? m.subscription : "" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
30
35
|
// userAgentOf is the User-Agent of the HTTP request a tool call came in
|
|
31
36
|
// ("" if there's none, e.g. over stdio).
|
|
32
37
|
function userAgentOf(extra) {
|
|
@@ -36,45 +41,51 @@ function userAgentOf(extra) {
|
|
|
36
41
|
const v = headers["user-agent"] ?? headers["User-Agent"];
|
|
37
42
|
return (Array.isArray(v) ? v[0] : v) ?? "";
|
|
38
43
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
out =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
out._meta = { ...(out._meta ?? {}), ...a.meta };
|
|
44
|
+
/** @internal Packs a tool call as Wakeshift takes it: empty fields left out, the agent's claims trimmed. */
|
|
45
|
+
export function packCall(c) {
|
|
46
|
+
const out = { client: c.clientName ?? "" };
|
|
47
|
+
if (c.user)
|
|
48
|
+
out.user = c.user;
|
|
49
|
+
if (c.meta && Object.keys(c.meta).length > 0)
|
|
50
|
+
out.meta = c.meta;
|
|
51
|
+
if (c.session)
|
|
52
|
+
out.session = c.session;
|
|
53
|
+
if (c.userAgent)
|
|
54
|
+
out.user_agent = c.userAgent;
|
|
55
|
+
if (c.harness?.trim())
|
|
56
|
+
out.harness = c.harness.trim();
|
|
57
|
+
if (c.conversation?.trim())
|
|
58
|
+
out.conversation = c.conversation.trim();
|
|
59
|
+
if (c.tool?.trim())
|
|
60
|
+
out.tool = c.tool.trim();
|
|
57
61
|
return out;
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* @internal The call to one of attach's own tools (subscribe_events and the
|
|
65
|
+
* others): its _meta and User-Agent, the client's name, the user, and
|
|
66
|
+
* session_key, harness and conversation_id from its arguments.
|
|
67
|
+
*/
|
|
68
|
+
export function toolCallOf(extra, client, user, args, tool) {
|
|
69
|
+
return packCall({
|
|
70
|
+
meta: extra?._meta,
|
|
71
|
+
clientName: client,
|
|
72
|
+
user,
|
|
73
|
+
session: str(args, SESSION_ARG),
|
|
74
|
+
userAgent: userAgentOf(extra),
|
|
75
|
+
harness: str(args, HARNESS_ARG),
|
|
76
|
+
conversation: str(args, CONVERSATION_ARG),
|
|
77
|
+
tool,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
64
80
|
// str is a tool argument's value if it's a string.
|
|
65
81
|
function str(args, name) {
|
|
66
82
|
return typeof args?.[name] === "string" ? args[name] : undefined;
|
|
67
83
|
}
|
|
68
84
|
/**
|
|
69
|
-
* The Wakeshift client for an app:
|
|
70
|
-
*
|
|
85
|
+
* The Wakeshift client for an app: define the events your app offers,
|
|
86
|
+
* attach them to your MCP server, and emit them when they happen.
|
|
71
87
|
*/
|
|
72
88
|
export class Wakeshift {
|
|
73
|
-
/**
|
|
74
|
-
* An older key's mode (wst_test_…, wst_live_…); null for a current key.
|
|
75
|
-
* @deprecated Modes mean nothing now, and current keys have none.
|
|
76
|
-
*/
|
|
77
|
-
mode;
|
|
78
89
|
core;
|
|
79
90
|
key;
|
|
80
91
|
// The app's ID: from the key, if it names the app, or else from app().
|
|
@@ -89,10 +100,9 @@ export class Wakeshift {
|
|
|
89
100
|
throw new Error("wakeshift: no app key (WAKESHIFT_KEY?)");
|
|
90
101
|
this.key = parseKey(opts.key);
|
|
91
102
|
this.knownAppId = this.key.appId;
|
|
92
|
-
this.mode = this.key.mode;
|
|
93
103
|
// Test keys come from staging and local consoles, and one sent to
|
|
94
104
|
// production fails in a way that's hard to read: ask for the core.
|
|
95
|
-
const core = opts.core || process.env.WAKESHIFT_CORE || (
|
|
105
|
+
const core = opts.core || process.env.WAKESHIFT_CORE || (isTestKey(opts.key) ? "" : DEFAULT_CORE);
|
|
96
106
|
if (!core) {
|
|
97
107
|
throw new Error("wakeshift: no core URL for this test key: set WAKESHIFT_CORE, or pass { core }, to the address of the console the key came from (for example https://app.staging.wakeshift.ai)");
|
|
98
108
|
}
|
|
@@ -113,7 +123,8 @@ export class Wakeshift {
|
|
|
113
123
|
return this.knownAppId;
|
|
114
124
|
}
|
|
115
125
|
/**
|
|
116
|
-
* Which app the key is, as Wakeshift says: its ID and name
|
|
126
|
+
* Which app the key is, as Wakeshift says: its ID and name, and the
|
|
127
|
+
* key's. Asks once
|
|
117
128
|
* (GET /api/apps/v1/app) and remembers the answer; afterwards
|
|
118
129
|
* `wake.appId` works for any key.
|
|
119
130
|
*/
|
|
@@ -125,7 +136,8 @@ export class Wakeshift {
|
|
|
125
136
|
}
|
|
126
137
|
const id = this.knownAppId ?? r.id;
|
|
127
138
|
this.knownAppId = id;
|
|
128
|
-
|
|
139
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
140
|
+
return { id, name: str(r.name), key: { id: str(r.key?.key_id), name: str(r.key?.name) } };
|
|
129
141
|
})
|
|
130
142
|
.catch((err) => {
|
|
131
143
|
this.appInfo = undefined; // a later call asks again
|
|
@@ -145,205 +157,105 @@ export class Wakeshift {
|
|
|
145
157
|
warn(message) {
|
|
146
158
|
this.warnFn(message);
|
|
147
159
|
}
|
|
148
|
-
/** Reports a warning, once per kind. */
|
|
160
|
+
/** @internal Reports a warning, once per kind. */
|
|
149
161
|
warnOnce(kind, message) {
|
|
150
162
|
if (this.warned.has(kind))
|
|
151
163
|
return;
|
|
152
164
|
this.warned.add(kind);
|
|
153
165
|
this.warnFn(message);
|
|
154
166
|
}
|
|
155
|
-
/**
|
|
156
|
-
* The arguments the agent passes Wakeshift's part of the call in, as a
|
|
157
|
-
* zod shape for your subscribe tool's input (and any other tool whose
|
|
158
|
-
* results you annotate): `inputSchema: { repo: z.string(), ...wake.args }`.
|
|
159
|
-
* session_key (the key a result gives the conversation), harness (the
|
|
160
|
-
* program the agent runs in) and conversation_id (its own ID for the
|
|
161
|
-
* conversation, where its program gives one). Wakeshift adds nothing to
|
|
162
|
-
* tools you don't spread it into.
|
|
163
|
-
*/
|
|
164
|
-
get args() {
|
|
165
|
-
return argsShape;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Packs the tool call from an MCP tool callback's `extra`: its _meta and
|
|
169
|
-
* User-Agent, the client's name (from `server`, or pass `client`), the
|
|
170
|
-
* user, and session_key, harness and conversation_id from `args`.
|
|
171
|
-
*/
|
|
172
|
-
call(extra, opts = {}) {
|
|
173
|
-
let client = opts.client;
|
|
174
|
-
if (client === undefined) {
|
|
175
|
-
if (!opts.server)
|
|
176
|
-
throw new Error("wakeshift: wake.call needs the MCP client's name: pass { server } (your McpServer), or { client }");
|
|
177
|
-
client = clientNameOf(opts.server);
|
|
178
|
-
}
|
|
179
|
-
const args = opts.args;
|
|
180
|
-
return this.callOf({
|
|
181
|
-
meta: extra?._meta,
|
|
182
|
-
clientName: client,
|
|
183
|
-
user: opts.user,
|
|
184
|
-
session: opts.session ?? str(args, SESSION_ARG),
|
|
185
|
-
userAgent: userAgentOf(extra),
|
|
186
|
-
harness: opts.harness ?? str(args, HARNESS_ARG),
|
|
187
|
-
conversation: opts.conversation ?? str(args, CONVERSATION_ARG),
|
|
188
|
-
tool: opts.tool,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
/** Packs a tool call without an MCP framework. */
|
|
192
|
-
callOf(c) {
|
|
193
|
-
const out = { client: c.clientName ?? "" };
|
|
194
|
-
if (c.user)
|
|
195
|
-
out.user = c.user;
|
|
196
|
-
if (c.meta && Object.keys(c.meta).length > 0)
|
|
197
|
-
out.meta = c.meta;
|
|
198
|
-
if (c.session)
|
|
199
|
-
out.session = c.session;
|
|
200
|
-
if (c.userAgent)
|
|
201
|
-
out.user_agent = c.userAgent;
|
|
202
|
-
if (c.harness?.trim())
|
|
203
|
-
out.harness = c.harness.trim();
|
|
204
|
-
if (c.conversation?.trim())
|
|
205
|
-
out.conversation = c.conversation.trim();
|
|
206
|
-
if (c.tool?.trim())
|
|
207
|
-
out.tool = c.tool.trim();
|
|
208
|
-
return out;
|
|
209
|
-
}
|
|
210
|
-
// recognised reads what the core recognised the caller as, and warns
|
|
211
|
-
// once if it was nothing and the agent didn't say (an older core that
|
|
212
|
-
// doesn't say: no warning).
|
|
213
|
-
recognised(agent) {
|
|
214
|
-
if (agent && !agent.harness && !agent.claimed)
|
|
215
|
-
this.warnOnce("unknown_client", UNKNOWN_CLIENT_WARNING);
|
|
216
|
-
return { harness: agent?.harness ?? null, via: agent?.via ?? null, claimed: agent?.claimed ?? null };
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Subscribes the calling conversation to topic (again: refreshes it).
|
|
220
|
-
* Check the caller may see the topic first: Wakeshift only refuses another
|
|
221
|
-
* user's u/<user>/… topics.
|
|
222
|
-
*/
|
|
223
|
-
async subscribe(call, topic, opts = {}) {
|
|
224
|
-
const body = { call, topic, once: !!opts.once };
|
|
225
|
-
if (opts.filter && Object.keys(opts.filter).length > 0)
|
|
226
|
-
body.filter = opts.filter;
|
|
227
|
-
if (opts.replay !== undefined)
|
|
228
|
-
body.replay = opts.replay;
|
|
229
|
-
if (opts.ttl !== undefined)
|
|
230
|
-
body.ttl = opts.ttl;
|
|
231
|
-
const out = await this.request("POST", "/topics/subscribe", body);
|
|
232
|
-
const agent = this.recognised(out.agent);
|
|
233
|
-
const addition = out.addition ?? {};
|
|
234
|
-
const s = out.subscription;
|
|
235
|
-
return {
|
|
236
|
-
id: s.id,
|
|
237
|
-
topic: s.topic,
|
|
238
|
-
filter: s.filter,
|
|
239
|
-
once: s.once,
|
|
240
|
-
expiresAt: new Date(s.expires_at),
|
|
241
|
-
session: out.session,
|
|
242
|
-
addition,
|
|
243
|
-
agent,
|
|
244
|
-
result: (text) => applyAddition(addition, text),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/** Ends the conversation's subscription to topic, or to all the app's topics. Returns how many ended. */
|
|
248
|
-
async unsubscribe(call, topic) {
|
|
249
|
-
const out = await this.request("POST", "/topics/unsubscribe", topic ? { call, topic } : { call });
|
|
250
|
-
return out.unsubscribed;
|
|
251
|
-
}
|
|
252
|
-
/** Wakes every conversation subscribed to one of topics whose filter the event passes (each once). */
|
|
253
|
-
async publish(topics, event) {
|
|
254
|
-
const out = await this.request("POST", "/topics/publish", { topics: typeof topics === "string" ? [topics] : topics, event });
|
|
255
|
-
return { matched: out.matched, duplicate: !!out.duplicate };
|
|
256
|
-
}
|
|
257
|
-
/** Wakes exactly one subscription's conversation, whatever its filter. */
|
|
258
|
-
async send(subscriptionId, event) {
|
|
259
|
-
const out = await this.request("POST", `/subscriptions/${encodeURIComponent(subscriptionId)}/send`, { event });
|
|
260
|
-
return out.status;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* How many conversations are subscribed to topic, and how many are
|
|
264
|
-
* connected now, and how. Only reads: nothing is delivered, minted or ended.
|
|
265
|
-
*/
|
|
266
|
-
async topicStatus(topic) {
|
|
267
|
-
const out = await this.request("GET", `/topics/status?topic=${encodeURIComponent(topic)}`);
|
|
268
|
-
return { topic: out.topic, subscriptions: out.subscriptions, connected: out.connected, via: { stream: 0, poll: 0, hooks: 0, webhook: 0, ...out.via } };
|
|
269
|
-
}
|
|
270
|
-
/** Whether a subscription is active and its conversation connected now, and how. Only reads. */
|
|
271
|
-
async subscriptionStatus(subscriptionId) {
|
|
272
|
-
const out = await this.request("GET", `/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
273
|
-
const s = { id: out.id, topic: out.topic, status: out.status, connected: out.connected };
|
|
274
|
-
if (out.via)
|
|
275
|
-
s.via = out.via;
|
|
276
|
-
if (out.expires_at)
|
|
277
|
-
s.expiresAt = new Date(out.expires_at);
|
|
278
|
-
return s;
|
|
279
|
-
}
|
|
280
|
-
/** A capability URL that publishes to topic: a POST's body becomes an event. */
|
|
281
|
-
async publishUrl(topic) {
|
|
282
|
-
return this.request("POST", "/topics/publish-urls", { topic });
|
|
283
|
-
}
|
|
284
|
-
/** Stops a publish URL working, a topic's or an event's (410 from then on). */
|
|
167
|
+
/** Stops a publish URL working (410 from then on). */
|
|
285
168
|
async revokePublishUrl(id) {
|
|
286
169
|
await this.request("DELETE", `/events/publish-urls/${encodeURIComponent(id)}`);
|
|
287
170
|
}
|
|
288
|
-
/** What Wakeshift would add to any other tool result for this conversation (creates nothing). */
|
|
289
|
-
async addition(call) {
|
|
290
|
-
const out = await this.request("POST", "/topics/annotate", { call });
|
|
291
|
-
return { ...out, agent: this.recognised(out.agent) };
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Adds what Wakeshift has for the calling conversation to a tool result
|
|
295
|
-
* of yours (a copy): what's waiting for a harness that can't be woken, a
|
|
296
|
-
* fresh connector beacon while a Codex thread's connector isn't running.
|
|
297
|
-
* Only results you pass here (and a subscription's result()) change.
|
|
298
|
-
*/
|
|
299
|
-
async annotate(call, result) {
|
|
300
|
-
return applyAddition(await this.addition(call), result);
|
|
301
|
-
}
|
|
302
|
-
/** Ends every subscription to topic; each conversation is told. Returns how many ended. */
|
|
303
|
-
async closeTopic(topic) {
|
|
304
|
-
return (await this.request("POST", "/topics/close", { topic })).closed;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Ends one subscription by its id, telling its conversation that the app
|
|
308
|
-
* ended it. Returns false if it had already ended (or there's no such
|
|
309
|
-
* subscription of the app's).
|
|
310
|
-
*/
|
|
311
|
-
async endSubscription(subscriptionId) {
|
|
312
|
-
return (await this.request("POST", `/subscriptions/${encodeURIComponent(subscriptionId)}/end`, {})).ended;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Ends the subscriptions of every conversation of one of the app's users
|
|
316
|
-
* to topic, or to all of the app's topics (they lost access to something),
|
|
317
|
-
* telling each conversation. Returns how many ended.
|
|
318
|
-
*/
|
|
319
|
-
async unsubscribeUser(user, topic) {
|
|
320
|
-
return (await this.request("POST", "/topics/unsubscribe", topic ? { user, topic } : { user })).unsubscribed;
|
|
321
|
-
}
|
|
322
171
|
/**
|
|
323
172
|
* Deletes everything Wakeshift keeps for one of the app's users: their
|
|
324
|
-
* conversations, subscriptions, events and deliveries,
|
|
325
|
-
* their
|
|
326
|
-
* streams close. Returns false if there was nothing to forget.
|
|
173
|
+
* conversations, subscriptions, events and deliveries, publish URLs for
|
|
174
|
+
* them, and their ID. Open streams close. Returns false if there was nothing to forget.
|
|
327
175
|
*/
|
|
328
176
|
async forgetUser(user) {
|
|
329
177
|
return (await this.request("POST", "/users/forget", { user })).forgotten;
|
|
330
178
|
}
|
|
331
179
|
/**
|
|
332
|
-
* Tells Wakeshift what the app may do, replacing what it said before
|
|
333
|
-
*
|
|
334
|
-
*
|
|
180
|
+
* Tells Wakeshift what the app may do, replacing what it said before: m,
|
|
181
|
+
* with the events you defined (defineEvent) after m's own events. An
|
|
182
|
+
* event both defined and listed in m.events is an error, and nothing is
|
|
183
|
+
* sent. You needn't call it: a process that serves your tools (attach)
|
|
184
|
+
* sends the manifest automatically, merged into the one Wakeshift has.
|
|
185
|
+
* Call it to remove an event or clear a setting. Once called, m's
|
|
186
|
+
* settings are what every automatic send carries (even if this one
|
|
187
|
+
* failed), with the events defined by then. Returns the manifest as
|
|
188
|
+
* Wakeshift keeps it.
|
|
335
189
|
*/
|
|
336
190
|
async publishManifest(m) {
|
|
337
|
-
const
|
|
338
|
-
const
|
|
191
|
+
const version = this.manifestVersion; // before the body: a later defineEvent is sent again
|
|
192
|
+
const body = this.manifestBody(m);
|
|
339
193
|
this.manifestBase = m;
|
|
340
|
-
this.
|
|
341
|
-
|
|
194
|
+
const run = this.serial(() => this.request("PUT", "/manifest", body));
|
|
195
|
+
this.track(run, version);
|
|
196
|
+
return run;
|
|
197
|
+
}
|
|
198
|
+
// serial runs a send after every send before it, explicit or automatic,
|
|
199
|
+
// so the last manifest Wakeshift gets is the newest.
|
|
200
|
+
sending = Promise.resolve();
|
|
201
|
+
serial(send) {
|
|
202
|
+
const run = this.sending.then(send, send);
|
|
203
|
+
this.sending = run.catch(() => undefined);
|
|
204
|
+
return run;
|
|
205
|
+
}
|
|
206
|
+
// track makes a send the one event calls wait for: once it's done,
|
|
207
|
+
// Wakeshift has the manifest for the events defined at version, unless it
|
|
208
|
+
// failed (the next call sends again) or an event was defined since.
|
|
209
|
+
track(send, version) {
|
|
210
|
+
const p = send.then(() => {
|
|
211
|
+
if (this.manifestVersion !== version && this.published === p)
|
|
212
|
+
this.published = null;
|
|
213
|
+
});
|
|
214
|
+
this.published = p;
|
|
215
|
+
p.catch(() => {
|
|
216
|
+
if (this.published === p)
|
|
217
|
+
this.published = null;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// The manifest as Wakeshift gets it: m's events, then the defined ones,
|
|
221
|
+
// refusing a name in both.
|
|
222
|
+
manifestBody(m) {
|
|
223
|
+
const listed = m.events ?? [];
|
|
224
|
+
for (const name of this.events.keys()) {
|
|
225
|
+
if (listed.some((e) => e.name === name)) {
|
|
226
|
+
throw new Error(`wakeshift: event ${name} is defined with defineEvent and listed in the manifest's events: keep one`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const events = [...listed, ...[...this.events.values()].map((e) => e.manifest)];
|
|
230
|
+
const { events: _listed, ...rest } = m;
|
|
231
|
+
return events.length > 0 ? { ...rest, events } : rest;
|
|
232
|
+
}
|
|
233
|
+
// The automatic send, from a process that serves the app's tools: the
|
|
234
|
+
// settings the app last published (only those it set), with the defined
|
|
235
|
+
// events, merged into the manifest Wakeshift has, so it never removes an
|
|
236
|
+
// event or clears a setting another process sent.
|
|
237
|
+
async sendManifest() {
|
|
238
|
+
const body = {};
|
|
239
|
+
for (const [k, v] of Object.entries(this.manifestBody(this.manifestBase ?? {}))) {
|
|
240
|
+
if (v === undefined || v === null || v === "" || v === false || (Array.isArray(v) && v.length === 0))
|
|
241
|
+
continue;
|
|
242
|
+
body[k] = v;
|
|
243
|
+
}
|
|
244
|
+
if (!body.events)
|
|
245
|
+
return; // nothing to add (attach refuses with no events, but never send an empty list)
|
|
246
|
+
await this.request("POST", "/manifest/merge", body);
|
|
342
247
|
}
|
|
343
248
|
// ── Events (MCP Events, natively or bridged) ──────────────────────────
|
|
344
249
|
events = new Map();
|
|
345
|
-
|
|
250
|
+
// What publishManifest was last given (null: nothing yet); whether this
|
|
251
|
+
// process serves the app's tools (attach: only then is the manifest sent
|
|
252
|
+
// automatically); whether Wakeshift has the manifest for the events
|
|
253
|
+
// defined now (the send; null: not yet, or a defineEvent since); and how
|
|
254
|
+
// many defineEvents there were.
|
|
255
|
+
manifestBase = null;
|
|
256
|
+
serving = false;
|
|
346
257
|
published = null;
|
|
258
|
+
manifestVersion = 0;
|
|
347
259
|
/**
|
|
348
260
|
* Defines one of the app's event types: what a subscriber picks (args),
|
|
349
261
|
* what each occurrence carries (payload), and the line an agent reads
|
|
@@ -363,7 +275,8 @@ export class Wakeshift {
|
|
|
363
275
|
throw new Error(`wakeshift: event ${name} is defined twice`);
|
|
364
276
|
const ev = new WakeEvent(this, name, opts);
|
|
365
277
|
this.events.set(name, ev);
|
|
366
|
-
this.published = null; // the manifest changed
|
|
278
|
+
this.published = null; // the manifest changed: sent again when next needed
|
|
279
|
+
this.manifestVersion++;
|
|
367
280
|
return ev;
|
|
368
281
|
}
|
|
369
282
|
/**
|
|
@@ -375,21 +288,28 @@ export class Wakeshift {
|
|
|
375
288
|
* unsubscribe_events and list_subscriptions for every client, so a person
|
|
376
289
|
* can always see and stop what they're subscribed to.
|
|
377
290
|
*
|
|
378
|
-
* wake.attach(server, { identify: (extra) => ({ user: myUser(extra) }) })
|
|
291
|
+
* const attached = wake.attach(server, { identify: (extra) => ({ user: myUser(extra) }) })
|
|
292
|
+
*
|
|
293
|
+
* It returns the handle your own tools use to subscribe the conversation
|
|
294
|
+
* calling them: wrap a tool's callback with attached.events, and give it
|
|
295
|
+
* Wakeshift's arguments with attached.inputs.
|
|
379
296
|
*/
|
|
380
297
|
attach(server, opts = {}) {
|
|
381
|
-
attachEvents(this, server, [...this.events.values()], opts);
|
|
298
|
+
const attached = attachEvents(this, server, [...this.events.values()], opts);
|
|
299
|
+
this.serving = true; // the manifest is sent automatically from now on
|
|
300
|
+
return attached;
|
|
382
301
|
}
|
|
383
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* @internal Sends the manifest with the defined events, once (again after
|
|
304
|
+
* a defineEvent), from a process that serves the app's tools (attach). A
|
|
305
|
+
* process that only emits never sends it: its events may be fewer, or
|
|
306
|
+
* older, than its server's.
|
|
307
|
+
*/
|
|
384
308
|
ensureEvents() {
|
|
385
|
-
if (!this.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (this.published === p)
|
|
390
|
-
this.published = null; // try again next time
|
|
391
|
-
});
|
|
392
|
-
}
|
|
309
|
+
if (!this.serving)
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
if (!this.published)
|
|
312
|
+
this.track(this.serial(() => this.sendManifest()), this.manifestVersion);
|
|
393
313
|
return this.published;
|
|
394
314
|
}
|
|
395
315
|
/** @internal An event's emit. */
|
|
@@ -401,7 +321,7 @@ export class Wakeshift {
|
|
|
401
321
|
if (e.to)
|
|
402
322
|
body.to = e.to;
|
|
403
323
|
const out = await this.request("POST", "/events/emit", body);
|
|
404
|
-
return { matched: out.matched, duplicate: !!out.duplicate };
|
|
324
|
+
return { matched: out.matched, duplicate: !!out.duplicate, unmatched: recipientsOf(out.unmatched), ended: recipientsOf(out.ended) };
|
|
405
325
|
}
|
|
406
326
|
/** @internal A call to the app API (the events machinery's), after the manifest with the defined events is published. */
|
|
407
327
|
async requestApp(method, path, body) {
|
|
@@ -411,8 +331,8 @@ export class Wakeshift {
|
|
|
411
331
|
// ── Users' workspaces ──────────────────────────────────────────────────
|
|
412
332
|
/**
|
|
413
333
|
* The workspace Wakeshift keeps for one of the app's users (user: the
|
|
414
|
-
* app's own ID for them, as
|
|
415
|
-
*
|
|
334
|
+
* app's own ID for them, as attach's identify gives it), created on first
|
|
335
|
+
* use. Wakeshift keeps that user's conversations there.
|
|
416
336
|
*/
|
|
417
337
|
async workspace(user, name) {
|
|
418
338
|
return (await this.request("POST", "/workspaces", { external_id: user, name })).workspace;
|
|
@@ -455,13 +375,16 @@ export class Wakeshift {
|
|
|
455
375
|
json = undefined;
|
|
456
376
|
}
|
|
457
377
|
if (!resp.ok) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
378
|
+
// Wakeshift's code, message and reason where it sent them as strings, else
|
|
379
|
+
// the body's start, else the status's text, as every SDK reads it (test/errors.json).
|
|
380
|
+
const e = json?.error !== null && typeof json?.error === "object" ? json.error : {};
|
|
381
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
382
|
+
throw new WakeshiftError(resp.status, str(e.code), str(e.message) || text.slice(0, 200) || resp.statusText, {
|
|
383
|
+
reason: str(e.reason) || undefined,
|
|
461
384
|
});
|
|
462
385
|
}
|
|
463
386
|
// A 2xx that isn't Wakeshift's JSON: a proxy's page, or a wrong core.
|
|
464
|
-
if (json === null || typeof json !== "object") {
|
|
387
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
465
388
|
throw new WakeshiftError(resp.status, "bad_response", `Wakeshift at ${this.core} answered ${resp.status} without a JSON object. Check that WAKESHIFT_CORE, or the core option, is the address of the console your key came from`);
|
|
466
389
|
}
|
|
467
390
|
return json;
|