negotium 0.1.11 → 0.1.13

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.
Files changed (165) hide show
  1. package/README.md +11 -2
  2. package/dist/canonical-mcp-bridge.js +2 -3
  3. package/dist/canonical-mcp-bridge.js.map +1 -1
  4. package/dist/{chunk-gz12qcf0.js → chunk-a9y2rwxf.js} +2 -2
  5. package/dist/{chunk-gz12qcf0.js.map → chunk-a9y2rwxf.js.map} +3 -3
  6. package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
  7. package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
  8. package/dist/cron.js +292 -256
  9. package/dist/cron.js.map +23 -22
  10. package/dist/hosted-agent.js +3012 -17
  11. package/dist/hosted-agent.js.map +30 -3
  12. package/dist/main.js +31323 -36
  13. package/dist/main.js.map +198 -3
  14. package/dist/prompts.js +12 -9
  15. package/dist/prompts.js.map +5 -5
  16. package/dist/registry.js +10 -6
  17. package/dist/registry.js.map +3 -3
  18. package/dist/rollout.js +1 -1
  19. package/dist/runtime/cron/store.ts +24 -1
  20. package/dist/runtime/src/application/submit-user-message.ts +3 -0
  21. package/dist/runtime/src/index.ts +7 -0
  22. package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
  23. package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
  24. package/dist/runtime/src/platform/config.ts +2 -2
  25. package/dist/runtime/src/platform/mcp-config.ts +13 -0
  26. package/dist/runtime/src/prompts/builders.ts +20 -6
  27. package/dist/runtime/src/query/active-rooms.ts +2 -0
  28. package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
  29. package/dist/runtime/src/runtime/errors.ts +12 -12
  30. package/dist/runtime/src/runtime/turn-runner.ts +12 -0
  31. package/dist/runtime/src/storage/api-messages.ts +7 -5
  32. package/dist/runtime/src/storage/api-topics.ts +5 -2
  33. package/dist/runtime/src/storage/forum-db.ts +4 -1
  34. package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
  35. package/dist/runtime/src/storage/session-asks.ts +13 -5
  36. package/dist/runtime/src/storage/storage-contract.ts +54 -0
  37. package/dist/runtime/src/storage/storage-host.ts +89 -36
  38. package/dist/runtime/src/storage/storage-public-host.ts +16 -0
  39. package/dist/runtime/src/storage/storage-public.ts +21 -5
  40. package/dist/runtime/src/storage/token-stats.ts +1 -1
  41. package/dist/runtime/src/types/api.ts +2 -0
  42. package/dist/runtime/src/types.ts +2 -0
  43. package/dist/runtime/src/version.ts +1 -1
  44. package/dist/runtime-helpers.js +29 -18
  45. package/dist/runtime-helpers.js.map +8 -7
  46. package/dist/storage.js +260 -202
  47. package/dist/storage.js.map +11 -9
  48. package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
  49. package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
  50. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
  51. package/dist/types/packages/core/src/storage/api-messages.d.ts +6 -6
  52. package/dist/types/packages/core/src/storage/api-topics.d.ts +20 -2
  53. package/dist/types/packages/core/src/storage/forum-db.d.ts +2 -1
  54. package/dist/types/packages/core/src/storage/session-asks.d.ts +3 -2
  55. package/dist/types/packages/core/src/storage/storage-contract.d.ts +46 -0
  56. package/dist/types/packages/core/src/storage/storage-host.d.ts +12 -19
  57. package/dist/types/packages/core/src/storage/storage-public-host.d.ts +7 -0
  58. package/dist/types/packages/core/src/storage/storage-public.d.ts +4 -2
  59. package/dist/types/packages/core/src/types/api.d.ts +2 -0
  60. package/dist/types/packages/core/src/types.d.ts +2 -0
  61. package/dist/types/packages/core/src/version.d.ts +1 -1
  62. package/dist/types/packages/module-cron/src/store.d.ts +22 -2
  63. package/dist/vault.js +2 -2
  64. package/dist/vault.js.map +3 -3
  65. package/package.json +1 -1
  66. package/dist/chunk-01xcc856.js +0 -149
  67. package/dist/chunk-01xcc856.js.map +0 -10
  68. package/dist/chunk-1a6kv9vt.js +0 -118
  69. package/dist/chunk-1a6kv9vt.js.map +0 -10
  70. package/dist/chunk-1b62d730.js +0 -117
  71. package/dist/chunk-1b62d730.js.map +0 -10
  72. package/dist/chunk-1k7t7bnx.js +0 -97
  73. package/dist/chunk-1k7t7bnx.js.map +0 -10
  74. package/dist/chunk-28apmz4r.js +0 -431
  75. package/dist/chunk-28apmz4r.js.map +0 -11
  76. package/dist/chunk-2fvydfkx.js +0 -141
  77. package/dist/chunk-2fvydfkx.js.map +0 -10
  78. package/dist/chunk-41ygvg5k.js +0 -2387
  79. package/dist/chunk-41ygvg5k.js.map +0 -26
  80. package/dist/chunk-4q966ttx.js +0 -371
  81. package/dist/chunk-4q966ttx.js.map +0 -11
  82. package/dist/chunk-51rcsavw.js +0 -1712
  83. package/dist/chunk-51rcsavw.js.map +0 -18
  84. package/dist/chunk-59d0shyj.js +0 -103
  85. package/dist/chunk-59d0shyj.js.map +0 -10
  86. package/dist/chunk-5fzgvn79.js +0 -3536
  87. package/dist/chunk-5fzgvn79.js.map +0 -20
  88. package/dist/chunk-61px0717.js +0 -153
  89. package/dist/chunk-61px0717.js.map +0 -11
  90. package/dist/chunk-66y0hr8c.js +0 -2136
  91. package/dist/chunk-66y0hr8c.js.map +0 -29
  92. package/dist/chunk-69k45zg8.js +0 -250
  93. package/dist/chunk-69k45zg8.js.map +0 -11
  94. package/dist/chunk-6ah5farg.js +0 -130
  95. package/dist/chunk-6ah5farg.js.map +0 -10
  96. package/dist/chunk-6d4fmea5.js +0 -117
  97. package/dist/chunk-6d4fmea5.js.map +0 -12
  98. package/dist/chunk-8cahx7xc.js +0 -46
  99. package/dist/chunk-8cahx7xc.js.map +0 -10
  100. package/dist/chunk-8f20fk4n.js +0 -252
  101. package/dist/chunk-8f20fk4n.js.map +0 -10
  102. package/dist/chunk-91ckrvk8.js +0 -318
  103. package/dist/chunk-91ckrvk8.js.map +0 -10
  104. package/dist/chunk-93krf6ng.js +0 -3069
  105. package/dist/chunk-93krf6ng.js.map +0 -32
  106. package/dist/chunk-9qx1t77a.js +0 -214
  107. package/dist/chunk-9qx1t77a.js.map +0 -10
  108. package/dist/chunk-beqhdr9w.js +0 -589
  109. package/dist/chunk-beqhdr9w.js.map +0 -11
  110. package/dist/chunk-bftftsgq.js +0 -1637
  111. package/dist/chunk-bftftsgq.js.map +0 -15
  112. package/dist/chunk-c500xe9z.js +0 -33
  113. package/dist/chunk-c500xe9z.js.map +0 -10
  114. package/dist/chunk-cjsnkvq2.js +0 -23
  115. package/dist/chunk-cjsnkvq2.js.map +0 -10
  116. package/dist/chunk-cxa7yghg.js +0 -577
  117. package/dist/chunk-cxa7yghg.js.map +0 -14
  118. package/dist/chunk-edb07sjp.js +0 -567
  119. package/dist/chunk-edb07sjp.js.map +0 -14
  120. package/dist/chunk-f361dwba.js +0 -589
  121. package/dist/chunk-f361dwba.js.map +0 -11
  122. package/dist/chunk-f5xgprg1.js +0 -59
  123. package/dist/chunk-f5xgprg1.js.map +0 -10
  124. package/dist/chunk-ffefbd3r.js +0 -17
  125. package/dist/chunk-ffefbd3r.js.map +0 -10
  126. package/dist/chunk-fswhzdf5.js +0 -64
  127. package/dist/chunk-fswhzdf5.js.map +0 -10
  128. package/dist/chunk-g144wdrs.js +0 -2907
  129. package/dist/chunk-g144wdrs.js.map +0 -24
  130. package/dist/chunk-h980hb2n.js +0 -512
  131. package/dist/chunk-h980hb2n.js.map +0 -13
  132. package/dist/chunk-h9vbhhgr.js +0 -1337
  133. package/dist/chunk-h9vbhhgr.js.map +0 -16
  134. package/dist/chunk-hwy3n31w.js +0 -21
  135. package/dist/chunk-hwy3n31w.js.map +0 -9
  136. package/dist/chunk-jtrk0khr.js +0 -108
  137. package/dist/chunk-jtrk0khr.js.map +0 -10
  138. package/dist/chunk-jzz91jwh.js +0 -70
  139. package/dist/chunk-jzz91jwh.js.map +0 -10
  140. package/dist/chunk-m9e3tbr8.js +0 -708
  141. package/dist/chunk-m9e3tbr8.js.map +0 -14
  142. package/dist/chunk-mh5mexsf.js +0 -147
  143. package/dist/chunk-mh5mexsf.js.map +0 -11
  144. package/dist/chunk-npd8n03v.js +0 -2152
  145. package/dist/chunk-npd8n03v.js.map +0 -15
  146. package/dist/chunk-pg1azh6n.js +0 -2
  147. package/dist/chunk-pg1azh6n.js.map +0 -9
  148. package/dist/chunk-qwy57177.js +0 -237
  149. package/dist/chunk-qwy57177.js.map +0 -11
  150. package/dist/chunk-r31q21h5.js +0 -53
  151. package/dist/chunk-r31q21h5.js.map +0 -10
  152. package/dist/chunk-rbvctv1n.js +0 -366
  153. package/dist/chunk-rbvctv1n.js.map +0 -10
  154. package/dist/chunk-sa874j5c.js +0 -9
  155. package/dist/chunk-sa874j5c.js.map +0 -10
  156. package/dist/chunk-sctcs5zb.js +0 -281
  157. package/dist/chunk-sctcs5zb.js.map +0 -10
  158. package/dist/chunk-tsstrcfc.js +0 -122
  159. package/dist/chunk-tsstrcfc.js.map +0 -10
  160. package/dist/chunk-wk2qf31s.js +0 -58
  161. package/dist/chunk-wk2qf31s.js.map +0 -10
  162. package/dist/chunk-wx7ag4dd.js +0 -2936
  163. package/dist/chunk-wx7ag4dd.js.map +0 -20
  164. package/dist/chunk-znx1fpfd.js +0 -61
  165. package/dist/chunk-znx1fpfd.js.map +0 -10
