negotium 0.1.49 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-helpers.js +58 -74
  3. package/dist/agent-helpers.js.map +15 -15
  4. package/dist/background-bash.js +22 -10
  5. package/dist/background-bash.js.map +3 -3
  6. package/dist/browser-runtime.js +35 -68
  7. package/dist/browser-runtime.js.map +7 -7
  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 +36 -18
  11. package/dist/hosted-agent.js.map +7 -7
  12. package/dist/main.js +562 -98
  13. package/dist/main.js.map +21 -19
  14. package/dist/mcp-factories.js +62 -78
  15. package/dist/mcp-factories.js.map +16 -16
  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/src/agents/maestro-registry.ts +1 -1
  24. package/dist/runtime/src/agents/rollout/shared.ts +12 -4
  25. package/dist/runtime/src/mcp/mcp-helpers.ts +1 -1
  26. package/dist/runtime/src/mcp/wiki-server.ts +1 -1
  27. package/dist/runtime/src/migration/single-user.ts +621 -0
  28. package/dist/runtime/src/platform/config.ts +24 -11
  29. package/dist/runtime/src/platform/mcp-config.ts +4 -4
  30. package/dist/runtime/src/platform/playwright/manager.ts +10 -28
  31. package/dist/runtime/src/runtime/attachments.ts +3 -3
  32. package/dist/runtime/src/storage/browser-profiles.ts +11 -9
  33. package/dist/runtime/src/storage/conversations.ts +5 -18
  34. package/dist/runtime/src/storage/storage-host.ts +1 -1
  35. package/dist/runtime/src/storage/tasks.ts +2 -14
  36. package/dist/runtime/src/storage/vault.ts +3 -2
  37. package/dist/runtime/src/version.ts +1 -1
  38. package/dist/runtime-helpers.js +22 -10
  39. package/dist/runtime-helpers.js.map +5 -5
  40. package/dist/storage.js +5 -19
  41. package/dist/storage.js.map +5 -5
  42. package/dist/types/packages/core/src/mcp/mcp-helpers.d.ts +1 -1
  43. package/dist/types/packages/core/src/platform/config.d.ts +9 -1
  44. package/dist/types/packages/core/src/runtime/attachments.d.ts +1 -1
  45. package/dist/types/packages/core/src/storage/conversations.d.ts +2 -2
  46. package/dist/types/packages/core/src/version.d.ts +1 -1
  47. package/dist/vault.js +25 -12
  48. package/dist/vault.js.map +4 -4
  49. package/install-browser-rs.mjs +1 -1
  50. package/package.json +2 -2
@@ -69,10 +69,15 @@ function parsePortEnv(envValue: string | undefined, fallback: number): number {
69
69
  export const WORKSPACE_DIR = resolveLocalStateDir("NEGOTIUM_WORKSPACE_DIR", "workspace");
70
70
  export const TOPIC_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "topics");
71
71
  export const SHARED_WIKI_DIR = resolve(WORKSPACE_DIR, "wiki");
72
- export const CONTEXTS_DIR = resolve(WORKSPACE_DIR, "contexts");
73
- export const BROWSER_PROFILES_DIR = resolve(WORKSPACE_DIR, "browser-profiles");
74
- export const DM_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "dm");
75
- export const SESSION_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "sessions");
72
+ export const CRON_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "cron");
73
+ export const BROWSER_DIR = resolveLocalStateDir("NEGOTIUM_BROWSER_DIR", "browser");
74
+ export const BROWSER_PROFILES_DIR = resolve(BROWSER_DIR, "profiles");
75
+ export const BINARIES_DIR = resolve(STATE_DIR, "binaries");
76
+ export const SECRETS_DIR = resolve(STATE_DIR, "secrets");
77
+ // Legacy names remain public, but their durable contents now live under data.
78
+ export const CONTEXTS_DIR = resolve(STATE_DIR, "data", "contexts");
79
+ export const DM_WORKSPACE_DIR = resolve(STATE_DIR, "data", "dm");
80
+ export const SESSION_WORKSPACE_DIR = resolve(STATE_DIR, "data", "sessions");
76
81
  // The Claude Agent SDK ships a platform-matched Claude Code binary. Keep that
