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,144 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { LOG_DIR } from "#platform/config";
|
|
5
|
+
import { appendJsonlEntry, readJsonlLines } from "#platform/jsonl";
|
|
6
|
+
import { logger } from "#platform/logger";
|
|
7
|
+
|
|
8
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
9
|
+
|
|
10
|
+
export interface QueryRecord {
|
|
11
|
+
timestamp: string; // ISO 8601 UTC
|
|
12
|
+
session: string; // topic name
|
|
13
|
+
inputTokens: number;
|
|
14
|
+
outputTokens: number;
|
|
15
|
+
cacheCreationInputTokens: number;
|
|
16
|
+
cacheReadInputTokens: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Bucket {
|
|
20
|
+
inputTokens: number;
|
|
21
|
+
outputTokens: number;
|
|
22
|
+
cacheCreationInputTokens: number;
|
|
23
|
+
cacheReadInputTokens: number;
|
|
24
|
+
queries: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function emptyBucket(): Bucket {
|
|
28
|
+
return {
|
|
29
|
+
inputTokens: 0,
|
|
30
|
+
outputTokens: 0,
|
|
31
|
+
cacheCreationInputTokens: 0,
|
|
32
|
+
cacheReadInputTokens: 0,
|
|
33
|
+
queries: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function queriesPath(userId: number | string): string {
|
|
38
|
+
const rawUserId = String(userId);
|
|
39
|
+
// Preserve existing filenames for ordinary IDs, but never let an external
|
|
40
|
+
// identity introduce path separators or unbounded filename length.
|
|
41
|
+
const fileId = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,255}$/.test(rawUserId)
|
|
42
|
+
? rawUserId
|
|
43
|
+
: `sha256-${createHash("sha256").update(rawUserId).digest("hex")}`;
|
|
44
|
+
return join(LOG_DIR, `token-queries-${fileId}.jsonl`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadRecords(userId: number | string): QueryRecord[] {
|
|
48
|
+
try {
|
|
49
|
+
return readJsonlLines(queriesPath(userId)).flatMap((line) => {
|
|
50
|
+
try {
|
|
51
|
+
return [JSON.parse(line) as QueryRecord];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function calcCost(
|
|
62
|
+
b: Pick<
|
|
63
|
+
Bucket,
|
|
64
|
+
"inputTokens" | "outputTokens" | "cacheCreationInputTokens" | "cacheReadInputTokens"
|
|
65
|
+
>,
|
|
66
|
+
): number {
|
|
67
|
+
return (
|
|
68
|
+
(b.inputTokens / 1_000_000) * 3.0 +
|
|
69
|
+
(b.outputTokens / 1_000_000) * 15.0 +
|
|
70
|
+
(b.cacheCreationInputTokens / 1_000_000) * 3.75 +
|
|
71
|
+
(b.cacheReadInputTokens / 1_000_000) * 0.3
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 쿼리 완료 시 호출 — JSONL에 한 줄 추가 */
|
|
76
|
+
export function recordUsage(
|
|
77
|
+
userId: number | string,
|
|
78
|
+
session: string,
|
|
79
|
+
usage: {
|
|
80
|
+
inputTokens: number;
|
|
81
|
+
outputTokens: number;
|
|
82
|
+
cacheCreationInputTokens?: number;
|
|
83
|
+
cacheReadInputTokens?: number;
|
|
84
|
+
},
|
|
85
|
+
) {
|
|
86
|
+
const record: QueryRecord = {
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
session,
|
|
89
|
+
inputTokens: usage.inputTokens,
|
|
90
|
+
outputTokens: usage.outputTokens,
|
|
91
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
92
|
+
cacheReadInputTokens: usage.cacheReadInputTokens ?? 0,
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
appendJsonlEntry(queriesPath(userId), record);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
logger.warn({ err: e, userId }, "token-stats: Failed to record");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getStats(
|
|
102
|
+
userId: number | string,
|
|
103
|
+
from?: string,
|
|
104
|
+
to?: string,
|
|
105
|
+
): {
|
|
106
|
+
total: Bucket;
|
|
107
|
+
byHour: Record<string, Bucket>;
|
|
108
|
+
bySession: Record<string, Bucket>;
|
|
109
|
+
estimatedCostUsd: number;
|
|
110
|
+
} {
|
|
111
|
+
const records = loadRecords(userId);
|
|
112
|
+
|
|
113
|
+
const fromTs = from ? new Date(from).getTime() : 0;
|
|
114
|
+
const toTs = to ? new Date(to).getTime() : Infinity;
|
|
115
|
+
|
|
116
|
+
if ((from && Number.isNaN(fromTs)) || (to && Number.isNaN(toTs))) {
|
|
117
|
+
logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
|
|
118
|
+
return { total: emptyBucket(), byHour: {}, bySession: {}, estimatedCostUsd: 0 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const total = emptyBucket();
|
|
122
|
+
const byHour: Record<string, Bucket> = {};
|
|
123
|
+
const bySession: Record<string, Bucket> = {};
|
|
124
|
+
|
|
125
|
+
for (const r of records) {
|
|
126
|
+
const ts = new Date(r.timestamp).getTime();
|
|
127
|
+
if (ts < fromTs || ts > toTs) continue;
|
|
128
|
+
|
|
129
|
+
const hourKey = r.timestamp.slice(0, 13); // "2026-03-28T14"
|
|
130
|
+
|
|
131
|
+
if (!byHour[hourKey]) byHour[hourKey] = emptyBucket();
|
|
132
|
+
if (!bySession[r.session]) bySession[r.session] = emptyBucket();
|
|
133
|
+
|
|
134
|
+
for (const bucket of [total, byHour[hourKey], bySession[r.session]]) {
|
|
135
|
+
bucket.inputTokens += r.inputTokens;
|
|
136
|
+
bucket.outputTokens += r.outputTokens;
|
|
137
|
+
bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
|
|
138
|
+
bucket.cacheReadInputTokens += r.cacheReadInputTokens;
|
|
139
|
+
bucket.queries += 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { total, byHour, bySession, estimatedCostUsd: calcCost(total) };
|
|
144
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { db } from "#storage/forum-db";
|
|
2
|
+
|
|
3
|
+
db.exec(`
|
|
4
|
+
CREATE TABLE IF NOT EXISTS api_topic_archive_state (
|
|
5
|
+
topic_id TEXT PRIMARY KEY,
|
|
6
|
+
last_archived_rowid INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
last_archive_path TEXT,
|
|
8
|
+
updated_at TEXT NOT NULL
|
|
9
|
+
)
|
|
10
|
+
`);
|
|
11
|
+
|
|
12
|
+
export interface TopicArchiveState {
|
|
13
|
+
topicId: string;
|
|
14
|
+
lastArchivedRowid: number;
|
|
15
|
+
lastArchivePath?: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TopicArchiveStateRow {
|
|
20
|
+
topic_id: string;
|
|
21
|
+
last_archived_rowid: number;
|
|
22
|
+
last_archive_path: string | null;
|
|
23
|
+
updated_at: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getTopicArchiveState(topicId: string): TopicArchiveState | null {
|
|
27
|
+
const row = db.query("SELECT * FROM api_topic_archive_state WHERE topic_id = ?").get(topicId) as
|
|
28
|
+
| TopicArchiveStateRow
|
|
29
|
+
| undefined;
|
|
30
|
+
if (!row) return null;
|
|
31
|
+
return {
|
|
32
|
+
topicId: row.topic_id,
|
|
33
|
+
lastArchivedRowid: row.last_archived_rowid,
|
|
34
|
+
lastArchivePath: row.last_archive_path ?? undefined,
|
|
35
|
+
updatedAt: row.updated_at,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setTopicArchiveState(
|
|
40
|
+
topicId: string,
|
|
41
|
+
lastArchivedRowid: number,
|
|
42
|
+
lastArchivePath?: string,
|
|
43
|
+
): void {
|
|
44
|
+
db.query(
|
|
45
|
+
`INSERT INTO api_topic_archive_state (topic_id, last_archived_rowid, last_archive_path, updated_at)
|
|
46
|
+
VALUES (?, ?, ?, ?)
|
|
47
|
+
ON CONFLICT(topic_id) DO UPDATE SET
|
|
48
|
+
last_archived_rowid = excluded.last_archived_rowid,
|
|
49
|
+
last_archive_path = excluded.last_archive_path,
|
|
50
|
+
updated_at = excluded.updated_at`,
|
|
51
|
+
).run(topicId, lastArchivedRowid, lastArchivePath ?? null, new Date().toISOString());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Remove the idle-archive cursor for a topic that no longer exists. */
|
|
55
|
+
export function deleteTopicArchiveState(topicId: string): boolean {
|
|
56
|
+
return (
|
|
57
|
+
db.query("DELETE FROM api_topic_archive_state WHERE topic_id = ?").run(topicId).changes > 0
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { logger } from "#platform/logger";
|
|
4
|
+
import { sanitizeTopicName } from "#security/sanitize";
|
|
5
|
+
import { getAllMessagesForTopic, getMessagesForTopicAfterRowid } from "#storage/api-messages";
|
|
6
|
+
import { formatTopicArchiveTranscriptRecord } from "#storage/topic-transcript";
|
|
7
|
+
import { getSharedWikiDir } from "#storage/wiki";
|
|
8
|
+
|
|
9
|
+
export interface TopicArchiveResult {
|
|
10
|
+
path: string;
|
|
11
|
+
messageCount: number;
|
|
12
|
+
lastRowid: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TopicArchiveOptions {
|
|
16
|
+
afterRowid?: number;
|
|
17
|
+
reason?: "delete" | "idle";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Forensic-archive a topic's SQLite-backed messages into the shared
|
|
22
|
+
* `wiki/archive/` as JSONL transcript records.
|
|
23
|
+
*
|
|
24
|
+
* Otium keeps conversations in the `api_messages` table (SQLite), not in the
|
|
25
|
+
* `PROJECT_ROOT/logs` activity-log files that Otium's `archiveSessionLogs`
|
|
26
|
+
* scans — so this is Otium's native equivalent. Each JSONL record keeps the raw
|
|
27
|
+
* DB message under `message`, plus a human-readable `line`/`role`/`speaker` for
|
|
28
|
+
* the background wiki-archiver turn.
|
|
29
|
+
*
|
|
30
|
+
* Output lands in the **shared** wiki root (see `getSharedWikiDir`) because
|
|
31
|
+
* that is the root the wiki MCP serves in topic-id mode; writing it elsewhere
|
|
32
|
+
* would make the archive invisible to later `wiki_query` calls.
|
|
33
|
+
*
|
|
34
|
+
* Returns `null` when the selected range has no messages.
|
|
35
|
+
*/
|
|
36
|
+
export function archiveTopicMessages(
|
|
37
|
+
topicId: string,
|
|
38
|
+
topicTitle: string,
|
|
39
|
+
options: TopicArchiveOptions = {},
|
|
40
|
+
): TopicArchiveResult | null {
|
|
41
|
+
const rows =
|
|
42
|
+
options.afterRowid !== undefined
|
|
43
|
+
? getMessagesForTopicAfterRowid(topicId, options.afterRowid)
|
|
44
|
+
: getAllMessagesForTopic(topicId);
|
|
45
|
+
if (rows.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
const safeTopic = sanitizeTopicName(topicTitle, true);
|
|
48
|
+
const archiveDir = join(getSharedWikiDir(), "archive");
|
|
49
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
// Collision-proof file name: a topic deleted twice on the same day (e.g.
|
|
52
|
+
// recreated then deleted again) must not clobber the earlier archive.
|
|
53
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
54
|
+
const reasonSuffix = options.reason === "idle" ? "_idle" : "";
|
|
55
|
+
let filename = `${safeTopic}_${date}${reasonSuffix}.jsonl`;
|
|
56
|
+
let counter = 1;
|
|
57
|
+
while (existsSync(join(archiveDir, filename))) {
|
|
58
|
+
counter++;
|
|
59
|
+
filename = `${safeTopic}_${date}${reasonSuffix}_${counter}.jsonl`;
|
|
60
|
+
}
|
|
61
|
+
const path = join(archiveDir, filename);
|
|
62
|
+
const lastRowid = rows.reduce((max, row) => Math.max(max, row.rowid ?? 0), 0);
|
|
63
|
+
|
|
64
|
+
const body = `${rows
|
|
65
|
+
.map((r, index) => JSON.stringify(formatTopicArchiveTranscriptRecord(r, topicTitle, index + 1)))
|
|
66
|
+
.join("\n")}\n`;
|
|
67
|
+
writeFileSync(path, body);
|
|
68
|
+
|
|
69
|
+
logger.info(
|
|
70
|
+
{ topicId, topicTitle, archive: path, messageCount: rows.length, lastRowid },
|
|
71
|
+
"archiveTopicMessages: archived topic messages",
|
|
72
|
+
);
|
|
73
|
+
return { path, messageCount: rows.length, lastRowid };
|
|
74
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { db } from "#storage/forum-db";
|
|
2
|
+
import { type AgentKind, isAgentKind } from "#types";
|
|
3
|
+
|
|
4
|
+
export interface TopicAgentSwitchState {
|
|
5
|
+
name: string;
|
|
6
|
+
agent: AgentKind;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toTopicAgentSwitchState(row: {
|
|
10
|
+
name: string;
|
|
11
|
+
agent: string | null;
|
|
12
|
+
}): TopicAgentSwitchState {
|
|
13
|
+
if (!isAgentKind(row.agent)) throw new Error(`Invalid agent in DB: ${row.agent}`);
|
|
14
|
+
return {
|
|
15
|
+
name: row.name,
|
|
16
|
+
agent: row.agent,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getTopicAgentSwitchState(
|
|
21
|
+
userId: number,
|
|
22
|
+
topicName: string,
|
|
23
|
+
): TopicAgentSwitchState | null {
|
|
24
|
+
const row = db
|
|
25
|
+
.query<{ name: string; agent: string | null }, [string, string]>(
|
|
26
|
+
"SELECT name, agent FROM topics WHERE user_id = ? AND name = ?",
|
|
27
|
+
)
|
|
28
|
+
.get(String(userId), topicName);
|
|
29
|
+
return row ? toTopicAgentSwitchState(row) : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SetTopicAgentAndSessionOptions {
|
|
33
|
+
userId: number;
|
|
34
|
+
topicName: string;
|
|
35
|
+
agent: AgentKind;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setTopicAgentAndSession(opts: SetTopicAgentAndSessionOptions): void {
|
|
40
|
+
const { userId, topicName, agent, sessionId } = opts;
|
|
41
|
+
db.transaction(() => {
|
|
42
|
+
const r = db
|
|
43
|
+
.query("UPDATE topics SET agent = ?, session_id = ? WHERE user_id = ? AND name = ?")
|
|
44
|
+
.run(agent, sessionId, String(userId), topicName);
|
|
45
|
+
if (r.changes === 0) {
|
|
46
|
+
throw new Error(`setTopicAgentAndSession: row missing for ${userId}/${topicName}`);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SetTopicAgentAndClearSessionOptions {
|
|
52
|
+
userId: number;
|
|
53
|
+
topicName: string;
|
|
54
|
+
agent: AgentKind;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setTopicAgentAndClearSession(opts: SetTopicAgentAndClearSessionOptions): boolean {
|
|
58
|
+
const { userId, topicName, agent } = opts;
|
|
59
|
+
let changed = false;
|
|
60
|
+
db.transaction(() => {
|
|
61
|
+
const r = db
|
|
62
|
+
.query("UPDATE topics SET agent = ?, session_id = NULL WHERE user_id = ? AND name = ?")
|
|
63
|
+
.run(agent, String(userId), topicName);
|
|
64
|
+
changed = r.changes > 0;
|
|
65
|
+
})();
|
|
66
|
+
return changed;
|
|
67
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface TopicArchiveMessageRow {
|
|
2
|
+
id: string;
|
|
3
|
+
topic_id: string;
|
|
4
|
+
parent_id: string | null;
|
|
5
|
+
author_id: string;
|
|
6
|
+
text: string;
|
|
7
|
+
query_id: string | null;
|
|
8
|
+
agent_type: string | null;
|
|
9
|
+
model: string | null;
|
|
10
|
+
attachments: string | null;
|
|
11
|
+
usage: string | null;
|
|
12
|
+
deleted: number;
|
|
13
|
+
created_at: string;
|
|
14
|
+
rowid?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type TopicArchiveRole = "user" | "assistant" | "system";
|
|
18
|
+
|
|
19
|
+
export interface TopicArchiveTranscriptRecord {
|
|
20
|
+
type: "message";
|
|
21
|
+
index: number;
|
|
22
|
+
topicId: string;
|
|
23
|
+
topicTitle: string;
|
|
24
|
+
id: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
rowid?: number;
|
|
27
|
+
role: TopicArchiveRole;
|
|
28
|
+
speaker: string;
|
|
29
|
+
line: string;
|
|
30
|
+
text: string;
|
|
31
|
+
authorId: string;
|
|
32
|
+
parentId?: string;
|
|
33
|
+
queryId?: string;
|
|
34
|
+
agentType?: string;
|
|
35
|
+
model?: string;
|
|
36
|
+
attachments?: unknown;
|
|
37
|
+
usage?: unknown;
|
|
38
|
+
message: TopicArchiveMessageRow;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseJsonField(value: string | null): unknown | undefined {
|
|
42
|
+
if (!value) return undefined;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(value);
|
|
45
|
+
} catch {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function transcriptRole(row: TopicArchiveMessageRow): TopicArchiveRole {
|
|
51
|
+
if (row.author_id === "system") return "system";
|
|
52
|
+
if (row.author_id === "ai" || row.agent_type) return "assistant";
|
|
53
|
+
return "user";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function transcriptSpeaker(row: TopicArchiveMessageRow, role: TopicArchiveRole): string {
|
|
57
|
+
if (role === "assistant") return row.agent_type ? `assistant:${row.agent_type}` : "assistant";
|
|
58
|
+
if (role === "system") return "system";
|
|
59
|
+
return `user:${row.author_id}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function oneLine(text: string): string {
|
|
63
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
64
|
+
return compact.length > 2000 ? `${compact.slice(0, 2000)}...` : compact;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatTopicArchiveTranscriptRecord(
|
|
68
|
+
row: TopicArchiveMessageRow,
|
|
69
|
+
topicTitle: string,
|
|
70
|
+
index: number,
|
|
71
|
+
): TopicArchiveTranscriptRecord {
|
|
72
|
+
const role = transcriptRole(row);
|
|
73
|
+
const speaker = transcriptSpeaker(row, role);
|
|
74
|
+
const attachments = parseJsonField(row.attachments);
|
|
75
|
+
const usage = parseJsonField(row.usage);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
type: "message",
|
|
79
|
+
index,
|
|
80
|
+
topicId: row.topic_id,
|
|
81
|
+
topicTitle,
|
|
82
|
+
id: row.id,
|
|
83
|
+
createdAt: row.created_at,
|
|
84
|
+
...(row.rowid !== undefined ? { rowid: row.rowid } : {}),
|
|
85
|
+
role,
|
|
86
|
+
speaker,
|
|
87
|
+
line: `[${row.created_at}] ${speaker}: ${oneLine(row.text)}`,
|
|
88
|
+
text: row.text,
|
|
89
|
+
authorId: row.author_id,
|
|
90
|
+
...(row.parent_id ? { parentId: row.parent_id } : {}),
|
|
91
|
+
...(row.query_id ? { queryId: row.query_id } : {}),
|
|
92
|
+
...(row.agent_type ? { agentType: row.agent_type } : {}),
|
|
93
|
+
...(row.model ? { model: row.model } : {}),
|
|
94
|
+
...(attachments !== undefined ? { attachments } : {}),
|
|
95
|
+
...(usage !== undefined ? { usage } : {}),
|
|
96
|
+
message: row,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { VAULT_MASTER_KEY } from "#platform/config";
|
|
3
|
+
|
|
4
|
+
const ENVELOPE_PREFIX = "otium-vault:v1:";
|
|
5
|
+
const IV_BYTES = 12;
|
|
6
|
+
const KEY_BYTES = 32;
|
|
7
|
+
|
|
8
|
+
function encryptionKey(): Buffer {
|
|
9
|
+
return createHash("sha256")
|
|
10
|
+
.update("otium-vault-value-v1\0", "utf8")
|
|
11
|
+
.update(VAULT_MASTER_KEY, "utf8")
|
|
12
|
+
.digest()
|
|
13
|
+
.subarray(0, KEY_BYTES);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function aad(userId: string, key: string): Buffer {
|
|
17
|
+
return Buffer.from(`${userId}\0${key.toUpperCase()}`, "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isEncryptedVaultValue(value: string): boolean {
|
|
21
|
+
return value.startsWith(ENVELOPE_PREFIX);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Encrypt one vault row. The user/key binding prevents ciphertext row swapping. */
|
|
25
|
+
export function encryptVaultValue(userId: string, key: string, value: string): string {
|
|
26
|
+
const iv = randomBytes(IV_BYTES);
|
|
27
|
+
const cipher = createCipheriv("aes-256-gcm", encryptionKey(), iv);
|
|
28
|
+
cipher.setAAD(aad(userId, key));
|
|
29
|
+
const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
30
|
+
const tag = cipher.getAuthTag();
|
|
31
|
+
return `${ENVELOPE_PREFIX}${iv.toString("base64url")}.${ciphertext.toString("base64url")}.${tag.toString("base64url")}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decrypt a stored value. Plaintext rows are accepted for rolling upgrades and
|
|
36
|
+
* are re-encrypted by the storage layer immediately after a successful read.
|
|
37
|
+
*/
|
|
38
|
+
export function decryptVaultValue(
|
|
39
|
+
userId: string,
|
|
40
|
+
key: string,
|
|
41
|
+
storedValue: string,
|
|
42
|
+
): { value: string; legacyPlaintext: boolean } {
|
|
43
|
+
if (!isEncryptedVaultValue(storedValue)) {
|
|
44
|
+
return { value: storedValue, legacyPlaintext: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const encoded = storedValue.slice(ENVELOPE_PREFIX.length);
|
|
48
|
+
const [ivPart, ciphertextPart, tagPart, ...extra] = encoded.split(".");
|
|
49
|
+
if (!ivPart || ciphertextPart === undefined || !tagPart || extra.length > 0) {
|
|
50
|
+
throw new Error("Invalid encrypted vault value");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const iv = Buffer.from(ivPart, "base64url");
|
|
54
|
+
const ciphertext = Buffer.from(ciphertextPart, "base64url");
|
|
55
|
+
const tag = Buffer.from(tagPart, "base64url");
|
|
56
|
+
if (iv.length !== IV_BYTES || tag.length !== 16) {
|
|
57
|
+
throw new Error("Invalid encrypted vault value");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const decipher = createDecipheriv("aes-256-gcm", encryptionKey(), iv);
|
|
61
|
+
decipher.setAAD(aad(userId, key));
|
|
62
|
+
decipher.setAuthTag(tag);
|
|
63
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
64
|
+
return { value: plaintext.toString("utf8"), legacyPlaintext: false };
|
|
65
|
+
}
|