@@ -1,1637 +0,0 @@
1
- // @bun
2
- import {
3
- McpHost,
4
- McpManifest
5
- } from "./chunk-edb07sjp.js";
6
- import {
7
- TopicServiceError,
8
- TopicValidationError,
9
- acquireRuntimeProcessLease,
10
- createSelfConfigToolDefinitions,
11
- deleteVaultEntry,
12
- executeVaultCommand,
13
- listBackgroundSessionsForUser,
14
- listVaultEntries,
15
- nodeRequestHandlerNames,
16
- onShutdown,
17
- registerTopic,
18
- restartTopicSession,
19
- runNodeRequestHandlers,
20
- runShutdown,
21
- saveVaultEntry,
22
- startNegotiumNodeModules,
23
- startSessionInboxWorker,
24
- submitUserMessage,
25
- switchTopicEffort,
26
- switchTopicModel,
27
- topicService,
28
- visualToolDefinitions
29
- } from "./chunk-93krf6ng.js";
30
- import"./chunk-fswhzdf5.js";
31
- import {
32
- TopicArchiveRequiredError,
33
- deleteTopicCascade,
34
- sessionInboxPath
35
- } from "./chunk-cxa7yghg.js";
36
- import"./chunk-59d0shyj.js";
37
- import"./chunk-rbvctv1n.js";
38
- import"./chunk-1k7t7bnx.js";
39
- import {
40
- createAskUserToolDefinition,
41
- dispatchPeerRuntimeAskUser,
42
- dispatchPeerRuntimeFile,
43
- dispatchPeerRuntimeSelfConfig,
44
- dispatchPeerRuntimeSpawn,
45
- startDurableTurnRequestWorker
46
- } from "./chunk-g144wdrs.js";
47
- import {
48
- TopicDeriveBusyError,
49
- TopicTitleConflictError,
50
- getTopics,
51
- getVisibleTopics,
52
- isParticipant
53
- } from "./chunk-91ckrvk8.js";
54
- import {
55
- createSpawnSubagentToolDefinition,
56
- createSubagentManagementToolDefinitions,
57
- errorResult,
58
- killAllBgBash,
59
- setFileHooks,
60
- storeLocalFileAsUpload,
61
- sweepStaleSubagentCards,
62
- textResult
63
- } from "./chunk-66y0hr8c.js";
64
- import {
65
- WsHub,
66
- appendApiMessage,
67
- ensurePersonalGeneral,
68
- killAllPlaywright,
69
- latestRuntimeEventSeq,
70
- listApiMessages,
71
- listRecentRuntimeEventsForTopic,
72
- listRuntimeEventsAfter,
73
- runtimeBus
74
- } from "./chunk-h9vbhhgr.js";
75
- import {
76
- FROM_AUTO_CONTINUE,
77
- getTopic,
78
- getTopicByNameForUser
79
- } from "./chunk-f361dwba.js";
80
- import"./chunk-8cahx7xc.js";
81
- import"./chunk-6d4fmea5.js";
82
- import"./chunk-61px0717.js";
83
- import"./chunk-tsstrcfc.js";
84
- import {
85
- getApiTopicConfig
86
- } from "./chunk-6ah5farg.js";
87
- import {
88
- NEGOTIUM_VERSION,
89
- RUNTIME_MCP_BASE_PATH,
90
- RUNTIME_MCP_KEY,
91
- errMsg,
92
- isSensitivePath,
93
- killOwnedCodexTreesForShutdown,
94
- resolveRuntimeMcpToken,
95
- setNodeMcpServers,
96
- setRuntimeMcpPort
97
- } from "./chunk-41ygvg5k.js";
98
- import"./chunk-m9e3tbr8.js";
99
- import {
100
- appendJsonlEntry
101
- } from "./chunk-mh5mexsf.js";
102
- import {
103
- DATA_DIR,
104
- EFFORT_VALUES,
105
- NEGOTIUM_PORT,
106
- NODE_CONTROL_TOKEN,
107
- RUN_DIR,
108
- STATE_DIR,
109
- WORKSPACE_DIR
110
- } from "./chunk-qwy57177.js";
111
- import"./chunk-j0kjscb7.js";
112
- import {
113
- abortAllRooms,
114
- abortRoom,
115
- getRoomQuery,
116
- listRunningTopicQueries
117
- } from "./chunk-h980hb2n.js";
118
- import {
119
- logger
120
- } from "./chunk-ffefbd3r.js";
121
- import"./chunk-pg1azh6n.js";
122
- import"./chunk-1a6kv9vt.js";
123
- import"./chunk-f5xgprg1.js";
124
- import {
125
- __require,
126
- __toESM
127
- } from "./chunk-hwy3n31w.js";
128
-
129
- // ../../packages/node/src/index.ts
130
- import { createServer } from "net";
131
- // ../../packages/mcp/src/node-tools.ts
132
- import { z } from "zod";
133
- function resolveTopicForUser(ctx, ref) {
134
- const trimmed = ref.trim();
135
- if (!trimmed)
136
- return { error: "Error: topic is required." };
137
- const notFound = `Error: topic '${trimmed}' not found (or not uniquely named). Use list_topics to see available topics.`;
138
- const byId = getTopic(trimmed);
139
- if (byId) {
140
- if (!isParticipant(byId, ctx.userId))
141
- return { error: notFound };
142
- return { topic: byId };
143
- }
144
- const byTitle = getTopicByNameForUser(trimmed, ctx.userId);
145
- if (byTitle)
146
- return { topic: byTitle };
147
- return { error: notFound };
148
- }
149
- function describeTopic(topic) {
150
- const running = getRoomQuery(topic.id) ? "turn running" : "idle";
151
- const agent = topic.agent ? `agent: ${topic.agent}` : "no agent";
152
- return `- "${topic.title}" (id: ${topic.id}, kind: ${topic.kind ?? "agent"}, ${agent}, ${running})`;
153
- }
154
- function registerNodeTools(server, ctx) {
155
- server.tool("register_topic", "Create a new topic (agent room) on this negotium node, owned by the calling user. " + "Returns the new topic's id, title, agent, and model. Use the session-comm tell_session " + "tool to hand the new topic work.", {
156
- title: z.string().describe("Unique title for the new topic."),
157
- agent: z.enum(["claude", "codex", "maestro"]).optional().describe("AI backend for the room. Defaults to maestro."),
158
- model: z.string().optional().describe("Model override, must be valid for the agent."),
159
- effort: z.enum(EFFORT_VALUES).optional().describe("Reasoning effort override for the room."),
160
- description: z.string().optional().describe("Short description of the topic's purpose.")
161
- }, async ({ title, agent, model, effort, description }) => {
162
- try {
163
- const topic = registerTopic({
164
- title,
165
- userId: ctx.userId,
166
- agent,
167
- model,
168
- effort,
169
- description
170
- });
171
- return textResult([
172
- `Topic registered.`,
173
- `id: ${topic.id}`,
174
- `title: ${topic.title}`,
175
- `agent: ${topic.agent ?? "none"}`,
176
- `model: ${topic.defaultModel}`
177
- ].join(`
178
- `));
179
- } catch (err) {
180
- if (err instanceof TopicValidationError)
181
- return errorResult(`Error: ${err.message}`);
182
- logger.error({ err, title }, "negotium MCP: register_topic failed");
183
- return errorResult(`Error: failed to register topic: ${errMsg(err)}`);
184
- }
185
- });
186
- server.tool("list_topics", "List the calling user's topics on this negotium node: title, id, kind, agent, and whether a turn is currently running.", {}, async () => {
187
- const topics = getTopics().filter((topic) => isParticipant(topic, ctx.userId));
188
- if (topics.length === 0) {
189
- return textResult("No topics found. Use register_topic to create one.");
190
- }
191
- return textResult(topics.map(describeTopic).join(`
192
- `));
193
- });
194
- server.tool("abort_topic", "Abort the running turn in another topic on this node. Fire-and-forget: also queues an abort " + "signal in the topic's inbox so a turn that has not started yet is cancelled too. Returns whether an active turn was aborted.", {
195
- topic: z.string().describe("Target topic title or id.")
196
- }, async ({ topic }) => {
197
- const resolved = resolveTopicForUser(ctx, topic);
198
- if ("error" in resolved)
199
- return errorResult(resolved.error);
200
- const target = resolved.topic;
201
- if (target.id === ctx.topicId) {
202
- return errorResult("Error: cannot abort the current topic from within its own turn.");
203
- }
204
- const aborted = abortRoom(target.id);
205
- try {
206
- appendJsonlEntry(sessionInboxPath(ctx.userId, target.id), {
207
- type: "abort",
208
- timestamp: new Date().toISOString()
209
- });
210
- } catch (err) {
211
- logger.warn({ err, topicId: target.id }, "negotium MCP: abort inbox write failed");
212
- }
213
- return textResult(aborted ? `Aborted the active turn in "${target.title}".` : `No active turn in "${target.title}"; abort signal queued in its inbox.`);
214
- });
215
- server.tool("restart_topic", "Reset another topic's AI context while preserving the topic and its visible message history. " + "The next message starts a fresh provider session. Only the topic owner can restart it.", {
216
- topic: z.string().describe("Target topic title or id.")
217
- }, async ({ topic }) => {
218
- const resolved = resolveTopicForUser(ctx, topic);
219
- if ("error" in resolved)
220
- return errorResult(resolved.error);
221
- const target = resolved.topic;
222
- if (target.id === ctx.topicId) {
223
- return errorResult("Error: cannot restart the current topic from within its own turn.");
224
- }
225
- if (target.kind === "manager") {
226
- return errorResult("Error: manager rooms are system-managed and cannot be restarted.");
227
- }
228
- const result = await restartTopicSession(target.id, ctx.userId, "runtime-mcp-session-reset");
229
- return result.isError ? errorResult(`Error: ${result.text}`) : textResult(result.text);
230
- });
231
- server.tool("delete_topic", "Delete a topic owned by the calling user after archiving its conversation history. Deletion is blocked " + "when the archive cannot be written; pass force: true only as an explicit escape hatch that accepts losing history.", {
232
- topic: z.string().describe("Target topic title or id."),
233
- force: z.boolean().optional().describe("Delete even if archiving the conversation history fails. Default false.")
234
- }, async ({ topic, force }) => {
235
- const resolved = resolveTopicForUser(ctx, topic);
236
- if ("error" in resolved)
237
- return errorResult(resolved.error);
238
- const target = resolved.topic;
239
- if (target.id === ctx.topicId) {
240
- return errorResult("Error: cannot delete the current topic from within its own turn.");
241
- }
242
- if (target.kind === "manager") {
243
- return errorResult("Error: manager rooms are system-managed and cannot be deleted.");
244
- }
245
- const isOwner = target.participants.some((participant) => participant.userId === ctx.userId && participant.role === "owner");
246
- if (!isOwner) {
247
- return errorResult("Error: only the topic owner can delete it.");
248
- }
249
- try {
250
- await deleteTopicCascade(target, ctx.userId, { force: force === true });
251
- } catch (err) {
252
- if (err instanceof TopicArchiveRequiredError) {
253
- return errorResult([
254
- `Error: deleting "${target.title}" was blocked because its conversation history could not be archived.`,
255
- "Topics are archived before deletion so no history is lost. Fix the archive failure and retry, or pass force: true to delete anyway and accept losing the history."
256
- ].join(`
257
- `));
258
- }
259
- logger.error({ err, topicId: target.id }, "negotium MCP: delete_topic failed");
260
- return errorResult(`Error: failed to delete "${target.title}": ${errMsg(err)}`);
261
- }
262
- return textResult(`Topic "${target.title}" (id: ${target.id}) deleted.`);
263
- });
264
- }
265
- // ../../packages/mcp/src/server.ts
266
- import { randomUUID as randomUUID2 } from "crypto";
267
- import { realpathSync, statSync } from "fs";
268
- import { basename, extname, resolve } from "path";
269
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
270
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
271
- import { z as z2 } from "zod";
272
-
273
- // ../../packages/mcp/src/sse-transport.ts
274
- import { randomUUID } from "crypto";
275
- import {
276
- JSONRPCMessageSchema
277
- } from "@modelcontextprotocol/sdk/types.js";
278
-
279
- class SseTransport {
280
- endpoint;
281
- keepAliveMs;
282
- sessionId = randomUUID();
283
- onclose;
284
- onerror;
285
- onmessage;
286
- encoder = new TextEncoder;
287
- controller;
288
- closed = false;
289
- stream;
290
- keepAliveTimer;
291
- constructor(endpoint, req, keepAliveMs = 20000) {
292
- this.endpoint = endpoint;
293
- this.keepAliveMs = keepAliveMs;
294
- this.stream = new ReadableStream({
295
- start: (controller) => {
296
- this.controller = controller;
297
- },
298
- cancel: () => {
299
- this.close();
300
- }
301
- });
302
- req?.signal.addEventListener("abort", () => {
303
- this.close();
304
- });
305
- }
306
- response() {
307
- return new Response(this.stream, {
308
- status: 200,
309
- headers: {
310
- "Content-Type": "text/event-stream",
311
- "Cache-Control": "no-cache, no-transform",
312
- Connection: "keep-alive"
313
- }
314
- });
315
- }
316
- async start() {
317
- if (this.closed)
318
- return;
319
- const url = new URL(this.endpoint, "http://localhost");
320
- url.searchParams.set("sessionId", this.sessionId);
321
- this.enqueue(`event: endpoint
322
- data: ${url.pathname}${url.search}${url.hash}
323
-
324
- `);
325
- this.startKeepAlive();
326
- }
327
- async send(message, _options) {
328
- this.enqueue(`event: message
329
- data: ${JSON.stringify(message)}
330
-
331
- `);
332
- }
333
- async handleMessage(raw, req) {
334
- const parsed = JSONRPCMessageSchema.parse(raw);
335
- this.onmessage?.(parsed, {
336
- requestInfo: {
337
- headers: Object.fromEntries(req.headers.entries()),
338
- url: new URL(req.url)
339
- }
340
- });
341
- }
342
- async close() {
343
- if (this.closed)
344
- return;
345
- this.closed = true;
346
- try {
347
- this.controller?.close();
348
- } catch {}
349
- if (this.keepAliveTimer) {
350
- clearInterval(this.keepAliveTimer);
351
- this.keepAliveTimer = undefined;
352
- }
353
- this.onclose?.();
354
- }
355
- startKeepAlive() {
356
- if (this.closed || this.keepAliveTimer || this.keepAliveMs <= 0)
357
- return;
358
- this.keepAliveTimer = setInterval(() => {
359
- this.enqueue(`: ping
360
-
361
- `);
362
- }, this.keepAliveMs);
363
- }
364
- enqueue(payload) {
365
- if (this.closed)
366
- return;
367
- try {
368
- this.controller?.enqueue(this.encoder.encode(payload));
369
- } catch (err) {
370
- const error = err instanceof Error ? err : new Error(String(err));
371
- this.onerror?.(error);
372
- this.close();
373
- }
374
- }
375
- }
376
-
377
- // ../../packages/mcp/src/server.ts
378
- var SSE_PATH = `${RUNTIME_MCP_BASE_PATH}/sse`;
379
- var SSE_MESSAGE_PATH = `${RUNTIME_MCP_BASE_PATH}/message`;
380
- var STREAMABLE_PATH = `${RUNTIME_MCP_BASE_PATH}/mcp`;
381
- var sseSessions = new Map;
382
- var streamableSessions = new Map;
383
- function requireTopicAccess(ctx) {
384
- const topic = getTopic(ctx.topicId);
385
- if (!topic)
386
- return { error: `Error: topic '${ctx.topicId}' not found.` };
387
- if (!topic.participants.some((p) => p.userId === ctx.userId)) {
388
- return { error: "Error: user is not a member of this topic." };
389
- }
390
- return { topic };
391
- }
392
- function isPathInside(baseDir, filePath) {
393
- const cwd = resolve(baseDir);
394
- const normalized = resolve(filePath);
395
- try {
396
- const realCwd = realpathSync(cwd);
397
- const real = realpathSync(normalized);
398
- return real === realCwd || real.startsWith(`${realCwd}/`);
399
- } catch (err) {
400
- if (err.code === "ENOENT") {
401
- return normalized === cwd || normalized.startsWith(`${cwd}/`);
402
- }
403
- return false;
404
- }
405
- }
406
- function localFileInfo(ctx, filePath) {
407
- if (!isPathInside(ctx.cwd, filePath)) {
408
- return { error: "Access denied. File must be within the topic workspace." };
409
- }
410
- const normalizedPath = resolve(filePath);
411
- if (isSensitivePath(normalizedPath)) {
412
- return { error: "Access denied. Path matches the sensitive-file blacklist." };
413
- }
414
- try {
415
- const stats = statSync(normalizedPath);
416
- if (!stats.isFile())
417
- return { error: `${filePath} is not a file` };
418
- return {
419
- normalizedPath,
420
- name: basename(filePath),
421
- ext: extname(filePath).toLowerCase(),
422
- sizeBytes: stats.size,
423
- sizeMB: (stats.size / (1024 * 1024)).toFixed(2)
424
- };
425
- } catch {
426
- return { error: `File not found at ${filePath}` };
427
- }
428
- }
429
- async function deliverFile(ctx, filePath) {
430
- const access = requireTopicAccess(ctx);
431
- if ("error" in access)
432
- return errorResult(access.error);
433
- const info = localFileInfo(ctx, filePath);
434
- if ("error" in info)
435
- return errorResult(`Error: ${info.error}`);
436
- if (ctx.peerBridge) {
437
- const bridged = await dispatchPeerRuntimeFile({
438
- bridge: ctx.peerBridge,
439
- userId: ctx.userId,
440
- agent: ctx.agent,
441
- model: ctx.model,
442
- path: info.normalizedPath,
443
- source: "runtime.send_file"
444
- });
445
- if (!bridged)
446
- return errorResult("Error: the peer file bridge is not available on this node.");
447
- if (!bridged.ok)
448
- return errorResult(`Error: Failed to send file on hub: ${bridged.error}`);
449
- return textResult(`File sent to chat: ${info.name} (${info.ext || "no extension"}, ${info.sizeMB} MB)`);
450
- }
451
- const attachment = storeLocalFileAsUpload(info.normalizedPath, {
452
- ownerUserId: ctx.userId,
453
- topicId: ctx.topicId
454
- });
455
- if (!attachment)
456
- return errorResult("Error: Failed to store file for delivery.");
457
- const cfg = getApiTopicConfig(ctx.topicId);
458
- const msg = {
459
- id: randomUUID2(),
460
- topicId: ctx.topicId,
461
- authorId: "ai",
462
- text: `\uD83D\uDCCE ${attachment.filename}`,
463
- agentType: ctx.agent,
464
- model: ctx.model ?? cfg?.model ?? "unknown",
465
- attachments: [attachment],
466
- createdAt: new Date().toISOString()
467
- };
468
- appendApiMessage(msg);
469
- WsHub.get().broadcastMessage(ctx.topicId, msg);
470
- return textResult([
471
- `File sent to chat: ${info.name} (${info.ext || "no extension"}, ${info.sizeMB} MB)`,
472
- `Path: ${info.normalizedPath}`
473
- ].join(`
474
- `));
475
- }
476
- function appendAutoContinue(ctx, field) {
477
- if (!ctx.autoContinue)
478
- return;
479
- appendJsonlEntry(sessionInboxPath(ctx.userId, ctx.topicId), {
480
- type: "tell",
481
- from: FROM_AUTO_CONTINUE,
482
- message: `The topic ${field} setting changed. Continue the user's previous work with the new configuration.`,
483
- depth: 0,
484
- silent: false,
485
- timestamp: new Date().toISOString()
486
- });
487
- }
488
- function buildNegotiumMcpServer(ctx) {
489
- const server = new McpServer({ name: RUNTIME_MCP_KEY, version: "1.0.0" });
490
- if (ctx.visualTools === true) {
491
- for (const def of visualToolDefinitions) {
492
- const handler = ctx.peerBridge && (def.name === "show_html" || def.name === "show_mermaid") ? async () => textResult("Visual queued for ordered display on the canonical hub.") : def.handler;
493
- server.tool(def.name, def.description, def.schema, handler);
494
- }
495
- }
496
- const selfConfigCtx = {
497
- topicId: ctx.topicId,
498
- userId: ctx.userId,
499
- cwd: ctx.cwd,
500
- currentUserPrompt: ctx.currentUserPrompt,
501
- onConfigChanged: (field) => appendAutoContinue(ctx, field)
502
- };
503
- for (const def of createSelfConfigToolDefinitions(selfConfigCtx)) {
504
- const handler = ctx.peerBridge ? async (input) => {
505
- const dispatched = dispatchPeerRuntimeSelfConfig({
506
- bridge: ctx.peerBridge,
507
- userId: ctx.userId,
508
- tool: def.name,
509
- input,
510
- ...ctx.currentUserPrompt ? { currentUserPrompt: ctx.currentUserPrompt } : {}
511
- });
512
- if (!dispatched) {
513
- return errorResult("Error: the peer self-config bridge is not available on this node.");
514
- }
515
- return dispatched;
516
- } : def.handler;
517
- server.tool(def.name, def.description, def.schema, handler);
518
- }
519
- const askUserTool = createAskUserToolDefinition({
520
- userId: ctx.userId,
521
- topicId: ctx.topicId,
522
- queryId: ctx.queryId,
523
- agent: ctx.agent,
524
- model: ctx.model
525
- });
526
- const askHandler = ctx.peerBridge ? async (input) => {
527
- const dispatched = dispatchPeerRuntimeAskUser({
528
- bridge: ctx.peerBridge,
529
- userId: ctx.userId,
530
- agent: ctx.agent,
531
- model: ctx.model,
532
- input
533
- });
534
- if (!dispatched) {
535
- return errorResult("Error: the peer ask-user bridge is not available on this node.");
536
- }
537
- return dispatched;
538
- } : askUserTool.handler;
539
- server.tool(askUserTool.name, askUserTool.description, askUserTool.schema, askHandler);
540
- const topic = getTopic(ctx.topicId);
541
- const peerBridge = ctx.peerBridge;
542
- const canSpawnSubagents = peerBridge ? peerBridge.canSpawnSubagents : topic?.kind === "agent" && !topic.isSubagent;
543
- if (canSpawnSubagents) {
544
- const spawnTool = createSpawnSubagentToolDefinition({
545
- userId: ctx.userId,
546
- topicId: ctx.topicId,
547
- queryId: ctx.queryId,
548
- agent: ctx.agent,
549
- model: ctx.model
550
- });
551
- const spawnHandler = peerBridge ? async (input) => {
552
- const dispatched = dispatchPeerRuntimeSpawn({
553
- bridge: peerBridge,
554
- userId: ctx.userId,
555
- agent: ctx.agent,
556
- model: ctx.model,
557
- input
558
- });
559
- if (!dispatched) {
560
- return errorResult("Error: the peer runtime bridge is not available on this node.");
561
- }
562
- return dispatched;
563
- } : spawnTool.handler;
564
- server.tool(spawnTool.name, spawnTool.description, spawnTool.schema, spawnHandler);
565
- if (!peerBridge) {
566
- for (const def of createSubagentManagementToolDefinitions({
567
- userId: ctx.userId,
568
- topicId: ctx.topicId
569
- })) {
570
- server.tool(def.name, def.description, def.schema, def.handler);
571
- }
572
- }
573
- }
574
- server.tool("send_file", "Send a local file to the user in the chat. Use this when you want to share a file (image, document, PDF, code, etc.) with the user. The file will appear as a downloadable item in the chat.", { file_path: z2.string().describe("Absolute path to the file to send") }, async ({ file_path }) => deliverFile(ctx, file_path));
575
- server.tool("send_files", "Send multiple local files to the user in the chat at once.", { file_paths: z2.array(z2.string()).describe("Array of absolute file paths to send") }, async ({ file_paths }) => {
576
- const results = await Promise.all(file_paths.map((filePath) => deliverFile(ctx, filePath)));
577
- const hasError = results.some((result) => ("isError" in result) && result.isError);
578
- return {
579
- content: [
580
- {
581
- type: "text",
582
- text: results.map((result) => result.content.map((c) => c.text).join(`
583
- `)).join(`
584
-
585
- `)
586
- }
587
- ],
588
- ...hasError ? { isError: true } : {}
589
- };
590
- });
591
- registerNodeTools(server, ctx);
592
- return server;
593
- }
594
- function jsonRpcError(status, code, message) {
595
- return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status });
596
- }
597
- function unauthorized() {
598
- return jsonRpcError(401, -32001, "Unauthorized");
599
- }
600
- var closingServers = new WeakSet;
601
- async function closeServer(server) {
602
- if (closingServers.has(server))
603
- return;
604
- closingServers.add(server);
605
- try {
606
- await server.close();
607
- } catch (err) {
608
- logger.warn({ err }, "negotium MCP: server close failed");
609
- }
610
- }
611
- async function handleSse(req, token, ctx) {
612
- if (req.method !== "GET")
613
- return new Response("Method Not Allowed", { status: 405 });
614
- const endpoint = `${SSE_MESSAGE_PATH}?token=${encodeURIComponent(token)}`;
615
- const server = buildNegotiumMcpServer(ctx);
616
- const transport = new SseTransport(endpoint, req);
617
- const response = transport.response();
618
- sseSessions.set(transport.sessionId, { token, transport, server });
619
- transport.onclose = () => {
620
- sseSessions.delete(transport.sessionId);
621
- closeServer(server);
622
- };
623
- transport.onerror = (err) => {
624
- logger.warn({ err }, "negotium MCP: SSE transport error");
625
- };
626
- try {
627
- await server.connect(transport);
628
- } catch (err) {
629
- sseSessions.delete(transport.sessionId);
630
- await transport.close();
631
- await closeServer(server);
632
- throw err;
633
- }
634
- return response;
635
- }
636
- async function handleSseMessage(req, url, token) {
637
- if (req.method !== "POST")
638
- return new Response("Method Not Allowed", { status: 405 });
639
- const sessionId = url.searchParams.get("sessionId");
640
- if (!sessionId)
641
- return new Response("Missing sessionId", { status: 400 });
642
- const session = sseSessions.get(sessionId);
643
- if (!session || session.token !== token)
644
- return new Response("SSE session not found", { status: 404 });
645
- let body;
646
- try {
647
- body = await req.json();
648
- } catch {
649
- return new Response("Invalid JSON", { status: 400 });
650
- }
651
- try {
652
- await session.transport.handleMessage(body, req);
653
- } catch (err) {
654
- logger.warn({ err, sessionId }, "negotium MCP: invalid SSE message");
655
- return new Response("Invalid JSON-RPC message", { status: 400 });
656
- }
657
- return new Response("Accepted", { status: 202 });
658
- }
659
- async function createStreamableSession(token, ctx) {
660
- const server = buildNegotiumMcpServer(ctx);
661
- let transport;
662
- transport = new WebStandardStreamableHTTPServerTransport({
663
- sessionIdGenerator: () => randomUUID2(),
664
- onsessioninitialized: (sessionId) => {
665
- streamableSessions.set(sessionId, { token, transport, server });
666
- },
667
- onsessionclosed: (sessionId) => {
668
- streamableSessions.delete(sessionId);
669
- closeServer(server);
670
- }
671
- });
672
- transport.onclose = () => {
673
- if (transport.sessionId)
674
- streamableSessions.delete(transport.sessionId);
675
- closeServer(server);
676
- };
677
- transport.onerror = (err) => {
678
- logger.warn({ err }, "negotium MCP: streamable HTTP transport error");
679
- };
680
- await server.connect(transport);
681
- return { token, transport, server };
682
- }
683
- async function handleStreamable(req, token, ctx) {
684
- const sessionId = req.headers.get("mcp-session-id");
685
- let session = sessionId ? streamableSessions.get(sessionId) : undefined;
686
- if (session && session.token !== token)
687
- return unauthorized();
688
- if (!session) {
689
- if (sessionId)
690
- return jsonRpcError(404, -32001, "Session not found");
691
- if (req.method !== "POST") {
692
- return jsonRpcError(400, -32000, "Mcp-Session-Id header is required");
693
- }
694
- if (!ctx)
695
- return unauthorized();
696
- session = await createStreamableSession(token, ctx);
697
- }
698
- return session.transport.handleRequest(req);
699
- }
700
- async function handleNegotiumMcpRequest(req) {
701
- const url = new URL(req.url);
702
- if (url.pathname !== RUNTIME_MCP_BASE_PATH && !url.pathname.startsWith(`${RUNTIME_MCP_BASE_PATH}/`)) {
703
- return null;
704
- }
705
- const token = url.searchParams.get("token") ?? "";
706
- try {
707
- if (url.pathname === SSE_PATH) {
708
- const ctx = resolveRuntimeMcpToken(token);
709
- if (!ctx)
710
- return unauthorized();
711
- return handleSse(req, token, ctx);
712
- }
713
- if (url.pathname === SSE_MESSAGE_PATH)
714
- return handleSseMessage(req, url, token);
715
- if (url.pathname === STREAMABLE_PATH) {
716
- return handleStreamable(req, token, resolveRuntimeMcpToken(token));
717
- }
718
- return jsonRpcError(404, -32001, `${RUNTIME_MCP_KEY} endpoint not found`);
719
- } catch (err) {
720
- logger.error({ err, path: url.pathname }, "negotium MCP: request failed");
721
- return jsonRpcError(500, -32603, "Internal MCP server error");
722
- }
723
- }
724
- // ../../packages/node/src/control.ts
725
- import { randomUUID as randomUUID4, timingSafeEqual } from "crypto";
726
- import {
727
- chmodSync,
728
- existsSync as existsSync2,
729
- mkdirSync as mkdirSync2,
730
- readFileSync as readFileSync2,
731
- renameSync,
732
- unlinkSync,
733
- writeFileSync as writeFileSync2
734
- } from "fs";
735
- import { dirname, resolve as resolve2 } from "path";
736
-
737
- // ../../packages/node/src/files.ts
738
- import { randomUUID as randomUUID3 } from "crypto";
739
- import {
740
- copyFileSync,
741
- existsSync,
742
- mkdirSync,
743
- readFileSync,
744
- rmSync,
745
- statSync as statSync2,
746
- writeFileSync
747
- } from "fs";
748
- import { basename as basename2, extname as extname2, join } from "path";
749
- var MAX_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
750
- var FILE_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
751
- var MIME_BY_EXT = {
752
- ".csv": "text/csv",
753
- ".gif": "image/gif",
754
- ".html": "text/html",
755
- ".jpeg": "image/jpeg",
756
- ".jpg": "image/jpeg",
757
- ".json": "application/json",
758
- ".md": "text/markdown",
759
- ".mov": "video/quicktime",
760
- ".mp3": "audio/mpeg",
761
- ".mp4": "video/mp4",
762
- ".pdf": "application/pdf",
763
- ".png": "image/png",
764
- ".svg": "image/svg+xml",
765
- ".txt": "text/plain",
766
- ".wav": "audio/wav",
767
- ".webm": "video/webm",
768
- ".webp": "image/webp",
769
- ".zip": "application/zip"
770
- };
771
- function safeExtension(filename) {
772
- const extension = extname2(basename2(filename));
773
- return /^\.[A-Za-z0-9]{1,16}$/.test(extension) ? extension.toLowerCase() : "";
774
- }
775
- function attachmentType(mimeType) {
776
- if (mimeType.startsWith("image/"))
777
- return "image";
778
- if (mimeType.startsWith("audio/"))
779
- return "audio";
780
- return "file";
781
- }
782
- function contentDisposition(filename) {
783
- return `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`;
784
- }
785
-
786
- class NodeFileStore {
787
- uploadDir;
788
- constructor(uploadDir = join(DATA_DIR, "uploads")) {
789
- this.uploadDir = uploadDir;
790
- }
791
- #ensureDir() {
792
- mkdirSync(this.uploadDir, { recursive: true });
793
- }
794
- #metadataPath(fileId) {
795
- return join(this.uploadDir, `${fileId}.meta.json`);
796
- }
797
- #metadata(fileId) {
798
- if (!FILE_ID_RE.test(fileId))
799
- return null;
800
- try {
801
- const parsed = JSON.parse(readFileSync(this.#metadataPath(fileId), "utf8"));
802
- if (typeof parsed.filename !== "string" || typeof parsed.mimeType !== "string" || typeof parsed.sizeBytes !== "number" || typeof parsed.savedName !== "string") {
803
- return null;
804
- }
805
- return parsed;
806
- } catch {
807
- return null;
808
- }
809
- }
810
- #attachment(fileId, metadata) {
811
- const userQuery = metadata.ownerUserId ? `?user=${encodeURIComponent(metadata.ownerUserId)}` : "";
812
- return {
813
- id: fileId,
814
- type: attachmentType(metadata.mimeType),
815
- filename: metadata.filename,
816
- url: `/api/v1/control/files/${fileId}${userQuery}`,
817
- mimeType: metadata.mimeType,
818
- sizeBytes: metadata.sizeBytes
819
- };
820
- }
821
- hooks = {
822
- resolveAttachmentByFileId: (fileId) => {
823
- const metadata = this.#metadata(fileId);
824
- if (!metadata || !existsSync(join(this.uploadDir, metadata.savedName)))
825
- return null;
826
- return this.#attachment(fileId, metadata);
827
- },
828
- resolveUploadedFilePathByFileId: (fileId) => {
829
- const metadata = this.#metadata(fileId);
830
- if (!metadata)
831
- return null;
832
- const path = join(this.uploadDir, metadata.savedName);
833
- return existsSync(path) ? path : null;
834
- },
835
- storeLocalFileAsUpload: (absPath, access = {}) => this.store(absPath, access),
836
- deleteFilesForTopic: (topicId) => this.deleteForTopic(topicId)
837
- };
838
- store(absPath, access = {}) {
839
- this.#ensureDir();
840
- const fileId = randomUUID3();
841
- const extension = safeExtension(absPath);
842
- const savedName = `${fileId}${extension}`;
843
- const savedPath = join(this.uploadDir, savedName);
844
- try {
845
- const stats = statSync2(absPath);
846
- if (!stats.isFile() || stats.size > MAX_UPLOAD_BYTES)
847
- return null;
848
- const metadata = {
849
- filename: basename2(absPath),
850
- mimeType: MIME_BY_EXT[extension] ?? "application/octet-stream",
851
- sizeBytes: stats.size,
852
- savedName,
853
- ...access.ownerUserId ? { ownerUserId: access.ownerUserId } : {},
854
- ...access.topicId ? { topicId: access.topicId } : {},
855
- ...access.visibility ? { visibility: access.visibility } : {}
856
- };
857
- copyFileSync(absPath, savedPath);
858
- writeFileSync(this.#metadataPath(fileId), JSON.stringify(metadata), { mode: 384 });
859
- return this.#attachment(fileId, metadata);
860
- } catch (error) {
861
- rmSync(savedPath, { force: true });
862
- rmSync(this.#metadataPath(fileId), { force: true });
863
- logger.warn({ err: error, absPath }, "node files: failed to store local upload");
864
- return null;
865
- }
866
- }
867
- response(fileId, userId) {
868
- const metadata = this.#metadata(fileId);
869
- if (!metadata)
870
- return null;
871
- const topic = metadata.topicId ? getTopic(metadata.topicId) : null;
872
- const allowed = metadata.visibility === "workspace" || metadata.ownerUserId === userId || Boolean(topic && isParticipant(topic, userId));
873
- if (!allowed)
874
- return null;
875
- const path = join(this.uploadDir, metadata.savedName);
876
- if (!existsSync(path))
877
- return null;
878
- return new Response(Bun.file(path), {
879
- headers: {
880
- "content-disposition": contentDisposition(metadata.filename),
881
- "content-length": String(metadata.sizeBytes),
882
- "content-type": metadata.mimeType,
883
- "x-content-type-options": "nosniff"
884
- }
885
- });
886
- }
887
- deleteForTopic(topicId) {
888
- this.#ensureDir();
889
- for (const name of new Bun.Glob("*.meta.json").scanSync({ cwd: this.uploadDir })) {
890
- const fileId = name.slice(0, -".meta.json".length);
891
- const metadata = this.#metadata(fileId);
892
- if (metadata?.topicId !== topicId)
893
- continue;
894
- rmSync(join(this.uploadDir, metadata.savedName), { force: true });
895
- rmSync(this.#metadataPath(fileId), { force: true });
896
- }
897
- }
898
- }
899
- var nodeFileStore = new NodeFileStore;
900
-
901
- // ../../packages/node/src/control.ts
902
- var NODE_CONTROL_PROTOCOL_VERSION = 1;
903
- var NODE_CONTROL_BASE_PATH = "/api/v1/control";
904
- var NODE_DAEMON_ROLE = "node-daemon";
905
- var NODE_DAEMON_INFO_PATH = resolve2(RUN_DIR, "node-daemon.json");
906
- var NODE_VERSION = NEGOTIUM_VERSION;
907
- function jsonError(status, error) {
908
- return Response.json({ ok: false, error }, { status });
909
- }
910
- function topicServiceError(error) {
911
- const status = error.code === "TOPIC_NOT_FOUND" ? 404 : error.code === "TOPIC_FORBIDDEN" ? 403 : 400;
912
- return jsonError(status, error.message);
913
- }
914
- function safeEqual(left, right) {
915
- const a = Buffer.from(left);
916
- const b = Buffer.from(right);
917
- return a.length === b.length && timingSafeEqual(a, b);
918
- }
919
- function authorized(req) {
920
- const header = req.headers.get("authorization") ?? "";
921
- const token = header.startsWith("Bearer ") ? header.slice(7) : "";
922
- return Boolean(token) && safeEqual(token, NODE_CONTROL_TOKEN);
923
- }
924
- async function bodyRecord(req) {
925
- try {
926
- const value = await req.json();
927
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
928
- } catch {
929
- return {};
930
- }
931
- }
932
- function requiredText(value, name) {
933
- if (typeof value !== "string" || !value.trim())
934
- throw new Error(`${name} is required`);
935
- return value.trim();
936
- }
937
- function topicsForUser(userId) {
938
- const runningTopics = listRunningTopicQueries();
939
- return getVisibleTopics().filter((topic) => isParticipant(topic, userId)).map((topic) => {
940
- const runningQueryId = runningTopics.get(topic.id);
941
- return { ...topic, running: Boolean(runningQueryId), runningQueryId };
942
- });
943
- }
944
- function topicForUser(topicId, userId) {
945
- const topic = getTopic(topicId);
946
- return topic && isParticipant(topic, userId) ? topic : null;
947
- }
948
- function runtimeEvent(event) {
949
- return {
950
- type: event.type,
951
- topicId: event.topicId,
952
- payload: event.payload,
953
- seq: event.seq,
954
- createdAt: event.createdAt
955
- };
956
- }
957
- function createEventStream(req, userId, after) {
958
- const encoder = new TextEncoder;
959
- const allowedTopics = new Set(topicsForUser(userId).map((topic) => topic.id));
960
- let cursor = Math.max(0, after);
961
- let closed = false;
962
- let pumping = false;
963
- let pollTimer;
964
- let heartbeatTimer;
965
- const stream = new ReadableStream({
966
- start(controller) {
967
- const send = (event, data, id) => {
968
- if (closed)
969
- return;
970
- const lines = [
971
- id === undefined ? "" : `id: ${id}`,
972
- `event: ${event}`,
973
- `data: ${JSON.stringify(data)}`
974
- ].filter(Boolean).join(`
975
- `);
976
- controller.enqueue(encoder.encode(`${lines}
977
-
978
- `));
979
- };
980
- const close = () => {
981
- if (closed)
982
- return;
983
- closed = true;
984
- if (pollTimer)
985
- clearInterval(pollTimer);
986
- if (heartbeatTimer)
987
- clearInterval(heartbeatTimer);
988
- try {
989
- controller.close();
990
- } catch {}
991
- };
992
- const pump = () => {
993
- if (closed || pumping)
994
- return;
995
- pumping = true;
996
- try {
997
- while (!closed) {
998
- const events = listRuntimeEventsAfter(cursor, 500);
999
- if (events.length === 0)
1000
- break;
1001
- for (const event of events) {
1002
- cursor = event.seq;
1003
- if (event.type === "topic-created" || event.type === "topic-updated") {
1004
- const topic = getTopic(event.topicId);
1005
- if (topic && isParticipant(topic, userId))
1006
- allowedTopics.add(event.topicId);
1007
- else
1008
- allowedTopics.delete(event.topicId);
1009
- }
1010
- const visible = allowedTopics.has(event.topicId);
1011
- if (visible)
1012
- send("runtime", runtimeEvent(event), event.seq);
1013
- if (event.type === "topic-deleted")
1014
- allowedTopics.delete(event.topicId);
1015
- }
1016
- send("cursor", { cursor }, cursor);
1017
- if (events.length < 500)
1018
- break;
1019
- }
1020
- } finally {
1021
- pumping = false;
1022
- }
1023
- };
1024
- send("ready", { protocolVersion: NODE_CONTROL_PROTOCOL_VERSION, cursor });
1025
- pump();
1026
- pollTimer = setInterval(pump, 100);
1027
- heartbeatTimer = setInterval(() => {
1028
- if (!closed)
1029
- controller.enqueue(encoder.encode(`: heartbeat ${Date.now()}
1030
-
1031
- `));
1032
- }, 15000);
1033
- req.signal.addEventListener("abort", close, { once: true });
1034
- },
1035
- cancel() {
1036
- closed = true;
1037
- if (pollTimer)
1038
- clearInterval(pollTimer);
1039
- if (heartbeatTimer)
1040
- clearInterval(heartbeatTimer);
1041
- }
1042
- });
1043
- return new Response(stream, {
1044
- headers: {
1045
- "cache-control": "no-cache, no-transform",
1046
- connection: "keep-alive",
1047
- "content-type": "text/event-stream; charset=utf-8",
1048
- "x-accel-buffering": "no"
1049
- }
1050
- });
1051
- }
1052
- function createNodeControlHandler(options) {
1053
- return async (req) => {
1054
- const url = new URL(req.url);
1055
- if (!url.pathname.startsWith(NODE_CONTROL_BASE_PATH))
1056
- return null;
1057
- if (!authorized(req))
1058
- return jsonError(401, "Unauthorized");
1059
- const path = url.pathname.slice(NODE_CONTROL_BASE_PATH.length) || "/";
1060
- try {
1061
- if (req.method === "GET" && path === "/status") {
1062
- return Response.json({
1063
- ok: true,
1064
- protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
1065
- nodeVersion: NODE_VERSION,
1066
- pid: process.pid,
1067
- port: options.port(),
1068
- stateDir: STATE_DIR,
1069
- startedAt: options.startedAt
1070
- });
1071
- }
1072
- if (req.method === "POST" && path === "/shutdown") {
1073
- setTimeout(options.requestShutdown, 10);
1074
- return Response.json({ ok: true });
1075
- }
1076
- if (req.method === "GET" && path === "/session") {
1077
- const userId = requiredText(url.searchParams.get("user"), "user");
1078
- ensurePersonalGeneral(userId);
1079
- return Response.json({
1080
- ok: true,
1081
- protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
1082
- nodeVersion: NODE_VERSION,
1083
- topics: topicsForUser(userId),
1084
- cursor: latestRuntimeEventSeq()
1085
- });
1086
- }
1087
- if (req.method === "GET" && path === "/topics") {
1088
- const userId = requiredText(url.searchParams.get("user"), "user");
1089
- return Response.json({ ok: true, topics: topicsForUser(userId) });
1090
- }
1091
- if (req.method === "GET" && path === "/background-sessions") {
1092
- const userId = requiredText(url.searchParams.get("user"), "user");
1093
- return Response.json({ ok: true, sessions: listBackgroundSessionsForUser(userId) });
1094
- }
1095
- const fileMatch = path.match(/^\/files\/([0-9a-f-]+)$/i);
1096
- if (fileMatch && req.method === "GET") {
1097
- const userId = requiredText(url.searchParams.get("user"), "user");
1098
- return nodeFileStore.response(fileMatch[1], userId) ?? jsonError(404, "File not found");
1099
- }
1100
- if (req.method === "POST" && path === "/vault/command") {
1101
- const body = await bodyRecord(req);
1102
- const userId = requiredText(body.userId, "userId");
1103
- const commandLine = requiredText(body.commandLine, "commandLine");
1104
- const result = executeVaultCommand(userId, commandLine);
1105
- if (result === null)
1106
- return jsonError(400, "Invalid Vault command");
1107
- return Response.json({ ok: true, result });
1108
- }
1109
- if (req.method === "GET" && path === "/vault") {
1110
- const userId = requiredText(url.searchParams.get("user"), "user");
1111
- return Response.json({ ok: true, entries: listVaultEntries(userId) });
1112
- }
1113
- if (req.method === "POST" && path === "/vault") {
1114
- const body = await bodyRecord(req);
1115
- const userId = requiredText(body.userId, "userId");
1116
- const key = requiredText(body.key, "key");
1117
- if (typeof body.value !== "string")
1118
- throw new Error("value is required");
1119
- const description = body.description === undefined ? "" : String(body.description);
1120
- return Response.json({
1121
- ok: true,
1122
- result: saveVaultEntry(userId, key, body.value, description)
1123
- });
1124
- }
1125
- if (req.method === "DELETE" && path === "/vault") {
1126
- const userId = requiredText(url.searchParams.get("user"), "user");
1127
- const key = requiredText(url.searchParams.get("key"), "key");
1128
- return Response.json({ ok: true, deleted: deleteVaultEntry(userId, key) });
1129
- }
1130
- if (req.method === "POST" && path === "/topics") {
1131
- const body = await bodyRecord(req);
1132
- const userId = requiredText(body.userId, "userId");
1133
- const title = requiredText(body.title, "title");
1134
- const agent = body.agent;
1135
- if (agent !== undefined && !["claude", "codex", "maestro"].includes(String(agent))) {
1136
- return jsonError(400, "Invalid agent");
1137
- }
1138
- const topic = topicService.create({
1139
- title,
1140
- userId,
1141
- kind: "agent",
1142
- ...agent ? { agent } : {}
1143
- });
1144
- return Response.json({ ok: true, topic }, { status: 201 });
1145
- }
1146
- if (req.method === "GET" && path === "/events") {
1147
- const userId = requiredText(url.searchParams.get("user"), "user");
1148
- const parsed = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
1149
- return createEventStream(req, userId, Number.isFinite(parsed) ? parsed : 0);
1150
- }
1151
- const messagesMatch = path.match(/^\/topics\/([^/]+)\/messages$/);
1152
- if (messagesMatch && req.method === "GET") {
1153
- const topicId = decodeURIComponent(messagesMatch[1]);
1154
- const userId = requiredText(url.searchParams.get("user"), "user");
1155
- if (!topicForUser(topicId, userId))
1156
- return jsonError(404, "Topic not found");
1157
- const cursor = url.searchParams.get("cursor");
1158
- const parsedLimit = Number.parseInt(url.searchParams.get("limit") ?? "50", 10);
1159
- const result = listApiMessages(topicId, {
1160
- cursor,
1161
- limit: Number.isFinite(parsedLimit) ? parsedLimit : 50
1162
- });
1163
- return Response.json({
1164
- ok: true,
1165
- messages: result.page,
1166
- cursor: result.cursor,
1167
- hasMore: result.hasMore
1168
- });
1169
- }
1170
- if (messagesMatch && req.method === "POST") {
1171
- const topicId = decodeURIComponent(messagesMatch[1]);
1172
- const body = await bodyRecord(req);
1173
- const userId = requiredText(body.userId, "userId");
1174
- const text = requiredText(body.text, "text");
1175
- const topic = topicForUser(topicId, userId);
1176
- if (!topic)
1177
- return jsonError(404, "Topic not found");
1178
- const { message } = submitUserMessage({
1179
- topic,
1180
- userId,
1181
- text,
1182
- sourceAdapter: "terminal",
1183
- startTurn: options.startTurn
1184
- });
1185
- return Response.json({ ok: true, message }, { status: 201 });
1186
- }
1187
- const recentMatch = path.match(/^\/topics\/([^/]+)\/recent-events$/);
1188
- if (recentMatch && req.method === "GET") {
1189
- const topicId = decodeURIComponent(recentMatch[1]);
1190
- const userId = requiredText(url.searchParams.get("user"), "user");
1191
- if (!topicForUser(topicId, userId))
1192
- return jsonError(404, "Topic not found");
1193
- const events = listRecentRuntimeEventsForTopic(topicId).map(runtimeEvent);
1194
- return Response.json({ ok: true, events });
1195
- }
1196
- const modelMatch = path.match(/^\/topics\/([^/]+)\/model$/);
1197
- if (modelMatch && req.method === "POST") {
1198
- const topicId = decodeURIComponent(modelMatch[1]);
1199
- const body = await bodyRecord(req);
1200
- const userId = requiredText(body.userId, "userId");
1201
- const model = requiredText(body.model, "model");
1202
- if (!topicForUser(topicId, userId))
1203
- return jsonError(404, "Topic not found");
1204
- const result = switchTopicModel({ topicId, userId, model });
1205
- if (!result.ok)
1206
- return jsonError(400, result.error);
1207
- return Response.json({ ok: true, model: result.model, result: result.text });
1208
- }
1209
- const effortMatch = path.match(/^\/topics\/([^/]+)\/effort$/);
1210
- if (effortMatch && req.method === "POST") {
1211
- const topicId = decodeURIComponent(effortMatch[1]);
1212
- const body = await bodyRecord(req);
1213
- const userId = requiredText(body.userId, "userId");
1214
- const effort = requiredText(body.effort, "effort");
1215
- if (!topicForUser(topicId, userId))
1216
- return jsonError(404, "Topic not found");
1217
- const result = switchTopicEffort({ topicId, userId, effort });
1218
- if (!result.ok)
1219
- return jsonError(400, result.error);
1220
- return Response.json({ ok: true, effort: result.effort, result: result.text });
1221
- }
1222
- const deleteMatch = path.match(/^\/topics\/([^/]+)$/);
1223
- if (deleteMatch && req.method === "DELETE") {
1224
- const topicId = decodeURIComponent(deleteMatch[1]);
1225
- const userId = requiredText(url.searchParams.get("user"), "user");
1226
- await topicService.delete({ topicId, userId });
1227
- return Response.json({ ok: true });
1228
- }
1229
- const abortMatch = path.match(/^\/topics\/([^/]+)\/abort$/);
1230
- if (abortMatch && req.method === "POST") {
1231
- const topicId = decodeURIComponent(abortMatch[1]);
1232
- const body = await bodyRecord(req);
1233
- const userId = requiredText(body.userId, "userId");
1234
- return Response.json({ ok: true, aborted: topicService.abortTurn(topicId, userId) });
1235
- }
1236
- const resetMatch = path.match(/^\/topics\/([^/]+)\/session\/reset$/);
1237
- if (resetMatch && req.method === "POST") {
1238
- const topicId = decodeURIComponent(resetMatch[1]);
1239
- const body = await bodyRecord(req);
1240
- const userId = requiredText(body.userId, "userId");
1241
- const result = await topicService.reset({
1242
- topicId,
1243
- userId,
1244
- reason: "node-control-session-reset"
1245
- });
1246
- if (result.isError)
1247
- return jsonError(409, result.text);
1248
- return Response.json({ ok: true, result: result.text });
1249
- }
1250
- const compactMatch = path.match(/^\/topics\/([^/]+)\/session\/compact$/);
1251
- if (compactMatch && req.method === "POST") {
1252
- const topicId = decodeURIComponent(compactMatch[1]);
1253
- const body = await bodyRecord(req);
1254
- const userId = requiredText(body.userId, "userId");
1255
- const result = await topicService.compact({
1256
- topicId,
1257
- userId,
1258
- reason: "node-control-session-compact",
1259
- compactSession: options.compactSession
1260
- });
1261
- if (result.isError)
1262
- return jsonError(409, result.text);
1263
- return Response.json({ ok: true, result: result.text });
1264
- }
1265
- const deriveMatch = path.match(/^\/topics\/([^/]+)\/derive$/);
1266
- if (deriveMatch && req.method === "POST") {
1267
- const topicId = decodeURIComponent(deriveMatch[1]);
1268
- const body = await bodyRecord(req);
1269
- const userId = requiredText(body.userId, "userId");
1270
- if (typeof body.copyHistory !== "boolean") {
1271
- return jsonError(400, "copyHistory must be a boolean");
1272
- }
1273
- const copyHistory = body.copyHistory;
1274
- const source = topicForUser(topicId, userId);
1275
- if (!source || source.kind === "manager")
1276
- return jsonError(404, "Topic not found");
1277
- if (body.name !== undefined && typeof body.name !== "string") {
1278
- return jsonError(400, "name must be a string");
1279
- }
1280
- const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : undefined;
1281
- const derived = await topicService.derive({
1282
- sourceTopicId: topicId,
1283
- userId,
1284
- copyHistory,
1285
- ...name ? { name } : {}
1286
- });
1287
- if (!derived)
1288
- return jsonError(500, "Failed to derive topic");
1289
- return Response.json({ ok: true, topic: derived }, { status: 201 });
1290
- }
1291
- const answerMatch = path.match(/^\/questions\/([^/]+)\/answer$/);
1292
- if (answerMatch && req.method === "POST") {
1293
- const messageId = decodeURIComponent(answerMatch[1]);
1294
- const body = await bodyRecord(req);
1295
- const topicId = requiredText(body.topicId, "topicId");
1296
- const userId = requiredText(body.userId, "userId");
1297
- const label = requiredText(body.label, "label");
1298
- const result = topicService.answerQuestion(topicId, messageId, label, userId);
1299
- return Response.json(result, { status: result.ok ? 200 : 409 });
1300
- }
1301
- return jsonError(404, "Control route not found");
1302
- } catch (error) {
1303
- if (error instanceof TopicServiceError)
1304
- return topicServiceError(error);
1305
- if (error instanceof TopicDeriveBusyError)
1306
- return jsonError(409, error.message);
1307
- if (error instanceof TopicTitleConflictError)
1308
- return jsonError(409, error.message);
1309
- return jsonError(400, error instanceof Error ? error.message : String(error));
1310
- }
1311
- };
1312
- }
1313
- function writeNodeDaemonInfo(port, startedAt) {
1314
- const info = {
1315
- schemaVersion: 1,
1316
- protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
1317
- nodeVersion: NODE_VERSION,
1318
- pid: process.pid,
1319
- port,
1320
- stateDir: STATE_DIR,
1321
- startedAt
1322
- };
1323
- mkdirSync2(dirname(NODE_DAEMON_INFO_PATH), { recursive: true });
1324
- const temporary = `${NODE_DAEMON_INFO_PATH}.${process.pid}.${randomUUID4()}.tmp`;
1325
- writeFileSync2(temporary, `${JSON.stringify(info, null, 2)}
1326
- `, { mode: 384 });
1327
- chmodSync(temporary, 384);
1328
- renameSync(temporary, NODE_DAEMON_INFO_PATH);
1329
- chmodSync(NODE_DAEMON_INFO_PATH, 384);
1330
- return info;
1331
- }
1332
- function readNodeDaemonInfo() {
1333
- if (!existsSync2(NODE_DAEMON_INFO_PATH))
1334
- return null;
1335
- try {
1336
- const value = JSON.parse(readFileSync2(NODE_DAEMON_INFO_PATH, "utf8"));
1337
- if (value?.schemaVersion !== 1 || value.stateDir !== STATE_DIR || !Number.isInteger(value.protocolVersion) || !Number.isInteger(value.pid) || !Number.isInteger(value.port) || value.port <= 0) {
1338
- return null;
1339
- }
1340
- return value;
1341
- } catch {
1342
- return null;
1343
- }
1344
- }
1345
- function removeNodeDaemonInfo(expected) {
1346
- const current = readNodeDaemonInfo();
1347
- if (!current || current.pid !== expected.pid || current.port !== expected.port)
1348
- return;
1349
- try {
1350
- unlinkSync(NODE_DAEMON_INFO_PATH);
1351
- } catch {}
1352
- }
1353
- async function fetchWithTimeout(url, init, timeoutMs) {
1354
- const controller = new AbortController;
1355
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1356
- try {
1357
- return await fetch(url, { ...init, signal: controller.signal });
1358
- } finally {
1359
- clearTimeout(timer);
1360
- }
1361
- }
1362
- async function inspectNodeDaemon(timeoutMs = 750) {
1363
- const info = readNodeDaemonInfo();
1364
- if (!info)
1365
- return { running: false };
1366
- try {
1367
- const response = await fetchWithTimeout(`http://127.0.0.1:${info.port}${NODE_CONTROL_BASE_PATH}/status`, { headers: { authorization: `Bearer ${NODE_CONTROL_TOKEN}` } }, timeoutMs);
1368
- if (!response.ok)
1369
- throw new Error(`status returned HTTP ${response.status}`);
1370
- const status = await response.json();
1371
- if (status.protocolVersion !== NODE_CONTROL_PROTOCOL_VERSION || status.stateDir !== STATE_DIR || status.pid !== info.pid) {
1372
- throw new Error("node identity does not match the local state directory");
1373
- }
1374
- return { running: true, info };
1375
- } catch (error) {
1376
- return {
1377
- running: false,
1378
- info,
1379
- error: error instanceof Error ? error.message : String(error)
1380
- };
1381
- }
1382
- }
1383
- async function waitForNodeDaemon(timeoutMs = 1e4) {
1384
- const deadline = Date.now() + timeoutMs;
1385
- let lastError = "node did not publish connection information";
1386
- while (Date.now() < deadline) {
1387
- const status = await inspectNodeDaemon();
1388
- if (status.running && status.info) {
1389
- return {
1390
- baseUrl: `http://127.0.0.1:${status.info.port}`,
1391
- token: NODE_CONTROL_TOKEN,
1392
- info: status.info
1393
- };
1394
- }
1395
- if (status.error)
1396
- lastError = status.error;
1397
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
1398
- }
1399
- throw new Error(`Negotium node failed to start: ${lastError}`);
1400
- }
1401
- async function stopNodeDaemon(timeoutMs = 3000) {
1402
- const status = await inspectNodeDaemon();
1403
- if (!status.running || !status.info)
1404
- return false;
1405
- const response = await fetchWithTimeout(`http://127.0.0.1:${status.info.port}${NODE_CONTROL_BASE_PATH}/shutdown`, {
1406
- method: "POST",
1407
- headers: { authorization: `Bearer ${NODE_CONTROL_TOKEN}` }
1408
- }, timeoutMs);
1409
- if (!response.ok)
1410
- throw new Error(`node shutdown returned HTTP ${response.status}`);
1411
- return true;
1412
- }
1413
-
1414
- // ../../packages/node/src/index.ts
1415
- async function availableLoopbackPort() {
1416
- return new Promise((resolve3, reject) => {
1417
- const probe = createServer();
1418
- probe.unref();
1419
- probe.once("error", reject);
1420
- probe.listen(0, "127.0.0.1", () => {
1421
- const address = probe.address();
1422
- const port = address && typeof address === "object" ? address.port : 0;
1423
- probe.close((error) => {
1424
- if (error)
1425
- reject(error);
1426
- else if (port > 0)
1427
- resolve3(port);
1428
- else
1429
- reject(new Error("failed to allocate a loopback port"));
1430
- });
1431
- });
1432
- });
1433
- }
1434
- async function wireNodeMcps(host, manifest) {
1435
- const entries = [];
1436
- for (const spec of manifest.list()) {
1437
- if (!manifest.isEnabled(spec.key))
1438
- continue;
1439
- try {
1440
- if (spec.transport === "http") {
1441
- const instance = await host.ensure(spec.key);
1442
- if (!instance.port)
1443
- throw new Error("no port allocated");
1444
- entries.push({ key: spec.key, kind: "http", port: instance.port });
1445
- } else {
1446
- entries.push({
1447
- key: spec.key,
1448
- kind: "stdio",
1449
- command: spec.command,
1450
- args: spec.args,
1451
- ...spec.env ? { env: spec.env } : {}
1452
- });
1453
- }
1454
- } catch (err) {
1455
- logger.warn({ err, key: spec.key }, "node mcp: failed to bring up manifest server");
1456
- }
1457
- }
1458
- setNodeMcpServers(entries);
1459
- if (entries.length > 0) {
1460
- logger.info({ keys: entries.map((e) => e.key) }, "node mcp: manifest servers installed");
1461
- }
1462
- }
1463
- function startNode(opts = {}) {
1464
- sweepStaleSubagentCards();
1465
- setFileHooks(nodeFileStore.hooks);
1466
- const startedAt = new Date().toISOString();
1467
- const processLease = opts.singleton ? acquireRuntimeProcessLease(NODE_DAEMON_ROLE, {
1468
- onLost: () => {
1469
- logger.error("node daemon: singleton lease lost; shutting down");
1470
- runShutdown("test");
1471
- }
1472
- }) : null;
1473
- if (opts.singleton && !processLease) {
1474
- throw new Error(`a Negotium node is already running for ${STATE_DIR}`);
1475
- }
1476
- let requestStop = () => {
1477
- runShutdown("test");
1478
- };
1479
- let server;
1480
- const control = createNodeControlHandler({
1481
- port: () => server?.port ?? 0,
1482
- startedAt,
1483
- requestShutdown: () => requestStop()
1484
- });
1485
- try {
1486
- server = Bun.serve({
1487
- port: opts.port ?? NEGOTIUM_PORT,
1488
- hostname: "127.0.0.1",
1489
- idleTimeout: 240,
1490
- ...opts.maxRequestBodySize ? { maxRequestBodySize: opts.maxRequestBodySize } : {},
1491
- async fetch(req) {
1492
- const controlResponse = await control(req);
1493
- if (controlResponse)
1494
- return controlResponse;
1495
- const plugin = await runNodeRequestHandlers(req);
1496
- if (plugin)
1497
- return plugin;
1498
- const mcp = await handleNegotiumMcpRequest(req);
1499
- if (mcp)
1500
- return mcp;
1501
- const url = new URL(req.url);
1502
- if (url.pathname === "/health") {
1503
- return Response.json({
1504
- ok: true,
1505
- name: "negotium",
1506
- pid: process.pid,
1507
- protocolVersion: NODE_CONTROL_PROTOCOL_VERSION,
1508
- stateDir: STATE_DIR
1509
- });
1510
- }
1511
- return new Response("negotium node", { status: 404 });
1512
- }
1513
- });
1514
- } catch (error) {
1515
- processLease?.stop();
1516
- throw error;
1517
- }
1518
- const port = server.port;
1519
- if (!port) {
1520
- processLease?.stop();
1521
- throw new Error("negotium node failed to bind a port");
1522
- }
1523
- setRuntimeMcpPort(port);
1524
- const stopTurnRequests = startDurableTurnRequestWorker();
1525
- const stopInbox = startSessionInboxWorker();
1526
- let modules;
1527
- try {
1528
- modules = startNegotiumNodeModules(opts.modules ?? [], {
1529
- port,
1530
- stateDir: STATE_DIR,
1531
- dataDir: DATA_DIR,
1532
- runDir: RUN_DIR,
1533
- workspaceDir: WORKSPACE_DIR,
1534
- bus: runtimeBus()
1535
- });
1536
- } catch (error) {
1537
- stopTurnRequests();
1538
- stopInbox();
1539
- server.stop(true);
1540
- processLease?.stop();
1541
- throw error;
1542
- }
1543
- const mcpHost = new McpHost;
1544
- const manifest = new McpManifest;
1545
- const stopSweeper = mcpHost.startSweeper();
1546
- let resolveCompleted;
1547
- const completed = new Promise((resolve3) => {
1548
- resolveCompleted = resolve3;
1549
- });
1550
- let advertised = null;
1551
- try {
1552
- advertised = opts.advertise ? writeNodeDaemonInfo(port, startedAt) : null;
1553
- } catch (error) {
1554
- stopSweeper();
1555
- stopTurnRequests();
1556
- stopInbox();
1557
- server.stop(true);
1558
- processLease?.stop();
1559
- modules.stop();
1560
- mcpHost.stopAll();
1561
- throw error;
1562
- }
1563
- wireNodeMcps(mcpHost, manifest);
1564
- onShutdown("node-server", 130, () => {
1565
- stopTurnRequests();
1566
- stopInbox();
1567
- server.stop(true);
1568
- });
1569
- if (advertised) {
1570
- onShutdown("node-daemon-advertisement", 129, () => {
1571
- removeNodeDaemonInfo({ pid: advertised.pid, port: advertised.port });
1572
- });
1573
- }
1574
- if (processLease)
1575
- onShutdown("node-daemon-lease", 128, () => processLease.stop());
1576
- onShutdown("active-agent-turns", 120, async () => {
1577
- abortAllRooms();
1578
- await killOwnedCodexTreesForShutdown();
1579
- });
1580
- onShutdown("node-modules", 110, () => modules.stop());
1581
- onShutdown("node-mcp-host", 50, async () => {
1582
- stopSweeper();
1583
- await mcpHost.stopAll();
1584
- });
1585
- onShutdown("playwright", 50, () => killAllPlaywright());
1586
- onShutdown("background-bash", 50, () => killAllBgBash());
1587
- onShutdown("node-completed", -100, resolveCompleted);
1588
- logger.info({
1589
- port,
1590
- stateDir: STATE_DIR,
1591
- plugins: nodeRequestHandlerNames(),
1592
- modules: modules.names,
1593
- capabilities: modules.capabilities
1594
- }, "negotium node started");
1595
- const stop = () => runShutdown("test");
1596
- requestStop = () => {
1597
- stop();
1598
- };
1599
- return {
1600
- port,
1601
- completed,
1602
- stop
1603
- };
1604
- }
1605
- async function startDefaultNode(opts = {}) {
1606
- const modules = [];
1607
- if (process.env.NEGOTIUM_CRON !== "0") {
1608
- const { createCronModule } = await import("./chunk-51rcsavw.js");
1609
- modules.push(createCronModule());
1610
- }
1611
- const port = opts.port === 0 ? await availableLoopbackPort() : opts.port;
1612
- return startNode({ ...opts, port, modules });
1613
- }
1614
- async function runNodeDaemon(opts = {}) {
1615
- const node = await startDefaultNode({
1616
- port: opts.port ?? 0,
1617
- advertise: true,
1618
- singleton: true
1619
- });
1620
- await node.completed;
1621
- }
1622
- export {
1623
- waitForNodeDaemon,
1624
- stopNodeDaemon,
1625
- startNode,
1626
- startDefaultNode,
1627
- runNodeDaemon,
1628
- readNodeDaemonInfo,
1629
- inspectNodeDaemon,
1630
- NODE_DAEMON_INFO_PATH,
1631
- NODE_CONTROL_PROTOCOL_VERSION,
1632
- NODE_CONTROL_BASE_PATH
1633
- };
1634
-
1635
- export { NODE_CONTROL_PROTOCOL_VERSION, NODE_CONTROL_BASE_PATH, startDefaultNode };
1636
-
1637
- //# debugId=47FD99100B6F70D764756E2164756E21