negotium 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-0ay6cjdy.js +1331 -0
  3. package/dist/chunk-0ay6cjdy.js.map +16 -0
  4. package/dist/chunk-1k7t7bnx.js +97 -0
  5. package/dist/chunk-1k7t7bnx.js.map +10 -0
  6. package/dist/chunk-5ahkew9c.js +806 -0
  7. package/dist/chunk-5ahkew9c.js.map +17 -0
  8. package/dist/chunk-6cpn7ht2.js +287 -0
  9. package/dist/chunk-6cpn7ht2.js.map +13 -0
  10. package/dist/chunk-6kctn20e.js +3529 -0
  11. package/dist/chunk-6kctn20e.js.map +20 -0
  12. package/dist/chunk-7xqj7aex.js +77 -0
  13. package/dist/chunk-7xqj7aex.js.map +11 -0
  14. package/dist/chunk-9p0ww3tk.js +41 -0
  15. package/dist/chunk-9p0ww3tk.js.map +10 -0
  16. package/dist/chunk-a92a7nrk.js +252 -0
  17. package/dist/chunk-a92a7nrk.js.map +10 -0
  18. package/dist/chunk-c500xe9z.js +33 -0
  19. package/dist/chunk-c500xe9z.js.map +10 -0
  20. package/dist/chunk-cds1qxsf.js +1644 -0
  21. package/dist/chunk-cds1qxsf.js.map +17 -0
  22. package/dist/chunk-e4407g8x.js +274 -0
  23. package/dist/chunk-e4407g8x.js.map +10 -0
  24. package/dist/chunk-e5pbggsh.js +363 -0
  25. package/dist/chunk-e5pbggsh.js.map +11 -0
  26. package/dist/chunk-edb07sjp.js +567 -0
  27. package/dist/chunk-edb07sjp.js.map +14 -0
  28. package/dist/chunk-exd0v74a.js +3012 -0
  29. package/dist/chunk-exd0v74a.js.map +32 -0
  30. package/dist/chunk-fgrme9tr.js +4398 -0
  31. package/dist/chunk-fgrme9tr.js.map +47 -0
  32. package/dist/chunk-fka9eqe1.js +581 -0
  33. package/dist/chunk-fka9eqe1.js.map +11 -0
  34. package/dist/chunk-g82jygez.js +108 -0
  35. package/dist/chunk-g82jygez.js.map +10 -0
  36. package/dist/chunk-ghs2j1fr.js +312 -0
  37. package/dist/chunk-ghs2j1fr.js.map +10 -0
  38. package/dist/chunk-gmy11efb.js +125 -0
  39. package/dist/chunk-gmy11efb.js.map +10 -0
  40. package/dist/chunk-gxma4c70.js +110 -0
  41. package/dist/chunk-gxma4c70.js.map +10 -0
  42. package/dist/chunk-hkdqhm34.js +61 -0
  43. package/dist/chunk-hkdqhm34.js.map +10 -0
  44. package/dist/chunk-hpa0cpzh.js +134 -0
  45. package/dist/chunk-hpa0cpzh.js.map +10 -0
  46. package/dist/chunk-hwy3n31w.js +21 -0
  47. package/dist/chunk-hwy3n31w.js.map +9 -0
  48. package/dist/chunk-j2behpy7.js +214 -0
  49. package/dist/chunk-j2behpy7.js.map +10 -0
  50. package/dist/chunk-jc0at0tc.js +102 -0
  51. package/dist/chunk-jc0at0tc.js.map +10 -0
  52. package/dist/chunk-jkqq5ef6.js +243 -0
  53. package/dist/chunk-jkqq5ef6.js.map +11 -0
  54. package/dist/chunk-jzkw2f01.js +51 -0
  55. package/dist/chunk-jzkw2f01.js.map +10 -0
  56. package/dist/chunk-jzz91jwh.js +70 -0
  57. package/dist/chunk-jzz91jwh.js.map +10 -0
  58. package/dist/chunk-k9z47mab.js +247 -0
  59. package/dist/chunk-k9z47mab.js.map +12 -0
  60. package/dist/chunk-n17yxpa8.js +23 -0
  61. package/dist/chunk-n17yxpa8.js.map +10 -0
  62. package/dist/chunk-naa09ags.js +2143 -0
  63. package/dist/chunk-naa09ags.js.map +15 -0
  64. package/dist/chunk-ns3ke88n.js +142 -0
  65. package/dist/chunk-ns3ke88n.js.map +10 -0
  66. package/dist/chunk-p3cr2ejg.js +1626 -0
  67. package/dist/chunk-p3cr2ejg.js.map +15 -0
  68. package/dist/chunk-q5bcpf23.js +2925 -0
  69. package/dist/chunk-q5bcpf23.js.map +20 -0
  70. package/dist/chunk-rafc55bs.js +424 -0
  71. package/dist/chunk-rafc55bs.js.map +11 -0
  72. package/dist/chunk-rbrxjvbj.js +2830 -0
  73. package/dist/chunk-rbrxjvbj.js.map +23 -0
  74. package/dist/chunk-sa874j5c.js +9 -0
  75. package/dist/chunk-sa874j5c.js.map +10 -0
  76. package/dist/chunk-sgsz2fpq.js +289 -0
  77. package/dist/chunk-sgsz2fpq.js.map +10 -0
  78. package/dist/chunk-sm6fh5c5.js +569 -0
  79. package/dist/chunk-sm6fh5c5.js.map +14 -0
  80. package/dist/chunk-t9zcg8f1.js +499 -0
  81. package/dist/chunk-t9zcg8f1.js.map +13 -0
  82. package/dist/chunk-vp2db42d.js +121 -0
  83. package/dist/chunk-vp2db42d.js.map +10 -0
  84. package/dist/chunk-w13jjf4a.js +46 -0
  85. package/dist/chunk-w13jjf4a.js.map +10 -0
  86. package/dist/chunk-x1arr7ze.js +63 -0
  87. package/dist/chunk-x1arr7ze.js.map +10 -0
  88. package/dist/chunk-xfq68rch.js +583 -0
  89. package/dist/chunk-xfq68rch.js.map +11 -0
  90. package/dist/main.js +132 -20
  91. package/dist/main.js.map +4 -4
  92. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  93. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  94. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  95. package/dist/runtime/src/agents/archiver.ts +396 -0
  96. package/dist/runtime/src/agents/auth-check.ts +64 -0
  97. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  98. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  99. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  100. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  101. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  102. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  103. package/dist/runtime/src/agents/contracts.ts +69 -0
  104. package/dist/runtime/src/agents/deep-map.ts +13 -0
  105. package/dist/runtime/src/agents/execution-host.ts +91 -0
  106. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  107. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  108. package/dist/runtime/src/agents/fork.ts +65 -0
  109. package/dist/runtime/src/agents/hosted-agent.ts +48 -0
  110. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  111. package/dist/runtime/src/agents/index.ts +205 -0
  112. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  113. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  114. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  115. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  116. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  117. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  118. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  119. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  120. package/dist/runtime/src/agents/registry.ts +24 -0
  121. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  122. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  123. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  124. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  125. package/dist/runtime/src/agents/task-events.ts +44 -0
  126. package/dist/runtime/src/agents/tool-format.ts +352 -0
  127. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  128. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  129. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  130. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  131. package/dist/runtime/src/application/submit-user-message.ts +47 -0
  132. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  133. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  134. package/dist/runtime/src/application/topic-service.ts +139 -0
  135. package/dist/runtime/src/application/vault-command.ts +170 -0
  136. package/dist/runtime/src/bus.ts +319 -0
  137. package/dist/runtime/src/index.ts +333 -0
  138. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  139. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  140. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  141. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  142. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  143. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  144. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  145. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  146. package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
  147. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  148. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  149. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  150. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  151. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  152. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  153. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  154. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  155. package/dist/runtime/src/mcp/task-server.ts +173 -0
  156. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  157. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  158. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  159. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  160. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  161. package/dist/runtime/src/media/file-events.ts +33 -0
  162. package/dist/runtime/src/media/text-extractor.ts +312 -0
  163. package/dist/runtime/src/media/video.ts +92 -0
  164. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  165. package/dist/runtime/src/outbox/index.ts +8 -0
  166. package/dist/runtime/src/outbox/utils.ts +51 -0
  167. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  168. package/dist/runtime/src/platform/config.ts +371 -0
  169. package/dist/runtime/src/platform/constants.ts +18 -0
  170. package/dist/runtime/src/platform/debug-users.ts +14 -0
  171. package/dist/runtime/src/platform/delay.ts +1 -0
  172. package/dist/runtime/src/platform/error.ts +9 -0
  173. package/dist/runtime/src/platform/file-utils.ts +16 -0
  174. package/dist/runtime/src/platform/jsonl.ts +202 -0
  175. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  176. package/dist/runtime/src/platform/logger.ts +31 -0
  177. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  178. package/dist/runtime/src/platform/mcp-config.ts +865 -0
  179. package/dist/runtime/src/platform/modules.ts +145 -0
  180. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  181. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  182. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  183. package/dist/runtime/src/prompts/builders.ts +357 -0
  184. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  185. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  186. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  187. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  188. package/dist/runtime/src/query/active-rooms.ts +532 -0
  189. package/dist/runtime/src/query/control.ts +313 -0
  190. package/dist/runtime/src/query/index.ts +24 -0
  191. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  192. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  193. package/dist/runtime/src/query/state.ts +94 -0
  194. package/dist/runtime/src/query/types.ts +83 -0
  195. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  196. package/dist/runtime/src/runtime/attachments.ts +151 -0
  197. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  198. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  199. package/dist/runtime/src/runtime/errors.ts +69 -0
  200. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  201. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  202. package/dist/runtime/src/runtime/footer.ts +23 -0
  203. package/dist/runtime/src/runtime/inbox.ts +946 -0
  204. package/dist/runtime/src/runtime/tasks.ts +56 -0
  205. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  206. package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
  207. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  208. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  209. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  210. package/dist/runtime/src/runtime/visuals.ts +171 -0
  211. package/dist/runtime/src/security/sanitize.ts +27 -0
  212. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  213. package/dist/runtime/src/storage/activity-log.ts +152 -0
  214. package/dist/runtime/src/storage/api-messages.ts +617 -0
  215. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  216. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  217. package/dist/runtime/src/storage/api-topics.ts +844 -0
  218. package/dist/runtime/src/storage/app-settings.ts +44 -0
  219. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  220. package/dist/runtime/src/storage/conversations.ts +268 -0
  221. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  222. package/dist/runtime/src/storage/forum/index.ts +4 -0
  223. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  224. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  225. package/dist/runtime/src/storage/forum-db.ts +23 -0
  226. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  227. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  228. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  229. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  230. package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
  231. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  232. package/dist/runtime/src/storage/session-asks.ts +347 -0
  233. package/dist/runtime/src/storage/sqlite.ts +129 -0
  234. package/dist/runtime/src/storage/tasks.ts +179 -0
  235. package/dist/runtime/src/storage/token-stats.ts +144 -0
  236. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  237. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  238. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  239. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  240. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  241. package/dist/runtime/src/storage/vault.ts +225 -0
  242. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  243. package/dist/runtime/src/storage/wiki.ts +85 -0
  244. package/dist/runtime/src/topics/create.ts +123 -0
  245. package/dist/runtime/src/topics/derive.ts +398 -0
  246. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  247. package/dist/runtime/src/topics/links.ts +7 -0
  248. package/dist/runtime/src/topics/personal-general.ts +65 -0
  249. package/dist/runtime/src/topics/session.ts +381 -0
  250. package/dist/runtime/src/types/api.ts +497 -0
  251. package/dist/runtime/src/types.ts +255 -0
  252. package/dist/runtime/src/version.ts +1 -0
  253. package/dist/runtime/tsconfig.json +13 -0
  254. package/package.json +16 -3
  255. package/dist/main.d.ts +0 -3
  256. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import "./stdio-protect";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { z } from "zod";
