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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global (workspace-wide) application settings — a single shared record, not
|
|
3
|
+
* per-user and not per-topic.
|
|
4
|
+
*
|
|
5
|
+
* Currently holds the global AI name (default "Otium"): the AI is one named
|
|
6
|
+
* entity for the whole workspace. Changing it is an admin-only action (see the
|
|
7
|
+
* settings route). Loaded synchronously at import time so `getGlobalAiName()`
|
|
8
|
+
* is always populated for callers like the system-prompt builder.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { DATA_DIR } from "#platform/config";
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_AI_NAME = "Otium";
|
|
15
|
+
|
|
16
|
+
const SETTINGS_FILE = join(DATA_DIR, "otium-settings.json");
|
|
17
|
+
|
|
18
|
+
let aiName = DEFAULT_AI_NAME;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(SETTINGS_FILE)) {
|
|
22
|
+
const data = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as { aiName?: unknown };
|
|
23
|
+
if (typeof data.aiName === "string" && data.aiName.trim()) {
|
|
24
|
+
aiName = data.aiName.trim();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Corrupt/missing file → keep the default.
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getGlobalAiName(): string {
|
|
32
|
+
return aiName || DEFAULT_AI_NAME;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Set the global AI name (empty → reset to default). Persists to disk. */
|
|
36
|
+
export function setGlobalAiName(name: string): string {
|
|
37
|
+
aiName = name.trim() || DEFAULT_AI_NAME;
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify({ aiName }, null, 2));
|
|
40
|
+
} catch {
|
|
41
|
+
// Best-effort persistence; the in-memory value still updates.
|
|
42
|
+
}
|
|
43
|
+
return aiName;
|
|
44
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { db } from "#storage/forum-db";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BROWSER_PROFILE = "default";
|
|
4
|
+
|
|
5
|
+
export interface BrowserProfileRecord {
|
|
6
|
+
name: string;
|
|
7
|
+
createdAt: string | null;
|
|
8
|
+
topics: Array<{ id: string; title: string }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeBrowserProfileName(name: string): string {
|
|
12
|
+
const value = name.trim().toLowerCase();
|
|
13
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,47}$/.test(value)) {
|
|
14
|
+
throw new Error("Profile name must be 1-48 lowercase letters, numbers, '-' or '_'.");
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS browser_profiles (
|
|
21
|
+
owner_id TEXT NOT NULL,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (owner_id, name)
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
export function getBrowserProfileOwner(topicId: string, fallbackUserId: string): string {
|
|
29
|
+
return (
|
|
30
|
+
db
|
|
31
|
+
.query<{ browser_profile_owner: string | null }, string>(
|
|
32
|
+
"SELECT browser_profile_owner FROM api_topics WHERE id = ?",
|
|
33
|
+
)
|
|
34
|
+
.get(topicId)?.browser_profile_owner ?? fallbackUserId
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isTopicBrowserProfileOwner(topicId: string, userId: string): boolean {
|
|
39
|
+
return getBrowserProfileOwner(topicId, "") === userId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getTopicBrowserProfile(topicId: string): string {
|
|
43
|
+
const value = db
|
|
44
|
+
.query<{ browser_profile: string | null }, string>(
|
|
45
|
+
"SELECT browser_profile FROM api_topics WHERE id = ?",
|
|
46
|
+
)
|
|
47
|
+
.get(topicId)?.browser_profile;
|
|
48
|
+
return value || DEFAULT_BROWSER_PROFILE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hasBrowserProfileTopic(topicId: string): boolean {
|
|
52
|
+
return Boolean(
|
|
53
|
+
db.query<{ id: string }, string>("SELECT id FROM api_topics WHERE id = ?").get(topicId),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createBrowserProfile(ownerId: string, rawName: string): string {
|
|
58
|
+
const name = normalizeBrowserProfileName(rawName);
|
|
59
|
+
if (name === DEFAULT_BROWSER_PROFILE) return name;
|
|
60
|
+
db.query(
|
|
61
|
+
"INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)",
|
|
62
|
+
).run(ownerId, name, new Date().toISOString());
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function assignTopicBrowserProfile(opts: {
|
|
67
|
+
topicId: string;
|
|
68
|
+
actorUserId: string;
|
|
69
|
+
profile: string;
|
|
70
|
+
}): { previous: string; profile: string } {
|
|
71
|
+
const actualOwner = getBrowserProfileOwner(opts.topicId, "");
|
|
72
|
+
if (!actualOwner) throw new Error(`Topic "${opts.topicId}" not found or has no owner.`);
|
|
73
|
+
if (actualOwner !== opts.actorUserId) {
|
|
74
|
+
throw new Error(`Only the topic owner can change its browser profile.`);
|
|
75
|
+
}
|
|
76
|
+
const profile = normalizeBrowserProfileName(opts.profile);
|
|
77
|
+
if (profile !== DEFAULT_BROWSER_PROFILE) createBrowserProfile(actualOwner, profile);
|
|
78
|
+
const previous = getTopicBrowserProfile(opts.topicId);
|
|
79
|
+
const result = db
|
|
80
|
+
.query("UPDATE api_topics SET browser_profile = ? WHERE id = ?")
|
|
81
|
+
.run(profile, opts.topicId);
|
|
82
|
+
if (Number(result.changes ?? 0) !== 1) throw new Error(`Topic "${opts.topicId}" not found.`);
|
|
83
|
+
return { previous, profile };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function listBrowserProfiles(ownerId: string): BrowserProfileRecord[] {
|
|
87
|
+
const records = db
|
|
88
|
+
.query<{ name: string; created_at: string }, string>(
|
|
89
|
+
"SELECT name, created_at FROM browser_profiles WHERE owner_id = ? ORDER BY name",
|
|
90
|
+
)
|
|
91
|
+
.all(ownerId);
|
|
92
|
+
const topics = db
|
|
93
|
+
.query<{ id: string; title: string; browser_profile: string | null }, string>(
|
|
94
|
+
`SELECT t.id, t.title, t.browser_profile FROM api_topics t
|
|
95
|
+
WHERE t.browser_profile_owner = ?
|
|
96
|
+
ORDER BY t.title`,
|
|
97
|
+
)
|
|
98
|
+
.all(ownerId);
|
|
99
|
+
|
|
100
|
+
const byName = new Map<string, BrowserProfileRecord>();
|
|
101
|
+
byName.set(DEFAULT_BROWSER_PROFILE, {
|
|
102
|
+
name: DEFAULT_BROWSER_PROFILE,
|
|
103
|
+
createdAt: null,
|
|
104
|
+
topics: [],
|
|
105
|
+
});
|
|
106
|
+
for (const row of records) {
|
|
107
|
+
byName.set(row.name, { name: row.name, createdAt: row.created_at, topics: [] });
|
|
108
|
+
}
|
|
109
|
+
for (const topic of topics) {
|
|
110
|
+
const name = topic.browser_profile || DEFAULT_BROWSER_PROFILE;
|
|
111
|
+
const record = byName.get(name) ?? { name, createdAt: null, topics: [] };
|
|
112
|
+
record.topics.push({ id: topic.id, title: topic.title });
|
|
113
|
+
byName.set(name, record);
|
|
114
|
+
}
|
|
115
|
+
return [...byName.values()].sort((a, b) => {
|
|
116
|
+
if (a.name === DEFAULT_BROWSER_PROFILE) return -1;
|
|
117
|
+
if (b.name === DEFAULT_BROWSER_PROFILE) return 1;
|
|
118
|
+
return a.name.localeCompare(b.name);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function deleteBrowserProfile(ownerId: string, rawName: string): void {
|
|
123
|
+
const name = normalizeBrowserProfileName(rawName);
|
|
124
|
+
if (name === DEFAULT_BROWSER_PROFILE) throw new Error("The default profile cannot be deleted.");
|
|
125
|
+
const used = db
|
|
126
|
+
.query<{ count: number }, [string, string]>(
|
|
127
|
+
`SELECT COUNT(*) AS count FROM api_topics t
|
|
128
|
+
WHERE t.browser_profile = ? AND t.browser_profile_owner = ?`,
|
|
129
|
+
)
|
|
130
|
+
.get(name, ownerId)?.count;
|
|
131
|
+
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);
|
|
133
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { DATA_DIR } from "#platform/config";
|
|
11
|
+
import { appendJsonlLine } from "#platform/jsonl";
|
|
12
|
+
import { logger } from "#platform/logger";
|
|
13
|
+
import { sanitizeTopicName } from "#security/sanitize";
|
|
14
|
+
import type { AgentKind, UnifiedEvent } from "#types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Per-topic conversation log (UnifiedEvent stream) used as the
|
|
18
|
+
* **provider-agnostic source of truth** for cross-agent portability.
|
|
19
|
+
*
|
|
20
|
+
* Storage layout:
|
|
21
|
+
* {DATA_DIR}/conversations/{userId}/{sanitizedTopicName}.jsonl
|
|
22
|
+
*
|
|
23
|
+
* Each line is a `ConversationEntry` JSON object. Append-only: every yielded
|
|
24
|
+
* UnifiedEvent during a `runAgent()` turn is captured here by the recording
|
|
25
|
+
* wrapper, so on agent switch we can rebuild a synthetic native rollout for
|
|
26
|
+
* the target SDK and preserve the conversation across providers.
|
|
27
|
+
*
|
|
28
|
+
* The Claude/Codex SDK rollouts (~/.claude/projects/, ~/.codex/sessions/) are
|
|
29
|
+
* intentionally treated as opaque side-effects of the SDKs only this file
|
|
30
|
+
* is canonical.
|
|
31
|
+
*/
|
|
32
|
+
export interface ConversationEntry {
|
|
33
|
+
ts: string;
|
|
34
|
+
/**
|
|
35
|
+
* Agent that produced this event, frozen at write time. A topic that has
|
|
36
|
+
* been switched (via `set_agent`) will have a mixed-agent log: earlier
|
|
37
|
+
* entries keep the agent that originally generated them. Replay code
|
|
38
|
+
* (rollout-codec `extractChatPairs`) intentionally ignores this field —
|
|
39
|
+
* the cross-agent rollout's whole point is to feed past dialogue, no
|
|
40
|
+
* matter who produced it, into the *new* SDK as if it were native.
|
|
41
|
+
*/
|
|
42
|
+
agent: AgentKind;
|
|
43
|
+
event: UnifiedEvent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate that a userId stringifies to a safe path component. Rejects empties,
|
|
48
|
+
* slashes, and traversal sequences before they touch the filesystem. The
|
|
49
|
+
* conversation log is local-only so we keep this strict but simple if a
|
|
50
|
+
* caller sends something unexpected it is their bug to fix, not ours to coerce.
|
|
51
|
+
*/
|
|
52
|
+
function safeUserIdComponent(userId: number | string): string {
|
|
53
|
+
const str = String(userId);
|
|
54
|
+
if (!str || /[/\\]|\.\./.test(str)) {
|
|
55
|
+
throw new Error(`conversations: refusing unsafe userId path component: ${str}`);
|
|
56
|
+
}
|
|
57
|
+
return str;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function conversationDir(userId: number | string): string {
|
|
61
|
+
return join(DATA_DIR, "conversations", safeUserIdComponent(userId));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function topicFilename(topicName: string): string {
|
|
65
|
+
const t = sanitizeTopicName(topicName, true);
|
|
66
|
+
return `${t}.jsonl`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Compute the absolute path for a given user/topic conversation log. */
|
|
70
|
+
export function getConversationPath(userId: number | string, topicName: string): string {
|
|
71
|
+
return join(conversationDir(userId), topicFilename(topicName));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Append a single UnifiedEvent for the given topic. Creates the parent
|
|
76
|
+
* directory and the file as needed. Best-effort: I/O failures are logged but
|
|
77
|
+
* never throw, since recording must not break the live stream to Telegram.
|
|
78
|
+
*
|
|
79
|
+
* **Concurrency note (review item M2, revised):** within the bot process the
|
|
80
|
+
* single-threaded event loop already serializes writes — but this module is
|
|
81
|
+
* NOT single-process: the self-config MCP server (a separate stdio process,
|
|
82
|
+
* via `topic-agent-switch`) and provider bridge helpers append to the same
|
|
83
|
+
* topic logs.
|
|
84
|
+
* Cross-process interleaving on macOS is real for lines beyond PIPE_BUF
|
|
85
|
+
* (512B), and a torn line is silently dropped by `readConversation` —
|
|
86
|
+
* corrupting the canonical source for cross-agent rollout reconstruction.
|
|
87
|
+
* Writes therefore go through `appendJsonlLine` (sidecar `.lock` via O_EXCL,
|
|
88
|
+
* stale-lock reclaim, unlocked-append fallback on timeout — interleave is
|
|
89
|
+
* accepted over dropping the entry).
|
|
90
|
+
*
|
|
91
|
+
* The append stays SYNCHRONOUS on purpose: a previous attempt at a
|
|
92
|
+
* Promise-chained per-topic queue made writes async (durability gap before a
|
|
93
|
+
* synchronous `readConversation`) and broke `set_agent` in the bridge tests.
|
|
94
|
+
*/
|
|
95
|
+
export function appendConversationEvent(
|
|
96
|
+
userId: number | string,
|
|
97
|
+
topicName: string,
|
|
98
|
+
agent: AgentKind,
|
|
99
|
+
event: UnifiedEvent,
|
|
100
|
+
): void {
|
|
101
|
+
try {
|
|
102
|
+
appendConversationEventStrict(userId, topicName, agent, event);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger.warn(
|
|
105
|
+
{ err, userId, topicName, eventType: event.type },
|
|
106
|
+
"appendConversationEvent: write failed",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Strict variant for state transitions where the conversation log is a manifest
|
|
113
|
+
* rather than telemetry. Throws on I/O failure so callers can avoid committing
|
|
114
|
+
* DB state that points at an unmanifested SDK session.
|
|
115
|
+
*/
|
|
116
|
+
export function appendConversationEventStrict(
|
|
117
|
+
userId: number | string,
|
|
118
|
+
topicName: string,
|
|
119
|
+
agent: AgentKind,
|
|
120
|
+
event: UnifiedEvent,
|
|
121
|
+
): void {
|
|
122
|
+
const path = getConversationPath(userId, topicName);
|
|
123
|
+
const entry: ConversationEntry = {
|
|
124
|
+
ts: new Date().toISOString(),
|
|
125
|
+
agent,
|
|
126
|
+
event,
|
|
127
|
+
};
|
|
128
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
129
|
+
appendJsonlLine(path, JSON.stringify(entry));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read all entries for a topic in chronological order. Returns an empty array
|
|
134
|
+
* if the file does not exist. Malformed lines are skipped with a warning so a
|
|
135
|
+
* single corrupted entry does not poison the whole conversation.
|
|
136
|
+
*
|
|
137
|
+
* NOTE(perf): reads the whole file each call. Topics in the kilobyte range
|
|
138
|
+
* are fine; if a single topic ever grows into multi-megabyte territory,
|
|
139
|
+
* consider a streaming reader (`readline`/`Bun.file().stream()`) and a
|
|
140
|
+
* size-bounded tail.
|
|
141
|
+
*/
|
|
142
|
+
export function readConversation(userId: number | string, topicName: string): ConversationEntry[] {
|
|
143
|
+
const path = getConversationPath(userId, topicName);
|
|
144
|
+
const out: ConversationEntry[] = [];
|
|
145
|
+
if (!existsSync(path)) return out;
|
|
146
|
+
let raw: string;
|
|
147
|
+
try {
|
|
148
|
+
raw = readFileSync(path, "utf8");
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logger.warn({ err, path }, "readConversation: read failed");
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
for (const line of raw.split("\n")) {
|
|
154
|
+
if (!line.trim()) continue;
|
|
155
|
+
try {
|
|
156
|
+
out.push(JSON.parse(line) as ConversationEntry);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logger.warn(
|
|
159
|
+
{ err, line: line.slice(0, 200) },
|
|
160
|
+
"readConversation: malformed JSONL line skipped",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Atomically replace a provider-neutral topic log with an explicit event set. */
|
|
168
|
+
export function replaceConversationStrict(
|
|
169
|
+
userId: number | string,
|
|
170
|
+
topicName: string,
|
|
171
|
+
entries: ConversationEntry[],
|
|
172
|
+
): void {
|
|
173
|
+
const path = getConversationPath(userId, topicName);
|
|
174
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
175
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
176
|
+
try {
|
|
177
|
+
writeFileSync(
|
|
178
|
+
tempPath,
|
|
179
|
+
entries.length > 0 ? `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n` : "",
|
|
180
|
+
{ flag: "wx" },
|
|
181
|
+
);
|
|
182
|
+
renameSync(tempPath, path);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
try {
|
|
185
|
+
unlinkSync(tempPath);
|
|
186
|
+
} catch {}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Copy the unified conversation log of `srcTopic` into the file path that
|
|
193
|
+
* `dstTopic` will read from. Used by `/fork` so a forked child topic inherits
|
|
194
|
+
* the parent's full cross-agent history, not just the agent's native SDK
|
|
195
|
+
* rollout. `/spawn` intentionally does not call this; it starts with no
|
|
196
|
+
* conversation history. Without this copy, a fork followed immediately by
|
|
197
|
+
* `/agent <other>` would feed the empty child log to `switchTopicAgent` and
|
|
198
|
+
* the new agent would start from zero — see the bug report from 2026-05-24.
|
|
199
|
+
*
|
|
200
|
+
* Semantics:
|
|
201
|
+
* - Reads the parent via `readConversation`, matching the history an in-place
|
|
202
|
+
* `set_agent` on the parent would have seen.
|
|
203
|
+
* - Writes a single file at the child's canonical path.
|
|
204
|
+
* - Refuses to overwrite a non-empty destination (returns `{copied:false}`).
|
|
205
|
+
* `/fork`'s only caller runs this immediately after topic creation when
|
|
206
|
+
* the dst file is guaranteed empty, so a non-empty dst means a programmer
|
|
207
|
+
* error somewhere upstream — fail loud rather than silently merging.
|
|
208
|
+
* - On any I/O error: throws. The caller (`createChildTopic`) already has
|
|
209
|
+
* a rollback path that wraps this call.
|
|
210
|
+
*/
|
|
211
|
+
export function cloneConversationLog(opts: {
|
|
212
|
+
userId: number | string;
|
|
213
|
+
srcTopic: string;
|
|
214
|
+
dstTopic: string;
|
|
215
|
+
}): { copied: boolean; entries: number } {
|
|
216
|
+
const { userId, srcTopic, dstTopic } = opts;
|
|
217
|
+
const dstPath = getConversationPath(userId, dstTopic);
|
|
218
|
+
if (existsSync(dstPath) && readFileSync(dstPath, "utf8").trim().length > 0) {
|
|
219
|
+
logger.warn(
|
|
220
|
+
{ userId, srcTopic, dstTopic, dstPath },
|
|
221
|
+
"cloneConversationLog: dst already non-empty — refusing to overwrite",
|
|
222
|
+
);
|
|
223
|
+
return { copied: false, entries: 0 };
|
|
224
|
+
}
|
|
225
|
+
const entries = readConversation(userId, srcTopic);
|
|
226
|
+
if (entries.length === 0) {
|
|
227
|
+
return { copied: false, entries: 0 };
|
|
228
|
+
}
|
|
229
|
+
const body = `${entries.map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
230
|
+
mkdirSync(dirname(dstPath), { recursive: true });
|
|
231
|
+
writeFileSync(dstPath, body);
|
|
232
|
+
return { copied: true, entries: entries.length };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Walk the unified log backwards and return the most recent SDK-emitted
|
|
237
|
+
* session id for the given agent — or null if that agent has never run on
|
|
238
|
+
* this topic.
|
|
239
|
+
*
|
|
240
|
+
* Used by `set_agent` to round-trip the SAME native rollout file across
|
|
241
|
+
* agent switches: when the user does claude → codex → claude, we reuse
|
|
242
|
+
* the original claude sessionId so the synthetic rollout lands at the
|
|
243
|
+
* same path the SDK already manages, preserving prompt-cache continuity
|
|
244
|
+
* and avoiding orphan `~/.claude/projects/<dir>/<id>.jsonl` files.
|
|
245
|
+
*
|
|
246
|
+
* Behavior:
|
|
247
|
+
* - First-ever switch to an agent (no prior session events) → null,
|
|
248
|
+
* caller falls back to a fresh randomUUID/uuidv7. Same as before.
|
|
249
|
+
* - Roundtrip switch (prior session exists) → that sessionId.
|
|
250
|
+
*
|
|
251
|
+
* `session` events are emitted on every turn by claude-/codex-provider and
|
|
252
|
+
* captured into the unified log by runAgent's append wrapper. The most
|
|
253
|
+
* recent one for the target agent is by definition the current SDK-side
|
|
254
|
+
* resume key for that agent.
|
|
255
|
+
*/
|
|
256
|
+
export function findLastSessionIdForAgent(
|
|
257
|
+
entries: ConversationEntry[],
|
|
258
|
+
agent: AgentKind,
|
|
259
|
+
): string | null {
|
|
260
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
261
|
+
const entry = entries[i];
|
|
262
|
+
if (entry.agent !== agent) continue;
|
|
263
|
+
if (entry.event.type === "session") {
|
|
264
|
+
return entry.event.sessionId;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AgentKind, isAgentKind } from "#types";
|
|
2
|
+
import { db } from "./schema";
|
|
3
|
+
|
|
4
|
+
export function getTopicAgent(userId: number, topicName: string): AgentKind {
|
|
5
|
+
const row = db
|
|
6
|
+
.query<{ agent: string | null }, [string, string]>(
|
|
7
|
+
"SELECT agent FROM topics WHERE user_id = ? AND name = ?",
|
|
8
|
+
)
|
|
9
|
+
.get(String(userId), topicName);
|
|
10
|
+
const value = row?.agent;
|
|
11
|
+
if (!isAgentKind(value)) throw new Error(`Invalid agent in DB: ${value}`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setTopicAgent(userId: number, topicName: string, agent: AgentKind): boolean {
|
|
16
|
+
const result = db
|
|
17
|
+
.query("UPDATE topics SET agent = ? WHERE user_id = ? AND name = ?")
|
|
18
|
+
.run(agent, String(userId), topicName);
|
|
19
|
+
return result.changes > 0;
|
|
20
|
+
}
|