substrattice 0.1.1 → 0.1.3
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/README.md +10 -4
- package/dist/bridge.js +130 -0
- package/dist/index.js +161 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,12 +61,18 @@ All env vars are optional — you can also pass `room`/`token`/`url` straight to
|
|
|
61
61
|
|
|
62
62
|
| Tool | Purpose |
|
|
63
63
|
|------|---------|
|
|
64
|
-
| `omni_connect` | Join/spin up a room as an agent. |
|
|
65
|
-
| `omni_wait_for_message` | Block for the next request
|
|
64
|
+
| `omni_connect` | Join/spin up a room as an agent. Accepts `role` (planner/coder/reviewer…) and a stable wake `key` so a reconnect reclaims your slot + queued work. |
|
|
65
|
+
| `omni_wait_for_message` | Block for the next live request; loop on it. |
|
|
66
66
|
| `omni_reply` | Send your answer back into the room. |
|
|
67
|
+
| `omni_inbox` | Your **async** inbox — open work left for you/your role to answer later (even when you were offline). |
|
|
68
|
+
| `omni_leave_handoff` | Leave async work for a teammate or another AI role to pick up later (linked to a repo/url/artifact). |
|
|
69
|
+
| `omni_resolve_handoff` | Progress/answer a handoff: in_progress / done (attach the result artifact) / cancelled. |
|
|
67
70
|
| `omni_share_artifact` | Publish work/results the room can see and keep. |
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
71
|
+
| `omni_edit_artifact` | Co-edit a shared artifact (collaborative coding); bumps its version, optimistic concurrency. |
|
|
72
|
+
| `omni_request_action` | Propose a governed connector action (e.g. github.open_pr) into the host approval queue. |
|
|
73
|
+
| `omni_stream` | Live-stream your sandbox activity (tool/terminal/progress) into the room. |
|
|
74
|
+
| `omni_upload` | Upload a real file as a downloadable artifact. |
|
|
75
|
+
| `omni_status` / `omni_history` / `omni_help` | Connection state · room audit feed · the full guide. |
|
|
70
76
|
|
|
71
77
|
## Links
|
|
72
78
|
|
package/dist/bridge.js
CHANGED
|
@@ -14,6 +14,7 @@ export class OmniBridge {
|
|
|
14
14
|
agentId = "";
|
|
15
15
|
room = "";
|
|
16
16
|
label = "";
|
|
17
|
+
role = "";
|
|
17
18
|
connected = false;
|
|
18
19
|
/** Whether the agent has completed the forced onboarding (read the room's
|
|
19
20
|
* skills/rules). The loop is gated on this so bots can't skip context. */
|
|
@@ -32,10 +33,17 @@ export class OmniBridge {
|
|
|
32
33
|
connect(opts) {
|
|
33
34
|
this.room = opts.room;
|
|
34
35
|
this.label = opts.label ?? "Claude Code";
|
|
36
|
+
this.role = opts.role ?? "";
|
|
35
37
|
this.httpUrl = opts.url.replace(/\/$/, "");
|
|
36
38
|
this.token = opts.token;
|
|
37
39
|
const wsBase = opts.url.replace(/^http/, "ws").replace(/\/$/, "");
|
|
38
40
|
const qs = new URLSearchParams({ room: opts.room, label: this.label });
|
|
41
|
+
if (opts.role)
|
|
42
|
+
qs.set("role", opts.role);
|
|
43
|
+
// Stable wake key: lets this runner reclaim its roster slot + drain queued
|
|
44
|
+
// work after a reconnect (the server also derives one from owner+role).
|
|
45
|
+
if (opts.key)
|
|
46
|
+
qs.set("key", opts.key);
|
|
39
47
|
if (opts.token)
|
|
40
48
|
qs.set("token", opts.token);
|
|
41
49
|
const url = `${wsBase}/agent?${qs.toString()}`;
|
|
@@ -198,6 +206,128 @@ export class OmniBridge {
|
|
|
198
206
|
const data = (await res.json());
|
|
199
207
|
return { id: data.artifact.id };
|
|
200
208
|
}
|
|
209
|
+
/** Fetch one artifact (e.g. to read the current version before co-editing). */
|
|
210
|
+
async getArtifact(artifactId) {
|
|
211
|
+
if (!this.httpUrl)
|
|
212
|
+
throw new Error("not connected");
|
|
213
|
+
const res = await fetch(`${this.httpUrl}/api/artifacts/${encodeURIComponent(artifactId)}`, {
|
|
214
|
+
headers: { cookie: `omni_session=${this.token}` },
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok)
|
|
217
|
+
return null;
|
|
218
|
+
const data = (await res.json());
|
|
219
|
+
return data.artifact;
|
|
220
|
+
}
|
|
221
|
+
/** Co-edit a shared artifact (collaborative coding). Bumps its version; pass
|
|
222
|
+
* `expectedVersion` for optimistic concurrency (409 if another edit landed). */
|
|
223
|
+
async editArtifact(input) {
|
|
224
|
+
if (!this.httpUrl)
|
|
225
|
+
throw new Error("not connected");
|
|
226
|
+
const body = {};
|
|
227
|
+
if (input.content !== undefined)
|
|
228
|
+
body.content = input.content;
|
|
229
|
+
if (input.title !== undefined)
|
|
230
|
+
body.title = input.title;
|
|
231
|
+
if (input.language !== undefined)
|
|
232
|
+
body.language = input.language;
|
|
233
|
+
if (input.expectedVersion !== undefined)
|
|
234
|
+
body.expectedVersion = input.expectedVersion;
|
|
235
|
+
const res = await fetch(`${this.httpUrl}/api/artifacts/${encodeURIComponent(input.id)}`, {
|
|
236
|
+
method: "PATCH",
|
|
237
|
+
headers: {
|
|
238
|
+
"content-type": "application/json",
|
|
239
|
+
"x-omni-csrf": "1",
|
|
240
|
+
cookie: `omni_session=${this.token}`,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(body),
|
|
243
|
+
});
|
|
244
|
+
if (!res.ok)
|
|
245
|
+
throw new Error(`edit failed: HTTP ${res.status} ${await res.text()}`);
|
|
246
|
+
const data = (await res.json());
|
|
247
|
+
return { id: data.artifact.id, version: data.artifact.version };
|
|
248
|
+
}
|
|
249
|
+
/** Your async **inbox**: open work left in this room addressed to your role (or
|
|
250
|
+
* to "any" AI), waiting to be answered. This is how you "respond later". */
|
|
251
|
+
async inbox() {
|
|
252
|
+
if (!this.httpUrl || !this.room)
|
|
253
|
+
return [];
|
|
254
|
+
const roleQ = this.role ? `&toRole=${encodeURIComponent(this.role)}` : "";
|
|
255
|
+
const res = await fetch(`${this.httpUrl}/api/handoffs?room=${encodeURIComponent(this.room)}&status=open${roleQ}`, {
|
|
256
|
+
headers: { cookie: `omni_session=${this.token}` },
|
|
257
|
+
});
|
|
258
|
+
if (!res.ok)
|
|
259
|
+
throw new Error(`inbox failed: HTTP ${res.status} ${await res.text()}`);
|
|
260
|
+
const data = (await res.json());
|
|
261
|
+
return data.items;
|
|
262
|
+
}
|
|
263
|
+
/** Leave async work for a teammate or another AI role to pick up later. */
|
|
264
|
+
async leaveHandoff(input) {
|
|
265
|
+
if (!this.httpUrl)
|
|
266
|
+
throw new Error("not connected");
|
|
267
|
+
const body = { roomId: this.room, title: input.title, body: input.body };
|
|
268
|
+
if (input.toUserId)
|
|
269
|
+
body.toUserId = input.toUserId;
|
|
270
|
+
if (input.toAgentRole)
|
|
271
|
+
body.toAgentRole = input.toAgentRole;
|
|
272
|
+
if (input.repo)
|
|
273
|
+
body.repo = input.repo;
|
|
274
|
+
if (input.url)
|
|
275
|
+
body.url = input.url;
|
|
276
|
+
if (input.artifactId)
|
|
277
|
+
body.artifactId = input.artifactId;
|
|
278
|
+
const res = await fetch(`${this.httpUrl}/api/handoffs`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "content-type": "application/json", "x-omni-csrf": "1", cookie: `omni_session=${this.token}` },
|
|
281
|
+
body: JSON.stringify(body),
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok)
|
|
284
|
+
throw new Error(`leave failed: HTTP ${res.status} ${await res.text()}`);
|
|
285
|
+
const data = (await res.json());
|
|
286
|
+
return { id: data.handoff.id };
|
|
287
|
+
}
|
|
288
|
+
/** Answer (or progress) a handoff: mark it in_progress/done/cancelled, with an
|
|
289
|
+
* optional result summary and the artifact you produced as the answer. */
|
|
290
|
+
async resolveHandoff(input) {
|
|
291
|
+
if (!this.httpUrl)
|
|
292
|
+
throw new Error("not connected");
|
|
293
|
+
const body = {};
|
|
294
|
+
if (input.status)
|
|
295
|
+
body.status = input.status;
|
|
296
|
+
if (input.result !== undefined)
|
|
297
|
+
body.result = input.result;
|
|
298
|
+
if (input.resultArtifactId)
|
|
299
|
+
body.resultArtifactId = input.resultArtifactId;
|
|
300
|
+
const res = await fetch(`${this.httpUrl}/api/handoffs/${encodeURIComponent(input.id)}`, {
|
|
301
|
+
method: "PATCH",
|
|
302
|
+
headers: { "content-type": "application/json", "x-omni-csrf": "1", cookie: `omni_session=${this.token}` },
|
|
303
|
+
body: JSON.stringify(body),
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok)
|
|
306
|
+
throw new Error(`resolve failed: HTTP ${res.status} ${await res.text()}`);
|
|
307
|
+
const data = (await res.json());
|
|
308
|
+
return { id: data.handoff.id, status: data.handoff.status };
|
|
309
|
+
}
|
|
310
|
+
/** Upload a FILE into the room (a real downloadable file artifact). Text by
|
|
311
|
+
* default; pass base64 for binary. Returns the created artifact id. */
|
|
312
|
+
async uploadFile(input) {
|
|
313
|
+
if (!this.httpUrl)
|
|
314
|
+
throw new Error("not connected");
|
|
315
|
+
const bytes = Buffer.from(input.content, input.base64 ? "base64" : "utf8");
|
|
316
|
+
const res = await fetch(`${this.httpUrl}/api/uploads?room=${encodeURIComponent(this.room)}`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: {
|
|
319
|
+
"x-omni-csrf": "1",
|
|
320
|
+
"content-type": input.contentType ?? "text/plain; charset=utf-8",
|
|
321
|
+
"x-filename": encodeURIComponent(input.filename),
|
|
322
|
+
cookie: `omni_session=${this.token}`,
|
|
323
|
+
},
|
|
324
|
+
body: bytes,
|
|
325
|
+
});
|
|
326
|
+
if (!res.ok)
|
|
327
|
+
throw new Error(`upload failed: HTTP ${res.status} ${await res.text()}`);
|
|
328
|
+
const data = (await res.json());
|
|
329
|
+
return { id: data.artifact.id };
|
|
330
|
+
}
|
|
201
331
|
/** Recent room history (actions, artifacts, activity) — the audit/activity feed. */
|
|
202
332
|
async history(limit = 20) {
|
|
203
333
|
if (!this.httpUrl || !this.room)
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (`omni_wait_for_message`), you think with full context, and you answer
|
|
10
10
|
* (`omni_reply`). No fresh `claude -p` per turn.
|
|
11
11
|
*
|
|
12
|
-
* Env (auto-connect on start if set): OMNI_URL, OMNI_TOKEN, OMNI_ROOM, OMNI_LABEL.
|
|
12
|
+
* Env (auto-connect on start if set): OMNI_URL, OMNI_TOKEN, OMNI_ROOM, OMNI_LABEL, OMNI_ROLE, OMNI_AGENT_KEY.
|
|
13
13
|
*/
|
|
14
14
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -67,6 +67,8 @@ async function autoConnect() {
|
|
|
67
67
|
token: env.OMNI_TOKEN ?? "",
|
|
68
68
|
room: env.OMNI_ROOM,
|
|
69
69
|
label: env.OMNI_LABEL ?? "Claude Code",
|
|
70
|
+
role: env.OMNI_ROLE,
|
|
71
|
+
key: env.OMNI_AGENT_KEY,
|
|
70
72
|
});
|
|
71
73
|
return `Connected to room ${env.OMNI_ROOM} as agent ${agentId} ("${bridge.label}").`;
|
|
72
74
|
}
|
|
@@ -85,6 +87,8 @@ const tools = [
|
|
|
85
87
|
token: { type: "string", description: "Agent token for the room (overrides OMNI_TOKEN env)." },
|
|
86
88
|
url: { type: "string", description: "Server URL (overrides OMNI_URL env)." },
|
|
87
89
|
label: { type: "string", description: "Display name shown in the room (optional)." },
|
|
90
|
+
role: { type: "string", description: "Declared role for this agent (e.g. planner, coder, reviewer) — shown in the roster (optional)." },
|
|
91
|
+
key: { type: "string", description: "Stable wake key (optional) — reconnecting with the same key reclaims this agent's roster slot and drains work queued while it was offline." },
|
|
88
92
|
},
|
|
89
93
|
required: ["room"],
|
|
90
94
|
},
|
|
@@ -148,6 +152,56 @@ const tools = [
|
|
|
148
152
|
required: ["connector", "action"],
|
|
149
153
|
},
|
|
150
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: "omni_upload",
|
|
157
|
+
description: "Upload a FILE into the room — a real, downloadable file artifact (code, a doc, data), not pasted text. Provide a filename + content (text); set base64:true for binary. Use this to hand a teammate an actual file.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
filename: { type: "string", description: "File name, e.g. parser.ts or report.md." },
|
|
162
|
+
content: { type: "string", description: "File contents (utf-8 text, or base64 if base64:true)." },
|
|
163
|
+
content_type: { type: "string", description: "MIME type (optional), e.g. text/markdown." },
|
|
164
|
+
base64: { type: "boolean", description: "True if content is base64-encoded binary." },
|
|
165
|
+
},
|
|
166
|
+
required: ["filename", "content"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "omni_inbox",
|
|
171
|
+
description: "Your ASYNC INBOX: open work other people left in this room for you (or your role) to answer LATER — even when they're not around. Call this on connect and whenever you catch up. Each item has a title, body, and optional linked repo/url/artifact. Do the work in your own environment, share results with omni_share_artifact, then close it with omni_resolve_handoff. This is how async, leave-it-and-walk-away collaboration works.",
|
|
172
|
+
inputSchema: { type: "object", properties: {} },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "omni_leave_handoff",
|
|
176
|
+
description: "Leave ASYNC WORK for a teammate or another AI role to pick up later (without them being live). Provide a title + body; optionally address a person (to_user_id), an AI role (to_agent_role, e.g. 'coder' or 'any'), and link a repo/url/artifact. The work waits durably in the room until it's answered.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
title: { type: "string", description: "Short headline for the work." },
|
|
181
|
+
body: { type: "string", description: "Describe the task / leave the message." },
|
|
182
|
+
to_user_id: { type: "string", description: "Address a specific teammate (account id), optional." },
|
|
183
|
+
to_agent_role: { type: "string", description: "Address an AI role (e.g. coder, reviewer) or 'any', optional." },
|
|
184
|
+
repo: { type: "string", description: "Linked repo (owner/name or URL), optional." },
|
|
185
|
+
url: { type: "string", description: "Any external link, optional." },
|
|
186
|
+
artifact_id: { type: "string", description: "A related artifact id (the spec/input), optional." },
|
|
187
|
+
},
|
|
188
|
+
required: ["title", "body"],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "omni_resolve_handoff",
|
|
193
|
+
description: "Progress or ANSWER a handoff from your inbox: set status to in_progress (you're on it), done (answered — attach the result artifact you produced), or cancelled. Include a short result summary so the room sees what you did.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
id: { type: "string", description: "Handoff id (from omni_inbox)." },
|
|
198
|
+
status: { type: "string", description: "in_progress | done | cancelled" },
|
|
199
|
+
result: { type: "string", description: "Short summary of the answer/outcome." },
|
|
200
|
+
result_artifact_id: { type: "string", description: "The artifact you produced as the answer (from omni_share_artifact)." },
|
|
201
|
+
},
|
|
202
|
+
required: ["id"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
151
205
|
{
|
|
152
206
|
name: "omni_history",
|
|
153
207
|
description: "Read the room's recent history — the audit/activity feed of governed actions (who ran what, who approved, outcome), shared artifacts, and agent sandbox activity. Use it to catch up on what's happened.",
|
|
@@ -180,6 +234,21 @@ const tools = [
|
|
|
180
234
|
required: ["title", "kind"],
|
|
181
235
|
},
|
|
182
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: "omni_edit_artifact",
|
|
239
|
+
description: "Co-edit a SHARED artifact in the room (collaborative coding): replace its content/title/language. Bumps its version and updates it live for everyone — multiple agents and humans can iterate on the same code/doc. Best practice: read it first (you get its version), then pass expected_version so a concurrent edit can't silently clobber yours (a conflict returns 409 — re-read and retry).",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
id: { type: "string", description: "Artifact id to edit (from omni_share_artifact or a shared card)." },
|
|
244
|
+
content: { type: "string", description: "New full content (replaces the prior text)." },
|
|
245
|
+
title: { type: "string", description: "New title (optional)." },
|
|
246
|
+
language: { type: "string", description: "New language/format hint (optional)." },
|
|
247
|
+
expected_version: { type: "number", description: "The version you edited (optimistic concurrency; 409 if it moved on)." },
|
|
248
|
+
},
|
|
249
|
+
required: ["id"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
183
252
|
];
|
|
184
253
|
const server = new Server({ name: "omni", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
185
254
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
@@ -193,6 +262,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
193
262
|
token: args.token ? String(args.token) : env.OMNI_TOKEN ?? "",
|
|
194
263
|
room: String(args.room),
|
|
195
264
|
label: args.label ? String(args.label) : env.OMNI_LABEL ?? "Claude Code",
|
|
265
|
+
role: args.role ? String(args.role) : env.OMNI_ROLE,
|
|
266
|
+
key: args.key ? String(args.key) : env.OMNI_AGENT_KEY,
|
|
196
267
|
});
|
|
197
268
|
roomSkills = await bridge.listSkills();
|
|
198
269
|
const skills = roomSkills;
|
|
@@ -293,6 +364,79 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
293
364
|
return text(`Could not share artifact: ${e.message}`);
|
|
294
365
|
}
|
|
295
366
|
}
|
|
367
|
+
if (name === "omni_edit_artifact") {
|
|
368
|
+
if (!bridge.connected)
|
|
369
|
+
return text("Not connected — call omni_connect first.");
|
|
370
|
+
const aid = String(args.id ?? "");
|
|
371
|
+
if (!aid)
|
|
372
|
+
return text("Provide the artifact `id` to edit.");
|
|
373
|
+
try {
|
|
374
|
+
const { id, version } = await bridge.editArtifact({
|
|
375
|
+
id: aid,
|
|
376
|
+
content: args.content !== undefined ? String(args.content) : undefined,
|
|
377
|
+
title: args.title !== undefined ? String(args.title) : undefined,
|
|
378
|
+
language: args.language !== undefined ? String(args.language) : undefined,
|
|
379
|
+
expectedVersion: args.expected_version !== undefined ? Number(args.expected_version) : undefined,
|
|
380
|
+
});
|
|
381
|
+
return text(`Edited artifact ${id} → version ${version}. Everyone in the room sees the update live.`);
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
return text(`Could not edit artifact: ${e.message} (on a version conflict, re-fetch with omni_history/the shared card and retry).`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (name === "omni_inbox") {
|
|
388
|
+
if (!bridge.connected)
|
|
389
|
+
return text("Not connected — call omni_connect first.");
|
|
390
|
+
try {
|
|
391
|
+
const items = await bridge.inbox();
|
|
392
|
+
if (items.length === 0)
|
|
393
|
+
return text("📭 Your async inbox is empty — no open work left for you in this room.");
|
|
394
|
+
const lines = items.map((h) => `• [${h.id}] ${h.title}${h.toAgentRole ? ` (for @${h.toAgentRole})` : ""}\n ${h.body.slice(0, 240)}${h.body.length > 240 ? "…" : ""}` +
|
|
395
|
+
`${h.repo ? `\n repo: ${h.repo}` : ""}${h.url ? `\n link: ${h.url}` : ""}${h.artifactId ? `\n artifact: ${h.artifactId}` : ""}`);
|
|
396
|
+
return text(`📥 ${items.length} item(s) waiting for you:\n\n${lines.join("\n\n")}\n\nDo the work, share results with omni_share_artifact, then close with omni_resolve_handoff({ id, status: "done", result, result_artifact_id }).`);
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
return text(`Could not read inbox: ${e.message}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (name === "omni_leave_handoff") {
|
|
403
|
+
if (!bridge.connected)
|
|
404
|
+
return text("Not connected — call omni_connect first.");
|
|
405
|
+
try {
|
|
406
|
+
const { id } = await bridge.leaveHandoff({
|
|
407
|
+
title: String(args.title ?? "Untitled"),
|
|
408
|
+
body: String(args.body ?? ""),
|
|
409
|
+
toUserId: args.to_user_id ? String(args.to_user_id) : undefined,
|
|
410
|
+
toAgentRole: args.to_agent_role ? String(args.to_agent_role) : undefined,
|
|
411
|
+
repo: args.repo ? String(args.repo) : undefined,
|
|
412
|
+
url: args.url ? String(args.url) : undefined,
|
|
413
|
+
artifactId: args.artifact_id ? String(args.artifact_id) : undefined,
|
|
414
|
+
});
|
|
415
|
+
return text(`Left async work ${id} in the room — it waits until it's picked up.`);
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
return text(`Could not leave handoff: ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (name === "omni_resolve_handoff") {
|
|
422
|
+
if (!bridge.connected)
|
|
423
|
+
return text("Not connected — call omni_connect first.");
|
|
424
|
+
const hid = String(args.id ?? "");
|
|
425
|
+
if (!hid)
|
|
426
|
+
return text("Provide the handoff `id` to resolve.");
|
|
427
|
+
try {
|
|
428
|
+
const { id, status } = await bridge.resolveHandoff({
|
|
429
|
+
id: hid,
|
|
430
|
+
status: args.status ? String(args.status) : undefined,
|
|
431
|
+
result: args.result !== undefined ? String(args.result) : undefined,
|
|
432
|
+
resultArtifactId: args.result_artifact_id ? String(args.result_artifact_id) : undefined,
|
|
433
|
+
});
|
|
434
|
+
return text(`Handoff ${id} is now ${status}. The room sees the update.`);
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
return text(`Could not resolve handoff: ${e.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
296
440
|
if (name === "omni_stream") {
|
|
297
441
|
if (!bridge.connected)
|
|
298
442
|
return text("Not connected — call omni_connect first.");
|
|
@@ -330,6 +474,22 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
330
474
|
? `Proposed ${connector}.${action} to the room — it's now in the HOST'S APPROVAL QUEUE and will run once a human approves. The outcome will appear in the room.`
|
|
331
475
|
: `Ran ${connector}.${action} (read-only) — the result is posted in the room.`);
|
|
332
476
|
}
|
|
477
|
+
if (name === "omni_upload") {
|
|
478
|
+
if (!bridge.connected)
|
|
479
|
+
return text("Not connected — call omni_connect first.");
|
|
480
|
+
try {
|
|
481
|
+
const { id } = await bridge.uploadFile({
|
|
482
|
+
filename: String(args.filename || "file.txt"),
|
|
483
|
+
content: String(args.content ?? ""),
|
|
484
|
+
contentType: args.content_type ? String(args.content_type) : undefined,
|
|
485
|
+
base64: !!args.base64,
|
|
486
|
+
});
|
|
487
|
+
return text(`Uploaded "${args.filename}" as a downloadable file artifact (${id}). The room can open/download it.`);
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
return text(`Upload failed: ${e.message}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
333
493
|
if (name === "omni_history") {
|
|
334
494
|
if (!bridge.connected)
|
|
335
495
|
return text("Not connected — call omni_connect first.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "substrattice",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"mcpName": "io.github.gen-rl-millz/substrattice",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Omni MCP server — lets a live agent session (Claude Code, …) join Omni rooms and answer as itself, memory + tools intact. Spin up/join a room, wait for work, reply, and share artifacts.",
|