5
+ import { errMsg } from "#platform/error";
6
+ import {
7
+ createTasks,
8
+ deleteTasks,
9
+ readTasks,
10
+ renderTaskList,
11
+ TASK_STATUS_VALUES,
12
+ taskScopeKey,
13
+ updateTasks,
14
+ writeTasks,
15
+ } from "#storage/tasks";
16
+ import { connectStdio, mcpError, mcpOk, parseUserIdArg } from "./mcp-helpers";
17
+
18
+ const args = process.argv.slice(2);
19
+ const userId = parseUserIdArg(args);
20
+ const topic = args.find((arg) => arg.startsWith("--topic="))?.slice("--topic=".length) || "";
21
+ const topicId =
22
+ args.find((arg) => arg.startsWith("--topic-id="))?.slice("--topic-id=".length) || undefined;
23
+ const scopeKey = topic ? taskScopeKey({ topicId, session: topic }) : "";
24
+
25
+ const server = new McpServer({ name: "task", version: "1.0.0" });
26
+
27
+ function requireContext(): ReturnType<typeof mcpError> | null {
28
+ if (!userId || !scopeKey) {
29
+ return mcpError("Error: missing userId/topic context.");
30
+ }
31
+ return null;
32
+ }
33
+
34
+ const STATUS_ENUM = z.enum(TASK_STATUS_VALUES);
35
+
36
+ server.tool(
37
+ "task_create",
38
+ "Add one or more tasks to this topic's shared Otium task list. Use this instead of provider-native todo/task tools.",
39
+ {
40
+ tasks: z
41
+ .array(
42
+ z.object({
43
+ subject: z.string().min(1).describe("Short imperative task description"),
44
+ active_form: z
45
+ .string()
46
+ .optional()
47
+ .describe("Label to show while the task is in progress"),
48
+ blocked_by: z.array(z.string()).optional().describe("Task ids blocking this task"),
49
+ owner: z.string().optional().describe("Owner/agent label for multi-agent work"),
50
+ }),
51
+ )
52
+ .min(1),
53
+ },
54
+ async ({ tasks: inputs }) => {
55
+ const guard = requireContext();
56
+ if (guard) return guard;
57
+ try {
58
+ const current = readTasks(userId, scopeKey);
59
+ const { tasks, created } = createTasks(
60
+ current,
61
+ inputs.map((task) => ({
62
+ subject: task.subject,
63
+ activeForm: task.active_form,
64
+ blockedBy: task.blocked_by,
65
+ owner: task.owner,
66
+ })),
67
+ );
68
+ writeTasks(userId, scopeKey, tasks);
69
+ const ids = created.map((task) => `#${task.id}`).join(", ");
70
+ return mcpOk(`${created.length} task(s) created (${ids})\n\n${renderTaskList(tasks)}`);
71
+ } catch (e) {
72
+ return mcpError(`task_create failed: ${errMsg(e)}`);
73
+ }
74
+ },
75
+ );
76
+
77
+ server.tool(
78
+ "task_update",
79
+ "Update task status/content in this topic's shared Otium task list. Batch related status changes in one call.",
80
+ {
81
+ updates: z
82
+ .array(
83
+ z.object({
84
+ id: z.string().describe("Target task id"),
85
+ status: STATUS_ENUM.optional().describe("New status"),
86
+ subject: z.string().min(1).optional().describe("Replacement task description"),
87
+ active_form: z
88
+ .string()
89
+ .optional()
90
+ .describe("Replacement in-progress label; empty string clears it"),
91
+ blocked_by: z.array(z.string()).optional().describe("Replacement blocker id list"),
92
+ owner: z.string().optional().describe("Replacement owner; empty string clears it"),
93
+ }),
94
+ )
95
+ .min(1),
96
+ },
97
+ async ({ updates }) => {
98
+ const guard = requireContext();
99
+ if (guard) return guard;
100
+ try {
101
+ const current = readTasks(userId, scopeKey);
102
+ const { tasks, missing } = updateTasks(
103
+ current,
104
+ updates.map((update) => ({
105
+ id: update.id,
106
+ status: update.status,
107
+ subject: update.subject,
108
+ activeForm: update.active_form,
109
+ blockedBy: update.blocked_by,
110
+ owner: update.owner,
111
+ })),
112
+ );
113
+ writeTasks(userId, scopeKey, tasks);
114
+ const warn = missing.length > 0 ? `\nMissing ids ignored: ${missing.join(", ")}` : "";
115
+ return mcpOk(`${renderTaskList(tasks)}${warn}`);
116
+ } catch (e) {
117
+ return mcpError(`task_update failed: ${errMsg(e)}`);
118
+ }
119
+ },
120
+ );
121
+
122
+ server.tool("task_list", "Read this topic's shared Otium task list.", {}, async () => {
123
+ const guard = requireContext();
124
+ if (guard) return guard;
125
+ try {
126
+ return mcpOk(renderTaskList(readTasks(userId, scopeKey)));
127
+ } catch (e) {
128
+ return mcpError(`task_list failed: ${errMsg(e)}`);
129
+ }
130
+ });
131
+
132
+ server.tool(
133
+ "task_get",
134
+ "Read one task from this topic's shared Otium task list as JSON.",
135
+ { id: z.string().describe("Task id") },
136
+ async ({ id }) => {
137
+ const guard = requireContext();
138
+ if (guard) return guard;
139
+ try {
140
+ const task = readTasks(userId, scopeKey).find((entry) => entry.id === id);
141
+ if (!task) return mcpError(`Task #${id} not found.`);
142
+ return mcpOk(JSON.stringify(task, null, 2));
143
+ } catch (e) {
144
+ return mcpError(`task_get failed: ${errMsg(e)}`);
145
+ }
146
+ },
147
+ );
148
+
149
+ server.tool(
150
+ "task_delete",
151
+ "Delete tasks from this topic's shared Otium task list. Use all=true only when starting a new plan.",
152
+ {
153
+ ids: z.array(z.string()).optional().describe("Task ids to delete"),
154
+ all: z.boolean().optional().describe("Delete the whole list"),
155
+ },
156
+ async ({ ids, all }) => {
157
+ const guard = requireContext();
158
+ if (guard) return guard;
159
+ if (!all && (!ids || ids.length === 0)) {
160
+ return mcpError("Provide ids or all=true.");
161
+ }
162
+ try {
163
+ const current = readTasks(userId, scopeKey);
164
+ const { tasks, removed } = deleteTasks(current, { ids, all });
165
+ writeTasks(userId, scopeKey, tasks);
166
+ return mcpOk(`${removed} task(s) deleted\n\n${renderTaskList(tasks)}`);
167
+ } catch (e) {
168
+ return mcpError(`task_delete failed: ${errMsg(e)}`);
169
+ }
170
+ },
171
+ );
172
+
173
+ await connectStdio(server);
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import { connectStdio, parseUserIdArg } from "#mcp/mcp-helpers";
5
+ import { calcCost, getStats } from "#storage/token-stats";
6
+
7
+ const args = process.argv.slice(2);
8
+ const userId = parseUserIdArg(args);
9
+ if (!userId) {
10
+ process.stderr.write("token-stats-server: --user-id is required (decimal integer)\n");
11
+ process.exit(1);
12
+ }
13
+
14
+ const server = new McpServer({ name: "token-stats", version: "1.0.0" });
15
+
16
+ server.tool(
17
+ "get_usage_stats",
18
+ [
19
+ "Claude 사용량 통계를 조회합니다. 완료된 쿼리마다 세션/시간 정보가 기록됩니다. 현재 실행 중인 turn은 완료 후 집계됩니다.",
20
+ "",
21
+ "from/to 생략 시 전체 기간. 자연어 시간 표현을 ISO 형식으로 변환해서 넘겨주세요.",
22
+ " 예: '지난 3시간' → from = 현재 -3h",
23
+ " 예: '오늘 오전' → from = 오늘 00:00Z, to = 오늘 12:00Z",
24
+ " 예: '어제' → from = 어제 00:00Z, to = 어제 23:59Z",
25
+ "",
26
+ "groupBy: 'session'으로 설정하면 세션별 토큰 사용량 랭킹을 반환합니다.",
27
+ ].join("\n"),
28
+ {
29
+ from: z.string().optional().describe("시작 시각 ISO 8601. 예: '2026-03-28T09:00:00Z'"),
30
+ to: z.string().optional().describe("종료 시각 ISO 8601. 생략 시 현재"),
31
+ groupBy: z.enum(["hour", "session"]).optional().describe("집계 기준. 기본값: hour"),
32
+ },
33
+ async ({ from, to, groupBy = "hour" }) => {
34
+ const { total, byHour, bySession, estimatedCostUsd } = getStats(userId, from, to);
35
+
36
+ const lines: string[] = [
37
+ "📊 Claude 사용량",
38
+ from || to ? `기간: ${from ?? "전체"} ~ ${to ?? "현재"}` : "기간: 전체",
39
+ "",
40
+ `쿼리 횟수: ${total.queries.toLocaleString()}회`,
41
+ `입력 토큰: ${total.inputTokens.toLocaleString()}`,
42
+ `출력 토큰: ${total.outputTokens.toLocaleString()}`,
43
+ `캐시 쓰기: ${total.cacheCreationInputTokens.toLocaleString()}`,
44
+ `캐시 읽기: ${total.cacheReadInputTokens.toLocaleString()}`,
45
+ `예상 비용: $${estimatedCostUsd.toFixed(4)} USD`,
46
+ ];
47
+
48
+ if (groupBy === "session") {
49
+ const sorted = Object.entries(bySession)
50
+ .map(([name, b]) => ({ name, ...b, cost: calcCost(b) }))
51
+ .sort((a, b) => b.cost - a.cost);
52
+
53
+ lines.push("", `🗂 세션별 사용량 (${sorted.length}개)`);
54
+ for (const s of sorted) {
55
+ lines.push(
56
+ ` ${s.name} 쿼리 ${s.queries}회 입력 ${s.inputTokens.toLocaleString()} / 출력 ${s.outputTokens.toLocaleString()} $${s.cost.toFixed(4)}`,
57
+ );
58
+ }
59
+ } else {
60
+ const hours = Object.keys(byHour).sort();
61
+ if (hours.length > 0 && hours.length <= 72) {
62
+ lines.push("", "📅 시간별 현황");
63
+ for (const h of hours) {
64
+ const b = byHour[h];
65
+ if (b.queries === 0) continue;
66
+ lines.push(
67
+ ` ${h} 쿼리 ${b.queries}회 입력 ${b.inputTokens.toLocaleString()} / 출력 ${b.outputTokens.toLocaleString()} $${calcCost(b).toFixed(4)}`,
68
+ );
69
+ }
70
+ } else if (hours.length > 72) {
71
+ const byDate: Record<
72
+ string,
73
+ { queries: number; inputTokens: number; outputTokens: number; cost: number }
74
+ > = {};
75
+ for (const h of hours) {
76
+ const date = h.slice(0, 10);
77
+ if (!byDate[date])
78
+ byDate[date] = { queries: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
79
+ const b = byHour[h];
80
+ byDate[date].queries += b.queries;
81
+ byDate[date].inputTokens += b.inputTokens;
82
+ byDate[date].outputTokens += b.outputTokens;
83
+ byDate[date].cost += calcCost(b);
84
+ }
85
+ lines.push("", `📅 일별 현황 (${Object.keys(byDate).length}일)`);
86
+ for (const [date, d] of Object.entries(byDate).sort()) {
87
+ lines.push(
88
+ ` ${date} 쿼리 ${d.queries}회 입력 ${d.inputTokens.toLocaleString()} / 출력 ${d.outputTokens.toLocaleString()} $${d.cost.toFixed(4)}`,
89
+ );
90
+ }
91
+ }
92
+ }
93
+
94
+ return { content: [{ type: "text", text: lines.join("\n") }] };
95
+ },
96
+ );
97
+
98
+ await connectStdio(server);
@@ -0,0 +1,219 @@
1
+ import { logger } from "#platform/logger";
2
+ import { redactVaultSecrets, vaultSubstituteDetailed } from "#storage/vault";
3
+
4
+ const SAFE_RESPONSE_HEADERS = new Set([
5
+ "content-type",
6
+ "content-length",
7
+ "location",
8
+ "retry-after",
9
+ "x-ratelimit-limit",
10
+ "x-ratelimit-remaining",
11
+ "x-ratelimit-reset",
12
+ ]);
13
+
14
+ const FORBIDDEN_REQUEST_HEADERS = new Set([
15
+ "connection",
16
+ "content-length",
17
+ "host",
18
+ "proxy-authorization",
19
+ "transfer-encoding",
20
+ ]);
21
+
22
+ export interface VaultHttpRequest {
23
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
24
+ url: string;
25
+ headers?: Record<string, string>;
26
+ body?: string;
27
+ timeoutMs?: number;
28
+ maxResponseBytes?: number;
29
+ }
30
+
31
+ export interface VaultHttpResult {
32
+ ok: boolean;
33
+ status?: number;
34
+ statusText?: string;
35
+ headers?: Record<string, string>;
36
+ body?: string;
37
+ truncated?: boolean;
38
+ error?: string;
39
+ }
40
+
41
+ function substituteObject(
42
+ userId: string,
43
+ values: Record<string, string>,
44
+ ): { values: Record<string, string>; usedKeys: string[] } {
45
+ const used = new Set<string>();
46
+ const substituted: Record<string, string> = {};
47
+ for (const [key, value] of Object.entries(values)) {
48
+ if (FORBIDDEN_REQUEST_HEADERS.has(key.toLowerCase())) {
49
+ throw new Error(`Header "${key}" is not allowed`);
50
+ }
51
+ const result = vaultSubstituteDetailed(userId, value);
52
+ for (const usedKey of result.usedKeys) used.add(usedKey);
53
+ substituted[key] = result.text;
54
+ }
55
+ return { values: substituted, usedKeys: [...used] };
56
+ }
57
+
58
+ function safeResponseHeaders(userId: string, headers: Headers): Record<string, string> {
59
+ const output: Record<string, string> = {};
60
+ for (const [key, value] of headers.entries()) {
61
+ if (!SAFE_RESPONSE_HEADERS.has(key.toLowerCase())) continue;
62
+ output[key] = redactVaultSecrets(userId, value);
63
+ }
64
+ return output;
65
+ }
66
+
67
+ async function readBoundedBody(
68
+ response: Response,
69
+ maxBytes: number,
70
+ ): Promise<{ bytes: Uint8Array; truncated: boolean }> {
71
+ if (!response.body) return { bytes: new Uint8Array(), truncated: false };
72
+ const reader = response.body.getReader();
73
+ const chunks: Uint8Array[] = [];
74
+ let keptBytes = 0;
75
+ let truncated = false;
76
+ try {
77
+ while (true) {
78
+ const next = await reader.read();
79
+ if (next.done) break;
80
+ const remaining = maxBytes - keptBytes;
81
+ if (remaining <= 0) {
82
+ truncated = true;
83
+ await reader.cancel();
84
+ break;
85
+ }
86
+ const visible = next.value.subarray(0, remaining);
87
+ chunks.push(visible);
88
+ keptBytes += visible.byteLength;
89
+ if (visible.byteLength < next.value.byteLength) {
90
+ truncated = true;
91
+ await reader.cancel();
92
+ break;
93
+ }
94
+ }
95
+ } finally {
96
+ reader.releaseLock();
97
+ }
98
+
99
+ const bytes = new Uint8Array(keptBytes);
100
+ let offset = 0;
101
+ for (const chunk of chunks) {
102
+ bytes.set(chunk, offset);
103
+ offset += chunk.byteLength;
104
+ }
105
+ return { bytes, truncated };
106
+ }
107
+
108
+ /**
109
+ * Execute an HTTPS request inside the credential boundary. The caller supplies
110
+ * only {{KEY}} references; expanded headers/body never leave this function.
111
+ */
112
+ export async function executeVaultHttpRequest(
113
+ userId: string,
114
+ request: VaultHttpRequest,
115
+ fetchImpl: typeof fetch = fetch,
116
+ ): Promise<VaultHttpResult> {
117
+ let parsedUrl: URL;
118
+ try {
119
+ parsedUrl = new URL(request.url);
120
+ } catch {
121
+ return { ok: false, error: "url must be an absolute HTTPS URL" };
122
+ }
123
+ if (parsedUrl.protocol !== "https:") {
124
+ return { ok: false, error: "Vault credentials may only be sent over HTTPS" };
125
+ }
126
+ if (parsedUrl.username || parsedUrl.password || /\{\{[^}]+\}\}/.test(request.url)) {
127
+ return {
128
+ ok: false,
129
+ error: "Keep Vault placeholders out of URLs; put credentials in headers or body",
130
+ };
131
+ }
132
+
133
+ let headers: Record<string, string>;
134
+ let headerKeys: string[];
135
+ try {
136
+ const substituted = substituteObject(userId, request.headers ?? {});
137
+ headers = substituted.values;
138
+ headerKeys = substituted.usedKeys;
139
+ } catch (error) {
140
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
141
+ }
142
+
143
+ const bodyResult =
144
+ request.body === undefined
145
+ ? { text: undefined, usedKeys: [] as string[] }
146
+ : vaultSubstituteDetailed(userId, request.body);
147
+ const usedKeys = [...new Set([...headerKeys, ...bodyResult.usedKeys])].sort();
148
+ if (usedKeys.length === 0) {
149
+ return {
150
+ ok: false,
151
+ error: "No valid Vault placeholder was found in headers or body",
152
+ };
153
+ }
154
+ if (request.method === "GET" && request.body !== undefined) {
155
+ return { ok: false, error: "GET requests cannot include a credential-bearing body" };
156
+ }
157
+
158
+ const controller = new AbortController();
159
+ const timeoutMs = Math.min(Math.max(request.timeoutMs ?? 30_000, 1_000), 120_000);
160
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
161
+ const startedAt = Date.now();
162
+ try {
163
+ const response = await fetchImpl(parsedUrl, {
164
+ method: request.method,
165
+ headers,
166
+ body: request.method === "GET" ? undefined : bodyResult.text,
167
+ redirect: "manual",
168
+ signal: controller.signal,
169
+ });
170
+ const maxBytes = Math.min(Math.max(request.maxResponseBytes ?? 256 * 1024, 1_024), 1024 * 1024);
171
+ const { bytes, truncated } = await readBoundedBody(response, maxBytes);
172
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
173
+ const textual =
174
+ contentType.startsWith("text/") ||
175
+ contentType.includes("json") ||
176
+ contentType.includes("xml") ||
177
+ contentType.includes("javascript") ||
178
+ contentType === "";
179
+ const body = textual
180
+ ? redactVaultSecrets(userId, new TextDecoder().decode(bytes))
181
+ : `[binary response omitted: ${bytes.byteLength} bytes, ${contentType || "unknown content type"}]`;
182
+
183
+ logger.info(
184
+ {
185
+ userId,
186
+ vaultKeys: usedKeys,
187
+ host: parsedUrl.hostname,
188
+ method: request.method,
189
+ status: response.status,
190
+ durationMs: Date.now() - startedAt,
191
+ },
192
+ "vault credential used",
193
+ );
194
+
195
+ return {
196
+ ok: response.ok,
197
+ status: response.status,
198
+ statusText: response.statusText,
199
+ headers: safeResponseHeaders(userId, response.headers),
200
+ body,
201
+ truncated,
202
+ };
203
+ } catch (error) {
204
+ const raw = error instanceof Error ? error.message : String(error);
205
+ logger.warn(
206
+ {
207
+ userId,
208
+ vaultKeys: usedKeys,
209
+ host: parsedUrl.hostname,
210
+ method: request.method,
211
+ durationMs: Date.now() - startedAt,
212
+ },
213
+ "vault credential request failed",
214
+ );
215
+ return { ok: false, error: redactVaultSecrets(userId, raw) };
216
+ } finally {
217
+ clearTimeout(timeout);
218
+ }
219
+ }
@@ -0,0 +1,153 @@
1
+ import { spawn } from "node:child_process";
2
+ import { logger } from "#platform/logger";
3
+ import { redactVaultSecrets, vaultSubstituteDetailed } from "#storage/vault";
4
+
5
+ export interface VaultRunRequest {
6
+ command: string;
7
+ timeoutMs?: number;
8
+ maxOutputBytes?: number;
9
+ cwd?: string;
10
+ }
11
+
12
+ export interface VaultRunResult {
13
+ ok: boolean;
14
+ exitCode: number | null;
15
+ signal: NodeJS.Signals | null;
16
+ stdout: string;
17
+ stderr: string;
18
+ truncated: boolean;
19
+ usedKeys: string[];
20
+ error?: string;
21
+ }
22
+
23
+ function appendBounded(
24
+ chunks: Buffer[],
25
+ chunk: Buffer,
26
+ state: { bytes: number; truncated: boolean },
27
+ maxBytes: number,
28
+ ): void {
29
+ if (state.bytes >= maxBytes) {
30
+ state.truncated = true;
31
+ return;
32
+ }
33
+ const remaining = maxBytes - state.bytes;
34
+ const visible = chunk.subarray(0, remaining);
35
+ chunks.push(visible);
36
+ state.bytes += visible.byteLength;
37
+ if (visible.byteLength < chunk.byteLength) state.truncated = true;
38
+ }
39
+
40
+ /** Execute a placeholder-bearing shell command without exposing expanded input/output to the SDK. */
41
+ export async function executeVaultRun(
42
+ userId: string,
43
+ request: VaultRunRequest,
44
+ ): Promise<VaultRunResult> {
45
+ const substitution = vaultSubstituteDetailed(userId, request.command);
46
+ if (substitution.usedKeys.length === 0) {
47
+ return {
48
+ ok: false,
49
+ exitCode: null,
50
+ signal: null,
51
+ stdout: "",
52
+ stderr: "",
53
+ truncated: false,
54
+ usedKeys: [],
55
+ error: "No valid Vault placeholder was found in command",
56
+ };
57
+ }
58
+
59
+ const timeoutMs = Math.min(Math.max(request.timeoutMs ?? 120_000, 1_000), 600_000);
60
+ const maxOutputBytes = Math.min(
61
+ Math.max(request.maxOutputBytes ?? 512 * 1024, 1_024),
62
+ 2 * 1024 * 1024,
63
+ );
64
+ const stdoutChunks: Buffer[] = [];
65
+ const stderrChunks: Buffer[] = [];
66
+ const stdoutState = { bytes: 0, truncated: false };
67
+ const stderrState = { bytes: 0, truncated: false };
68
+ const startedAt = Date.now();
69
+
70
+ return await new Promise<VaultRunResult>((resolve) => {
71
+ // Feed the expanded script over stdin so plaintext never appears in argv or
72
+ // the provider-visible tool input. zsh -s reads commands from standard input.
73
+ const child = spawn(process.env.SHELL || "/bin/sh", ["-s"], {
74
+ cwd: request.cwd,
75
+ detached: true,
76
+ env: process.env,
77
+ stdio: ["pipe", "pipe", "pipe"],
78
+ });
79
+ let settled = false;
80
+ let timedOut = false;
81
+ const signalTree = (signal: NodeJS.Signals) => {
82
+ if (!child.pid) return;
83
+ try {
84
+ process.kill(-child.pid, signal);
85
+ } catch {
86
+ try {
87
+ child.kill(signal);
88
+ } catch {
89
+ // The process tree already exited.
90
+ }
91
+ }
92
+ };
93
+
94
+ const finish = (result: Omit<VaultRunResult, "usedKeys">) => {
95
+ if (settled) return;
96
+ settled = true;
97
+ clearTimeout(timeout);
98
+ logger.info(
99
+ {
100
+ userId,
101
+ vaultKeys: substitution.usedKeys,
102
+ exitCode: result.exitCode,
103
+ signal: result.signal,
104
+ durationMs: Date.now() - startedAt,
105
+ timedOut,
106
+ },
107
+ "vault credential command used",
108
+ );
109
+ resolve({ ...result, usedKeys: substitution.usedKeys });
110
+ };
111
+
112
+ child.stdout.on("data", (chunk: Buffer) =>
113
+ appendBounded(stdoutChunks, chunk, stdoutState, maxOutputBytes),
114
+ );
115
+ child.stderr.on("data", (chunk: Buffer) =>
116
+ appendBounded(stderrChunks, chunk, stderrState, maxOutputBytes),
117
+ );
118
+ child.once("error", (error) => {
119
+ finish({
120
+ ok: false,
121
+ exitCode: null,
122
+ signal: null,
123
+ stdout: "",
124
+ stderr: "",
125
+ truncated: false,
126
+ error: redactVaultSecrets(userId, error.message),
127
+ });
128
+ });
129
+ child.once("close", (exitCode, signal) => {
130
+ // vault_run is deliberately foreground-only. Clean up descendants a
131
+ // command attempted to leave behind so secrets do not linger in argv/env.
132
+ signalTree("SIGTERM");
133
+ const stdout = redactVaultSecrets(userId, Buffer.concat(stdoutChunks).toString("utf8"));
134
+ const stderr = redactVaultSecrets(userId, Buffer.concat(stderrChunks).toString("utf8"));
135
+ finish({
136
+ ok: !timedOut && exitCode === 0,
137
+ exitCode,
138
+ signal,
139
+ stdout,
140
+ stderr,
141
+ truncated: stdoutState.truncated || stderrState.truncated,
142
+ ...(timedOut ? { error: `Command timed out after ${timeoutMs}ms` } : {}),
143
+ });
144
+ });
145
+
146
+ const timeout = setTimeout(() => {
147
+ timedOut = true;
148
+ signalTree("SIGKILL");
149
+ }, timeoutMs);
150
+
151
+ child.stdin.end(substitution.text);
152
+ });
153
+ }