77
82
  // SDK/CLI pair together by default; only use an external executable when an
78
83
  // operator explicitly opts in. This avoids silently pairing an older SDK with
@@ -140,7 +145,7 @@ export function resolveBrowserRsBin(envValue?: string): string | undefined {
140
145
  }
141
146
  const candidate = override
142
147
  ? resolve(override)
143
- : resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs");
148
+ : resolve(BINARIES_DIR, "browser-rs", BROWSER_RS_VERSION, "browser-rs");
144
149
  try {
145
150
  accessSync(candidate, constants.X_OK);
146
151
  return browserRsMeetsMinimumVersion(candidate) ? candidate : undefined;
@@ -155,9 +160,7 @@ export const BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_B
155
160
  export function resolveBrowserMcpBin(envValue?: string): string {
156
161
  const override = envValue?.trim();
157
162
  if (override) return resolve(override);
158
- return (
159
- BROWSER_RS_BIN ?? resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs")
160
- );
163
+ return BROWSER_RS_BIN ?? resolve(BINARIES_DIR, "browser-rs", BROWSER_RS_VERSION, "browser-rs");
161
164
  }
162
165
 
163
166
  export const PLAYWRIGHT_MCP_BIN = resolveBrowserMcpBin(envText("NEGOTIUM_BROWSER_MCP_BIN"));
@@ -269,7 +272,7 @@ function loadOrCreateLocalSecret(
269
272
  options: { persistEnvValue?: boolean } = {},
270
273
  ): string {
271
274
  const envValue = envText(envKey);
272
- const secretFile = resolve(STATE_DIR, filename);
275
+ const secretFile = resolve(SECRETS_DIR, filename);
273
276
  mkdirSync(dirname(secretFile), { recursive: true });
274
277
  if (envValue) {
275
278
  if (options.persistEnvValue) {
@@ -325,6 +328,8 @@ export const hostname = process.env.HOSTNAME || "127.0.0.1";
325
328
  // Persistent state (survives restarts, long-lived)
326
329
  export const DATA_DIR = resolveLocalStateDir("NEGOTIUM_DATA_DIR", "data");
327
330
  export const LOG_DIR = resolveLocalStateDir("NEGOTIUM_LOG_DIR", "logs");
331
+ export const UPLOADS_DIR = resolve(DATA_DIR, "uploads");
332
+ export const VAULT_DIR = resolve(DATA_DIR, "vault");
328
333
  // SESSIONS_DB_PATH env override lets tests point the DB singleton at a temp file.
329
334
  export const SESSIONS_DB = process.env.SESSIONS_DB_PATH
330
335
  ? resolve(process.env.SESSIONS_DB_PATH)
@@ -333,7 +338,9 @@ export const DEBUG_FILE = resolve(DATA_DIR, "debug-users.json");
333
338
  export const USERS_LOG_DIR = resolve(DATA_DIR, "users");
334
339
 
335
340
  // Runtime IPC queues (transient, safe to clear on restart)
336
- export const RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "run");
341
+ /** @deprecated Use the runtime layout name; NEGOTIUM_RUN_DIR remains a compatibility override. */
342
+ export const RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "runtime");
343
+ export const RUNTIME_DIR = RUN_DIR;
337
344
  export const PROGRESS_DIR = resolve(RUN_DIR, "progress");
338
345
  export const DM_CMD_DIR = resolve(RUN_DIR, "dm-commands");
339
346
  export const DM_RESP_DIR = resolve(RUN_DIR, "dm-responses");
@@ -344,6 +351,8 @@ export const PLAYWRIGHT_MAX_PORT = parsePortEnv(process.env.PLAYWRIGHT_MAX_PORT,
344
351
  export const PLAYWRIGHT_PORTS_DIR = resolve(RUN_DIR, "playwright-ports");
345
352
  mkdirSync(STATE_DIR, { recursive: true });
346
353
  mkdirSync(DATA_DIR, { recursive: true });
354
+ mkdirSync(UPLOADS_DIR, { recursive: true });
355
+ mkdirSync(VAULT_DIR, { recursive: true, mode: 0o700 });
347
356
  mkdirSync(LOG_DIR, { recursive: true });
348
357
  mkdirSync(PROGRESS_DIR, { recursive: true });
349
358
  mkdirSync(DM_CMD_DIR, { recursive: true });
@@ -354,10 +363,14 @@ mkdirSync(PLAYWRIGHT_PORTS_DIR, { recursive: true });
354
363
  mkdirSync(WORKSPACE_DIR, { recursive: true });
355
364
  mkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });
356
365
  mkdirSync(SHARED_WIKI_DIR, { recursive: true });
366
+ mkdirSync(CRON_WORKSPACE_DIR, { recursive: true });
357
367
  mkdirSync(CONTEXTS_DIR, { recursive: true });
358
- mkdirSync(BROWSER_PROFILES_DIR, { recursive: true });
359
368
  mkdirSync(DM_WORKSPACE_DIR, { recursive: true });
360
369
  mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
370
+ mkdirSync(BROWSER_DIR, { recursive: true });
371
+ mkdirSync(BROWSER_PROFILES_DIR, { recursive: true });
372
+ mkdirSync(BINARIES_DIR, { recursive: true });
373
+ mkdirSync(SECRETS_DIR, { recursive: true, mode: 0o700 });
361
374
 
362
375
  /** Stale threshold for active-query state files (crash recovery) */
363
376
  export const ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000; // 10 minutes
@@ -902,7 +902,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
902
902
  if (opts.sessionType === "cron") {
903
903
  if (!opts.topicId) throw new Error("getMcpServersForQuery: cron sessionType requires topicId");
904
904
  return getCronMcpServers({
905
- userId: opts.userId || "default",
905
+ userId: opts.userId || "local",
906
906
  session: opts.session || "cron",
907
907
  topicId: opts.topicId,
908
908
  queryId: opts.queryId,
@@ -919,7 +919,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
919
919
  }
920
920
  if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
921
921
  return getDmMcpServers({
922
- userId: opts.userId || "default",
922
+ userId: opts.userId || "local",
923
923
  agent: opts.agent,
924
924
  playwrightPort: opts.playwrightPort,
925
925
  playwrightCapability: opts.playwrightCapability,
@@ -927,7 +927,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
927
927
  }
928
928
  if (opts.sessionType === "manager") {
929
929
  return getManagerMcpServers({
930
- userId: opts.userId || "default",
930
+ userId: opts.userId || "local",
931
931
  session: opts.session,
932
932
  topicId: opts.topicId,
933
933
  queryId: opts.queryId,
@@ -944,7 +944,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
944
944
  });
945
945
  }
946
946
  return getForumMcpServers({
947
- userId: opts.userId || "default",
947
+ userId: opts.userId || "local",
948
948
  session: opts.session || "default",
949
949
  topicId: opts.topicId,
950
950
  subagentParentTopicId: opts.subagentParentTopicId,
@@ -5,7 +5,6 @@ import {
5
5
  existsSync,
6
6
  mkdirSync,
7
7
  readFileSync,
8
- renameSync,
9
8
  rmSync,
10
9
  unlinkSync,
11
10
  writeFileSync,
@@ -123,7 +122,7 @@ export function resolvePlaywrightTopicBinding(
123
122
  function defaultTopicBinding(userId: string, topic: string | undefined): PlaywrightProfileBinding {
124
123
  if (!topic) return managerHost.resolveNamedBinding(userId, "default");
125
124
  const ownerId = getBrowserProfileOwner(topic, userId);
126
- const profile = migrateLegacyTopicProfile(ownerId, topic);
125
+ const profile = getTopicBrowserProfile(topic);
127
126
  return managerHost.resolveNamedBinding(ownerId, profile);
128
127
  }
129
128
 
@@ -153,12 +152,12 @@ function validateProfileBinding(binding: PlaywrightProfileBinding): PlaywrightPr
153
152
  return Object.freeze({ ...binding });
154
153
  }
155
154
 
156
- function defaultNamedBinding(ownerId: string, rawProfile: string): PlaywrightProfileBinding {
155
+ function defaultNamedBinding(_ownerId: string, rawProfile: string): PlaywrightProfileBinding {
157
156
  const profile = normalizeBrowserProfileName(rawProfile);
158
- const instanceKey = `profile:${encodeURIComponent(ownerId)}:${profile}`;
157
+ const instanceKey = `profile:${profile}`;
159
158
  return {
160
159
  instanceKey,
161
- ownerId,
160
+ ownerId: "local",
162
161
  profile,
163
162
  };
164
163
  }
@@ -167,22 +166,6 @@ export function legacyBrowserProfileName(topic: string): string {
167
166
  return `legacy_${createHash("sha256").update(topic).digest("hex").slice(0, 12)}`;
168
167
  }
169
168
 
170
- function migrateLegacyTopicProfile(ownerId: string, topic: string): string {
171
- const current = getTopicBrowserProfile(topic);
172
- if (current !== "default" || !hasBrowserProfileTopic(topic)) return current;
173
-
174
- const legacyDir = resolve(BROWSER_PROFILES_DIR, sanitizeTopicName(topic));
175
- if (!existsSync(legacyDir)) return current;
176
-
177
- const profile = legacyBrowserProfileName(topic);
178
- const profileDir = defaultProfileDir(ownerId, profile);
179
- mkdirSync(dirname(profileDir), { recursive: true });
180
- if (!existsSync(profileDir)) renameSync(legacyDir, profileDir);
181
- assignTopicBrowserProfile({ topicId: topic, actorUserId: ownerId, profile });
182
- logger.info({ ownerId, topic, profile }, "Adopted legacy topic browser profile");
183
- return profile;
184
- }
185
-
186
169
  function defaultChildEnvironment(context: PlaywrightChildEnvironmentContext): NodeJS.ProcessEnv {
187
170
  const childEnvironment: NodeJS.ProcessEnv = {};
188
171
  for (const key of [
@@ -281,6 +264,10 @@ interface InstanceKeyParts {
281
264
  }
282
265
 
283
266
  function parseInstanceKey(instanceKey: string): InstanceKeyParts {
267
+ const shared = /^profile:(.+)$/.exec(instanceKey);
268
+ if (shared && !shared[1]!.includes(":")) {
269
+ return { ownerId: "local", profile: normalizeBrowserProfileName(shared[1]!) };
270
+ }
284
271
  const match = /^profile:([^:]+):(.+)$/.exec(instanceKey);
285
272
  if (!match) return { ownerId: "legacy", profile: sanitizeTopicName(instanceKey) };
286
273
  return {
@@ -571,13 +558,8 @@ function releasePort(port: number) {
571
558
  usedPorts.delete(port);
572
559
  }
573
560
 
574
- function ownerDirectory(ownerId: string): string {
575
- const digest = createHash("sha256").update(ownerId).digest("hex").slice(0, 16);
576
- return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
577
- }
578
-
579
- function defaultProfileDir(ownerId: string, profile: string): string {
580
- return resolve(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
561
+ function defaultProfileDir(_ownerId: string, profile: string): string {
562
+ return resolve(BROWSER_PROFILES_DIR, profile);
581
563
  }
582
564
 
583
565
  /** Resolve the shared profile userDataDir for an instanceKey. */
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { basename, join } from "node:path";
4
- import { resolveTopicWorkspaceDir } from "#platform/config";
4
+ import { resolveTopicWorkspaceDir, UPLOADS_DIR } from "#platform/config";
5
5
  import { logger } from "#platform/logger";
6
6
  import { resolveAttachmentByFileId, resolveUploadedFilePathByFileId } from "#runtime/file-hooks";
7
7
  import type { AgentInputAttachment } from "#types";
@@ -104,13 +104,13 @@ export interface IngestedAttachment {
104
104
  }
105
105
 
106
106
  /**
107
- * Store an inbound channel attachment in the topic's workspace (`uploads/`)
107
+ * Store an inbound channel attachment in the canonical data upload store.
108
108
  * and return the canonical prompt fragment for it. Single implementation of
109
109
  * the "download → workspace → [Attached file: …] line" intake used by every
110
110
  * channel adapter (ported from clawgram's buildPromptFromMessage convention).
111
111
  */
112
112
  export function ingestAttachment(args: IngestAttachmentArgs): IngestedAttachment {
113
- const destDir = join(workspaceCwdFor(args.topicId), "uploads");
113
+ const destDir = join(UPLOADS_DIR, args.topicId);
114
114
  mkdirSync(destDir, { recursive: true });
115
115
  const safeName = safeAttachmentFilename(args.filename, "upload");
116
116
  // Timestamp alone collides for same-name files ingested in the same ms
@@ -55,11 +55,12 @@ export function hasBrowserProfileTopic(topicId: string): boolean {
55
55
  }
56
56
 
57
57
  export function createBrowserProfile(ownerId: string, rawName: string): string {
58
+ void ownerId;
58
59
  const name = normalizeBrowserProfileName(rawName);
59
60
  if (name === DEFAULT_BROWSER_PROFILE) return name;
60
61
  db.query(
61
62
  "INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)",
62
- ).run(ownerId, name, new Date().toISOString());
63
+ ).run("local", name, new Date().toISOString());
63
64
  return name;
64
65
  }
65
66
 
@@ -84,18 +85,18 @@ export function assignTopicBrowserProfile(opts: {
84
85
  }
85
86
 
86
87
  export function listBrowserProfiles(ownerId: string): BrowserProfileRecord[] {
88
+ void ownerId;
87
89
  const records = db
88
90
  .query<{ name: string; created_at: string }, string>(
89
91
  "SELECT name, created_at FROM browser_profiles WHERE owner_id = ? ORDER BY name",
90
92
  )
91
- .all(ownerId);
93
+ .all("local");
92
94
  const topics = db
93
- .query<{ id: string; title: string; browser_profile: string | null }, string>(
95
+ .query<{ id: string; title: string; browser_profile: string | null }, []>(
94
96
  `SELECT t.id, t.title, t.browser_profile FROM api_topics t
95
- WHERE t.browser_profile_owner = ?
96
97
  ORDER BY t.title`,
97
98
  )
98
- .all(ownerId);
99
+ .all();
99
100
 
100
101
  const byName = new Map<string, BrowserProfileRecord>();
101
102
  byName.set(DEFAULT_BROWSER_PROFILE, {
@@ -120,14 +121,15 @@ export function listBrowserProfiles(ownerId: string): BrowserProfileRecord[] {
120
121
  }
121
122
 
122
123
  export function deleteBrowserProfile(ownerId: string, rawName: string): void {
124
+ void ownerId;
123
125
  const name = normalizeBrowserProfileName(rawName);
124
126
  if (name === DEFAULT_BROWSER_PROFILE) throw new Error("The default profile cannot be deleted.");
125
127
  const used = db
126
- .query<{ count: number }, [string, string]>(
128
+ .query<{ count: number }, string>(
127
129
  `SELECT COUNT(*) AS count FROM api_topics t
128
- WHERE t.browser_profile = ? AND t.browser_profile_owner = ?`,
130
+ WHERE t.browser_profile = ?`,
129
131
  )
130
- .get(name, ownerId)?.count;
132
+ .get(name)?.count;
131
133
  if (used) throw new Error(`Profile "${name}" is assigned to ${used} topic(s).`);
132
- db.query("DELETE FROM browser_profiles WHERE owner_id = ? AND name = ?").run(ownerId, name);
134
+ db.query("DELETE FROM browser_profiles WHERE owner_id = ? AND name = ?").run("local", name);
133
135
  }
@@ -18,8 +18,8 @@ import type { AgentKind, UnifiedEvent } from "#types";
18
18
  * **provider-agnostic source of truth** for cross-agent portability.
19
19
  *
20
20
  * Storage layout:
21
- * {DATA_DIR}/conversations/{userId}/{sanitizedTopicName}.jsonl
22
- * {DATA_DIR}/conversations/{userId}/{sanitizedTopicName}.active.jsonl
21
+ * {DATA_DIR}/conversations/{sanitizedTopicName}.jsonl
22
+ * {DATA_DIR}/conversations/{sanitizedTopicName}.active.jsonl
23
23
  *
24
24
  * The first file is append-only and retains every yielded UnifiedEvent for
25
25
  * archive and teardown. The optional active file is a replaceable provider
@@ -44,22 +44,9 @@ export interface ConversationEntry {
44
44
  event: UnifiedEvent;
45
45
  }
46
46
 
47
- /**
48
- * Validate that a userId stringifies to a safe path component. Rejects empties,
49
- * slashes, and traversal sequences before they touch the filesystem. The
50
- * conversation log is local-only so we keep this strict but simple if a
51
- * caller sends something unexpected it is their bug to fix, not ours to coerce.
52
- */
53
- function safeUserIdComponent(userId: number | string): string {
54
- const str = String(userId);
55
- if (!str || /[/\\]|\.\./.test(str)) {
56
- throw new Error(`conversations: refusing unsafe userId path component: ${str}`);
57
- }
58
- return str;
59
- }
60
-
61
- function conversationDir(userId: number | string): string {
62
- return join(resolveStorageDataDir(), "conversations", safeUserIdComponent(userId));
47
+ /** Public userId remains in the API, but standalone storage has one flat local namespace. */
48
+ function conversationDir(_userId: number | string): string {
49
+ return join(resolveStorageDataDir(), "conversations");
63
50
  }
64
51
 
65
52
  function topicFilename(topicName: string): string {
@@ -59,7 +59,7 @@ function defaultWorkspaceDir(): string {
59
59
  }
60
60
 
61
61
  function defaultSessionAsksDir(): string {
62
- const runDir = envPath("NEGOTIUM_RUN_DIR", join(defaultStateDir(), "run"));
62
+ const runDir = envPath("NEGOTIUM_RUN_DIR", join(defaultStateDir(), "runtime"));
63
63
  return join(runDir, "session-asks");
64
64
  }
65
65
 
@@ -22,14 +22,6 @@ interface TaskFileShape {
22
22
  tasks: StoredTask[];
23
23
  }
24
24
 
25
- function safeUserIdComponent(userId: number | string): string {
26
- const str = String(userId);
27
- if (!str || /[/\\]|\.\./.test(str)) {
28
- throw new Error(`tasks: refusing unsafe userId path component: ${str}`);
29
- }
30
- return str;
31
- }
32
-
33
25
  function safeTaskScopeKey(scopeKey: string): string {
34
26
  const safe = sanitizeFileName(scopeKey);
35
27
  if (!safe || safe === "." || safe === "..") {
@@ -43,12 +35,8 @@ export function taskScopeKey(opts: { topicId?: string; session: string }): strin
43
35
  }
44
36
 
45
37
  export function getTaskFilePath(userId: number | string, scopeKey: string): string {
46
- return join(
47
- resolveStorageDataDir(),
48
- "tasks",
49
- safeUserIdComponent(userId),
50
- `${safeTaskScopeKey(scopeKey)}.json`,
51
- );
38
+ void userId;
39
+ return join(resolveStorageDataDir(), "tasks", `${safeTaskScopeKey(scopeKey)}.json`);
52
40
  }
53
41
 
54
42
  export function readTasks(userId: number | string, scopeKey: string): StoredTask[] {
@@ -60,8 +60,9 @@ function initializeVaultDatabase(database: VaultDatabase): void {
60
60
  }
61
61
 
62
62
  function openVaultDatabase(dataDir: string): VaultDatabase {
63
- const path = join(dataDir, "vault.db");
64
- mkdirSync(dataDir, { recursive: true });
63
+ const vaultDir = join(dataDir, "vault");
64
+ const path = join(vaultDir, "vault.db");
65
+ mkdirSync(vaultDir, { recursive: true, mode: 0o700 });
65
66
  const database = new Database(path, { create: true });
66
67
  // Defense in depth: the database and its sidecars are owner-only, while each
67
68
  // value is also encrypted independently by vault-crypto.
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.1.49";
1
+ export const NEGOTIUM_VERSION = "0.2.0";
@@ -442,10 +442,14 @@ function parsePortEnv(envValue, fallback) {
442
442
  var WORKSPACE_DIR = resolveLocalStateDir("NEGOTIUM_WORKSPACE_DIR", "workspace");
443
443
  var TOPIC_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "topics");
444
444
  var SHARED_WIKI_DIR = resolve(WORKSPACE_DIR, "wiki");
445
- var CONTEXTS_DIR = resolve(WORKSPACE_DIR, "contexts");
446
- var BROWSER_PROFILES_DIR = resolve(WORKSPACE_DIR, "browser-profiles");
447
- var DM_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "dm");
448
- var SESSION_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "sessions");
445
+ var CRON_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "cron");
446
+ var BROWSER_DIR = resolveLocalStateDir("NEGOTIUM_BROWSER_DIR", "browser");
447
+ var BROWSER_PROFILES_DIR = resolve(BROWSER_DIR, "profiles");
448
+ var BINARIES_DIR = resolve(STATE_DIR, "binaries");
449
+ var SECRETS_DIR = resolve(STATE_DIR, "secrets");
450
+ var CONTEXTS_DIR = resolve(STATE_DIR, "data", "contexts");
451
+ var DM_WORKSPACE_DIR = resolve(STATE_DIR, "data", "dm");
452
+ var SESSION_WORKSPACE_DIR = resolve(STATE_DIR, "data", "sessions");
449
453
  var CLAUDE_EXECUTABLE_ENV = envText("NEGOTIUM_CLAUDE_EXECUTABLE");
450
454
  var CLAUDE_EXECUTABLE = CLAUDE_EXECUTABLE_ENV ? resolve(CLAUDE_EXECUTABLE_ENV) : undefined;
451
455
  var BROWSER_RS_VERSION = "v0.1.16";
@@ -483,7 +487,7 @@ function resolveBrowserRsBin(envValue) {
483
487
  if (!override && !versionAtLeast(BROWSER_RS_VERSION.replace(/^v/, ""), BROWSER_RS_MIN_SECURE_VERSION)) {
484
488
  return;
485
489
  }
486
- const candidate = override ? resolve(override) : resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs");
490
+ const candidate = override ? resolve(override) : resolve(BINARIES_DIR, "browser-rs", BROWSER_RS_VERSION, "browser-rs");
487
491
  try {
488
492
  accessSync(candidate, constants.X_OK);
489
493
  return browserRsMeetsMinimumVersion(candidate) ? candidate : undefined;
@@ -496,7 +500,7 @@ function resolveBrowserMcpBin(envValue) {
496
500
  const override = envValue?.trim();
497
501
  if (override)
498
502
  return resolve(override);
499
- return BROWSER_RS_BIN ?? resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs");
503
+ return BROWSER_RS_BIN ?? resolve(BINARIES_DIR, "browser-rs", BROWSER_RS_VERSION, "browser-rs");
500
504
  }
501
505
  var PLAYWRIGHT_MCP_BIN = resolveBrowserMcpBin(envText("NEGOTIUM_BROWSER_MCP_BIN"));
502
506
  var TSX_BIN = resolveDependencyBin("tsx");
@@ -517,7 +521,7 @@ var BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);
517
521
  var BG_BASH_MAX_PORT = parsePortEnv(process.env.BG_BASH_MAX_PORT, 9799);
518
522
  function loadOrCreateLocalSecret(envKey, filename, options = {}) {
519
523
  const envValue = envText(envKey);
520
- const secretFile = resolve(STATE_DIR, filename);
524
+ const secretFile = resolve(SECRETS_DIR, filename);
521
525
  mkdirSync2(dirname2(secretFile), { recursive: true });
522
526
  if (envValue) {
523
527
  if (options.persistEnvValue) {
@@ -557,10 +561,12 @@ var NEGOTIUM_PORT = parseInt(process.env.NEGOTIUM_PORT || "7777", 10);
557
561
  var hostname = process.env.HOSTNAME || "127.0.0.1";
558
562
  var DATA_DIR = resolveLocalStateDir("NEGOTIUM_DATA_DIR", "data");
559
563
  var LOG_DIR = resolveLocalStateDir("NEGOTIUM_LOG_DIR", "logs");
564
+ var UPLOADS_DIR = resolve(DATA_DIR, "uploads");
565
+ var VAULT_DIR = resolve(DATA_DIR, "vault");
560
566
  var SESSIONS_DB = process.env.SESSIONS_DB_PATH ? resolve(process.env.SESSIONS_DB_PATH) : resolve(DATA_DIR, "sessions.db");
561
567
  var DEBUG_FILE = resolve(DATA_DIR, "debug-users.json");
562
568
  var USERS_LOG_DIR = resolve(DATA_DIR, "users");
563
- var RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "run");
569
+ var RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "runtime");
564
570
  var PROGRESS_DIR = resolve(RUN_DIR, "progress");
565
571
  var DM_CMD_DIR = resolve(RUN_DIR, "dm-commands");
566
572
  var DM_RESP_DIR = resolve(RUN_DIR, "dm-responses");
@@ -571,6 +577,8 @@ var PLAYWRIGHT_MAX_PORT = parsePortEnv(process.env.PLAYWRIGHT_MAX_PORT, 9499);
571
577
  var PLAYWRIGHT_PORTS_DIR = resolve(RUN_DIR, "playwright-ports");
572
578
  mkdirSync2(STATE_DIR, { recursive: true });
573
579
  mkdirSync2(DATA_DIR, { recursive: true });
580
+ mkdirSync2(UPLOADS_DIR, { recursive: true });
581
+ mkdirSync2(VAULT_DIR, { recursive: true, mode: 448 });
574
582
  mkdirSync2(LOG_DIR, { recursive: true });
575
583
  mkdirSync2(PROGRESS_DIR, { recursive: true });
576
584
  mkdirSync2(DM_CMD_DIR, { recursive: true });
@@ -581,10 +589,14 @@ mkdirSync2(PLAYWRIGHT_PORTS_DIR, { recursive: true });
581
589
  mkdirSync2(WORKSPACE_DIR, { recursive: true });
582
590
  mkdirSync2(TOPIC_WORKSPACE_DIR, { recursive: true });
583
591
  mkdirSync2(SHARED_WIKI_DIR, { recursive: true });
592
+ mkdirSync2(CRON_WORKSPACE_DIR, { recursive: true });
584
593
  mkdirSync2(CONTEXTS_DIR, { recursive: true });
585
- mkdirSync2(BROWSER_PROFILES_DIR, { recursive: true });
586
594
  mkdirSync2(DM_WORKSPACE_DIR, { recursive: true });
587
595
  mkdirSync2(SESSION_WORKSPACE_DIR, { recursive: true });
596
+ mkdirSync2(BROWSER_DIR, { recursive: true });
597
+ mkdirSync2(BROWSER_PROFILES_DIR, { recursive: true });
598
+ mkdirSync2(BINARIES_DIR, { recursive: true });
599
+ mkdirSync2(SECRETS_DIR, { recursive: true, mode: 448 });
588
600
  var ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000;
589
601
  var AGENTS_PROMPTS_DIR = resolve(PROJECT_ROOT, "src/prompts/agents");
590
602
  var RESOURCES_DIR = resolve(PROJECT_ROOT, "src/resources");
@@ -1102,4 +1114,4 @@ export {
1102
1114
  CLAUDE_EFFORT_VALUES
1103
1115
  };
1104
1116
 
1105
- //# debugId=4EBEE90DEED3C85F64756E2164756E21
1117
+ //# debugId=0849C2E75E1422A764756E2164756E21