negotium 0.1.49 → 0.2.1

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 (55) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-helpers.js +72 -78
  3. package/dist/agent-helpers.js.map +18 -17
  4. package/dist/background-bash.js +22 -10
  5. package/dist/background-bash.js.map +3 -3
  6. package/dist/browser-runtime.js +47 -72
  7. package/dist/browser-runtime.js.map +10 -9
  8. package/dist/{chunk-5rpw9d1f.js → chunk-kc5kmr95.js} +28 -11
  9. package/dist/{chunk-5rpw9d1f.js.map → chunk-kc5kmr95.js.map} +4 -4
  10. package/dist/hosted-agent.js +48 -22
  11. package/dist/hosted-agent.js.map +10 -9
  12. package/dist/main.js +576 -102
  13. package/dist/main.js.map +23 -20
  14. package/dist/mcp-factories.js +76 -82
  15. package/dist/mcp-factories.js.map +19 -18
  16. package/dist/prompts.js +22 -10
  17. package/dist/prompts.js.map +3 -3
  18. package/dist/query-runtime.js +22 -10
  19. package/dist/query-runtime.js.map +4 -4
  20. package/dist/registry.js +5 -12
  21. package/dist/registry.js.map +5 -5
  22. package/dist/rollout.js +1 -1
  23. package/dist/runtime/cron/mcp-server.ts +1 -1
  24. package/dist/runtime/src/agents/maestro-registry.ts +1 -1
  25. package/dist/runtime/src/agents/rollout/shared.ts +12 -4
  26. package/dist/runtime/src/mcp/mcp-helpers.ts +1 -1
  27. package/dist/runtime/src/mcp/wiki-server.ts +1 -1
  28. package/dist/runtime/src/migration/single-user.ts +621 -0
  29. package/dist/runtime/src/platform/config.ts +24 -11
  30. package/dist/runtime/src/platform/mcp-config.ts +4 -4
  31. package/dist/runtime/src/platform/playwright/manager.ts +10 -28
  32. package/dist/runtime/src/runtime/attachments.ts +3 -3
  33. package/dist/runtime/src/storage/browser-profiles.ts +11 -9
  34. package/dist/runtime/src/storage/conversations.ts +5 -18
  35. package/dist/runtime/src/storage/storage-host.ts +1 -1
  36. package/dist/runtime/src/storage/tasks.ts +2 -14
  37. package/dist/runtime/src/storage/vault-crypto-core.ts +65 -0
  38. package/dist/runtime/src/storage/vault-crypto.ts +8 -48
  39. package/dist/runtime/src/storage/vault.ts +3 -2
  40. package/dist/runtime/src/version.ts +1 -1
  41. package/dist/runtime-helpers.js +22 -10
  42. package/dist/runtime-helpers.js.map +5 -5
  43. package/dist/storage.js +5 -19
  44. package/dist/storage.js.map +5 -5
  45. package/dist/types/packages/core/src/mcp/mcp-helpers.d.ts +1 -1
  46. package/dist/types/packages/core/src/platform/config.d.ts +9 -1
  47. package/dist/types/packages/core/src/runtime/attachments.d.ts +1 -1
  48. package/dist/types/packages/core/src/storage/conversations.d.ts +2 -2
  49. package/dist/types/packages/core/src/storage/vault-crypto-core.d.ts +6 -0
  50. package/dist/types/packages/core/src/storage/vault-crypto.d.ts +2 -1
  51. package/dist/types/packages/core/src/version.d.ts +1 -1
  52. package/dist/vault.js +37 -16
  53. package/dist/vault.js.map +7 -6
  54. package/install-browser-rs.mjs +1 -1
  55. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -168,7 +168,7 @@ function resolveBrowserRsBin(envValue) {
168
168
  if (!override && !versionAtLeast(BROWSER_RS_VERSION.replace(/^v/, ""), BROWSER_RS_MIN_SECURE_VERSION)) {
169
169
  return;
170
170
  }
171
- const candidate = override ? resolve(override) : resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs");
171
+ const candidate = override ? resolve(override) : resolve(BINARIES_DIR, "browser-rs", BROWSER_RS_VERSION, "browser-rs");
172
172
  try {
173
173
  accessSync(candidate, constants.X_OK);
174
174
  return browserRsMeetsMinimumVersion(candidate) ? candidate : undefined;
@@ -180,7 +180,7 @@ function resolveBrowserMcpBin(envValue) {
180
180
  const override = envValue?.trim();
181
181
  if (override)
182
182
  return resolve(override);
183
- return BROWSER_RS_BIN ?? resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs");
183
+ return BROWSER_RS_BIN ?? resolve(BINARIES_DIR, "browser-rs", BROWSER_RS_VERSION, "browser-rs");
184
184
  }
185
185
  function resolveBrowserProxy() {
186
186
  const raw = envText("BROWSER_PROXY_URL");
@@ -212,7 +212,7 @@ function resolveTopicWorkspaceDir(topicId) {
212
212
  }
213
213
  function loadOrCreateLocalSecret(envKey, filename, options = {}) {
214
214
  const envValue = envText(envKey);
215
- const secretFile = resolve(STATE_DIR, filename);
215
+ const secretFile = resolve(SECRETS_DIR, filename);
216
216
  mkdirSync(dirname(secretFile), { recursive: true });
217
217
  if (envValue) {
218
218
  if (options.persistEnvValue) {
@@ -253,7 +253,7 @@ function resolveDefaultModel(agent, registryDefaultModel) {
253
253
  function codexAuthFilePath() {
254
254
  return process.env.NEGOTIUM_CODEX_AUTH_FILE || join(homedir(), ".codex", "auth.json");
255
255
  }
256
- var HOME, PROJECT_ROOT, STATE_DIR_ENV, STATE_DIR, WORKSPACE_DIR, TOPIC_WORKSPACE_DIR, SHARED_WIKI_DIR, CONTEXTS_DIR, BROWSER_PROFILES_DIR, DM_WORKSPACE_DIR, SESSION_WORKSPACE_DIR, CLAUDE_EXECUTABLE_ENV, CLAUDE_EXECUTABLE, DEFAULT_OUTPUT_LANGUAGE = "English", BROWSER_RS_VERSION = "v0.1.16", BROWSER_RS_MIN_SECURE_VERSION = "0.1.15", BROWSER_RS_BIN, PLAYWRIGHT_MCP_BIN, TSX_BIN, TSX_LOADER, TSCONFIG_PATH, SESSION_COMM_SERVER, TASK_SERVER, BROWSER_MCP_SSE_PROXY_SERVER, CANONICAL_MCP_PROXY_SERVER, WIKI_SERVER, TOKEN_STATS_SERVER, COMPACTION_LOG_SERVER, SYSTEM_HEALTH_SERVER, AGENT_HEALTH_SERVER, BACKGROUND_BASH_SERVER, VAULT_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT, RUNTIME_MCP_SECRET, NODE_CONTROL_TOKEN, VAULT_MASTER_KEY, NEGOTIUM_PORT, hostname, DATA_DIR, LOG_DIR, SESSIONS_DB, DEBUG_FILE, USERS_LOG_DIR, RUN_DIR, PROGRESS_DIR, DM_CMD_DIR, DM_RESP_DIR, SESSION_INBOX_DIR, SESSION_ASKS_DIR, PLAYWRIGHT_BASE_PORT, PLAYWRIGHT_MAX_PORT, PLAYWRIGHT_PORTS_DIR, ACTIVE_QUERY_STALE_MS, AGENTS_PROMPTS_DIR, RESOURCES_DIR, FILE_TAG_REGEX, MODEL_SONNET = "claude-sonnet-5", MODEL_OPUS = "claude-opus-5", MODEL_FABLE = "claude-fable-5", FALLBACK_AGENT, SESSION_AGENT, GATEWAY_AGENT, FALLBACK_MODEL, SESSION_MODEL, GATEWAY_MODEL, FFMPEG_BIN, FFPROBE_BIN, PYTHON_BIN, FASTER_WHISPER_WRAPPER, WHISPER_MODEL, TESSERACT_BIN, PDFTOTEXT_BIN, _envMaxTellDepth, MAX_TELL_DEPTH;
256
+ var HOME, PROJECT_ROOT, STATE_DIR_ENV, STATE_DIR, WORKSPACE_DIR, TOPIC_WORKSPACE_DIR, SHARED_WIKI_DIR, CRON_WORKSPACE_DIR, BROWSER_DIR, BROWSER_PROFILES_DIR, BINARIES_DIR, SECRETS_DIR, CONTEXTS_DIR, DM_WORKSPACE_DIR, SESSION_WORKSPACE_DIR, CLAUDE_EXECUTABLE_ENV, CLAUDE_EXECUTABLE, DEFAULT_OUTPUT_LANGUAGE = "English", BROWSER_RS_VERSION = "v0.1.16", BROWSER_RS_MIN_SECURE_VERSION = "0.1.15", BROWSER_RS_BIN, PLAYWRIGHT_MCP_BIN, TSX_BIN, TSX_LOADER, TSCONFIG_PATH, SESSION_COMM_SERVER, TASK_SERVER, BROWSER_MCP_SSE_PROXY_SERVER, CANONICAL_MCP_PROXY_SERVER, WIKI_SERVER, TOKEN_STATS_SERVER, COMPACTION_LOG_SERVER, SYSTEM_HEALTH_SERVER, AGENT_HEALTH_SERVER, BACKGROUND_BASH_SERVER, VAULT_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT, RUNTIME_MCP_SECRET, NODE_CONTROL_TOKEN, VAULT_MASTER_KEY, NEGOTIUM_PORT, hostname, DATA_DIR, LOG_DIR, UPLOADS_DIR, VAULT_DIR, SESSIONS_DB, DEBUG_FILE, USERS_LOG_DIR, RUN_DIR, PROGRESS_DIR, DM_CMD_DIR, DM_RESP_DIR, SESSION_INBOX_DIR, SESSION_ASKS_DIR, PLAYWRIGHT_BASE_PORT, PLAYWRIGHT_MAX_PORT, PLAYWRIGHT_PORTS_DIR, ACTIVE_QUERY_STALE_MS, AGENTS_PROMPTS_DIR, RESOURCES_DIR, FILE_TAG_REGEX, MODEL_SONNET = "claude-sonnet-5", MODEL_OPUS = "claude-opus-5", MODEL_FABLE = "claude-fable-5", FALLBACK_AGENT, SESSION_AGENT, GATEWAY_AGENT, FALLBACK_MODEL, SESSION_MODEL, GATEWAY_MODEL, FFMPEG_BIN, FFPROBE_BIN, PYTHON_BIN, FASTER_WHISPER_WRAPPER, WHISPER_MODEL, TESSERACT_BIN, PDFTOTEXT_BIN, _envMaxTellDepth, MAX_TELL_DEPTH;
257
257
  var init_config = __esm(() => {
258
258
  init_config_helpers();
259
259
  init_logger();
@@ -265,10 +265,14 @@ var init_config = __esm(() => {
265
265
  WORKSPACE_DIR = resolveLocalStateDir("NEGOTIUM_WORKSPACE_DIR", "workspace");
266
266
  TOPIC_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "topics");
267
267
  SHARED_WIKI_DIR = resolve(WORKSPACE_DIR, "wiki");
268
- CONTEXTS_DIR = resolve(WORKSPACE_DIR, "contexts");
269
- BROWSER_PROFILES_DIR = resolve(WORKSPACE_DIR, "browser-profiles");
270
- DM_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "dm");
271
- SESSION_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "sessions");
268
+ CRON_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "cron");
269
+ BROWSER_DIR = resolveLocalStateDir("NEGOTIUM_BROWSER_DIR", "browser");
270
+ BROWSER_PROFILES_DIR = resolve(BROWSER_DIR, "profiles");
271
+ BINARIES_DIR = resolve(STATE_DIR, "binaries");
272
+ SECRETS_DIR = resolve(STATE_DIR, "secrets");
273
+ CONTEXTS_DIR = resolve(STATE_DIR, "data", "contexts");
274
+ DM_WORKSPACE_DIR = resolve(STATE_DIR, "data", "dm");
275
+ SESSION_WORKSPACE_DIR = resolve(STATE_DIR, "data", "sessions");
272
276
  CLAUDE_EXECUTABLE_ENV = envText("NEGOTIUM_CLAUDE_EXECUTABLE");
273
277
  CLAUDE_EXECUTABLE = CLAUDE_EXECUTABLE_ENV ? resolve(CLAUDE_EXECUTABLE_ENV) : undefined;
274
278
  BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
@@ -297,10 +301,12 @@ var init_config = __esm(() => {
297
301
  hostname = process.env.HOSTNAME || "127.0.0.1";
298
302
  DATA_DIR = resolveLocalStateDir("NEGOTIUM_DATA_DIR", "data");
299
303
  LOG_DIR = resolveLocalStateDir("NEGOTIUM_LOG_DIR", "logs");
304
+ UPLOADS_DIR = resolve(DATA_DIR, "uploads");
305
+ VAULT_DIR = resolve(DATA_DIR, "vault");
300
306
  SESSIONS_DB = process.env.SESSIONS_DB_PATH ? resolve(process.env.SESSIONS_DB_PATH) : resolve(DATA_DIR, "sessions.db");
301
307
  DEBUG_FILE = resolve(DATA_DIR, "debug-users.json");
302
308
  USERS_LOG_DIR = resolve(DATA_DIR, "users");
303
- RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "run");
309
+ RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "runtime");
304
310
  PROGRESS_DIR = resolve(RUN_DIR, "progress");
305
311
  DM_CMD_DIR = resolve(RUN_DIR, "dm-commands");
306
312
  DM_RESP_DIR = resolve(RUN_DIR, "dm-responses");
@@ -311,6 +317,8 @@ var init_config = __esm(() => {
311
317
  PLAYWRIGHT_PORTS_DIR = resolve(RUN_DIR, "playwright-ports");
312
318
  mkdirSync(STATE_DIR, { recursive: true });
313
319
  mkdirSync(DATA_DIR, { recursive: true });
320
+ mkdirSync(UPLOADS_DIR, { recursive: true });
321
+ mkdirSync(VAULT_DIR, { recursive: true, mode: 448 });
314
322
  mkdirSync(LOG_DIR, { recursive: true });
315
323
  mkdirSync(PROGRESS_DIR, { recursive: true });
316
324
  mkdirSync(DM_CMD_DIR, { recursive: true });
@@ -321,10 +329,14 @@ var init_config = __esm(() => {
321
329
  mkdirSync(WORKSPACE_DIR, { recursive: true });
322
330
  mkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });
323
331
  mkdirSync(SHARED_WIKI_DIR, { recursive: true });
332
+ mkdirSync(CRON_WORKSPACE_DIR, { recursive: true });
324
333
  mkdirSync(CONTEXTS_DIR, { recursive: true });
325
- mkdirSync(BROWSER_PROFILES_DIR, { recursive: true });
326
334
  mkdirSync(DM_WORKSPACE_DIR, { recursive: true });
327
335
  mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
336
+ mkdirSync(BROWSER_DIR, { recursive: true });
337
+ mkdirSync(BROWSER_PROFILES_DIR, { recursive: true });
338
+ mkdirSync(BINARIES_DIR, { recursive: true });
339
+ mkdirSync(SECRETS_DIR, { recursive: true, mode: 448 });
328
340
  ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000;
329
341
  AGENTS_PROMPTS_DIR = resolve(PROJECT_ROOT, "src/prompts/agents");
330
342
  RESOURCES_DIR = resolve(PROJECT_ROOT, "src/resources");
@@ -399,9 +411,9 @@ var init_sqlite = __esm(async () => {
399
411
  }
400
412
  });
401
413
 
402
- // ../../packages/core/src/storage/vault-crypto.ts
414
+ // ../../packages/core/src/storage/vault-crypto-core.ts
403
415
  import { createCipheriv, createDecipheriv, createHash, randomBytes as randomBytes2 } from "crypto";
404
- function encryptionKey(masterKey = VAULT_MASTER_KEY) {
416
+ function encryptionKey(masterKey) {
405
417
  return createHash("sha256").update("otium-vault-value-v1\x00", "utf8").update(masterKey, "utf8").digest().subarray(0, KEY_BYTES);
406
418
  }
407
419
  function aad(userId, key) {
@@ -410,7 +422,7 @@ function aad(userId, key) {
410
422
  function isEncryptedVaultValue(value) {
411
423
  return value.startsWith(ENVELOPE_PREFIX);
412
424
  }
413
- function encryptVaultValue(userId, key, value, masterKey = VAULT_MASTER_KEY) {
425
+ function encryptVaultValueWithKey(userId, key, value, masterKey) {
414
426
  const iv = randomBytes2(IV_BYTES);
415
427
  const cipher = createCipheriv("aes-256-gcm", encryptionKey(masterKey), iv);
416
428
  cipher.setAAD(aad(userId, key));
@@ -418,7 +430,7 @@ function encryptVaultValue(userId, key, value, masterKey = VAULT_MASTER_KEY) {
418
430
  const tag = cipher.getAuthTag();
419
431
  return `${ENVELOPE_PREFIX}${iv.toString("base64url")}.${ciphertext.toString("base64url")}.${tag.toString("base64url")}`;
420
432
  }
421
- function decryptVaultValue(userId, key, storedValue, masterKey = VAULT_MASTER_KEY) {
433
+ function decryptVaultValueWithKey(userId, key, storedValue, masterKey) {
422
434
  if (!isEncryptedVaultValue(storedValue)) {
423
435
  return { value: storedValue, legacyPlaintext: true };
424
436
  }
@@ -440,8 +452,18 @@ function decryptVaultValue(userId, key, storedValue, masterKey = VAULT_MASTER_KE
440
452
  return { value: plaintext.toString("utf8"), legacyPlaintext: false };
441
453
  }
442
454
  var ENVELOPE_PREFIX = "otium-vault:v1:", IV_BYTES = 12, KEY_BYTES = 32;
455
+ var init_vault_crypto_core = () => {};
456
+
457
+ // ../../packages/core/src/storage/vault-crypto.ts
458
+ function encryptVaultValue(userId, key, value, masterKey = VAULT_MASTER_KEY) {
459
+ return encryptVaultValueWithKey(userId, key, value, masterKey);
460
+ }
461
+ function decryptVaultValue(userId, key, storedValue, masterKey = VAULT_MASTER_KEY) {
462
+ return decryptVaultValueWithKey(userId, key, storedValue, masterKey);
463
+ }
443
464
  var init_vault_crypto = __esm(() => {
444
465
  init_config();
466
+ init_vault_crypto_core();
445
467
  });
446
468
 
447
469
  // ../../packages/core/src/storage/vault.ts
@@ -481,8 +503,9 @@ function initializeVaultDatabase(database) {
481
503
  }
482
504
  }
483
505
  function openVaultDatabase(dataDir) {
484
- const path = join2(dataDir, "vault.db");
485
- mkdirSync2(dataDir, { recursive: true });
506
+ const vaultDir = join2(dataDir, "vault");
507
+ const path = join2(vaultDir, "vault.db");
508
+ mkdirSync2(vaultDir, { recursive: true, mode: 448 });
486
509
  const database = new Database(path, { create: true });
487
510
  chmodSync2(path, 384);
488
511
  initializeVaultDatabase(database);
@@ -1119,7 +1142,12 @@ var init_shared = __esm(() => {
1119
1142
  init_config();
1120
1143
  init_logger();
1121
1144
  __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1122
- trustedWorkspaceRoots = [WORKSPACE_DIR, DM_WORKSPACE_DIR, TOPIC_WORKSPACE_DIR].map((root) => resolve2(root));
1145
+ trustedWorkspaceRoots = [
1146
+ WORKSPACE_DIR,
1147
+ DM_WORKSPACE_DIR,
1148
+ SESSION_WORKSPACE_DIR,
1149
+ TOPIC_WORKSPACE_DIR
1150
+ ].map((root) => resolve2(root));
1123
1151
  FIXTURES_DIR = join4(__dirname2, "..", "fixtures");
1124
1152
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1125
1153
  });
@@ -2029,7 +2057,7 @@ function defaultWorkspaceDir() {
2029
2057
  return envPath("NEGOTIUM_WORKSPACE_DIR", join8(defaultStateDir(), "workspace"));
2030
2058
  }
2031
2059
  function defaultSessionAsksDir() {
2032
- const runDir = envPath("NEGOTIUM_RUN_DIR", join8(defaultStateDir(), "run"));
2060
+ const runDir = envPath("NEGOTIUM_RUN_DIR", join8(defaultStateDir(), "runtime"));
2033
2061
  return join8(runDir, "session-asks");
2034
2062
  }
2035
2063
  function defaultSessionsDatabasePath() {
@@ -2210,15 +2238,8 @@ import {
2210
2238
  writeFileSync as writeFileSync4
2211
2239
  } from "fs";
2212
2240
  import { dirname as dirname6, join as join9 } from "path";
2213
- function safeUserIdComponent(userId) {
2214
- const str = String(userId);
2215
- if (!str || /[/\\]|\.\./.test(str)) {
2216
- throw new Error(`conversations: refusing unsafe userId path component: ${str}`);
2217
- }
2218
- return str;
2219
- }
2220
- function conversationDir(userId) {
2221
- return join9(resolveStorageDataDir(), "conversations", safeUserIdComponent(userId));
2241
+ function conversationDir(_userId) {
2242
+ return join9(resolveStorageDataDir(), "conversations");
2222
2243
  }
2223
2244
  function topicFilename(topicName) {
2224
2245
  const t = sanitizeTopicName(topicName, true);
@@ -2500,7 +2521,7 @@ function writeRollout(options) {
2500
2521
  version: 1,
2501
2522
  cwd: options.cwd,
2502
2523
  createdAt: existingCreatedAt(path) ?? new Date().toISOString(),
2503
- sdkVersion: "0.1.53"
2524
+ sdkVersion: "0.1.54"
2504
2525
  }
2505
2526
  },
2506
2527
  ...messages
@@ -2677,13 +2698,6 @@ __export(exports_tasks, {
2677
2698
  });
2678
2699
  import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7, renameSync as renameSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
2679
2700
  import { dirname as dirname7, join as join12 } from "path";
2680
- function safeUserIdComponent2(userId) {
2681
- const str = String(userId);
2682
- if (!str || /[/\\]|\.\./.test(str)) {
2683
- throw new Error(`tasks: refusing unsafe userId path component: ${str}`);
2684
- }
2685
- return str;
2686
- }
2687
2701
  function safeTaskScopeKey(scopeKey) {
2688
2702
  const safe = sanitizeFileName(scopeKey);
2689
2703
  if (!safe || safe === "." || safe === "..") {
@@ -2695,7 +2709,7 @@ function taskScopeKey(opts) {
2695
2709
  return opts.topicId?.trim() || opts.session || "default";
2696
2710
  }
2697
2711
  function getTaskFilePath(userId, scopeKey) {
2698
- return join12(resolveStorageDataDir(), "tasks", safeUserIdComponent2(userId), `${safeTaskScopeKey(scopeKey)}.json`);
2712
+ return join12(resolveStorageDataDir(), "tasks", `${safeTaskScopeKey(scopeKey)}.json`);
2699
2713
  }
2700
2714
  function readTasks(userId, scopeKey) {
2701
2715
  const path = getTaskFilePath(userId, scopeKey);
@@ -3680,7 +3694,7 @@ function getMcpServersForQuery(opts) {
3680
3694
  if (!opts.topicId)
3681
3695
  throw new Error("getMcpServersForQuery: cron sessionType requires topicId");
3682
3696
  return getCronMcpServers({
3683
- userId: opts.userId || "default",
3697
+ userId: opts.userId || "local",
3684
3698
  session: opts.session || "cron",
3685
3699
  topicId: opts.topicId,
3686
3700
  queryId: opts.queryId,
@@ -3697,7 +3711,7 @@ function getMcpServersForQuery(opts) {
3697
3711
  }
3698
3712
  if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
3699
3713
  return getDmMcpServers({
3700
- userId: opts.userId || "default",
3714
+ userId: opts.userId || "local",
3701
3715
  agent: opts.agent,
3702
3716
  playwrightPort: opts.playwrightPort,
3703
3717
  playwrightCapability: opts.playwrightCapability
@@ -3705,7 +3719,7 @@ function getMcpServersForQuery(opts) {
3705
3719
  }
3706
3720
  if (opts.sessionType === "manager") {
3707
3721
  return getManagerMcpServers({
3708
- userId: opts.userId || "default",
3722
+ userId: opts.userId || "local",
3709
3723
  session: opts.session,
3710
3724
  topicId: opts.topicId,
3711
3725
  queryId: opts.queryId,
@@ -3722,7 +3736,7 @@ function getMcpServersForQuery(opts) {
3722
3736
  });
3723
3737
  }
3724
3738
  return getForumMcpServers({
3725
- userId: opts.userId || "default",
3739
+ userId: opts.userId || "local",
3726
3740
  session: opts.session || "default",
3727
3741
  topicId: opts.topicId,
3728
3742
  subagentParentTopicId: opts.subagentParentTopicId,
@@ -4580,7 +4594,7 @@ var exports_version = {};
4580
4594
  __export(exports_version, {
4581
4595
  NEGOTIUM_VERSION: () => NEGOTIUM_VERSION
4582
4596
  });
4583
- var NEGOTIUM_VERSION = "0.1.49";
4597
+ var NEGOTIUM_VERSION = "0.2.1";
4584
4598
 
4585
4599
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
4586
4600
  import { spawn as spawn3 } from "child_process";
@@ -9730,7 +9744,7 @@ function createBrowserProfile(ownerId, rawName) {
9730
9744
  const name = normalizeBrowserProfileName(rawName);
9731
9745
  if (name === DEFAULT_BROWSER_PROFILE)
9732
9746
  return name;
9733
- db.query("INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)").run(ownerId, name, new Date().toISOString());
9747
+ db.query("INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)").run("local", name, new Date().toISOString());
9734
9748
  return name;
9735
9749
  }
9736
9750
  function assignTopicBrowserProfile(opts) {
@@ -9750,10 +9764,9 @@ function assignTopicBrowserProfile(opts) {
9750
9764
  return { previous, profile };
9751
9765
  }
9752
9766
  function listBrowserProfiles(ownerId) {
9753
- const records = db.query("SELECT name, created_at FROM browser_profiles WHERE owner_id = ? ORDER BY name").all(ownerId);
9767
+ const records = db.query("SELECT name, created_at FROM browser_profiles WHERE owner_id = ? ORDER BY name").all("local");
9754
9768
  const topics = db.query(`SELECT t.id, t.title, t.browser_profile FROM api_topics t
9755
- WHERE t.browser_profile_owner = ?
9756
- ORDER BY t.title`).all(ownerId);
9769
+ ORDER BY t.title`).all();
9757
9770
  const byName = new Map;
9758
9771
  byName.set(DEFAULT_BROWSER_PROFILE, {
9759
9772
  name: DEFAULT_BROWSER_PROFILE,
@@ -9798,7 +9811,6 @@ import {
9798
9811
  existsSync as existsSync15,
9799
9812
  mkdirSync as mkdirSync10,
9800
9813
  readFileSync as readFileSync12,
9801
- renameSync as renameSync6,
9802
9814
  rmSync as rmSync2,
9803
9815
  unlinkSync as unlinkSync11,
9804
9816
  writeFileSync as writeFileSync8
@@ -9814,7 +9826,7 @@ function defaultTopicBinding(userId, topic) {
9814
9826
  if (!topic)
9815
9827
  return managerHost.resolveNamedBinding(userId, "default");
9816
9828
  const ownerId = getBrowserProfileOwner(topic, userId);
9817
- const profile = migrateLegacyTopicProfile(ownerId, topic);
9829
+ const profile = getTopicBrowserProfile(topic);
9818
9830
  return managerHost.resolveNamedBinding(ownerId, profile);
9819
9831
  }
9820
9832
  function resolvePlaywrightProfileBinding(ownerId, rawProfile) {
@@ -9826,34 +9838,15 @@ function validateProfileBinding(binding) {
9826
9838
  }
9827
9839
  return Object.freeze({ ...binding });
9828
9840
  }
9829
- function defaultNamedBinding(ownerId, rawProfile) {
9841
+ function defaultNamedBinding(_ownerId, rawProfile) {
9830
9842
  const profile = normalizeBrowserProfileName(rawProfile);
9831
- const instanceKey = `profile:${encodeURIComponent(ownerId)}:${profile}`;
9843
+ const instanceKey = `profile:${profile}`;
9832
9844
  return {
9833
9845
  instanceKey,
9834
- ownerId,
9846
+ ownerId: "local",
9835
9847
  profile
9836
9848
  };
9837
9849
  }
9838
- function legacyBrowserProfileName(topic) {
9839
- return `legacy_${createHash3("sha256").update(topic).digest("hex").slice(0, 12)}`;
9840
- }
9841
- function migrateLegacyTopicProfile(ownerId, topic) {
9842
- const current2 = getTopicBrowserProfile(topic);
9843
- if (current2 !== "default" || !hasBrowserProfileTopic(topic))
9844
- return current2;
9845
- const legacyDir = resolve12(BROWSER_PROFILES_DIR, sanitizeTopicName(topic));
9846
- if (!existsSync15(legacyDir))
9847
- return current2;
9848
- const profile = legacyBrowserProfileName(topic);
9849
- const profileDir = defaultProfileDir(ownerId, profile);
9850
- mkdirSync10(dirname10(profileDir), { recursive: true });
9851
- if (!existsSync15(profileDir))
9852
- renameSync6(legacyDir, profileDir);
9853
- assignTopicBrowserProfile({ topicId: topic, actorUserId: ownerId, profile });
9854
- logger.info({ ownerId, topic, profile }, "Adopted legacy topic browser profile");
9855
- return profile;
9856
- }
9857
9850
  function defaultChildEnvironment(context) {
9858
9851
  const childEnvironment = {};
9859
9852
  for (const key of [
@@ -9884,6 +9877,10 @@ function defaultChildEnvironment(context) {
9884
9877
  return childEnvironment;
9885
9878
  }
9886
9879
  function parseInstanceKey(instanceKey) {
9880
+ const shared = /^profile:(.+)$/.exec(instanceKey);
9881
+ if (shared && !shared[1].includes(":")) {
9882
+ return { ownerId: "local", profile: normalizeBrowserProfileName(shared[1]) };
9883
+ }
9887
9884
  const match = /^profile:([^:]+):(.+)$/.exec(instanceKey);
9888
9885
  if (!match)
9889
9886
  return { ownerId: "legacy", profile: sanitizeTopicName(instanceKey) };
@@ -10026,12 +10023,8 @@ async function allocatePort() {
10026
10023
  function releasePort(port) {
10027
10024
  usedPorts.delete(port);
10028
10025
  }
10029
- function ownerDirectory(ownerId) {
10030
- const digest = createHash3("sha256").update(ownerId).digest("hex").slice(0, 16);
10031
- return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
10032
- }
10033
- function defaultProfileDir(ownerId, profile) {
10034
- return resolve12(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
10026
+ function defaultProfileDir(_ownerId, profile) {
10027
+ return resolve12(BROWSER_PROFILES_DIR, profile);
10035
10028
  }
10036
10029
  function resolveUserDataDir(instanceKey) {
10037
10030
  return managerHost.resolveInstanceDataDir(instanceKey);
@@ -12440,7 +12433,7 @@ var init_idle_archiver = __esm(async () => {
12440
12433
  });
12441
12434
 
12442
12435
  // ../../packages/core/src/agents/topic-cleanup.ts
12443
- import { mkdirSync as mkdirSync12, renameSync as renameSync7, unlinkSync as unlinkSync12, writeFileSync as writeFileSync10 } from "fs";
12436
+ import { mkdirSync as mkdirSync12, renameSync as renameSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync10 } from "fs";
12444
12437
  import { dirname as dirname12 } from "path";
12445
12438
  function collectSessionIdsByAgent(entries, extraSessions = []) {
12446
12439
  const out = new Map;
@@ -12511,7 +12504,7 @@ function createTopicLogMaintenance(host) {
12511
12504
  writeFileSync10(tempPath, retained.length > 0 ? `${retained.map((entry) => JSON.stringify(entry)).join(`
12512
12505
  `)}
12513
12506
  ` : "", { flag: "wx" });
12514
- renameSync7(tempPath, path);
12507
+ renameSync6(tempPath, path);
12515
12508
  } catch (e) {
12516
12509
  try {
12517
12510
  unlinkSync12(tempPath);
@@ -14767,7 +14760,7 @@ var init_session_inbox_cleanup = __esm(() => {
14767
14760
  });
14768
14761
 
14769
14762
  // ../../packages/core/src/query/state.ts
14770
- import { mkdirSync as mkdirSync14, renameSync as renameSync8, unlinkSync as unlinkSync15, writeFileSync as writeFileSync12 } from "fs";
14763
+ import { mkdirSync as mkdirSync14, renameSync as renameSync7, unlinkSync as unlinkSync15, writeFileSync as writeFileSync12 } from "fs";
14771
14764
  import { basename as basename4, join as join24 } from "path";
14772
14765
  function createQueryStateStore(options) {
14773
14766
  const sanitize = options.sanitizeTopicId ?? sanitizeId;
@@ -14789,7 +14782,7 @@ function createQueryStateStore(options) {
14789
14782
  const target = queryStateFile(userId, topicId);
14790
14783
  const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
14791
14784
  writeFileSync12(tmp, JSON.stringify(state));
14792
- renameSync8(tmp, target);
14785
+ renameSync7(tmp, target);
14793
14786
  },
14794
14787
  clear(userId, topicId, legacyTopicName) {
14795
14788
  const paths = [
@@ -15943,7 +15936,7 @@ function promptWithAttachments(prompt, attachments) {
15943
15936
  return composeAttachmentPrompt(prompt, attachments.map(({ filename, path }) => attachmentPromptLine(filename, path)));
15944
15937
  }
15945
15938
  function ingestAttachment(args) {
15946
- const destDir = join26(workspaceCwdFor(args.topicId), "uploads");
15939
+ const destDir = join26(UPLOADS_DIR, args.topicId);
15947
15940
  mkdirSync16(destDir, { recursive: true });
15948
15941
  const safeName = safeAttachmentFilename(args.filename, "upload");
15949
15942
  const destPath = join26(destDir, `${Date.now()}-${randomUUID15().slice(0, 8)}-${safeName}`);
@@ -20616,7 +20609,7 @@ function renderTurnFooter(msg) {
20616
20609
 
20617
20610
  // ../../packages/core/src/outbox/file-ops.ts
20618
20611
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
20619
- import { appendFileSync as appendFileSync2, existsSync as existsSync21, readFileSync as readFileSync18, renameSync as renameSync9, unlinkSync as unlinkSync17 } from "fs";
20612
+ import { appendFileSync as appendFileSync2, existsSync as existsSync21, readFileSync as readFileSync18, renameSync as renameSync8, unlinkSync as unlinkSync17 } from "fs";
20620
20613
  function host() {
20621
20614
  return hostContext.getStore() ?? defaultHost2;
20622
20615
  }
@@ -20657,7 +20650,7 @@ function drainOutboxFile(filePath, label) {
20657
20650
  if (!existsSync21(filePath))
20658
20651
  return null;
20659
20652
  try {
20660
- renameSync9(filePath, processingPath);
20653
+ renameSync8(filePath, processingPath);
20661
20654
  } catch (e) {
20662
20655
  host().logger.warn({ err: e, filePath }, `${label}: failed to rename for processing`);
20663
20656
  return null;
@@ -23678,7 +23671,7 @@ import {
23678
23671
  openSync as openSync3,
23679
23672
  readdirSync as readdirSync9,
23680
23673
  readFileSync as readFileSync20,
23681
- renameSync as renameSync10,
23674
+ renameSync as renameSync9,
23682
23675
  statSync as statSync12,
23683
23676
  unlinkSync as unlinkSync18,
23684
23677
  writeFileSync as writeFileSync17
@@ -23798,7 +23791,7 @@ function atomicWriteFile(path, content) {
23798
23791
  const temporaryPath = `${path}.tmp.${process.pid}.${Date.now()}`;
23799
23792
  writeFileSync17(temporaryPath, content, "utf-8");
23800
23793
  try {
23801
- renameSync10(temporaryPath, path);
23794
+ renameSync9(temporaryPath, path);
23802
23795
  } catch (error2) {
23803
23796
  try {
23804
23797
  unlinkSync18(temporaryPath);
@@ -24531,7 +24524,7 @@ import {
24531
24524
  existsSync as existsSync25,
24532
24525
  mkdirSync as mkdirSync25,
24533
24526
  readdirSync as readdirSync10,
24534
- renameSync as renameSync11,
24527
+ renameSync as renameSync10,
24535
24528
  statSync as statSync13,
24536
24529
  unlinkSync as unlinkSync19,
24537
24530
  writeFileSync as writeFileSync18
@@ -24555,7 +24548,7 @@ function writeLog(entry) {
24555
24548
  const stat = statSync13(file);
24556
24549
  if (stat.size >= MAX_FILE_SIZE) {
24557
24550
  const rotated = file.replace(/\.jsonl$/, `.${Date.now()}.jsonl`);
24558
- renameSync11(file, rotated);
24551
+ renameSync10(file, rotated);
24559
24552
  }
24560
24553
  } catch {}
24561
24554
  try {
@@ -26143,7 +26136,7 @@ var init_spec = __esm(() => {
26143
26136
  });
26144
26137
 
26145
26138
  // ../../packages/mcp-host/src/manifest.ts
26146
- import { existsSync as existsSync26, mkdirSync as mkdirSync26, readFileSync as readFileSync21, renameSync as renameSync12, writeFileSync as writeFileSync19 } from "fs";
26139
+ import { existsSync as existsSync26, mkdirSync as mkdirSync26, readFileSync as readFileSync21, renameSync as renameSync11, writeFileSync as writeFileSync19 } from "fs";
26147
26140
  import { dirname as dirname17 } from "path";
26148
26141
  import { z as z13 } from "zod";
26149
26142
 
@@ -26212,7 +26205,7 @@ class McpManifest {
26212
26205
  const tmp = `${this.file}.tmp-${process.pid}`;
26213
26206
  writeFileSync19(tmp, `${JSON.stringify(payload, null, 2)}
26214
26207
  `);
26215
- renameSync12(tmp, this.file);
26208
+ renameSync11(tmp, this.file);
26216
26209
  }
26217
26210
  }
26218
26211
  var manifestEntrySchema, manifestFileSchema;
@@ -26243,7 +26236,7 @@ import {
26243
26236
  mkdirSync as mkdirSync27,
26244
26237
  readdirSync as readdirSync11,
26245
26238
  readFileSync as readFileSync22,
26246
- renameSync as renameSync13,
26239
+ renameSync as renameSync12,
26247
26240
  unlinkSync as unlinkSync20,
26248
26241
  writeFileSync as writeFileSync20
26249
26242
  } from "fs";
@@ -26563,7 +26556,7 @@ class McpHost {
26563
26556
  const file = join37(this.portsDir, fileName);
26564
26557
  const tmp = `${file}.tmp-${process.pid}`;
26565
26558
  writeFileSync20(tmp, String(port));
26566
- renameSync13(tmp, file);
26559
+ renameSync12(tmp, file);
26567
26560
  } catch (e) {
26568
26561
  this.log("warn", "Failed to write MCP port file", { fileName, port, err: String(e) });
26569
26562
  }
@@ -26961,7 +26954,7 @@ import {
26961
26954
  existsSync as existsSync28,
26962
26955
  mkdirSync as mkdirSync29,
26963
26956
  readFileSync as readFileSync24,
26964
- renameSync as renameSync14,
26957
+ renameSync as renameSync13,
26965
26958
  unlinkSync as unlinkSync21,
26966
26959
  writeFileSync as writeFileSync22
26967
26960
  } from "fs";
@@ -27456,7 +27449,7 @@ function writeNodeDaemonInfo(port, startedAt) {
27456
27449
  writeFileSync22(temporary, `${JSON.stringify(info, null, 2)}
27457
27450
  `, { mode: 384 });
27458
27451
  chmodSync5(temporary, 384);
27459
- renameSync14(temporary, NODE_DAEMON_INFO_PATH);
27452
+ renameSync13(temporary, NODE_DAEMON_INFO_PATH);
27460
27453
  chmodSync5(NODE_DAEMON_INFO_PATH, 384);
27461
27454
  return info;
27462
27455
  }
@@ -38926,7 +38919,7 @@ import {
38926
38919
  mkdirSync as mkdirSync32,
38927
38920
  openSync as openSync4,
38928
38921
  readFileSync as readFileSync25,
38929
- renameSync as renameSync15,
38922
+ renameSync as renameSync14,
38930
38923
  rmSync as rmSync9,
38931
38924
  statSync as statSync17,
38932
38925
  unlinkSync as unlinkSync22,
@@ -39047,7 +39040,7 @@ function withJoinCredentialLock(operation) {
39047
39040
  }
39048
39041
  const stalePath = `${lockPath}.stale.${process.pid}.${randomUUID28()}`;
39049
39042
  try {
39050
- renameSync15(lockPath, stalePath);
39043
+ renameSync14(lockPath, stalePath);
39051
39044
  rmSync9(stalePath, { recursive: true, force: true });
39052
39045
  } catch (staleError) {
39053
39046
  if (staleError.code !== "ENOENT")
@@ -39145,7 +39138,7 @@ function saveJoinWhileLocked(join41, options = {}) {
39145
39138
  closeSync4(fd);
39146
39139
  fd = undefined;
39147
39140
  if (options.replaceExisting) {
39148
- renameSync15(temporaryPath, path);
39141
+ renameSync14(temporaryPath, path);
39149
39142
  } else {
39150
39143
  linkSync(temporaryPath, path);
39151
39144
  unlinkSync22(temporaryPath);
@@ -41931,7 +41924,7 @@ import {
41931
41924
  mkdirSync as mkdirSync34,
41932
41925
  openSync as openSync5,
41933
41926
  readFileSync as readFileSync26,
41934
- renameSync as renameSync16,
41927
+ renameSync as renameSync15,
41935
41928
  unlinkSync as unlinkSync23,
41936
41929
  writeFileSync as writeFileSync24
41937
41930
  } from "fs";
@@ -42030,7 +42023,7 @@ function replacePendingEnrollment(pending2) {
42030
42023
  fsyncSync3(fd);
42031
42024
  closeSync5(fd);
42032
42025
  fd = undefined;
42033
- renameSync16(temporaryPath, path);
42026
+ renameSync15(temporaryPath, path);
42034
42027
  const directoryFd = openSync5(directory, "r");
42035
42028
  try {
42036
42029
  fsyncSync3(directoryFd);
@@ -43657,6 +43650,476 @@ var init_init = __esm(async () => {
43657
43650
  await init_src();
43658
43651
  });
43659
43652
 
43653
+ // ../../packages/core/src/migration/single-user.ts
43654
+ import { createHash as createHash11, randomUUID as randomUUID34 } from "crypto";
43655
+ import {
43656
+ chmodSync as chmodSync8,
43657
+ copyFileSync as copyFileSync6,
43658
+ existsSync as existsSync36,
43659
+ mkdirSync as mkdirSync35,
43660
+ readdirSync as readdirSync14,
43661
+ readFileSync as readFileSync27,
43662
+ renameSync as renameSync16,
43663
+ rmSync as rmSync11,
43664
+ statSync as statSync19,
43665
+ writeFileSync as writeFileSync25
43666
+ } from "fs";
43667
+ import { homedir as homedir12 } from "os";
43668
+ import { basename as basename14, dirname as dirname23, join as join42, resolve as resolve25 } from "path";
43669
+ function entries(path) {
43670
+ return existsSync36(path) && statSync19(path).isDirectory() ? readdirSync14(path) : [];
43671
+ }
43672
+ function processIsAlive2(pid) {
43673
+ if (!Number.isInteger(pid) || pid <= 0)
43674
+ return false;
43675
+ try {
43676
+ process.kill(pid, 0);
43677
+ return true;
43678
+ } catch (error2) {
43679
+ return error2.code === "EPERM";
43680
+ }
43681
+ }
43682
+ function assertNoActiveNode(stateDir2) {
43683
+ for (const runtimeName of ["run", "runtime"]) {
43684
+ const infoPath = join42(stateDir2, runtimeName, "node-daemon.json");
43685
+ if (!existsSync36(infoPath))
43686
+ continue;
43687
+ try {
43688
+ const value = JSON.parse(readFileSync27(infoPath, "utf8"));
43689
+ if (typeof value.pid === "number" && processIsAlive2(value.pid)) {
43690
+ throw new Error(`Refusing migration while Negotium pid ${value.pid} is active.`);
43691
+ }
43692
+ } catch (error2) {
43693
+ if (error2 instanceof SyntaxError) {
43694
+ throw new Error(`Refusing migration: unreadable process metadata at ${infoPath}.`);
43695
+ }
43696
+ throw error2;
43697
+ }
43698
+ }
43699
+ const dbPath = join42(stateDir2, "data", "sessions.db");
43700
+ if (!existsSync36(dbPath))
43701
+ return;
43702
+ const database = new Database(dbPath, { readonly: true });
43703
+ try {
43704
+ const hasLeases = database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='runtime_process_leases'").get();
43705
+ if (!hasLeases)
43706
+ return;
43707
+ const leases = database.query("SELECT role, pid FROM runtime_process_leases").all();
43708
+ const active2 = leases.find((lease) => processIsAlive2(lease.pid));
43709
+ if (active2)
43710
+ throw new Error(`Refusing migration while ${active2.role} pid ${active2.pid} is active.`);
43711
+ } finally {
43712
+ database.close();
43713
+ }
43714
+ }
43715
+ function oldOwnerDirectory(ownerId) {
43716
+ const digest = createHash11("sha256").update(ownerId).digest("hex").slice(0, 16);
43717
+ return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
43718
+ }
43719
+ function addDirectoryContents(operations, sourceDir, destinationDir, stagingDir) {
43720
+ for (const name of entries(sourceDir)) {
43721
+ operations.push({
43722
+ source: join42(sourceDir, name),
43723
+ staged: join42(stagingDir, randomUUID34()),
43724
+ ...destinationDir ? { destination: join42(destinationDir, name) } : {},
43725
+ state: "pending"
43726
+ });
43727
+ }
43728
+ }
43729
+ function assertNoCollisions(operations) {
43730
+ const destinations = new Set;
43731
+ for (const operation of operations) {
43732
+ if (!operation.destination)
43733
+ continue;
43734
+ if (existsSync36(operation.destination)) {
43735
+ throw new Error(`Migration collision at ${operation.destination}.`);
43736
+ }
43737
+ if (destinations.has(operation.destination)) {
43738
+ throw new Error(`Multiple legacy paths target ${operation.destination}.`);
43739
+ }
43740
+ destinations.add(operation.destination);
43741
+ }
43742
+ }
43743
+ function writeJournal(path, journal) {
43744
+ writeFileSync25(path, `${JSON.stringify(journal, null, 2)}
43745
+ `, { mode: 384 });
43746
+ }
43747
+ function rollbackFilesystem(journal, journalPath) {
43748
+ for (const operation of [...journal.operations].reverse()) {
43749
+ const current3 = operation.state === "placed" ? operation.destination : operation.staged;
43750
+ if (!current3 || !existsSync36(current3) || existsSync36(operation.source))
43751
+ continue;
43752
+ mkdirSync35(dirname23(operation.source), { recursive: true });
43753
+ renameSync16(current3, operation.source);
43754
+ operation.state = "pending";
43755
+ }
43756
+ journal.phase = "filesystem";
43757
+ writeJournal(journalPath, journal);
43758
+ }
43759
+ function tablesWithColumn(database, columnName) {
43760
+ const tables = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
43761
+ return tables.map((row) => row.name).filter((table) => database.query(`PRAGMA table_info("${table.replaceAll('"', '""')}")`).all().some((column) => column.name === columnName));
43762
+ }
43763
+ function selectedTopicIds(dbPath, source) {
43764
+ if (!existsSync36(dbPath))
43765
+ return null;
43766
+ const database = new Database(dbPath, { readonly: true });
43767
+ try {
43768
+ const hasMemberships = database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='topic_members'").get();
43769
+ if (!hasMemberships)
43770
+ return null;
43771
+ return new Set(database.query("SELECT topic_id FROM topic_members WHERE user_id = ?").all(source).map((row) => row.topic_id));
43772
+ } finally {
43773
+ database.close();
43774
+ }
43775
+ }
43776
+ function migrateDatabase(dbPath, source, masterKey, legacyTopicProfiles = []) {
43777
+ if (!existsSync36(dbPath))
43778
+ return [];
43779
+ const database = new Database(dbPath);
43780
+ const userTables = tablesWithColumn(database, "user_id");
43781
+ const ownerUserTables = tablesWithColumn(database, "owner_user_id");
43782
+ const migratedTables = [...new Set([...userTables, ...ownerUserTables])];
43783
+ const retainedTopicIds = selectedTopicIds(dbPath, source);
43784
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
43785
+ try {
43786
+ if (source !== CANONICAL_LOCAL_USER_ID) {
43787
+ for (const [column, tables] of [
43788
+ ["user_id", userTables],
43789
+ ["owner_user_id", ownerUserTables]
43790
+ ]) {
43791
+ for (const table of tables) {
43792
+ const collision = database.query(`SELECT COUNT(*) AS count FROM ${quote(table)} WHERE ${column} = ?`).get(CANONICAL_LOCAL_USER_ID)?.count;
43793
+ if (collision)
43794
+ throw new Error(`Database collision: ${table}.${column} already contains local rows.`);
43795
+ }
43796
+ }
43797
+ const hasUsers = database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'").get();
43798
+ if (hasUsers) {
43799
+ const localUser = database.query("SELECT 1 FROM users WHERE id = ?").get(CANONICAL_LOCAL_USER_ID);
43800
+ if (localUser)
43801
+ throw new Error("Database collision: users already contains canonical local.");
43802
+ }
43803
+ }
43804
+ database.exec("PRAGMA foreign_keys = OFF");
43805
+ database.exec("BEGIN IMMEDIATE");
43806
+ for (const table of userTables) {
43807
+ database.query(`DELETE FROM ${quote(table)} WHERE user_id <> ?`).run(source);
43808
+ if (table === "vault" && source !== CANONICAL_LOCAL_USER_ID) {
43809
+ if (!masterKey)
43810
+ throw new Error("Cannot migrate encrypted vault rows without vault-master-key.");
43811
+ const rows = database.query("SELECT key, value FROM vault WHERE user_id = ?").all(source);
43812
+ for (const row of rows) {
43813
+ const plaintext = decryptVaultValueWithKey(source, row.key, row.value, masterKey).value;
43814
+ database.query("UPDATE vault SET value = ? WHERE user_id = ? AND key = ?").run(encryptVaultValueWithKey(CANONICAL_LOCAL_USER_ID, row.key, plaintext, masterKey), source, row.key);
43815
+ }
43816
+ }
43817
+ database.query(`UPDATE ${quote(table)} SET user_id = ? WHERE user_id = ?`).run(CANONICAL_LOCAL_USER_ID, source);
43818
+ }
43819
+ for (const table of ownerUserTables) {
43820
+ database.query(`DELETE FROM ${quote(table)} WHERE owner_user_id <> ?`).run(source);
43821
+ database.query(`UPDATE ${quote(table)} SET owner_user_id = ? WHERE owner_user_id = ?`).run(CANONICAL_LOCAL_USER_ID, source);
43822
+ }
43823
+ if (retainedTopicIds) {
43824
+ database.exec("CREATE TEMP TABLE migration_selected_topics (id TEXT PRIMARY KEY)");
43825
+ for (const topicId of retainedTopicIds) {
43826
+ database.query("INSERT INTO migration_selected_topics (id) VALUES (?)").run(topicId);
43827
+ }
43828
+ const topicTables = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all().map((row) => row.name).filter((table) => database.query(`PRAGMA table_info(${quote(table)})`).all().some((column) => column.name === "topic_id"));
43829
+ for (const table of topicTables) {
43830
+ database.query(`DELETE FROM ${quote(table)} WHERE topic_id NOT IN (SELECT id FROM migration_selected_topics)`).run();
43831
+ }
43832
+ if (database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='api_topics'").get()) {
43833
+ database.query("DELETE FROM api_topics WHERE id NOT IN (SELECT id FROM migration_selected_topics)").run();
43834
+ }
43835
+ }
43836
+ if (database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='browser_profiles'").get()) {
43837
+ database.query("DELETE FROM browser_profiles WHERE owner_id <> ?").run(source);
43838
+ database.query("UPDATE browser_profiles SET owner_id = ? WHERE owner_id = ?").run(CANONICAL_LOCAL_USER_ID, source);
43839
+ }
43840
+ if (database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='api_topics'").get() && database.query("PRAGMA table_info(api_topics)").all().some((column) => column.name === "browser_profile_owner")) {
43841
+ database.query("UPDATE api_topics SET browser_profile_owner = ? WHERE browser_profile_owner = ?").run(CANONICAL_LOCAL_USER_ID, source);
43842
+ if (retainedTopicIds) {
43843
+ database.query("UPDATE api_topics SET browser_profile_owner = ? WHERE browser_profile_owner IS NOT NULL").run(CANONICAL_LOCAL_USER_ID);
43844
+ } else {
43845
+ database.query("DELETE FROM api_topics WHERE browser_profile_owner IS NOT NULL AND browser_profile_owner <> ?").run(CANONICAL_LOCAL_USER_ID);
43846
+ }
43847
+ for (const binding of legacyTopicProfiles) {
43848
+ database.query("UPDATE api_topics SET browser_profile = ?, browser_profile_owner = ? WHERE id = ?").run(binding.profile, CANONICAL_LOCAL_USER_ID, binding.topicId);
43849
+ }
43850
+ }
43851
+ if (database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'").get()) {
43852
+ database.query("DELETE FROM users WHERE id <> ?").run(source);
43853
+ database.query("UPDATE users SET id = ? WHERE id = ?").run(CANONICAL_LOCAL_USER_ID, source);
43854
+ }
43855
+ database.exec("COMMIT");
43856
+ database.exec("PRAGMA foreign_keys = ON");
43857
+ const fkErrors = database.query("PRAGMA foreign_key_check").all();
43858
+ if (fkErrors.length > 0)
43859
+ throw new Error("Database migration left foreign-key violations.");
43860
+ return migratedTables;
43861
+ } catch (error2) {
43862
+ try {
43863
+ database.exec("ROLLBACK");
43864
+ } catch {}
43865
+ throw error2;
43866
+ } finally {
43867
+ database.close();
43868
+ }
43869
+ }
43870
+ function migrateSingleUserState(options) {
43871
+ const stateDir2 = resolve25(options.stateDir ?? process.env.NEGOTIUM_STATE_DIR ?? join42(homedir12(), ".negotium"));
43872
+ const source = options.sourcePrincipal?.trim() || CANONICAL_LOCAL_USER_ID;
43873
+ if (!source || /[/\\]|\.\./.test(source))
43874
+ throw new Error("Invalid source principal.");
43875
+ if (!options.deleteOtherUsers || !options.confirmed) {
43876
+ throw new Error("Migration requires --delete-other-users and --yes; other principals are permanently deleted.");
43877
+ }
43878
+ const markerPath = join42(stateDir2, ".migration-0.2.0-single-user.json");
43879
+ if (existsSync36(markerPath)) {
43880
+ const marker = JSON.parse(readFileSync27(markerPath, "utf8"));
43881
+ if (marker.id !== SINGLE_USER_MIGRATION_ID)
43882
+ throw new Error(`Unexpected migration marker at ${markerPath}.`);
43883
+ return {
43884
+ status: "already-completed",
43885
+ markerPath,
43886
+ sourcePrincipal: marker.sourcePrincipal ?? source,
43887
+ movedPaths: marker.movedPaths ?? 0,
43888
+ deletedPaths: marker.deletedPaths ?? 0,
43889
+ migratedTables: marker.migratedTables ?? []
43890
+ };
43891
+ }
43892
+ assertNoActiveNode(stateDir2);
43893
+ mkdirSync35(stateDir2, { recursive: true });
43894
+ const stagingRoot = join42(stateDir2, ".migration-0.2.0-staging");
43895
+ const journalPath = join42(stagingRoot, "journal.json");
43896
+ mkdirSync35(stagingRoot, { recursive: true, mode: 448 });
43897
+ let journal;
43898
+ if (existsSync36(journalPath)) {
43899
+ journal = JSON.parse(readFileSync27(journalPath, "utf8"));
43900
+ if (journal.sourcePrincipal !== source)
43901
+ throw new Error("Interrupted migration used a different source principal.");
43902
+ } else {
43903
+ const operations = [];
43904
+ const retainedTopicIds = selectedTopicIds(join42(stateDir2, "data", "sessions.db"), source);
43905
+ addDirectoryContents(operations, join42(stateDir2, "run"), join42(stateDir2, "runtime"), stagingRoot);
43906
+ addDirectoryContents(operations, join42(stateDir2, "bin"), join42(stateDir2, "binaries"), stagingRoot);
43907
+ for (const secret of ["node-control-token", "runtime-mcp-secret", "vault-master-key"]) {
43908
+ const sourcePath = join42(stateDir2, secret);
43909
+ if (existsSync36(sourcePath))
43910
+ operations.push({
43911
+ source: sourcePath,
43912
+ staged: join42(stagingRoot, randomUUID34()),
43913
+ destination: join42(stateDir2, "secrets", secret),
43914
+ state: "pending"
43915
+ });
43916
+ }
43917
+ const legacyVaultDb = join42(stateDir2, "data", "vault.db");
43918
+ if (existsSync36(legacyVaultDb)) {
43919
+ operations.push({
43920
+ source: legacyVaultDb,
43921
+ staged: join42(stagingRoot, randomUUID34()),
43922
+ destination: join42(stateDir2, "data", "vault", "vault.db"),
43923
+ state: "pending"
43924
+ });
43925
+ }
43926
+ for (const store of ["conversations", "tasks", "users"]) {
43927
+ const root = join42(stateDir2, "data", store);
43928
+ addDirectoryContents(operations, join42(root, source), root, stagingRoot);
43929
+ for (const owner of entries(root)) {
43930
+ const ownerPath = join42(root, owner);
43931
+ if (owner !== source && statSync19(ownerPath).isDirectory()) {
43932
+ operations.push({
43933
+ source: ownerPath,
43934
+ staged: join42(stagingRoot, randomUUID34()),
43935
+ state: "pending"
43936
+ });
43937
+ }
43938
+ }
43939
+ }
43940
+ for (const legacyWorkspaceDir of ["contexts", "dm", "sessions"]) {
43941
+ addDirectoryContents(operations, join42(stateDir2, "workspace", legacyWorkspaceDir), join42(stateDir2, "data", legacyWorkspaceDir), stagingRoot);
43942
+ }
43943
+ for (const topicId of entries(join42(stateDir2, "workspace", "topics"))) {
43944
+ const topicPath = join42(stateDir2, "workspace", "topics", topicId);
43945
+ if (retainedTopicIds && !retainedTopicIds.has(topicId)) {
43946
+ operations.push({
43947
+ source: topicPath,
43948
+ staged: join42(stagingRoot, randomUUID34()),
43949
+ state: "pending"
43950
+ });
43951
+ } else {
43952
+ addDirectoryContents(operations, join42(topicPath, "uploads"), join42(stateDir2, "data", "uploads", topicId), stagingRoot);
43953
+ }
43954
+ }
43955
+ if (retainedTopicIds) {
43956
+ for (const topicId of entries(join42(stateDir2, "workspace", "wiki"))) {
43957
+ if (retainedTopicIds.has(topicId))
43958
+ continue;
43959
+ operations.push({
43960
+ source: join42(stateDir2, "workspace", "wiki", topicId),
43961
+ staged: join42(stagingRoot, randomUUID34()),
43962
+ state: "pending"
43963
+ });
43964
+ }
43965
+ }
43966
+ const legacyBrowserRoot = join42(stateDir2, "workspace", "browser-profiles");
43967
+ const selectedProfiles = join42(legacyBrowserRoot, "profiles", oldOwnerDirectory(source));
43968
+ addDirectoryContents(operations, selectedProfiles, join42(stateDir2, "browser", "profiles"), stagingRoot);
43969
+ for (const owner of entries(join42(legacyBrowserRoot, "profiles"))) {
43970
+ const ownerPath = join42(legacyBrowserRoot, "profiles", owner);
43971
+ if (ownerPath !== selectedProfiles) {
43972
+ operations.push({
43973
+ source: ownerPath,
43974
+ staged: join42(stagingRoot, randomUUID34()),
43975
+ state: "pending"
43976
+ });
43977
+ }
43978
+ }
43979
+ for (const topicId of entries(legacyBrowserRoot).filter((name) => name !== "profiles")) {
43980
+ if (retainedTopicIds && !retainedTopicIds.has(topicId)) {
43981
+ operations.push({
43982
+ source: join42(legacyBrowserRoot, topicId),
43983
+ staged: join42(stagingRoot, randomUUID34()),
43984
+ state: "pending"
43985
+ });
43986
+ continue;
43987
+ }
43988
+ const profile = `legacy_${createHash11("sha256").update(topicId).digest("hex").slice(0, 12)}`;
43989
+ operations.push({
43990
+ source: join42(legacyBrowserRoot, topicId),
43991
+ staged: join42(stagingRoot, randomUUID34()),
43992
+ destination: join42(stateDir2, "browser", "profiles", profile),
43993
+ state: "pending"
43994
+ });
43995
+ }
43996
+ assertNoCollisions(operations);
43997
+ journal = {
43998
+ id: SINGLE_USER_MIGRATION_ID,
43999
+ sourcePrincipal: source,
44000
+ createdAt: new Date().toISOString(),
44001
+ phase: "filesystem",
44002
+ operations
44003
+ };
44004
+ writeJournal(journalPath, journal);
44005
+ }
44006
+ const dbPath = join42(stateDir2, "data", "sessions.db");
44007
+ const dbBackup = join42(stagingRoot, "sessions.db.rollback");
44008
+ const vaultDbPath = join42(stateDir2, "data", "vault", "vault.db");
44009
+ const vaultDbBackup = join42(stagingRoot, "vault.db.rollback");
44010
+ try {
44011
+ for (const operation of journal.operations) {
44012
+ if (operation.state === "pending" && !existsSync36(operation.source) && existsSync36(operation.staged)) {
44013
+ operation.state = "staged";
44014
+ }
44015
+ if (operation.state === "staged" && operation.destination && !existsSync36(operation.staged) && existsSync36(operation.destination)) {
44016
+ operation.state = "placed";
44017
+ }
44018
+ if (operation.state === "pending") {
44019
+ mkdirSync35(dirname23(operation.staged), { recursive: true });
44020
+ renameSync16(operation.source, operation.staged);
44021
+ operation.state = "staged";
44022
+ writeJournal(journalPath, journal);
44023
+ }
44024
+ if (operation.state === "staged" && operation.destination) {
44025
+ mkdirSync35(dirname23(operation.destination), { recursive: true });
44026
+ renameSync16(operation.staged, operation.destination);
44027
+ operation.state = "placed";
44028
+ writeJournal(journalPath, journal);
44029
+ }
44030
+ }
44031
+ journal.phase = "database";
44032
+ writeJournal(journalPath, journal);
44033
+ if (existsSync36(dbPath) && !existsSync36(dbBackup))
44034
+ copyFileSync6(dbPath, dbBackup);
44035
+ if (existsSync36(vaultDbPath) && !existsSync36(vaultDbBackup))
44036
+ copyFileSync6(vaultDbPath, vaultDbBackup);
44037
+ const masterKeyPath = join42(stateDir2, "secrets", "vault-master-key");
44038
+ const masterKey = existsSync36(masterKeyPath) ? readFileSync27(masterKeyPath, "utf8").trim() : undefined;
44039
+ const legacyBrowserRoot = join42(stateDir2, "workspace", "browser-profiles");
44040
+ const legacyTopicProfiles = journal.operations.filter((operation) => dirname23(operation.source) === legacyBrowserRoot && operation.destination).map((operation) => ({
44041
+ topicId: basename14(operation.source),
44042
+ profile: basename14(operation.destination)
44043
+ }));
44044
+ const migratedTables = migrateDatabase(dbPath, source, masterKey, legacyTopicProfiles);
44045
+ const migratedVaultTables = migrateDatabase(vaultDbPath, source, masterKey);
44046
+ for (const table of migratedVaultTables)
44047
+ if (!migratedTables.includes(table))
44048
+ migratedTables.push(table);
44049
+ mkdirSync35(join42(stateDir2, "secrets"), { recursive: true, mode: 448 });
44050
+ chmodSync8(join42(stateDir2, "secrets"), 448);
44051
+ for (const secret of ["node-control-token", "runtime-mcp-secret", "vault-master-key"]) {
44052
+ const path = join42(stateDir2, "secrets", secret);
44053
+ if (existsSync36(path))
44054
+ chmodSync8(path, 384);
44055
+ }
44056
+ const deletedPaths = journal.operations.filter((operation) => !operation.destination).length;
44057
+ const movedPaths = journal.operations.length - deletedPaths;
44058
+ const marker = {
44059
+ id: SINGLE_USER_MIGRATION_ID,
44060
+ version: "0.2.0",
44061
+ completedAt: new Date().toISOString(),
44062
+ sourcePrincipal: source,
44063
+ canonicalPrincipal: CANONICAL_LOCAL_USER_ID,
44064
+ movedPaths,
44065
+ deletedPaths,
44066
+ migratedTables
44067
+ };
44068
+ for (const store of ["conversations", "tasks", "users"]) {
44069
+ rmSync11(join42(stateDir2, "data", store, source), { recursive: true, force: true });
44070
+ }
44071
+ rmSync11(join42(stateDir2, "workspace", "browser-profiles"), { recursive: true, force: true });
44072
+ writeFileSync25(markerPath, `${JSON.stringify(marker, null, 2)}
44073
+ `, { mode: 384, flag: "wx" });
44074
+ journal.phase = "complete";
44075
+ rmSync11(stagingRoot, { recursive: true, force: true });
44076
+ return {
44077
+ status: "completed",
44078
+ markerPath,
44079
+ sourcePrincipal: source,
44080
+ movedPaths,
44081
+ deletedPaths,
44082
+ migratedTables
44083
+ };
44084
+ } catch (error2) {
44085
+ if (existsSync36(dbBackup))
44086
+ copyFileSync6(dbBackup, dbPath);
44087
+ if (existsSync36(vaultDbBackup))
44088
+ copyFileSync6(vaultDbBackup, vaultDbPath);
44089
+ rollbackFilesystem(journal, journalPath);
44090
+ throw error2;
44091
+ }
44092
+ }
44093
+ var CANONICAL_LOCAL_USER_ID = "local", SINGLE_USER_MIGRATION_ID = "negotium-0.2.0-single-user";
44094
+ var init_single_user = __esm(async () => {
44095
+ init_vault_crypto_core();
44096
+ await init_sqlite();
44097
+ });
44098
+
44099
+ // ../cli/src/commands/migrate.ts
44100
+ var exports_migrate = {};
44101
+ __export(exports_migrate, {
44102
+ migrateCommand: () => migrateCommand
44103
+ });
44104
+ function option3(args, name) {
44105
+ const prefix = `--${name}=`;
44106
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
44107
+ }
44108
+ function migrateCommand(args) {
44109
+ if (args[0] !== "single-user") {
44110
+ throw new Error("usage: negotium migrate single-user [--source=local] --delete-other-users --yes");
44111
+ }
44112
+ const result = migrateSingleUserState({
44113
+ sourcePrincipal: option3(args, "source"),
44114
+ deleteOtherUsers: args.includes("--delete-other-users"),
44115
+ confirmed: args.includes("--yes")
44116
+ });
44117
+ console.log(`${result.status}: ${result.markerPath}`);
44118
+ }
44119
+ var init_migrate = __esm(async () => {
44120
+ await init_single_user();
44121
+ });
44122
+
43660
44123
  // ../cli/src/commands/topics.ts
43661
44124
  var exports_topics = {};
43662
44125
  __export(exports_topics, {
@@ -43758,12 +44221,12 @@ function vaultCommand(args) {
43758
44221
  switch (sub) {
43759
44222
  case undefined:
43760
44223
  case "list": {
43761
- const entries = vaultListWithValues(DEFAULT_USER);
43762
- if (entries.length === 0) {
44224
+ const entries2 = vaultListWithValues(DEFAULT_USER);
44225
+ if (entries2.length === 0) {
43763
44226
  console.log("vault is empty \u2014 `negotium vault set MY_KEY <value> [description]`");
43764
44227
  return;
43765
44228
  }
43766
- for (const entry of entries) {
44229
+ for (const entry of entries2) {
43767
44230
  console.log(`${entry.key} ${entry.description || ""}`.trimEnd());
43768
44231
  }
43769
44232
  return;
@@ -44087,6 +44550,12 @@ var CLI_COMMANDS = [
44087
44550
  description: "bootstrap ~/.negotium and check agent auth",
44088
44551
  group: "Setup"
44089
44552
  },
44553
+ {
44554
+ name: "migrate",
44555
+ usage: "migrate single-user [--source=local] --delete-other-users --yes",
44556
+ description: "explicitly migrate a 0.1 state tree to the 0.2 single-user layout",
44557
+ group: "Setup"
44558
+ },
44090
44559
  {
44091
44560
  name: "serve",
44092
44561
  usage: "serve [otium]",
@@ -44193,7 +44662,7 @@ async function runCanonicalNode(port) {
44193
44662
  });
44194
44663
  console.log(`negotium node listening on 127.0.0.1:${node.port} (ctrl-c to stop)`);
44195
44664
  await node.completed;
44196
- await new Promise((resolve25) => setImmediate(resolve25));
44665
+ await new Promise((resolve26) => setImmediate(resolve26));
44197
44666
  process.exit(0);
44198
44667
  }
44199
44668
  async function stopAdapter(name) {
@@ -44216,6 +44685,11 @@ switch (command) {
44216
44685
  initCommand2();
44217
44686
  break;
44218
44687
  }
44688
+ case "migrate": {
44689
+ const { migrateCommand: migrateCommand2 } = await init_migrate().then(() => exports_migrate);
44690
+ migrateCommand2(args);
44691
+ break;
44692
+ }
44219
44693
  case "-v":
44220
44694
  case "--version": {
44221
44695
  const { NEGOTIUM_VERSION: NEGOTIUM_VERSION2 } = await Promise.resolve().then(() => exports_version);
@@ -44324,4 +44798,4 @@ switch (command) {
44324
44798
  }
44325
44799
  }
44326
44800
 
44327
- //# debugId=90FDEC7EC6CF014764756E2164756E21
44801
+ //# debugId=9371DDB5367979FC64756E2164756E21