negotium 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/chunk-10ct8814.js +429 -0
- package/dist/chunk-10ct8814.js.map +11 -0
- package/dist/chunk-18fzftjf.js +214 -0
- package/dist/chunk-18fzftjf.js.map +10 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/chunk-2dgkah1z.js +706 -0
- package/dist/chunk-2dgkah1z.js.map +14 -0
- package/dist/chunk-3t4dtm26.js +61 -0
- package/dist/chunk-3t4dtm26.js.map +10 -0
- package/dist/chunk-3vn2vnj1.js +51 -0
- package/dist/chunk-3vn2vnj1.js.map +10 -0
- package/dist/chunk-6hknphtc.js +2054 -0
- package/dist/chunk-6hknphtc.js.map +29 -0
- package/dist/chunk-6tfzpcvk.js +2149 -0
- package/dist/chunk-6tfzpcvk.js.map +15 -0
- package/dist/chunk-8g0c4as0.js +23 -0
- package/dist/chunk-8g0c4as0.js.map +10 -0
- package/dist/chunk-9spx3h9n.js +279 -0
- package/dist/chunk-9spx3h9n.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-e9b2xpff.js +147 -0
- package/dist/chunk-e9b2xpff.js.map +10 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-eexqsee7.js +368 -0
- package/dist/chunk-eexqsee7.js.map +11 -0
- package/dist/chunk-efyxk97v.js +500 -0
- package/dist/chunk-efyxk97v.js.map +13 -0
- package/dist/chunk-epqxwkvh.js +3534 -0
- package/dist/chunk-epqxwkvh.js.map +20 -0
- package/dist/chunk-f1pzh3xh.js +115 -0
- package/dist/chunk-f1pzh3xh.js.map +10 -0
- package/dist/chunk-f5xgprg1.js +59 -0
- package/dist/chunk-f5xgprg1.js.map +10 -0
- package/dist/chunk-f97qybwk.js +584 -0
- package/dist/chunk-f97qybwk.js.map +11 -0
- package/dist/chunk-fvc7ncxd.js +252 -0
- package/dist/chunk-fvc7ncxd.js.map +10 -0
- package/dist/chunk-g7pvfj5e.js +574 -0
- package/dist/chunk-g7pvfj5e.js.map +14 -0
- package/dist/chunk-h8vmb9gf.js +315 -0
- package/dist/chunk-h8vmb9gf.js.map +10 -0
- package/dist/chunk-hgw7n8cm.js +43 -0
- package/dist/chunk-hgw7n8cm.js.map +10 -0
- package/dist/chunk-hsq7j9jk.js +126 -0
- package/dist/chunk-hsq7j9jk.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-kbm35t7w.js +1331 -0
- package/dist/chunk-kbm35t7w.js.map +16 -0
- package/dist/chunk-m4veth0c.js +1649 -0
- package/dist/chunk-m4veth0c.js.map +17 -0
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-mmgh670x.js +248 -0
- package/dist/chunk-mmgh670x.js.map +11 -0
- package/dist/chunk-n0kk5xbq.js +2358 -0
- package/dist/chunk-n0kk5xbq.js.map +26 -0
- package/dist/chunk-nmv41qx9.js +2847 -0
- package/dist/chunk-nmv41qx9.js.map +23 -0
- package/dist/chunk-nqdp52aj.js +113 -0
- package/dist/chunk-nqdp52aj.js.map +12 -0
- package/dist/chunk-ns2m25dz.js +139 -0
- package/dist/chunk-ns2m25dz.js.map +10 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/chunk-rmyfmrrr.js +1634 -0
- package/dist/chunk-rmyfmrrr.js.map +15 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sn51wnf1.js +3020 -0
- package/dist/chunk-sn51wnf1.js.map +32 -0
- package/dist/chunk-sqhr1a7e.js +587 -0
- package/dist/chunk-sqhr1a7e.js.map +11 -0
- package/dist/chunk-tmj5r2zv.js +2933 -0
- package/dist/chunk-tmj5r2zv.js.map +20 -0
- package/dist/chunk-vefj3880.js +56 -0
- package/dist/chunk-vefj3880.js.map +10 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-wry79q55.js +108 -0
- package/dist/chunk-wry79q55.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-z2xy5n2q.js +103 -0
- package/dist/chunk-z2xy5n2q.js.map +10 -0
- package/dist/hosted-agent.js +53 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +49 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +50 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +878 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +368 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +534 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +257 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +28 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type FSWatcher, mkdirSync, watch } from "node:fs";
|
|
2
|
+
import { logger } from "#platform/logger";
|
|
3
|
+
|
|
4
|
+
export const FALLBACK_INTERVAL_MS = 5_000; // fallback poll every 5s in case fs.watch misses events
|
|
5
|
+
|
|
6
|
+
export function debouncedFlush(fn: () => Promise<void>, label: string, delayMs: number) {
|
|
7
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
8
|
+
let running = false;
|
|
9
|
+
let pending = false;
|
|
10
|
+
|
|
11
|
+
async function run(): Promise<void> {
|
|
12
|
+
if (running) {
|
|
13
|
+
pending = true;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
while (true) {
|
|
17
|
+
running = true;
|
|
18
|
+
pending = false;
|
|
19
|
+
try {
|
|
20
|
+
await fn();
|
|
21
|
+
} catch (e) {
|
|
22
|
+
logger.error({ err: e }, `${label}: Unhandled error`);
|
|
23
|
+
} finally {
|
|
24
|
+
running = false;
|
|
25
|
+
}
|
|
26
|
+
if (!pending) break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
if (timer) clearTimeout(timer);
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
timer = null;
|
|
34
|
+
run();
|
|
35
|
+
}, delayMs);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function watchDir(dir: string, onChange: () => void): FSWatcher | null {
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
const watcher = watch(dir, { recursive: true }, () => onChange());
|
|
43
|
+
watcher.on("error", (e) => {
|
|
44
|
+
logger.warn({ err: e, dir }, "outbox: fs.watch error");
|
|
45
|
+
});
|
|
46
|
+
return watcher;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
logger.warn({ err: e, dir }, "outbox: Failed to watch dir");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { type ChildProcess, execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { BACKGROUND_BASH_SERVER, BG_BASH_BASE_PORT, BG_BASH_MAX_PORT } from "#platform/config";
|
|
3
|
+
import { delay } from "#platform/delay";
|
|
4
|
+
import { logger } from "#platform/logger";
|
|
5
|
+
|
|
6
|
+
// --- Instance key (mirrors Playwright manager convention) ---
|
|
7
|
+
|
|
8
|
+
export function makeBgBashKey(_userId: string, topic: string): string {
|
|
9
|
+
return `topic:${topic}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// --- State ---
|
|
13
|
+
|
|
14
|
+
interface BgBashInstance {
|
|
15
|
+
process: ChildProcess;
|
|
16
|
+
port: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
lastUsedAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const instances = new Map<string, BgBashInstance>();
|
|
22
|
+
const usedPorts = new Set<number>();
|
|
23
|
+
const spawning = new Map<string, Promise<number>>();
|
|
24
|
+
|
|
25
|
+
// --- Port allocation ---
|
|
26
|
+
|
|
27
|
+
function pidsOnPort(port: number): number[] {
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" })
|
|
30
|
+
.toString()
|
|
31
|
+
.trim()
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((pid) => parseInt(pid, 10))
|
|
34
|
+
.filter((pid) => !Number.isNaN(pid));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isPortInUse(port: number): boolean {
|
|
41
|
+
return pidsOnPort(port).length > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function processCommand(pid: number): string {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], { stdio: "pipe" })
|
|
47
|
+
.toString()
|
|
48
|
+
.trim();
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isBackgroundBashServerProcess(cmdline: string, port: number): boolean {
|
|
55
|
+
return cmdline.includes("background-bash-server.ts") && cmdline.includes(`--port=${port}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function killBackgroundBashOnPort(port: number): Promise<void> {
|
|
59
|
+
for (const pid of pidsOnPort(port)) {
|
|
60
|
+
const cmdline = processCommand(pid);
|
|
61
|
+
if (!isBackgroundBashServerProcess(cmdline, port)) {
|
|
62
|
+
logger.warn(
|
|
63
|
+
{ port, pid, cmdline: cmdline.slice(0, 120) },
|
|
64
|
+
"background-bash port occupied by non-background-bash process; skipping",
|
|
65
|
+
);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, "SIGKILL");
|
|
70
|
+
logger.info({ port, pid }, "Killed zombie background-bash server");
|
|
71
|
+
} catch (e) {
|
|
72
|
+
logger.warn({ err: e, port, pid }, "Failed to kill zombie background-bash server");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const deadline = Date.now() + 2000;
|
|
77
|
+
while (Date.now() < deadline) {
|
|
78
|
+
if (!isPortInUse(port)) return;
|
|
79
|
+
await delay(100);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function allocatePort(): Promise<number> {
|
|
84
|
+
for (let port = BG_BASH_BASE_PORT; port <= BG_BASH_MAX_PORT; port++) {
|
|
85
|
+
if (usedPorts.has(port)) continue;
|
|
86
|
+
usedPorts.add(port); // reserve before any await
|
|
87
|
+
if (isPortInUse(port)) {
|
|
88
|
+
await killBackgroundBashOnPort(port);
|
|
89
|
+
if (isPortInUse(port)) {
|
|
90
|
+
usedPorts.delete(port);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return port;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(
|
|
97
|
+
`No available ports for background-bash (range ${BG_BASH_BASE_PORT}-${BG_BASH_MAX_PORT}, ${instances.size} active)`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Health check ---
|
|
102
|
+
|
|
103
|
+
async function isHealthy(port: number): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
106
|
+
signal: AbortSignal.timeout(2000),
|
|
107
|
+
});
|
|
108
|
+
res.body?.cancel();
|
|
109
|
+
return res.ok;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Spawn ---
|
|
116
|
+
|
|
117
|
+
async function spawnBgBash(
|
|
118
|
+
key: string,
|
|
119
|
+
userId: string,
|
|
120
|
+
topic: string,
|
|
121
|
+
reservedPort?: number,
|
|
122
|
+
): Promise<number> {
|
|
123
|
+
const port = reservedPort ?? (await allocatePort());
|
|
124
|
+
|
|
125
|
+
const serverArgs = [
|
|
126
|
+
"run",
|
|
127
|
+
BACKGROUND_BASH_SERVER,
|
|
128
|
+
`--user-id=${userId}`,
|
|
129
|
+
`--topic=${topic}`,
|
|
130
|
+
`--port=${port}`,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const proc = spawn("bun", serverArgs, {
|
|
134
|
+
stdio: "ignore",
|
|
135
|
+
detached: false,
|
|
136
|
+
env: { ...process.env },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
proc.once("error", (err) => {
|
|
140
|
+
logger.error({ err, key }, "background-bash server error");
|
|
141
|
+
if (instances.get(key)?.process === proc) {
|
|
142
|
+
usedPorts.delete(port);
|
|
143
|
+
instances.delete(key);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
proc.once("exit", (code) => {
|
|
148
|
+
logger.info({ key, code }, "background-bash server exited");
|
|
149
|
+
if (instances.get(key)?.process === proc) {
|
|
150
|
+
usedPorts.delete(port);
|
|
151
|
+
instances.delete(key);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
instances.set(key, { process: proc, port, startedAt: now, lastUsedAt: now });
|
|
157
|
+
|
|
158
|
+
// Wait for health
|
|
159
|
+
const start = Date.now();
|
|
160
|
+
while (Date.now() - start < 8_000) {
|
|
161
|
+
if (await isHealthy(port)) {
|
|
162
|
+
logger.info({ key, port, pid: proc.pid }, "background-bash server ready");
|
|
163
|
+
return port;
|
|
164
|
+
}
|
|
165
|
+
await delay(200);
|
|
166
|
+
}
|
|
167
|
+
killBgBash(userId, topic);
|
|
168
|
+
throw new Error(`background-bash server failed health check after spawn on port ${port}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Public API ---
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Ensure a background-bash HTTP server is running for this (user, topic).
|
|
175
|
+
* Reuses existing healthy instances; respawns on failure.
|
|
176
|
+
* Returns the HTTP port to use in MCP config.
|
|
177
|
+
*/
|
|
178
|
+
export async function ensureBgBash(userId: string, topic: string): Promise<number> {
|
|
179
|
+
const key = makeBgBashKey(userId, topic);
|
|
180
|
+
|
|
181
|
+
const inProgress = spawning.get(key);
|
|
182
|
+
if (inProgress) return inProgress;
|
|
183
|
+
|
|
184
|
+
// Health-check → kill → spawn runs INSIDE the spawning promise: the old
|
|
185
|
+
// shape awaited isHealthy() between the guard check and spawning.set(), so
|
|
186
|
+
// two concurrent callers could both pass the guard — one killing the
|
|
187
|
+
// instance the other had just spawned, or double-spawning and leaking the
|
|
188
|
+
// first process's port from usedPorts. No await sits between the guard
|
|
189
|
+
// above and spawning.set() below, so the guard is airtight on a
|
|
190
|
+
// single-threaded event loop. (Same fix as playwright's ensurePlaywright.)
|
|
191
|
+
const promise = (async (): Promise<number> => {
|
|
192
|
+
const existing = instances.get(key);
|
|
193
|
+
if (existing && !existing.process.killed && existing.process.exitCode === null) {
|
|
194
|
+
if (await isHealthy(existing.port)) {
|
|
195
|
+
existing.lastUsedAt = Date.now();
|
|
196
|
+
return existing.port;
|
|
197
|
+
}
|
|
198
|
+
killBgBash(userId, topic);
|
|
199
|
+
} else if (existing) {
|
|
200
|
+
usedPorts.delete(existing.port);
|
|
201
|
+
instances.delete(key);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return spawnBgBash(key, userId, topic);
|
|
205
|
+
})().finally(() => spawning.delete(key));
|
|
206
|
+
spawning.set(key, promise);
|
|
207
|
+
return promise;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function killBgBash(userId: string, topic: string): void {
|
|
211
|
+
const key = makeBgBashKey(userId, topic);
|
|
212
|
+
const inst = instances.get(key);
|
|
213
|
+
if (!inst) return;
|
|
214
|
+
try {
|
|
215
|
+
inst.process.kill("SIGTERM");
|
|
216
|
+
} catch {}
|
|
217
|
+
usedPorts.delete(inst.port);
|
|
218
|
+
instances.delete(key);
|
|
219
|
+
logger.info({ key, port: inst.port }, "background-bash server killed");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function killBgBashForUser(_userId: string): void {
|
|
223
|
+
for (const key of [...instances.keys()]) {
|
|
224
|
+
const inst = instances.get(key);
|
|
225
|
+
if (!inst) continue;
|
|
226
|
+
try {
|
|
227
|
+
inst.process.kill("SIGTERM");
|
|
228
|
+
} catch {}
|
|
229
|
+
usedPorts.delete(inst.port);
|
|
230
|
+
instances.delete(key);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function killAllBgBash(): Promise<void> {
|
|
235
|
+
const entries = [...instances.entries()];
|
|
236
|
+
for (const [, inst] of entries) {
|
|
237
|
+
try {
|
|
238
|
+
inst.process.kill("SIGTERM");
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
instances.clear();
|
|
242
|
+
usedPorts.clear();
|
|
243
|
+
|
|
244
|
+
const deadline = Date.now() + 3000;
|
|
245
|
+
await Promise.all(
|
|
246
|
+
entries.map(
|
|
247
|
+
([, inst]) =>
|
|
248
|
+
new Promise<void>((resolve) => {
|
|
249
|
+
if (inst.process.exitCode !== null || inst.process.killed) {
|
|
250
|
+
resolve();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
inst.process.once("exit", () => resolve());
|
|
254
|
+
inst.process.once("error", () => resolve());
|
|
255
|
+
const t = setTimeout(resolve, Math.max(0, deadline - Date.now()));
|
|
256
|
+
t.unref?.();
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Evict idle instances every 30 minutes (2hr idle threshold)
|
|
263
|
+
const MAX_IDLE_MS = 2 * 60 * 60 * 1000;
|
|
264
|
+
setInterval(
|
|
265
|
+
() => {
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
for (const [key, inst] of instances) {
|
|
268
|
+
if (now - inst.lastUsedAt > MAX_IDLE_MS) {
|
|
269
|
+
logger.info({ key }, "evicting idle background-bash server");
|
|
270
|
+
try {
|
|
271
|
+
inst.process.kill("SIGTERM");
|
|
272
|
+
} catch {}
|
|
273
|
+
usedPorts.delete(inst.port);
|
|
274
|
+
instances.delete(key);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
30 * 60 * 1000,
|
|
279
|
+
).unref();
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { logger } from "#platform/logger";
|
|
7
|
+
import { type AgentKind, isAgentKind } from "#types";
|
|
8
|
+
|
|
9
|
+
export function envText(envKey: string): string | undefined {
|
|
10
|
+
const value = process.env[envKey]?.trim();
|
|
11
|
+
return value ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveAgentEnv(envKey: string, fallback: AgentKind, legacyEnvKey?: string): AgentKind {
|
|
15
|
+
const value = envText(envKey) ?? (legacyEnvKey ? envText(legacyEnvKey) : undefined);
|
|
16
|
+
return isAgentKind(value) ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const HOME = homedir();
|
|
20
|
+
|
|
21
|
+
// new URL("../..", import.meta.url) causes webpack to treat "../.." as a module import.
|
|
22
|
+
// Split into fileURLToPath → dirname → resolve to avoid that.
|
|
23
|
+
function resolveProjectRoot(): string {
|
|
24
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const packagedRuntime = resolve(moduleDir, "runtime");
|
|
26
|
+
if (existsSync(resolve(packagedRuntime, "src"))) return packagedRuntime;
|
|
27
|
+
return resolve(moduleDir, "../..");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PROJECT_ROOT = resolveProjectRoot();
|
|
31
|
+
|
|
32
|
+
/** Resolve a dependency executable from either a package-local or hoisted install. */
|
|
33
|
+
function resolveDependencyBin(name: string): string {
|
|
34
|
+
let dir = PROJECT_ROOT;
|
|
35
|
+
while (true) {
|
|
36
|
+
const candidate = resolve(dir, "node_modules", ".bin", name);
|
|
37
|
+
if (existsSync(candidate)) return candidate;
|
|
38
|
+
const parent = dirname(dir);
|
|
39
|
+
if (parent === dir) return candidate;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Each machine is one negotium node; all node state lives in one dotdir.
|
|
45
|
+
// NEGOTIUM_STATE_DIR overrides (useful for tests and multi-node-on-one-box).
|
|
46
|
+
const STATE_DIR_ENV = envText("NEGOTIUM_STATE_DIR");
|
|
47
|
+
export const STATE_DIR = STATE_DIR_ENV ? resolve(STATE_DIR_ENV) : resolve(HOME, ".negotium");
|
|
48
|
+
|
|
49
|
+
function resolveLocalStateDir(envKey: string, stateName: string): string {
|
|
50
|
+
const envValue = envText(envKey);
|
|
51
|
+
if (envValue) return resolve(envValue);
|
|
52
|
+
return resolve(STATE_DIR, stateName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parsePortEnv(envValue: string | undefined, fallback: number): number {
|
|
56
|
+
if (!envValue) return fallback;
|
|
57
|
+
const port = Number.parseInt(envValue, 10);
|
|
58
|
+
return Number.isInteger(port) && port > 0 ? port : fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const WORKSPACE_DIR = resolveLocalStateDir("NEGOTIUM_WORKSPACE_DIR", "workspace");
|
|
62
|
+
export const TOPIC_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "topics");
|
|
63
|
+
export const SHARED_WIKI_DIR = resolve(WORKSPACE_DIR, "wiki");
|
|
64
|
+
export const CONTEXTS_DIR = resolve(WORKSPACE_DIR, "contexts");
|
|
65
|
+
export const BROWSER_PROFILES_DIR = resolve(WORKSPACE_DIR, "browser-profiles");
|
|
66
|
+
export const DM_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "dm");
|
|
67
|
+
export const SESSION_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "sessions");
|
|
68
|
+
export const CLAUDE_EXECUTABLE = resolve(HOME, ".local/bin/claude");
|
|
69
|
+
|
|
70
|
+
// Browser automation uses the authenticated local Patchright HTTP wrapper.
|
|
71
|
+
export function resolveBrowserMcpBin(envValue?: string): string {
|
|
72
|
+
const override = envValue?.trim();
|
|
73
|
+
if (override) {
|
|
74
|
+
if (!/(^|\/)(mcp-patchright|mcp-patchright-http\.mjs)$/.test(override)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"NEGOTIUM_BROWSER_MCP_BIN must point to the authenticated mcp-patchright wrapper.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return override;
|
|
80
|
+
}
|
|
81
|
+
return PATCHRIGHT_MCP_BIN;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const PATCHRIGHT_MCP_BIN = resolve(PROJECT_ROOT, "scripts/mcp-patchright-http.mjs");
|
|
85
|
+
export const PLAYWRIGHT_MCP_BIN = resolveBrowserMcpBin(envText("NEGOTIUM_BROWSER_MCP_BIN"));
|
|
86
|
+
|
|
87
|
+
// --- Browser egress proxy ---
|
|
88
|
+
//
|
|
89
|
+
// On a datacenter host (AWS) the browser's egress IP is a known cloud range,
|
|
90
|
+
// so anti-bot services (Cloudflare, DataDome, reCAPTCHA) challenge or block it
|
|
91
|
+
// far more than a residential IP would. Routing the automation browser through
|
|
92
|
+
// a residential/ISP proxy moves the egress IP out of the datacenter range.
|
|
93
|
+
//
|
|
94
|
+
// Operators set BROWSER_PROXY_URL, e.g. http://user:pass@proxy.host:8080 or
|
|
95
|
+
// socks5://proxy.host:1080. Credentials in the URL are split out because
|
|
96
|
+
// Playwright takes them as separate fields. BROWSER_PROXY_BYPASS is an optional
|
|
97
|
+
// comma-separated no-proxy list (e.g. "localhost,127.0.0.1,*.internal").
|
|
98
|
+
//
|
|
99
|
+
// NOTE: Chromium does not support authentication for SOCKS proxies — put
|
|
100
|
+
// credentials only on http/https proxy URLs.
|
|
101
|
+
export type BrowserProxyConfig = {
|
|
102
|
+
server: string;
|
|
103
|
+
username?: string;
|
|
104
|
+
password?: string;
|
|
105
|
+
bypass?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function resolveBrowserProxy(): BrowserProxyConfig | null {
|
|
109
|
+
const raw = envText("BROWSER_PROXY_URL");
|
|
110
|
+
if (!raw) return null;
|
|
111
|
+
let url: URL;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
logger.warn({ raw }, "Ignoring malformed BROWSER_PROXY_URL");
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Playwright wants the server without embedded credentials.
|
|
119
|
+
const server = `${url.protocol}//${url.host}`;
|
|
120
|
+
const proxy: BrowserProxyConfig = { server };
|
|
121
|
+
if (url.username) proxy.username = decodeURIComponent(url.username);
|
|
122
|
+
if (url.password) proxy.password = decodeURIComponent(url.password);
|
|
123
|
+
const bypass = envText("BROWSER_PROXY_BYPASS");
|
|
124
|
+
if (bypass) proxy.bypass = bypass;
|
|
125
|
+
return proxy;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Node/tsx runtime for the `codex` agent's MCP servers ---
|
|
129
|
+
//
|
|
130
|
+
// codex 0.135's rmcp stdio MCP client cannot reliably complete the initialize
|
|
131
|
+
// handshake with servers spawned via `bun` (the JSON-RPC initialize is
|
|
132
|
+
// dropped/raced and no tools ever reach the model). Pure-node servers connect
|
|
133
|
+
// reliably, so codex turns launch the SAME .ts servers via node + tsx instead
|
|
134
|
+
// of `bun run`. claude/maestro keep using `bun run` (fast, native, unaffected).
|
|
135
|
+
//
|
|
136
|
+
// tsx transpiles .ts on the fly and resolves the `@/*` tsconfig path aliases,
|
|
137
|
+
// but only when it can find the tsconfig — and MCP servers run with cwd set to
|
|
138
|
+
// the user's workspace dir, not PROJECT_ROOT — so we pass TSX_TSCONFIG_PATH
|
|
139
|
+
// explicitly via env. Requires package.json `"type": "module"` so the servers'
|
|
140
|
+
// top-level `await` loads as ESM under node.
|
|
141
|
+
export const TSX_BIN = resolveDependencyBin("tsx");
|
|
142
|
+
export const TSCONFIG_PATH = resolve(PROJECT_ROOT, "tsconfig.json");
|
|
143
|
+
|
|
144
|
+
export const SESSION_COMM_SERVER = resolve(PROJECT_ROOT, "src/mcp/session-comm/server.ts");
|
|
145
|
+
|
|
146
|
+
export const TASK_SERVER = resolve(PROJECT_ROOT, "src/mcp/task-server.ts");
|
|
147
|
+
export const CANONICAL_MCP_PROXY_SERVER = resolve(
|
|
148
|
+
PROJECT_ROOT,
|
|
149
|
+
"src/mcp/canonical-proxy-server.ts",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
export const WIKI_SERVER = resolve(PROJECT_ROOT, "src/mcp/wiki-server.ts");
|
|
153
|
+
|
|
154
|
+
export const PADDLEOCR_SERVER = resolve(PROJECT_ROOT, "src/mcp/paddleocr-server.ts");
|
|
155
|
+
|
|
156
|
+
export const TOKEN_STATS_SERVER = resolve(PROJECT_ROOT, "src/mcp/token-stats-server.ts");
|
|
157
|
+
|
|
158
|
+
export const SYSTEM_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/system-health-server.ts");
|
|
159
|
+
|
|
160
|
+
export const AGENT_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/agent-health-server.ts");
|
|
161
|
+
|
|
162
|
+
export const BACKGROUND_BASH_SERVER = resolve(PROJECT_ROOT, "src/mcp/background-bash-server.ts");
|
|
163
|
+
|
|
164
|
+
export const VAULT_SERVER = resolve(PROJECT_ROOT, "src/mcp/vault-server.ts");
|
|
165
|
+
|
|
166
|
+
export const BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);
|
|
167
|
+
export const BG_BASH_MAX_PORT = parsePortEnv(process.env.BG_BASH_MAX_PORT, 9799);
|
|
168
|
+
|
|
169
|
+
function safeWorkspaceSegment(value: string, fallback: string): string {
|
|
170
|
+
const cleaned = value
|
|
171
|
+
.trim()
|
|
172
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
173
|
+
.replace(/^_+|_+$/g, "")
|
|
174
|
+
.slice(0, 160);
|
|
175
|
+
return cleaned || fallback;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Resolve the shared filesystem workspace for an API topic. */
|
|
179
|
+
export function resolveTopicWorkspaceDir(topicId: string): string {
|
|
180
|
+
return join(TOPIC_WORKSPACE_DIR, safeWorkspaceSegment(topicId, "topic"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isProductionEnv(): boolean {
|
|
184
|
+
return process.env.NODE_ENV === "production";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function loadOrCreateLocalSecret(
|
|
188
|
+
envKey: string,
|
|
189
|
+
filename: string,
|
|
190
|
+
options: { persistEnvValue?: boolean } = {},
|
|
191
|
+
): string {
|
|
192
|
+
const envValue = envText(envKey);
|
|
193
|
+
const secretFile = resolve(STATE_DIR, filename);
|
|
194
|
+
mkdirSync(dirname(secretFile), { recursive: true });
|
|
195
|
+
if (envValue) {
|
|
196
|
+
if (options.persistEnvValue) {
|
|
197
|
+
writeFileSync(secretFile, `${envValue}\n`, { mode: 0o600 });
|
|
198
|
+
chmodSync(secretFile, 0o600);
|
|
199
|
+
}
|
|
200
|
+
return envValue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (existsSync(secretFile)) {
|
|
204
|
+
const stored = readFileSync(secretFile, "utf-8").trim();
|
|
205
|
+
if (stored) {
|
|
206
|
+
chmodSync(secretFile, 0o600);
|
|
207
|
+
return stored;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const secret = randomBytes(32).toString("base64url");
|
|
212
|
+
try {
|
|
213
|
+
writeFileSync(secretFile, `${secret}\n`, { mode: 0o600, flag: "wx" });
|
|
214
|
+
return secret;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
217
|
+
const stored = readFileSync(secretFile, "utf-8").trim();
|
|
218
|
+
if (!stored) throw new Error(`Secret file exists but is empty: ${secretFile}`);
|
|
219
|
+
chmodSync(secretFile, 0o600);
|
|
220
|
+
return stored;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const RUNTIME_MCP_SECRET = loadOrCreateLocalSecret(
|
|
225
|
+
"RUNTIME_MCP_SECRET",
|
|
226
|
+
"runtime-mcp-secret",
|
|
227
|
+
);
|
|
228
|
+
/** Local bearer token for the loopback node-control API. */
|
|
229
|
+
export const NODE_CONTROL_TOKEN = loadOrCreateLocalSecret(
|
|
230
|
+
"NEGOTIUM_CONTROL_TOKEN",
|
|
231
|
+
"node-control-token",
|
|
232
|
+
);
|
|
233
|
+
export const VAULT_MASTER_KEY = loadOrCreateLocalSecret(
|
|
234
|
+
"NEGOTIUM_VAULT_MASTER_KEY",
|
|
235
|
+
"vault-master-key",
|
|
236
|
+
{ persistEnvValue: true },
|
|
237
|
+
);
|
|
238
|
+
// Agent/tool subprocesses inherit process.env. Keep the loaded key in this
|
|
239
|
+
// process only so `env`/`ps` inside an agent workspace cannot reveal it.
|
|
240
|
+
delete process.env.NEGOTIUM_VAULT_MASTER_KEY;
|
|
241
|
+
|
|
242
|
+
/** The node's single open port: runtime MCP endpoint + node API. */
|
|
243
|
+
export const NEGOTIUM_PORT = parseInt(process.env.NEGOTIUM_PORT || "7777", 10);
|
|
244
|
+
export const hostname = process.env.HOSTNAME || "127.0.0.1";
|
|
245
|
+
|
|
246
|
+
// Persistent state (survives restarts, long-lived)
|
|
247
|
+
export const DATA_DIR = resolveLocalStateDir("NEGOTIUM_DATA_DIR", "data");
|
|
248
|
+
export const LOG_DIR = resolveLocalStateDir("NEGOTIUM_LOG_DIR", "logs");
|
|
249
|
+
// SESSIONS_DB_PATH env override lets tests point the DB singleton at a temp file.
|
|
250
|
+
export const SESSIONS_DB = process.env.SESSIONS_DB_PATH
|
|
251
|
+
? resolve(process.env.SESSIONS_DB_PATH)
|
|
252
|
+
: resolve(DATA_DIR, "sessions.db");
|
|
253
|
+
export const DEBUG_FILE = resolve(DATA_DIR, "debug-users.json");
|
|
254
|
+
export const USERS_LOG_DIR = resolve(DATA_DIR, "users");
|
|
255
|
+
|
|
256
|
+
// Runtime IPC queues (transient, safe to clear on restart)
|
|
257
|
+
export const RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "run");
|
|
258
|
+
export const PROGRESS_DIR = resolve(RUN_DIR, "progress");
|
|
259
|
+
export const DM_CMD_DIR = resolve(RUN_DIR, "dm-commands");
|
|
260
|
+
export const DM_RESP_DIR = resolve(RUN_DIR, "dm-responses");
|
|
261
|
+
export const SESSION_INBOX_DIR = resolve(RUN_DIR, "session-inbox");
|
|
262
|
+
export const SESSION_ASKS_DIR = resolve(RUN_DIR, "session-asks");
|
|
263
|
+
export const PLAYWRIGHT_BASE_PORT = parsePortEnv(process.env.PLAYWRIGHT_BASE_PORT, 9100);
|
|
264
|
+
export const PLAYWRIGHT_MAX_PORT = parsePortEnv(process.env.PLAYWRIGHT_MAX_PORT, 9499);
|
|
265
|
+
export const PLAYWRIGHT_PORTS_DIR = resolve(RUN_DIR, "playwright-ports");
|
|
266
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
267
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
268
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
269
|
+
mkdirSync(PROGRESS_DIR, { recursive: true });
|
|
270
|
+
mkdirSync(DM_CMD_DIR, { recursive: true });
|
|
271
|
+
mkdirSync(DM_RESP_DIR, { recursive: true });
|
|
272
|
+
mkdirSync(SESSION_INBOX_DIR, { recursive: true });
|
|
273
|
+
mkdirSync(SESSION_ASKS_DIR, { recursive: true });
|
|
274
|
+
mkdirSync(PLAYWRIGHT_PORTS_DIR, { recursive: true });
|
|
275
|
+
mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
276
|
+
mkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });
|
|
277
|
+
mkdirSync(SHARED_WIKI_DIR, { recursive: true });
|
|
278
|
+
mkdirSync(CONTEXTS_DIR, { recursive: true });
|
|
279
|
+
mkdirSync(BROWSER_PROFILES_DIR, { recursive: true });
|
|
280
|
+
mkdirSync(DM_WORKSPACE_DIR, { recursive: true });
|
|
281
|
+
mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
|
|
282
|
+
|
|
283
|
+
/** Stale threshold for active-query state files (crash recovery) */
|
|
284
|
+
export const ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000; // 10 minutes
|
|
285
|
+
|
|
286
|
+
export const AGENTS_PROMPTS_DIR = resolve(PROJECT_ROOT, "src/prompts/agents");
|
|
287
|
+
export const RESOURCES_DIR = resolve(PROJECT_ROOT, "src/resources");
|
|
288
|
+
|
|
289
|
+
/** Returns process.env without CLAUDECODE, to prevent nested claude-code detection in subprocesses. */
|
|
290
|
+
export function getCleanEnv(): NodeJS.ProcessEnv {
|
|
291
|
+
const env = { ...process.env };
|
|
292
|
+
delete env.CLAUDECODE;
|
|
293
|
+
return env;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const FILE_EXTENSIONS_REGEX =
|
|
297
|
+
/(?:\/[^\s"'<>|*?[\]]+\.(?:png|jpg|jpeg|gif|webp|svg|pdf|csv|xlsx|xls|json|txt|md|html|zip|py|js|ts|tsx|jsx|css|xml|yaml|yml|docx|pptx))/gi;
|
|
298
|
+
|
|
299
|
+
export const FILE_TAG_REGEX = /\[FILE:(\/[^\]]+)\]/gi;
|
|
300
|
+
|
|
301
|
+
// Canonical Claude model IDs — update here when Anthropic releases new versions
|
|
302
|
+
export const MODEL_SONNET = "claude-sonnet-5";
|
|
303
|
+
export const MODEL_OPUS = "claude-opus-4-8";
|
|
304
|
+
export const MODEL_HAIKU = "claude-haiku-4-5-20251001";
|
|
305
|
+
export const MODEL_FABLE = "claude-fable-5"; // Mythos-class, announced 2026-06-09
|
|
306
|
+
|
|
307
|
+
// DeepSeek V4 (released 2026-04-24). API is OpenAI-compatible at
|
|
308
|
+
// https://api.deepseek.com/v1/chat/completions; thinking mode is enabled via
|
|
309
|
+
// `extra_body.thinking.type` + `reasoning_effort`. Legacy `deepseek-chat` /
|
|
310
|
+
// `deepseek-reasoner` are deprecated 2026-07-24.
|
|
311
|
+
export const MODEL_DEEPSEEK_V4_PRO = "deepseek-v4-pro";
|
|
312
|
+
export const MODEL_DEEPSEEK_V4_FLASH = "deepseek-v4-flash";
|
|
313
|
+
|
|
314
|
+
// Agent + model defaults split by session role. FALLBACK_* is the shared base;
|
|
315
|
+
// SESSION_* overrides topic + ephemeral; GATEWAY_* overrides dm + manager.
|
|
316
|
+
// DEFAULT_* is accepted as a legacy alias during the env migration window.
|
|
317
|
+
export const FALLBACK_AGENT: AgentKind = resolveAgentEnv(
|
|
318
|
+
"FALLBACK_AGENT",
|
|
319
|
+
"maestro",
|
|
320
|
+
"DEFAULT_AGENT",
|
|
321
|
+
);
|
|
322
|
+
export const SESSION_AGENT: AgentKind = resolveAgentEnv("SESSION_AGENT", FALLBACK_AGENT);
|
|
323
|
+
export const GATEWAY_AGENT: AgentKind = resolveAgentEnv("GATEWAY_AGENT", FALLBACK_AGENT);
|
|
324
|
+
|
|
325
|
+
export const FALLBACK_MODEL = envText("FALLBACK_MODEL") ?? envText("DEFAULT_MODEL");
|
|
326
|
+
|
|
327
|
+
function resolveModelEnv(envKey: string, agentConst: AgentKind): string | undefined {
|
|
328
|
+
return envText(envKey) ?? (agentConst === FALLBACK_AGENT ? FALLBACK_MODEL : undefined);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export const SESSION_MODEL = resolveModelEnv("SESSION_MODEL", SESSION_AGENT);
|
|
332
|
+
export const GATEWAY_MODEL = resolveModelEnv("GATEWAY_MODEL", GATEWAY_AGENT);
|
|
333
|
+
|
|
334
|
+
/** Resolve the effective display/default model for a topic (session context).
|
|
335
|
+
* Applies the session model override only when that role owns the agent;
|
|
336
|
+
* otherwise each registry's native default stays authoritative. */
|
|
337
|
+
export function resolveDefaultModel(agent: string, registryDefaultModel: string): string {
|
|
338
|
+
return agent === SESSION_AGENT && SESSION_MODEL ? SESSION_MODEL : registryDefaultModel;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── External tool binaries + media pipeline env ───────────────────
|
|
342
|
+
// (src/media/* 에서 사용. 미설정 시 fallback 의미는 기존 그대로:
|
|
343
|
+
// FFMPEG_BIN은 text-extractor에서 필수(undefined면 spawn 시점 실패),
|
|
344
|
+
// video.ts에서는 PATH의 ffmpeg/ffprobe로 fallback.)
|
|
345
|
+
export const FFMPEG_BIN = envText("FFMPEG_BIN");
|
|
346
|
+
export const FFPROBE_BIN = envText("FFPROBE_BIN");
|
|
347
|
+
export const PYTHON_BIN = envText("PYTHON_BIN") ?? "python3";
|
|
348
|
+
export const FASTER_WHISPER_WRAPPER =
|
|
349
|
+
envText("FASTER_WHISPER_WRAPPER") ?? resolve(PROJECT_ROOT, "scripts/faster-whisper-wrapper.py");
|
|
350
|
+
export const WHISPER_MODEL = envText("WHISPER_MODEL_FILE") ?? "turbo";
|
|
351
|
+
export const TESSERACT_BIN = envText("TESSERACT_BIN") ?? "tesseract";
|
|
352
|
+
export const PDFTOTEXT_BIN = envText("PDFTOTEXT_BIN") ?? "pdftotext";
|
|
353
|
+
|
|
354
|
+
// Max tell_session relay depth from origin user. ask_session forks reset to
|
|
355
|
+
// depth=0, so this only caps tell_session chains. Override via MAX_TELL_DEPTH
|
|
356
|
+
// (positive int); defaults to 3 when unset or invalid.
|
|
357
|
+
const _envMaxTellDepth = Number.parseInt(process.env.MAX_TELL_DEPTH ?? "", 10);
|
|
358
|
+
export const MAX_TELL_DEPTH =
|
|
359
|
+
Number.isInteger(_envMaxTellDepth) && _envMaxTellDepth > 0 ? _envMaxTellDepth : 3;
|
|
360
|
+
|
|
361
|
+
/** Codex CLI auth file. 호출 시점에 env를 읽는다 — 테스트가 런타임에
|
|
362
|
+
* NEGOTIUM_CODEX_AUTH_FILE을 바꾸므로 모듈 로드 상수로 만들면 안 된다. */
|
|
363
|
+
export function codexAuthFilePath(): string {
|
|
364
|
+
return process.env.NEGOTIUM_CODEX_AUTH_FILE || join(homedir(), ".codex", "auth.json");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// System defaults moved to per-agent registries
|
|
368
|
+
// (`src/agents/{claude,codex}-registry.ts`). Read via
|
|
369
|
+
// `getRegistry(agent).defaultModel` / `.defaultEffort`.
|
|
370
|
+
|
|
371
|
+
// MCP server builders -> src/platform/mcp-config.ts
|