negotium 0.1.12 → 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 (137) hide show
  1. package/dist/canonical-mcp-bridge.js +2 -3
  2. package/dist/canonical-mcp-bridge.js.map +1 -1
  3. package/dist/chunk-a9y2rwxf.js.map +1 -1
  4. package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
  5. package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
  6. package/dist/cron.js +49 -25
  7. package/dist/cron.js.map +15 -14
  8. package/dist/hosted-agent.js +3012 -17
  9. package/dist/hosted-agent.js.map +30 -3
  10. package/dist/main.js +31323 -36
  11. package/dist/main.js.map +198 -3
  12. package/dist/prompts.js +11 -8
  13. package/dist/prompts.js.map +4 -4
  14. package/dist/runtime/src/application/submit-user-message.ts +3 -0
  15. package/dist/runtime/src/index.ts +7 -0
  16. package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
  17. package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
  18. package/dist/runtime/src/platform/mcp-config.ts +13 -0
  19. package/dist/runtime/src/prompts/builders.ts +20 -6
  20. package/dist/runtime/src/query/active-rooms.ts +2 -0
  21. package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
  22. package/dist/runtime/src/runtime/errors.ts +12 -12
  23. package/dist/runtime/src/runtime/turn-runner.ts +12 -0
  24. package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
  25. package/dist/runtime/src/types/api.ts +2 -0
  26. package/dist/runtime/src/types.ts +2 -0
  27. package/dist/runtime/src/version.ts +1 -1
  28. package/dist/runtime-helpers.js.map +1 -1
  29. package/dist/storage.js.map +1 -1
  30. package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
  31. package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
  32. package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
  33. package/dist/types/packages/core/src/types/api.d.ts +2 -0
  34. package/dist/types/packages/core/src/types.d.ts +2 -0
  35. package/dist/types/packages/core/src/version.d.ts +1 -1
  36. package/dist/vault.js.map +1 -1
  37. package/package.json +1 -1
  38. package/dist/chunk-00q25k7p.js +0 -2387
  39. package/dist/chunk-00q25k7p.js.map +0 -26
  40. package/dist/chunk-03kv7mvj.js +0 -2152
  41. package/dist/chunk-03kv7mvj.js.map +0 -15
  42. package/dist/chunk-05pm51dh.js +0 -589
  43. package/dist/chunk-05pm51dh.js.map +0 -11
  44. package/dist/chunk-0eh16bk1.js +0 -2936
  45. package/dist/chunk-0eh16bk1.js.map +0 -20
  46. package/dist/chunk-0yfqyv88.js +0 -108
  47. package/dist/chunk-0yfqyv88.js.map +0 -10
  48. package/dist/chunk-1k7t7bnx.js +0 -97
  49. package/dist/chunk-1k7t7bnx.js.map +0 -10
  50. package/dist/chunk-3gkfr9vb.js +0 -431
  51. package/dist/chunk-3gkfr9vb.js.map +0 -11
  52. package/dist/chunk-5akd1gbj.js +0 -281
  53. package/dist/chunk-5akd1gbj.js.map +0 -10
  54. package/dist/chunk-7k19pcg8.js +0 -577
  55. package/dist/chunk-7k19pcg8.js.map +0 -14
  56. package/dist/chunk-81tbfbmt.js +0 -10
  57. package/dist/chunk-81tbfbmt.js.map +0 -10
  58. package/dist/chunk-82qhv1mq.js +0 -512
  59. package/dist/chunk-82qhv1mq.js.map +0 -13
  60. package/dist/chunk-8dy7e2a9.js +0 -103
  61. package/dist/chunk-8dy7e2a9.js.map +0 -10
  62. package/dist/chunk-8f2sqmrj.js +0 -1712
  63. package/dist/chunk-8f2sqmrj.js.map +0 -18
  64. package/dist/chunk-9hjeemfz.js +0 -372
  65. package/dist/chunk-9hjeemfz.js.map +0 -10
  66. package/dist/chunk-a4aa2ccv.js +0 -141
  67. package/dist/chunk-a4aa2ccv.js.map +0 -10
  68. package/dist/chunk-bsg215q3.js +0 -3536
  69. package/dist/chunk-bsg215q3.js.map +0 -20
  70. package/dist/chunk-bxnfsc7w.js +0 -58
  71. package/dist/chunk-bxnfsc7w.js.map +0 -10
  72. package/dist/chunk-c500xe9z.js +0 -33
  73. package/dist/chunk-c500xe9z.js.map +0 -10
  74. package/dist/chunk-cngrf7ab.js +0 -149
  75. package/dist/chunk-cngrf7ab.js.map +0 -10
  76. package/dist/chunk-d7tprf06.js +0 -53
  77. package/dist/chunk-d7tprf06.js.map +0 -10
  78. package/dist/chunk-ddnrmkrv.js +0 -61
  79. package/dist/chunk-ddnrmkrv.js.map +0 -10
  80. package/dist/chunk-edb07sjp.js +0 -567
  81. package/dist/chunk-edb07sjp.js.map +0 -14
  82. package/dist/chunk-ejcm6zx8.js +0 -591
  83. package/dist/chunk-ejcm6zx8.js.map +0 -11
  84. package/dist/chunk-ezs352qs.js +0 -237
  85. package/dist/chunk-ezs352qs.js.map +0 -11
  86. package/dist/chunk-f5xgprg1.js +0 -59
  87. package/dist/chunk-f5xgprg1.js.map +0 -10
  88. package/dist/chunk-fbtb3v0s.js +0 -1339
  89. package/dist/chunk-fbtb3v0s.js.map +0 -16
  90. package/dist/chunk-ffefbd3r.js +0 -17
  91. package/dist/chunk-ffefbd3r.js.map +0 -10
  92. package/dist/chunk-gaknyw04.js +0 -708
  93. package/dist/chunk-gaknyw04.js.map +0 -14
  94. package/dist/chunk-gxmny6tq.js +0 -1637
  95. package/dist/chunk-gxmny6tq.js.map +0 -15
  96. package/dist/chunk-h7bj5zwb.js +0 -3069
  97. package/dist/chunk-h7bj5zwb.js.map +0 -32
  98. package/dist/chunk-hp4g7wx7.js +0 -46
  99. package/dist/chunk-hp4g7wx7.js.map +0 -10
  100. package/dist/chunk-hwy3n31w.js +0 -21
  101. package/dist/chunk-hwy3n31w.js.map +0 -9
  102. package/dist/chunk-jzz91jwh.js +0 -70
  103. package/dist/chunk-jzz91jwh.js.map +0 -10
  104. package/dist/chunk-k778y16p.js +0 -153
  105. package/dist/chunk-k778y16p.js.map +0 -11
  106. package/dist/chunk-mh5mexsf.js +0 -147
  107. package/dist/chunk-mh5mexsf.js.map +0 -11
  108. package/dist/chunk-mnwaext5.js +0 -318
  109. package/dist/chunk-mnwaext5.js.map +0 -10
  110. package/dist/chunk-nkgs40ch.js +0 -23
  111. package/dist/chunk-nkgs40ch.js.map +0 -10
  112. package/dist/chunk-pz57vwsm.js +0 -250
  113. package/dist/chunk-pz57vwsm.js.map +0 -11
  114. package/dist/chunk-r6kdp7gn.js +0 -252
  115. package/dist/chunk-r6kdp7gn.js.map +0 -10
  116. package/dist/chunk-rkvrkcgx.js +0 -117
  117. package/dist/chunk-rkvrkcgx.js.map +0 -10
  118. package/dist/chunk-s74rdyqe.js +0 -117
  119. package/dist/chunk-s74rdyqe.js.map +0 -12
  120. package/dist/chunk-sa874j5c.js +0 -9
  121. package/dist/chunk-sa874j5c.js.map +0 -10
  122. package/dist/chunk-sffqdkvq.js +0 -122
  123. package/dist/chunk-sffqdkvq.js.map +0 -10
  124. package/dist/chunk-tcs9qjps.js +0 -64
  125. package/dist/chunk-tcs9qjps.js.map +0 -10
  126. package/dist/chunk-wx83ep8e.js +0 -131
  127. package/dist/chunk-wx83ep8e.js.map +0 -10
  128. package/dist/chunk-xag86ng3.js +0 -122
  129. package/dist/chunk-xag86ng3.js.map +0 -10
  130. package/dist/chunk-xfdkep71.js +0 -371
  131. package/dist/chunk-xfdkep71.js.map +0 -11
  132. package/dist/chunk-xhm1rfpt.js +0 -2907
  133. package/dist/chunk-xhm1rfpt.js.map +0 -24
  134. package/dist/chunk-xpcps28p.js +0 -2138
  135. package/dist/chunk-xpcps28p.js.map +0 -29
  136. package/dist/chunk-yb4rbev9.js +0 -214
  137. package/dist/chunk-yb4rbev9.js.map +0 -10
@@ -1,1339 +0,0 @@
1
- import {
2
- GENERAL_TOPIC_ID,
3
- getManagerTopicForUser,
4
- upsertTopic
5
- } from "./chunk-ejcm6zx8.js";
6
- import {
7
- getRegistry
8
- } from "./chunk-s74rdyqe.js";
9
- import {
10
- sanitizeTopicName
11
- } from "./chunk-k778y16p.js";
12
- import {
13
- getApiTopicConfig,
14
- setApiTopicConfig
15
- } from "./chunk-wx83ep8e.js";
16
- import {
17
- BROWSER_PROFILES_DIR,
18
- PATCHRIGHT_MCP_BIN,
19
- PLAYWRIGHT_BASE_PORT,
20
- PLAYWRIGHT_MAX_PORT,
21
- PLAYWRIGHT_MCP_BIN,
22
- PLAYWRIGHT_PORTS_DIR,
23
- resolveBrowserProxy,
24
- resolveDefaultModel
25
- } from "./chunk-ezs352qs.js";
26
- import {
27
- logger
28
- } from "./chunk-ffefbd3r.js";
29
- import {
30
- db
31
- } from "./chunk-81tbfbmt.js";
32
- import {
33
- registerStorageSchemaInitializer
34
- } from "./chunk-sffqdkvq.js";
35
-
36
- // ../../packages/core/src/storage/runtime-events.ts
37
- var RUNTIME_EVENT_TYPES = [
38
- "message",
39
- "message-updated",
40
- "ai-status",
41
- "topic-created",
42
- "topic-updated",
43
- "topic-deleted"
44
- ];
45
- db.exec(`
46
- CREATE TABLE IF NOT EXISTS runtime_events (
47
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
48
- source_id TEXT NOT NULL,
49
- event_type TEXT NOT NULL CHECK (
50
- event_type IN (
51
- 'message',
52
- 'message-updated',
53
- 'ai-status',
54
- 'topic-created',
55
- 'topic-updated',
56
- 'topic-deleted'
57
- )
58
- ),
59
- topic_id TEXT NOT NULL,
60
- payload_json TEXT NOT NULL,
61
- created_at TEXT NOT NULL
62
- )
63
- `);
64
- db.exec("CREATE INDEX IF NOT EXISTS idx_runtime_events_topic_seq ON runtime_events(topic_id, seq)");
65
- var types = new Set(RUNTIME_EVENT_TYPES);
66
- function rowToEvent(row) {
67
- if (!types.has(row.event_type))
68
- return null;
69
- try {
70
- return {
71
- seq: Number(row.seq),
72
- sourceId: row.source_id,
73
- type: row.event_type,
74
- topicId: row.topic_id,
75
- payload: JSON.parse(row.payload_json),
76
- createdAt: row.created_at
77
- };
78
- } catch {
79
- return null;
80
- }
81
- }
82
- function latestRuntimeEventSeq() {
83
- const row = db.query("SELECT MAX(seq) AS seq FROM runtime_events").get();
84
- return Number(row?.seq ?? 0);
85
- }
86
- function appendRuntimeEvent(sourceId, event) {
87
- const createdAt = new Date().toISOString();
88
- const result = db.query(`INSERT INTO runtime_events
89
- (source_id, event_type, topic_id, payload_json, created_at)
90
- VALUES (?, ?, ?, ?, ?)`).run(sourceId, event.type, event.topicId, JSON.stringify(event.payload ?? null), createdAt);
91
- const seq = Number(result.lastInsertRowid);
92
- return { seq, sourceId, ...event, createdAt };
93
- }
94
- function listRuntimeEventsAfter(seq, limit = 500) {
95
- const rows = db.query(`SELECT seq, source_id, event_type, topic_id, payload_json, created_at
96
- FROM runtime_events
97
- WHERE seq > ?
98
- ORDER BY seq ASC
99
- LIMIT ?`).all(Math.max(0, seq), Math.max(1, Math.min(limit, 5000)));
100
- return rows.map(rowToEvent).filter((event) => event !== null);
101
- }
102
- function listRecentRuntimeEventsForTopic(topicId, limit = 300) {
103
- const rows = db.query(`SELECT seq, source_id, event_type, topic_id, payload_json, created_at
104
- FROM (
105
- SELECT seq, source_id, event_type, topic_id, payload_json, created_at
106
- FROM runtime_events
107
- WHERE topic_id = ? AND event_type IN ('ai-status', 'message-updated')
108
- ORDER BY seq DESC
109
- LIMIT ?
110
- )
111
- ORDER BY seq ASC`).all(topicId, Math.max(1, Math.min(limit, 2000)));
112
- return rows.map(rowToEvent).filter((event) => event !== null);
113
- }
114
-
115
- // ../../packages/core/src/bus.ts
116
- import { randomUUID } from "crypto";
117
- class SqliteRuntimeBus {
118
- listeners = new Set;
119
- sourceId;
120
- pollIntervalMs;
121
- cursor;
122
- pollTimer = null;
123
- polling = false;
124
- constructor(options = {}) {
125
- this.sourceId = options.sourceId ?? `${process.pid}-${randomUUID()}`;
126
- this.pollIntervalMs = Math.max(25, options.pollIntervalMs ?? 100);
127
- this.cursor = latestRuntimeEventSeq();
128
- }
129
- deliver(event) {
130
- for (const listener of this.listeners) {
131
- try {
132
- listener(event);
133
- } catch {}
134
- }
135
- }
136
- emit(event) {
137
- const stored = appendRuntimeEvent(this.sourceId, event);
138
- this.deliver({ ...event, seq: stored.seq, createdAt: stored.createdAt });
139
- }
140
- startPolling() {
141
- if (this.pollTimer)
142
- return;
143
- this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
144
- this.pollTimer.unref?.();
145
- this.poll();
146
- }
147
- stopPolling() {
148
- if (!this.pollTimer)
149
- return;
150
- clearInterval(this.pollTimer);
151
- this.pollTimer = null;
152
- }
153
- poll() {
154
- if (this.polling || this.listeners.size === 0)
155
- return;
156
- this.polling = true;
157
- try {
158
- while (true) {
159
- const events = listRuntimeEventsAfter(this.cursor);
160
- if (events.length === 0)
161
- break;
162
- for (const event of events) {
163
- this.cursor = Math.max(this.cursor, event.seq);
164
- if (event.sourceId === this.sourceId)
165
- continue;
166
- this.deliver({
167
- type: event.type,
168
- topicId: event.topicId,
169
- payload: event.payload,
170
- seq: event.seq,
171
- createdAt: event.createdAt
172
- });
173
- }
174
- if (events.length < 500)
175
- break;
176
- }
177
- } finally {
178
- this.polling = false;
179
- }
180
- }
181
- broadcastMessage(topicId, message) {
182
- this.emit({ type: "message", topicId, payload: message });
183
- }
184
- broadcastMessageUpdated(topicId, messageId, patch) {
185
- this.emit({ type: "message-updated", topicId, payload: { messageId, patch } });
186
- }
187
- broadcastAiStatus(topicId, status) {
188
- this.emit({ type: "ai-status", topicId, payload: status });
189
- }
190
- broadcastTopicCreated(topic) {
191
- this.emit({ type: "topic-created", topicId: topic.id, payload: topic });
192
- }
193
- broadcastTopicUpdated(topicId) {
194
- this.emit({ type: "topic-updated", topicId, payload: null });
195
- }
196
- broadcastTopicDeleted(topicId) {
197
- this.emit({ type: "topic-deleted", topicId, payload: null });
198
- }
199
- broadcastAiActive(topicId, queryId) {
200
- this.broadcastAiStatus(topicId, { kind: "ai_active", queryId });
201
- }
202
- broadcastDone(topicId, queryId, usage, meta) {
203
- this.broadcastAiStatus(topicId, {
204
- kind: "ai_done",
205
- queryId,
206
- usage,
207
- agent: meta?.agent,
208
- model: meta?.model
209
- });
210
- }
211
- broadcastError(topicId, queryId, error) {
212
- this.broadcastAiStatus(topicId, { kind: "ai_error", queryId, error });
213
- }
214
- broadcastAborted(topicId, queryId, reason) {
215
- this.broadcastAiStatus(topicId, { kind: "ai_aborted", queryId, reason });
216
- }
217
- broadcastToolCall(topicId, queryId, name, input, label, toolUseId) {
218
- const id = toolUseId.trim() || `${queryId}:tool`;
219
- this.broadcastAiStatus(topicId, {
220
- kind: "tool_call",
221
- queryId,
222
- name,
223
- input,
224
- label,
225
- toolUseId: id
226
- });
227
- }
228
- broadcastToolOutput(topicId, queryId, toolUseId, content) {
229
- const id = toolUseId.trim() || `${queryId}:tool`;
230
- this.broadcastAiStatus(topicId, { kind: "tool_output", queryId, toolUseId: id, content });
231
- }
232
- broadcastToolStatus(topicId, queryId, kind, content, meta) {
233
- this.broadcastAiStatus(topicId, {
234
- kind: "tool_status",
235
- queryId,
236
- statusKind: kind,
237
- content,
238
- toolName: meta?.toolName,
239
- elapsed: meta?.elapsed
240
- });
241
- }
242
- broadcastFileReady(topicId, queryId, path, source) {
243
- this.broadcastAiStatus(topicId, { kind: "file_ready", queryId, path, source });
244
- }
245
- broadcastReasoning(topicId, queryId, content) {
246
- this.broadcastAiStatus(topicId, { kind: "reasoning", queryId, content });
247
- }
248
- broadcastVisual(topicId, queryId, url, id, title, kind) {
249
- this.broadcastAiStatus(topicId, { kind: "visual", queryId, url, id, title, visualKind: kind });
250
- }
251
- broadcastTyping(topicId, userId) {
252
- this.broadcastAiStatus(topicId, { kind: "typing", userId });
253
- }
254
- subscribe(listener) {
255
- this.listeners.add(listener);
256
- this.startPolling();
257
- return () => {
258
- this.listeners.delete(listener);
259
- if (this.listeners.size === 0)
260
- this.stopPolling();
261
- };
262
- }
263
- }
264
- var current = new SqliteRuntimeBus;
265
- function runtimeBus() {
266
- return current;
267
- }
268
- function setRuntimeBus(bus) {
269
- current = bus;
270
- }
271
- var WsHub = {
272
- get() {
273
- return current;
274
- }
275
- };
276
-
277
- // ../../packages/core/src/storage/api-messages.ts
278
- function initializeApiMessagesSchema() {
279
- db.exec(`
280
- CREATE TABLE IF NOT EXISTS api_messages (
281
- id TEXT PRIMARY KEY,
282
- topic_id TEXT NOT NULL,
283
- parent_id TEXT,
284
- author_id TEXT NOT NULL,
285
- source_adapter TEXT,
286
- text TEXT NOT NULL,
287
- query_id TEXT,
288
- agent_type TEXT,
289
- model TEXT,
290
- attachments TEXT,
291
- usage TEXT,
292
- deleted INTEGER NOT NULL DEFAULT 0,
293
- edited_at TEXT,
294
- reactions TEXT,
295
- kind TEXT,
296
- ask_user_question TEXT,
297
- mentions TEXT,
298
- thread_root_id TEXT,
299
- created_at TEXT NOT NULL
300
- )
301
- `);
302
- try {
303
- db.exec("ALTER TABLE api_messages ADD COLUMN source_adapter TEXT");
304
- } catch {}
305
- try {
306
- db.exec("ALTER TABLE api_messages ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0");
307
- } catch {}
308
- try {
309
- db.exec("ALTER TABLE api_messages ADD COLUMN edited_at TEXT");
310
- } catch {}
311
- try {
312
- db.exec("ALTER TABLE api_messages ADD COLUMN reactions TEXT");
313
- } catch {}
314
- try {
315
- db.exec("ALTER TABLE api_messages ADD COLUMN kind TEXT");
316
- } catch {}
317
- try {
318
- db.exec("ALTER TABLE api_messages ADD COLUMN ask_user_question TEXT");
319
- } catch {}
320
- try {
321
- db.exec("ALTER TABLE api_messages ADD COLUMN mentions TEXT");
322
- } catch {}
323
- try {
324
- db.exec("ALTER TABLE api_messages ADD COLUMN thread_root_id TEXT");
325
- } catch {}
326
- try {
327
- db.exec("ALTER TABLE api_messages ADD COLUMN subagent_card TEXT");
328
- } catch {}
329
- db.exec("CREATE INDEX IF NOT EXISTS idx_api_messages_topic ON api_messages(topic_id)");
330
- db.exec("CREATE INDEX IF NOT EXISTS idx_api_messages_thread_root ON api_messages(thread_root_id)");
331
- }
332
- registerStorageSchemaInitializer(initializeApiMessagesSchema, 30);
333
- var appendHooks = new Set;
334
- function emitApiMessageAppended(msg) {
335
- for (const hook of appendHooks) {
336
- queueMicrotask(() => {
337
- try {
338
- Promise.resolve(hook(msg)).catch(() => {
339
- return;
340
- });
341
- } catch {}
342
- });
343
- }
344
- }
345
- function rowToDto(r) {
346
- return {
347
- id: r.id,
348
- topicId: r.topic_id,
349
- parentId: r.parent_id ?? undefined,
350
- authorId: r.author_id,
351
- sourceAdapter: r.source_adapter ?? undefined,
352
- text: r.text,
353
- queryId: r.query_id ?? undefined,
354
- agentType: r.agent_type ?? undefined,
355
- model: r.model ?? undefined,
356
- attachments: r.attachments ? JSON.parse(r.attachments) : undefined,
357
- usage: r.usage ? JSON.parse(r.usage) : undefined,
358
- deleted: r.deleted !== 0,
359
- editedAt: r.edited_at ?? undefined,
360
- reactions: r.reactions ? JSON.parse(r.reactions) : undefined,
361
- kind: r.kind ?? undefined,
362
- askUserQuestion: r.ask_user_question ? JSON.parse(r.ask_user_question) : undefined,
363
- subagentCard: r.subagent_card ? JSON.parse(r.subagent_card) : undefined,
364
- mentions: r.mentions ? JSON.parse(r.mentions) : undefined,
365
- threadRootId: r.thread_root_id ?? undefined,
366
- createdAt: r.created_at
367
- };
368
- }
369
- function attachmentListHasFileId(raw, fileId) {
370
- if (!raw)
371
- return false;
372
- try {
373
- const attachments = JSON.parse(raw);
374
- return Array.isArray(attachments) && attachments.some((attachment) => typeof attachment === "object" && attachment !== null && ("id" in attachment) && attachment.id === fileId);
375
- } catch {
376
- return false;
377
- }
378
- }
379
- function appendApiMessage(msg, options = {}) {
380
- const notify = options.notify ?? true;
381
- const updateTopicLastMessageAt = options.updateTopicLastMessageAt ?? true;
382
- let inserted = false;
383
- db.transaction(() => {
384
- const result = db.query(`INSERT INTO api_messages
385
- (id, topic_id, parent_id, author_id, source_adapter, text, query_id, agent_type, model, attachments, usage, deleted, edited_at, reactions, kind, ask_user_question, subagent_card, mentions, thread_root_id, created_at)
386
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
387
- ON CONFLICT(id) DO NOTHING`).run(msg.id, msg.topicId, msg.parentId ?? null, msg.authorId, msg.sourceAdapter ?? null, msg.text, msg.queryId ?? null, msg.agentType ?? null, msg.model ?? null, msg.attachments ? JSON.stringify(msg.attachments) : null, msg.usage ? JSON.stringify(msg.usage) : null, msg.deleted ? 1 : 0, msg.editedAt ?? null, msg.reactions?.length ? JSON.stringify(msg.reactions) : null, msg.kind ?? null, msg.askUserQuestion ? JSON.stringify(msg.askUserQuestion) : null, msg.subagentCard ? JSON.stringify(msg.subagentCard) : null, msg.mentions?.length ? JSON.stringify(msg.mentions) : null, msg.threadRootId ?? null, msg.createdAt);
388
- inserted = Number(result.changes ?? 0) > 0;
389
- if (inserted && updateTopicLastMessageAt && !msg.deleted) {
390
- db.query(`UPDATE api_topics
391
- SET last_message_at = CASE
392
- WHEN last_message_at IS NULL OR last_message_at < ? THEN ?
393
- ELSE last_message_at
394
- END
395
- WHERE id = ?`).run(msg.createdAt, msg.createdAt, msg.topicId);
396
- }
397
- })();
398
- if (inserted && notify && !msg.deleted)
399
- emitApiMessageAppended(msg);
400
- }
401
- function getApiMessage(topicId, messageId) {
402
- const row = db.query("SELECT * FROM api_messages WHERE topic_id = ? AND id = ?").get(topicId, messageId);
403
- return row ? rowToDto(row) : null;
404
- }
405
- function topicHasAttachmentFileId(topicId, fileId) {
406
- const rows = db.query("SELECT attachments FROM api_messages WHERE topic_id = ? AND deleted = 0 AND attachments IS NOT NULL").all(topicId);
407
- return rows.some((row) => attachmentListHasFileId(row.attachments, fileId));
408
- }
409
- function updateApiMessageText(topicId, messageId, text, editedAt = new Date().toISOString()) {
410
- const res = db.query(`UPDATE api_messages
411
- SET text = ?, edited_at = ?
412
- WHERE topic_id = ? AND id = ? AND deleted = 0`).run(text, editedAt, topicId, messageId);
413
- if (Number(res.changes ?? 0) === 0)
414
- return null;
415
- return getApiMessage(topicId, messageId);
416
- }
417
- function updateApiMessageUsage(topicId, messageId, usage) {
418
- const res = db.query(`UPDATE api_messages
419
- SET usage = ?
420
- WHERE topic_id = ? AND id = ? AND deleted = 0`).run(JSON.stringify(usage), topicId, messageId);
421
- if (Number(res.changes ?? 0) === 0)
422
- return null;
423
- return getApiMessage(topicId, messageId);
424
- }
425
- function updateApiMessageAskUserQuestion(topicId, messageId, askUserQuestion, editedAt = new Date().toISOString()) {
426
- const res = db.query(`UPDATE api_messages
427
- SET ask_user_question = ?, edited_at = ?
428
- WHERE topic_id = ? AND id = ? AND deleted = 0 AND kind = 'ask_user_question'`).run(JSON.stringify(askUserQuestion), editedAt, topicId, messageId);
429
- if (Number(res.changes ?? 0) === 0)
430
- return null;
431
- return getApiMessage(topicId, messageId);
432
- }
433
- function updateApiMessageSubagentCard(topicId, messageId, subagentCard, editedAt = new Date().toISOString()) {
434
- const res = db.query(`UPDATE api_messages
435
- SET subagent_card = ?, edited_at = ?
436
- WHERE topic_id = ? AND id = ? AND deleted = 0 AND kind = 'subagent'`).run(JSON.stringify(subagentCard), editedAt, topicId, messageId);
437
- if (Number(res.changes ?? 0) === 0)
438
- return null;
439
- return getApiMessage(topicId, messageId);
440
- }
441
- function listApiMessagesByKind(kind) {
442
- const rows = db.query("SELECT * FROM api_messages WHERE kind = ? AND deleted = 0").all(kind);
443
- return rows.map(rowToDto);
444
- }
445
- function softDeleteApiMessage(topicId, messageId) {
446
- const res = db.query(`UPDATE api_messages
447
- SET deleted = 1, text = ''
448
- WHERE topic_id = ? AND id = ? AND deleted = 0`).run(topicId, messageId);
449
- if (Number(res.changes ?? 0) === 0)
450
- return null;
451
- return getApiMessage(topicId, messageId);
452
- }
453
- function listApiMessages(topicId, options) {
454
- const limit = Math.max(1, Math.min(options?.limit ?? 50, 200));
455
- let anchorRowid = null;
456
- if (options?.cursor) {
457
- const cur = db.query("SELECT rowid AS rowid FROM api_messages WHERE topic_id = ? AND id = ?").get(topicId, options.cursor);
458
- if (cur)
459
- anchorRowid = cur.rowid;
460
- }
461
- const rows = anchorRowid !== null ? db.query(`SELECT *, rowid AS rowid FROM api_messages
462
- WHERE topic_id = ? AND deleted = 0 AND thread_root_id IS NULL AND rowid < ?
463
- ORDER BY rowid DESC
464
- LIMIT ?`).all(topicId, anchorRowid, limit + 1) : db.query(`SELECT *, rowid AS rowid FROM api_messages
465
- WHERE topic_id = ? AND deleted = 0 AND thread_root_id IS NULL
466
- ORDER BY rowid DESC
467
- LIMIT ?`).all(topicId, limit + 1);
468
- const hasMore = rows.length > limit;
469
- const pageRows = (hasMore ? rows.slice(0, limit) : rows).reverse();
470
- const page = pageRows.map(rowToDto);
471
- const summaries = getThreadSummaries(page.map((m) => m.id));
472
- if (summaries.size > 0) {
473
- for (const m of page) {
474
- const s = summaries.get(m.id);
475
- if (s) {
476
- m.threadReplyCount = s.replyCount;
477
- m.threadLastReplyAt = s.lastReplyAt;
478
- }
479
- }
480
- }
481
- return {
482
- page,
483
- cursor: page.length > 0 ? page[0].id : undefined,
484
- hasMore
485
- };
486
- }
487
- function getThreadSummaries(rootIds) {
488
- const out = new Map;
489
- const ids = [...new Set(rootIds)].filter(Boolean);
490
- if (ids.length === 0)
491
- return out;
492
- const rows = db.query(`SELECT thread_root_id AS rootId, COUNT(*) AS replyCount, MAX(created_at) AS lastReplyAt
493
- FROM api_messages
494
- WHERE thread_root_id IN (${ids.map(() => "?").join(",")}) AND deleted = 0
495
- GROUP BY thread_root_id`).all(...ids);
496
- for (const r of rows) {
497
- out.set(r.rootId, { replyCount: Number(r.replyCount), lastReplyAt: r.lastReplyAt });
498
- }
499
- return out;
500
- }
501
- function deleteMessagesForTopic(topicId) {
502
- const res = db.query("DELETE FROM api_messages WHERE topic_id = ?").run(topicId);
503
- return Number(res.changes ?? 0);
504
- }
505
- function copyMessagesForTopic(sourceTopicId, targetTopicId) {
506
- const rows = db.query("SELECT * FROM api_messages WHERE topic_id = ? AND deleted = 0 ORDER BY rowid ASC").all(sourceTopicId);
507
- if (rows.length === 0)
508
- return 0;
509
- const idMap = new Map(rows.map((r) => [r.id, crypto.randomUUID()]));
510
- let copied = 0;
511
- for (const r of rows) {
512
- const newId = idMap.get(r.id);
513
- if (!newId)
514
- continue;
515
- appendApiMessage({
516
- ...rowToDto(r),
517
- id: newId,
518
- topicId: targetTopicId,
519
- parentId: r.parent_id ? idMap.get(r.parent_id) ?? r.parent_id : undefined,
520
- threadRootId: r.thread_root_id ? idMap.get(r.thread_root_id) ?? r.thread_root_id : undefined
521
- }, { notify: false, updateTopicLastMessageAt: false });
522
- copied++;
523
- }
524
- return copied;
525
- }
526
- function getAllMessagesForTopic(topicId) {
527
- return db.query("SELECT *, rowid AS rowid FROM api_messages WHERE topic_id = ? AND deleted = 0 ORDER BY rowid ASC").all(topicId);
528
- }
529
- function getMessagesForTopicAfterRowid(topicId, afterRowid) {
530
- return db.query("SELECT *, rowid AS rowid FROM api_messages WHERE topic_id = ? AND rowid > ? AND deleted = 0 ORDER BY rowid ASC").all(topicId, afterRowid);
531
- }
532
-
533
- // ../../packages/core/src/platform/delay.ts
534
- var delay = (ms) => new Promise((r) => setTimeout(r, ms));
535
-
536
- // ../../packages/core/src/storage/browser-profiles.ts
537
- var DEFAULT_BROWSER_PROFILE = "default";
538
- function normalizeBrowserProfileName(name) {
539
- const value = name.trim().toLowerCase();
540
- if (!/^[a-z0-9][a-z0-9_-]{0,47}$/.test(value)) {
541
- throw new Error("Profile name must be 1-48 lowercase letters, numbers, '-' or '_'.");
542
- }
543
- return value;
544
- }
545
- db.exec(`
546
- CREATE TABLE IF NOT EXISTS browser_profiles (
547
- owner_id TEXT NOT NULL,
548
- name TEXT NOT NULL,
549
- created_at TEXT NOT NULL,
550
- PRIMARY KEY (owner_id, name)
551
- )
552
- `);
553
- function getBrowserProfileOwner(topicId, fallbackUserId) {
554
- return db.query("SELECT browser_profile_owner FROM api_topics WHERE id = ?").get(topicId)?.browser_profile_owner ?? fallbackUserId;
555
- }
556
- function isTopicBrowserProfileOwner(topicId, userId) {
557
- return getBrowserProfileOwner(topicId, "") === userId;
558
- }
559
- function getTopicBrowserProfile(topicId) {
560
- const value = db.query("SELECT browser_profile FROM api_topics WHERE id = ?").get(topicId)?.browser_profile;
561
- return value || DEFAULT_BROWSER_PROFILE;
562
- }
563
- function hasBrowserProfileTopic(topicId) {
564
- return Boolean(db.query("SELECT id FROM api_topics WHERE id = ?").get(topicId));
565
- }
566
- function createBrowserProfile(ownerId, rawName) {
567
- const name = normalizeBrowserProfileName(rawName);
568
- if (name === DEFAULT_BROWSER_PROFILE)
569
- return name;
570
- db.query("INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)").run(ownerId, name, new Date().toISOString());
571
- return name;
572
- }
573
- function assignTopicBrowserProfile(opts) {
574
- const actualOwner = getBrowserProfileOwner(opts.topicId, "");
575
- if (!actualOwner)
576
- throw new Error(`Topic "${opts.topicId}" not found or has no owner.`);
577
- if (actualOwner !== opts.actorUserId) {
578
- throw new Error(`Only the topic owner can change its browser profile.`);
579
- }
580
- const profile = normalizeBrowserProfileName(opts.profile);
581
- if (profile !== DEFAULT_BROWSER_PROFILE)
582
- createBrowserProfile(actualOwner, profile);
583
- const previous = getTopicBrowserProfile(opts.topicId);
584
- const result = db.query("UPDATE api_topics SET browser_profile = ? WHERE id = ?").run(profile, opts.topicId);
585
- if (Number(result.changes ?? 0) !== 1)
586
- throw new Error(`Topic "${opts.topicId}" not found.`);
587
- return { previous, profile };
588
- }
589
-
590
- // ../../packages/core/src/platform/playwright/manager.ts
591
- import { execFileSync, spawn } from "child_process";
592
- import { createHash, randomBytes } from "crypto";
593
- import {
594
- cpSync,
595
- existsSync,
596
- mkdirSync,
597
- readdirSync,
598
- readFileSync,
599
- renameSync,
600
- rmSync,
601
- unlinkSync,
602
- writeFileSync
603
- } from "fs";
604
- import { dirname, join, resolve } from "path";
605
- function makeInstanceKey(userId, topic) {
606
- if (!topic)
607
- return makeBrowserProfileInstanceKey(userId, "default");
608
- const ownerId = getBrowserProfileOwner(topic, userId);
609
- const profile = migrateLegacyTopicProfile(ownerId, topic);
610
- return makeBrowserProfileInstanceKey(ownerId, profile);
611
- }
612
- function makeBrowserProfileInstanceKey(ownerId, rawProfile) {
613
- return `profile:${encodeURIComponent(ownerId)}:${normalizeBrowserProfileName(rawProfile)}`;
614
- }
615
- function legacyBrowserProfileName(topic) {
616
- return `legacy_${createHash("sha256").update(topic).digest("hex").slice(0, 12)}`;
617
- }
618
- function migrateLegacyTopicProfile(ownerId, topic) {
619
- const current2 = getTopicBrowserProfile(topic);
620
- if (current2 !== "default" || !hasBrowserProfileTopic(topic))
621
- return current2;
622
- const legacyDir = resolve(BROWSER_PROFILES_DIR, sanitizeTopicName(topic));
623
- if (!existsSync(legacyDir))
624
- return current2;
625
- const profile = legacyBrowserProfileName(topic);
626
- const profileDir = resolveProfileDir(ownerId, profile);
627
- mkdirSync(dirname(profileDir), { recursive: true });
628
- if (!existsSync(profileDir))
629
- renameSync(legacyDir, profileDir);
630
- assignTopicBrowserProfile({ topicId: topic, actorUserId: ownerId, profile });
631
- logger.info({ ownerId, topic, profile }, "Adopted legacy topic browser profile");
632
- return profile;
633
- }
634
- function parseInstanceKey(instanceKey) {
635
- const match = /^profile:([^:]+):(.+)$/.exec(instanceKey);
636
- if (!match)
637
- return { ownerId: "legacy", profile: sanitizeTopicName(instanceKey) };
638
- return {
639
- ownerId: decodeURIComponent(match[1]),
640
- profile: normalizeBrowserProfileName(match[2])
641
- };
642
- }
643
- function portFileName(instanceKey) {
644
- return createHash("sha256").update(instanceKey).digest("hex").slice(0, 24);
645
- }
646
- function writePortFile(instanceKey, port) {
647
- try {
648
- mkdirSync(PLAYWRIGHT_PORTS_DIR, { recursive: true });
649
- writeFileSync(join(PLAYWRIGHT_PORTS_DIR, portFileName(instanceKey)), String(port));
650
- } catch (e) {
651
- logger.warn({ err: e, instanceKey, port }, "Failed to save playwright port file");
652
- }
653
- }
654
- function deletePortFile(instanceKey) {
655
- try {
656
- unlinkSync(join(PLAYWRIGHT_PORTS_DIR, portFileName(instanceKey)));
657
- } catch (e) {
658
- logger.warn({ err: e, instanceKey }, "Failed to delete playwright port file");
659
- }
660
- }
661
- var BASE_PORT = PLAYWRIGHT_BASE_PORT;
662
- var MAX_PORT = PLAYWRIGHT_MAX_PORT;
663
- var MAX_IDLE_MS = 2 * 60 * 60 * 1000;
664
- var instances = new Map;
665
- var usedPorts = new Set;
666
- var spawning = new Map;
667
- async function withPlaywrightInstanceMaintenance(rawKeys, operation) {
668
- const keys = [...new Set(rawKeys)].sort();
669
- for (;; ) {
670
- const pending = keys.map((key) => spawning.get(key)).filter((promise) => promise !== undefined);
671
- if (pending.length > 0) {
672
- await Promise.allSettled(pending);
673
- continue;
674
- }
675
- let releaseBarrier;
676
- const barrier = new Promise((resolveBarrier) => {
677
- releaseBarrier = () => resolveBarrier(null);
678
- });
679
- for (const key of keys)
680
- spawning.set(key, barrier);
681
- try {
682
- return await operation();
683
- } finally {
684
- for (const key of keys) {
685
- if (spawning.get(key) === barrier)
686
- spawning.delete(key);
687
- }
688
- releaseBarrier();
689
- }
690
- }
691
- }
692
- var pinnedInstances = new Map;
693
- function pinPlaywrightInstance(instanceKey) {
694
- pinnedInstances.set(instanceKey, (pinnedInstances.get(instanceKey) ?? 0) + 1);
695
- }
696
- function unpinPlaywrightInstance(instanceKey) {
697
- const count = pinnedInstances.get(instanceKey);
698
- if (count === undefined)
699
- return;
700
- if (count <= 1)
701
- pinnedInstances.delete(instanceKey);
702
- else
703
- pinnedInstances.set(instanceKey, count - 1);
704
- }
705
- function getPlaywrightCapability(instanceKey) {
706
- return instances.get(instanceKey)?.capability;
707
- }
708
- var _onPlaywrightExit = null;
709
- function evictIdleInstance() {
710
- const now = Date.now();
711
- const oldestKey = selectIdleEvictionKey(instances, pinnedInstances.keys(), spawning.keys(), now, MAX_IDLE_MS);
712
- if (oldestKey) {
713
- const instance = instances.get(oldestKey);
714
- if (!instance)
715
- return null;
716
- const idleMin = ((now - instance.lastUsedAt) / 60000).toFixed(0);
717
- logger.info({ key: oldestKey, idleMin }, "Evicting idle playwright instance");
718
- killInstance(oldestKey);
719
- return instance.port;
720
- }
721
- return null;
722
- }
723
- function selectIdleEvictionKey(candidates, pinnedKeys, busyKeys, now, maxIdleMs) {
724
- const pinned = new Set(pinnedKeys);
725
- const busy = new Set(busyKeys);
726
- let oldest = null;
727
- for (const [key, instance] of candidates) {
728
- if (pinned.has(key) || busy.has(key))
729
- continue;
730
- if (now - instance.lastUsedAt < maxIdleMs)
731
- continue;
732
- if (!oldest || instance.lastUsedAt < oldest.lastUsedAt) {
733
- oldest = { key, lastUsedAt: instance.lastUsedAt };
734
- }
735
- }
736
- return oldest?.key ?? null;
737
- }
738
- async function allocatePort(expectedUserDataDir) {
739
- for (let port = BASE_PORT;port <= MAX_PORT; port++) {
740
- if (usedPorts.has(port))
741
- continue;
742
- usedPorts.add(port);
743
- if (isPortInUse(port)) {
744
- logger.warn({ port }, "Port occupied by external process, attempting cleanup");
745
- await killPlaywrightOnPort(port, expectedUserDataDir);
746
- if (isPortInUse(port)) {
747
- usedPorts.delete(port);
748
- continue;
749
- }
750
- }
751
- return port;
752
- }
753
- const evictedPort = evictIdleInstance();
754
- if (evictedPort !== null) {
755
- await waitForPortRelease(evictedPort);
756
- const reusablePort = selectReusablePort(BASE_PORT, MAX_PORT, usedPorts, isPortInUse);
757
- if (reusablePort !== null) {
758
- usedPorts.add(reusablePort);
759
- return reusablePort;
760
- }
761
- }
762
- throw new Error(`No available ports for Playwright MCP (${instances.size} active instances, range ${BASE_PORT}-${MAX_PORT})`);
763
- }
764
- function selectReusablePort(minPort, maxPort, reservedPorts, isOccupied) {
765
- for (let port = minPort;port <= maxPort; port++) {
766
- if (reservedPorts.has(port) || isOccupied(port))
767
- continue;
768
- return port;
769
- }
770
- return null;
771
- }
772
- function releasePort(port) {
773
- usedPorts.delete(port);
774
- }
775
- function isPortInUse(port) {
776
- try {
777
- execFileSync("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" });
778
- return true;
779
- } catch {
780
- return false;
781
- }
782
- }
783
- function extractUserDataDirArg(cmdline) {
784
- const m = cmdline.match(/--user-data-dir(?:\s+|=)(\S+)/);
785
- return m ? m[1] : null;
786
- }
787
- async function killPlaywrightOnPort(port, expectedUserDataDir) {
788
- try {
789
- const pids = execFileSync("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" }).toString().trim();
790
- if (!pids)
791
- return;
792
- for (const pid of pids.split(`
793
- `)) {
794
- try {
795
- const cmdline = execFileSync("ps", ["-p", pid, "-o", "command="], { stdio: "pipe" }).toString().trim();
796
- if (!cmdline.includes("mcp-patchright")) {
797
- logger.warn({ port, pid, cmdline: cmdline.slice(0, 80) }, "Port occupied by non-browser-MCP process, skipping");
798
- continue;
799
- }
800
- if (expectedUserDataDir) {
801
- const otherDataDir = extractUserDataDirArg(cmdline);
802
- if (!browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir)) {
803
- logger.warn({
804
- port,
805
- pid,
806
- otherDataDir,
807
- expectedUserDataDir
808
- }, "Port occupied by another topic's playwright-mcp, skipping");
809
- continue;
810
- }
811
- }
812
- const pidNum = parseInt(pid, 10);
813
- if (!Number.isNaN(pidNum)) {
814
- killProcessTreeChildren(pidNum);
815
- process.kill(pidNum, "SIGKILL");
816
- }
817
- logger.info({ pid, port }, "Killed zombie mcp-patchright");
818
- } catch (e) {
819
- logger.warn({ err: e, port }, "Failed to inspect process occupying port");
820
- }
821
- }
822
- } catch (e) {
823
- logger.warn({ err: e, port }, "Failed to check processes on port");
824
- }
825
- const start = Date.now();
826
- while (Date.now() - start < 3000) {
827
- if (!isPortInUse(port))
828
- return;
829
- await delay(200);
830
- }
831
- }
832
- function browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir) {
833
- const actualUserDataDir = extractUserDataDirArg(cmdline);
834
- return actualUserDataDir !== null && resolve(actualUserDataDir) === resolve(expectedUserDataDir);
835
- }
836
- function killBrowserProcsForUserDataDir(userDataDir) {
837
- const target = resolve(userDataDir);
838
- if (!target.startsWith(resolve(BROWSER_PROFILES_DIR)))
839
- return;
840
- let pids;
841
- try {
842
- pids = execFileSync("pgrep", ["-f", "--", userDataDir], { stdio: "pipe" }).toString().trim();
843
- } catch {
844
- return;
845
- }
846
- if (!pids)
847
- return;
848
- for (const pid of pids.split(`
849
- `)) {
850
- const pidNum = parseInt(pid.trim(), 10);
851
- if (Number.isNaN(pidNum) || pidNum === process.pid)
852
- continue;
853
- try {
854
- const cmdline = execFileSync("ps", ["-p", String(pidNum), "-o", "command="], {
855
- stdio: "pipe"
856
- }).toString().trim();
857
- const argDir = extractUserDataDirArg(cmdline);
858
- if (!argDir || resolve(argDir) !== target)
859
- continue;
860
- killProcessTreeChildren(pidNum);
861
- process.kill(pidNum, "SIGKILL");
862
- logger.info({ pid: pidNum, userDataDir }, "Reaped orphaned browser process by user-data-dir");
863
- } catch (e) {
864
- logger.debug({ err: e, pid: pidNum, userDataDir }, "Failed to reap browser proc by dir");
865
- }
866
- }
867
- }
868
- function selectOrphanBrowserPids(procs, liveUserDataDirs, profileRoot, selfPid) {
869
- const root = resolve(profileRoot);
870
- const live = new Set([...liveUserDataDirs].map((d) => resolve(d)));
871
- const out = [];
872
- for (const { pid, userDataDir } of procs) {
873
- if (pid === selfPid || !userDataDir)
874
- continue;
875
- const dir = resolve(userDataDir);
876
- if (!dir.startsWith(root))
877
- continue;
878
- if (live.has(dir))
879
- continue;
880
- out.push(pid);
881
- }
882
- return out;
883
- }
884
- function reapOrphanBrowsers() {
885
- const profileRoot = resolve(BROWSER_PROFILES_DIR);
886
- let pids;
887
- try {
888
- pids = execFileSync("pgrep", ["-f", "--", profileRoot], { stdio: "pipe" }).toString().trim();
889
- } catch {
890
- return;
891
- }
892
- if (!pids)
893
- return;
894
- const procs = [];
895
- for (const pid of pids.split(`
896
- `)) {
897
- const pidNum = parseInt(pid.trim(), 10);
898
- if (Number.isNaN(pidNum))
899
- continue;
900
- try {
901
- const cmdline = execFileSync("ps", ["-p", String(pidNum), "-o", "command="], {
902
- stdio: "pipe"
903
- }).toString().trim();
904
- procs.push({ pid: pidNum, userDataDir: extractUserDataDirArg(cmdline) });
905
- } catch {}
906
- }
907
- const liveDirs = [...instances.keys()].map(resolveUserDataDir);
908
- const orphanPids = selectOrphanBrowserPids(procs, liveDirs, profileRoot, process.pid);
909
- for (const pid of orphanPids) {
910
- try {
911
- killProcessTreeChildren(pid);
912
- process.kill(pid, "SIGKILL");
913
- logger.info({ pid }, "Reaped orphaned browser process (janitor)");
914
- } catch (e) {
915
- logger.debug({ err: e, pid }, "Janitor failed to reap orphaned browser process");
916
- }
917
- }
918
- }
919
- async function isHealthy(port) {
920
- for (const path of ["/health", "/sse?owner=__negotium_health__"]) {
921
- try {
922
- const res = await fetch(`http://127.0.0.1:${port}${path}`, {
923
- signal: AbortSignal.timeout(2000)
924
- });
925
- await res.body?.cancel();
926
- if (res.ok)
927
- return true;
928
- } catch {}
929
- }
930
- return false;
931
- }
932
- function cleanSingletonFiles(userDataDir) {
933
- try {
934
- const files = readdirSync(userDataDir);
935
- for (const f of files) {
936
- if (f.startsWith("Singleton")) {
937
- try {
938
- unlinkSync(resolve(userDataDir, f));
939
- logger.info({ file: f, userDataDir }, "Removed stale Singleton file");
940
- } catch (e) {
941
- logger.warn({ err: e, file: f }, "Failed to remove stale Chrome Singleton file");
942
- }
943
- }
944
- }
945
- } catch {}
946
- }
947
- function killProcessTreeChildren(pid) {
948
- try {
949
- const children = execFileSync("pgrep", ["-P", String(pid)], { stdio: "pipe" }).toString().trim();
950
- if (!children)
951
- return;
952
- const childPids = children.split(`
953
- `).map((cpid) => parseInt(cpid, 10)).filter((pidNum) => !Number.isNaN(pidNum));
954
- for (const childPid of childPids) {
955
- killProcessTreeChildren(childPid);
956
- }
957
- for (const signal of ["SIGTERM", "SIGKILL"]) {
958
- for (const childPid of childPids) {
959
- try {
960
- process.kill(childPid, signal);
961
- } catch (e) {
962
- logger.debug({ err: e, pid: childPid, signal }, "Failed to signal browser child process");
963
- }
964
- }
965
- }
966
- logger.info({ parentPid: pid, childPids }, "Killed browser-MCP child process tree");
967
- } catch {}
968
- }
969
- function ownerDirectory(ownerId) {
970
- const digest = createHash("sha256").update(ownerId).digest("hex").slice(0, 16);
971
- return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
972
- }
973
- function resolveProfileDir(ownerId, profile) {
974
- return resolve(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
975
- }
976
- function resolveUserDataDir(instanceKey) {
977
- const { ownerId, profile } = parseInstanceKey(instanceKey);
978
- return resolveProfileDir(ownerId, profile);
979
- }
980
- function killInstance(instanceKey, opts) {
981
- const inst = instances.get(instanceKey);
982
- if (!inst)
983
- return;
984
- if (inst.process.pid) {
985
- killProcessTreeChildren(inst.process.pid);
986
- }
987
- try {
988
- inst.process.kill("SIGTERM");
989
- } catch (e) {
990
- logger.warn({ err: e, instanceKey }, "Failed to kill playwright instance");
991
- }
992
- inst.process.removeAllListeners("error");
993
- inst.process.removeAllListeners("exit");
994
- cleanSingletonFiles(resolveUserDataDir(instanceKey));
995
- if (!opts?.keepPort)
996
- releasePort(inst.port);
997
- instances.delete(instanceKey);
998
- deletePortFile(instanceKey);
999
- logger.info({ instanceKey, port: inst.port, keepPort: !!opts?.keepPort }, "Killed Playwright MCP (with cleanup)");
1000
- }
1001
- async function spawnPlaywright(instanceKey, userId, reservedPort, browserBin = PLAYWRIGHT_MCP_BIN, allowFallback = true) {
1002
- const userDataDir = resolveUserDataDir(instanceKey);
1003
- const port = reservedPort ?? await allocatePort(userDataDir);
1004
- mkdirSync(userDataDir, { recursive: true });
1005
- const mcpArgs = [
1006
- "--port",
1007
- String(port),
1008
- "--host",
1009
- "127.0.0.1",
1010
- "--user-data-dir",
1011
- userDataDir
1012
- ];
1013
- const proxy = resolveBrowserProxy();
1014
- const capability = randomBytes(32).toString("hex");
1015
- const childEnv = {
1016
- ...process.env,
1017
- NEGOTIUM_BROWSER_CAPABILITY: capability,
1018
- ...proxy ? {
1019
- NEGOTIUM_BROWSER_PROXY_SERVER: proxy.server,
1020
- ...proxy.username ? { NEGOTIUM_BROWSER_PROXY_USERNAME: proxy.username } : {},
1021
- ...proxy.password ? { NEGOTIUM_BROWSER_PROXY_PASSWORD: proxy.password } : {},
1022
- ...proxy.bypass ? { NEGOTIUM_BROWSER_PROXY_BYPASS: proxy.bypass } : {}
1023
- } : {}
1024
- };
1025
- if (proxy) {
1026
- logger.info({ instanceKey, proxyServer: proxy.server }, "Browser egress proxy enabled");
1027
- }
1028
- const command = browserBin.endsWith(".mjs") ? process.execPath : browserBin;
1029
- const args = browserBin.endsWith(".mjs") ? [browserBin, ...mcpArgs] : mcpArgs;
1030
- const proc = spawn(command, args, {
1031
- stdio: "ignore",
1032
- detached: false,
1033
- env: childEnv
1034
- });
1035
- const reapCrashedBrowser = () => {
1036
- const userDataDir2 = resolveUserDataDir(instanceKey);
1037
- killBrowserProcsForUserDataDir(userDataDir2);
1038
- cleanSingletonFiles(userDataDir2);
1039
- };
1040
- proc.once("error", (err) => {
1041
- logger.error({ err, instanceKey }, "Playwright MCP error");
1042
- if (instances.get(instanceKey)?.process === proc) {
1043
- releasePort(port);
1044
- instances.delete(instanceKey);
1045
- reapCrashedBrowser();
1046
- }
1047
- });
1048
- proc.once("exit", (code) => {
1049
- logger.info({ instanceKey, code }, "Playwright MCP exited");
1050
- const wasOurs = instances.get(instanceKey)?.process === proc;
1051
- if (wasOurs) {
1052
- releasePort(port);
1053
- instances.delete(instanceKey);
1054
- reapCrashedBrowser();
1055
- _onPlaywrightExit?.(instanceKey, code);
1056
- }
1057
- });
1058
- const now = Date.now();
1059
- instances.set(instanceKey, {
1060
- process: proc,
1061
- port,
1062
- userId,
1063
- startedAt: now,
1064
- lastUsedAt: now,
1065
- capability
1066
- });
1067
- const ready = await waitForServer(port, 1e4) && await supportsOwnerCleanup(port, capability);
1068
- if (!ready) {
1069
- const exitCode = proc.exitCode;
1070
- killInstance(instanceKey);
1071
- if (allowFallback && browserBin !== PATCHRIGHT_MCP_BIN) {
1072
- logger.warn({ instanceKey, browserBin, fallback: PATCHRIGHT_MCP_BIN }, "Preferred browser MCP unavailable or lacks owner isolation; using Patchright fallback");
1073
- return spawnPlaywright(instanceKey, userId, undefined, PATCHRIGHT_MCP_BIN, false);
1074
- }
1075
- throw new Error(`Playwright MCP failed health check after spawn on port ${port}` + (exitCode === null ? "" : ` (exitCode=${exitCode})`));
1076
- }
1077
- writePortFile(instanceKey, port);
1078
- logger.info({ instanceKey, port, pid: proc.pid, ready }, "Playwright MCP started");
1079
- return port;
1080
- }
1081
- async function supportsOwnerCleanup(port, capability) {
1082
- try {
1083
- const response = await fetch(`http://127.0.0.1:${port}/owners`, {
1084
- method: "DELETE",
1085
- headers: {
1086
- "X-Browser-Owner": "__negotium_capability_probe__",
1087
- "X-Browser-Capability": capability
1088
- },
1089
- signal: AbortSignal.timeout(2000)
1090
- });
1091
- await response.body?.cancel();
1092
- return response.ok;
1093
- } catch {
1094
- return false;
1095
- }
1096
- }
1097
- async function ensurePlaywright(userId, topic) {
1098
- const instanceKey = makeInstanceKey(userId, topic);
1099
- const inProgress = spawning.get(instanceKey);
1100
- if (inProgress) {
1101
- const port = await inProgress;
1102
- if (port !== null)
1103
- return port;
1104
- return ensurePlaywright(userId, topic);
1105
- }
1106
- const promise = (async () => {
1107
- const existing = instances.get(instanceKey);
1108
- if (existing && !existing.process.killed && existing.process.exitCode === null) {
1109
- if (await isHealthy(existing.port)) {
1110
- existing.lastUsedAt = Date.now();
1111
- return existing.port;
1112
- }
1113
- logger.warn({ instanceKey }, "Playwright MCP unresponsive, restarting");
1114
- const oldPort = existing.port;
1115
- killInstance(instanceKey);
1116
- await waitForPortRelease(oldPort);
1117
- cleanSingletonFiles(resolveUserDataDir(instanceKey));
1118
- } else if (existing) {
1119
- releasePort(existing.port);
1120
- instances.delete(instanceKey);
1121
- cleanSingletonFiles(resolveUserDataDir(instanceKey));
1122
- }
1123
- return spawnPlaywright(instanceKey, userId);
1124
- })().finally(() => spawning.delete(instanceKey));
1125
- spawning.set(instanceKey, promise);
1126
- return promise;
1127
- }
1128
- async function waitForPortRelease(port, timeoutMs = 3000) {
1129
- const start = Date.now();
1130
- while (Date.now() - start < timeoutMs) {
1131
- if (!isPortInUse(port))
1132
- return;
1133
- await delay(200);
1134
- }
1135
- }
1136
- async function closeBrowserOwnerTabs(ownerId, rawProfile, owner) {
1137
- const instanceKey = makeBrowserProfileInstanceKey(ownerId, rawProfile);
1138
- const inProgress = spawning.get(instanceKey);
1139
- if (inProgress)
1140
- await inProgress;
1141
- const instance = instances.get(instanceKey);
1142
- if (!instance)
1143
- return 0;
1144
- const port = instance.port;
1145
- const response = await fetch(`http://127.0.0.1:${port}/owners`, {
1146
- method: "DELETE",
1147
- headers: {
1148
- "X-Browser-Owner": owner,
1149
- "X-Browser-Capability": instance.capability
1150
- },
1151
- signal: AbortSignal.timeout(5000)
1152
- });
1153
- if (!response.ok)
1154
- throw new Error(`browser MCP owner cleanup failed (${response.status})`);
1155
- const result = await response.json();
1156
- return typeof result.closed === "number" ? result.closed : 0;
1157
- }
1158
- async function waitForServer(port, timeoutMs) {
1159
- const start = Date.now();
1160
- while (Date.now() - start < timeoutMs) {
1161
- if (await isHealthy(port))
1162
- return true;
1163
- await delay(300);
1164
- }
1165
- logger.warn({ port, timeoutMs }, "Playwright MCP not ready before timeout");
1166
- return false;
1167
- }
1168
- async function cloneProfileForChild(opts) {
1169
- const srcOwner = getBrowserProfileOwner(opts.srcTopic, opts.userId);
1170
- const dstOwner = getBrowserProfileOwner(opts.dstTopic, opts.userId);
1171
- if (srcOwner !== dstOwner) {
1172
- const dstDir2 = resolveProfileDir(dstOwner, getTopicBrowserProfile(opts.dstTopic));
1173
- return {
1174
- copied: false,
1175
- srcDir: resolveProfileDir(srcOwner, getTopicBrowserProfile(opts.srcTopic)),
1176
- dstDir: dstDir2,
1177
- reason: "cross-owner-fresh-profile"
1178
- };
1179
- }
1180
- if (srcOwner === dstOwner && hasBrowserProfileTopic(opts.dstTopic)) {
1181
- const profile = getTopicBrowserProfile(opts.srcTopic);
1182
- assignTopicBrowserProfile({ topicId: opts.dstTopic, actorUserId: dstOwner, profile });
1183
- const sharedDir = resolveProfileDir(srcOwner, profile);
1184
- return {
1185
- copied: false,
1186
- srcDir: sharedDir,
1187
- dstDir: sharedDir,
1188
- reason: "shared-profile-assignment"
1189
- };
1190
- }
1191
- const srcKey = makeInstanceKey(opts.userId, opts.srcTopic);
1192
- const dstKey = makeInstanceKey(opts.userId, opts.dstTopic);
1193
- const srcDir = resolveUserDataDir(srcKey);
1194
- const dstDir = resolveUserDataDir(dstKey);
1195
- if (srcDir === dstDir) {
1196
- return { copied: false, srcDir, dstDir, reason: "same-dir" };
1197
- }
1198
- return withPlaywrightInstanceMaintenance([srcKey, dstKey], async () => {
1199
- const parentWasLive = instances.has(srcKey);
1200
- if (parentWasLive) {
1201
- logger.info({ srcKey }, "Quiescing parent Playwright before profile clone");
1202
- killInstance(srcKey);
1203
- await delay(1500);
1204
- }
1205
- if (instances.has(dstKey)) {
1206
- killInstance(dstKey);
1207
- await delay(500);
1208
- }
1209
- if (!existsSync(srcDir)) {
1210
- return { copied: false, srcDir, dstDir, reason: "src-missing" };
1211
- }
1212
- try {
1213
- if (existsSync(dstDir)) {
1214
- rmSync(dstDir, { recursive: true, force: true });
1215
- }
1216
- mkdirSync(dirname(dstDir), { recursive: true });
1217
- if (process.platform === "darwin") {
1218
- try {
1219
- execFileSync("cp", ["-cR", srcDir, dstDir], { stdio: "pipe" });
1220
- } catch {
1221
- cpSync(srcDir, dstDir, { recursive: true });
1222
- }
1223
- } else {
1224
- cpSync(srcDir, dstDir, { recursive: true });
1225
- }
1226
- } catch (e) {
1227
- const reason = `copy-failed: ${e instanceof Error ? e.message : String(e)}`;
1228
- logger.warn({ srcDir, dstDir, err: e }, "Playwright profile clone failed");
1229
- return { copied: false, srcDir, dstDir, reason };
1230
- }
1231
- cleanSingletonFiles(dstDir);
1232
- for (const f of ["DevToolsActivePort", "LOCK"]) {
1233
- try {
1234
- unlinkSync(resolve(dstDir, f));
1235
- } catch {}
1236
- }
1237
- logger.info({ srcKey, dstKey, srcDir, dstDir }, "Cloned Playwright profile for child topic");
1238
- return { copied: true, srcDir, dstDir };
1239
- });
1240
- }
1241
- function deleteTopicProfileDir(userId, topic) {
1242
- const key = makeInstanceKey(userId, topic);
1243
- const dir = resolveUserDataDir(key);
1244
- logger.info({ dir, userId, topic }, "Preserved shared browser profile on topic deletion");
1245
- return { deleted: false, dir };
1246
- }
1247
- async function killAllPlaywright() {
1248
- const procs = [...instances.entries()].map(([key, inst]) => ({ key, proc: inst.process }));
1249
- for (const { key } of procs)
1250
- killInstance(key);
1251
- await Promise.all(procs.map(async ({ key, proc }) => {
1252
- if (await waitForChildProcessExit(proc, 3000))
1253
- return;
1254
- logger.warn({ instanceKey: key, pid: proc.pid }, "Playwright MCP ignored SIGTERM");
1255
- try {
1256
- proc.kill("SIGKILL");
1257
- } catch (err) {
1258
- logger.warn({ err, instanceKey: key, pid: proc.pid }, "Failed to SIGKILL Playwright MCP");
1259
- }
1260
- if (!await waitForChildProcessExit(proc, 1000)) {
1261
- logger.warn({ instanceKey: key, pid: proc.pid }, "Playwright MCP did not report exit");
1262
- }
1263
- }));
1264
- }
1265
- function waitForChildProcessExit(proc, timeoutMs) {
1266
- if (proc.exitCode !== null || proc.signalCode !== null)
1267
- return Promise.resolve(true);
1268
- return new Promise((resolveWait) => {
1269
- let settled = false;
1270
- let timer;
1271
- const finish = (exited) => {
1272
- if (settled)
1273
- return;
1274
- settled = true;
1275
- clearTimeout(timer);
1276
- proc.off("exit", onExit);
1277
- resolveWait(exited);
1278
- };
1279
- const onExit = () => finish(true);
1280
- proc.once("exit", onExit);
1281
- timer = setTimeout(() => finish(proc.exitCode !== null || proc.signalCode !== null), timeoutMs);
1282
- if (typeof timer === "object")
1283
- timer.unref?.();
1284
- if (proc.exitCode !== null || proc.signalCode !== null)
1285
- finish(true);
1286
- });
1287
- }
1288
- setInterval(() => {
1289
- while (evictIdleInstance() !== null) {}
1290
- try {
1291
- reapOrphanBrowsers();
1292
- } catch (e) {
1293
- logger.debug({ err: e }, "reapOrphanBrowsers sweep failed");
1294
- }
1295
- }, 30 * 60 * 1000).unref();
1296
-
1297
- // ../../packages/core/src/topics/personal-general.ts
1298
- import { randomUUID as randomUUID2 } from "crypto";
1299
- var LEGACY_PERSONAL_GENERAL_DESCRIPTION = "\uB098\uB9CC\uC758 \uAC1C\uC778 \uACF5\uAC04\uC774\uC5D0\uC694. \uB300\uD654\uC640 AI \uC791\uC5C5\uC740 \uB2E4\uB978 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uAC1C\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.";
1300
- var PERSONAL_GENERAL_DESCRIPTION = "Your private General. Messages and membership are visible only to you. Workspace memory, wiki, and skills are shared with your workspace.";
1301
- function ensurePersonalGeneral(userId) {
1302
- const existing = getManagerTopicForUser(userId);
1303
- if (existing) {
1304
- if (existing.description === LEGACY_PERSONAL_GENERAL_DESCRIPTION) {
1305
- existing.description = PERSONAL_GENERAL_DESCRIPTION;
1306
- upsertTopic(existing);
1307
- }
1308
- if (!getApiTopicConfig(existing.id)) {
1309
- setApiTopicConfig(existing.id, { model: "deepseek-pro", modelLocked: true });
1310
- }
1311
- return existing;
1312
- }
1313
- const now = new Date().toISOString();
1314
- const registry = getRegistry("maestro");
1315
- const topic = {
1316
- id: randomUUID2(),
1317
- title: "General",
1318
- description: PERSONAL_GENERAL_DESCRIPTION,
1319
- kind: "manager",
1320
- agent: "maestro",
1321
- defaultModel: resolveDefaultModel("maestro", registry.defaultModel),
1322
- defaultEffort: registry.defaultEffort ?? "medium",
1323
- aiMode: "always",
1324
- aiMention: false,
1325
- participants: [{ userId, role: "owner" }],
1326
- createdAt: now,
1327
- lastMessageAt: now
1328
- };
1329
- upsertTopic(topic);
1330
- setApiTopicConfig(topic.id, { model: "deepseek-pro", modelLocked: true });
1331
- return topic;
1332
- }
1333
- function isLegacySharedGeneral(topicId) {
1334
- return topicId === GENERAL_TOPIC_ID;
1335
- }
1336
-
1337
- export { latestRuntimeEventSeq, listRuntimeEventsAfter, listRecentRuntimeEventsForTopic, runtimeBus, setRuntimeBus, WsHub, appendApiMessage, getApiMessage, topicHasAttachmentFileId, updateApiMessageText, updateApiMessageUsage, updateApiMessageAskUserQuestion, updateApiMessageSubagentCard, listApiMessagesByKind, softDeleteApiMessage, listApiMessages, deleteMessagesForTopic, copyMessagesForTopic, getAllMessagesForTopic, getMessagesForTopicAfterRowid, delay, getBrowserProfileOwner, isTopicBrowserProfileOwner, getTopicBrowserProfile, makeInstanceKey, pinPlaywrightInstance, unpinPlaywrightInstance, getPlaywrightCapability, ensurePlaywright, closeBrowserOwnerTabs, cloneProfileForChild, deleteTopicProfileDir, killAllPlaywright, ensurePersonalGeneral, isLegacySharedGeneral };
1338
-
1339
- //# debugId=EB61237B13BD70FE64756E2164756E21