wyrm-mcp 7.2.1 → 7.2.2

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 (150) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.js +1 -60
  4. package/dist/agent-daemon.js +4 -281
  5. package/dist/agent-loop.js +7 -332
  6. package/dist/analytics.js +13 -236
  7. package/dist/attribution.js +1 -49
  8. package/dist/audit.js +2 -457
  9. package/dist/auto-capture.js +3 -138
  10. package/dist/auto-orchestrator.js +1 -325
  11. package/dist/autoconfig.js +39 -840
  12. package/dist/buddy-runner.js +1 -109
  13. package/dist/buddy.js +14 -564
  14. package/dist/build-flags.js +1 -17
  15. package/dist/capabilities.js +3 -183
  16. package/dist/capture.js +1 -56
  17. package/dist/causality.js +6 -107
  18. package/dist/cli.js +20 -281
  19. package/dist/cloud/cli.js +5 -541
  20. package/dist/cloud/client.js +1 -221
  21. package/dist/cloud/crypto.js +1 -85
  22. package/dist/cloud/machine-id.js +2 -113
  23. package/dist/cloud/recovery.js +1 -60
  24. package/dist/cloud/sync-engine.js +7 -543
  25. package/dist/cloud-backup.js +5 -579
  26. package/dist/cloud-profile.js +1 -138
  27. package/dist/cloud-sync-entrypoint.js +1 -47
  28. package/dist/cloud-sync.js +2 -309
  29. package/dist/constellation.js +12 -168
  30. package/dist/context-build-budgeted.js +4 -144
  31. package/dist/context-ranking.js +1 -69
  32. package/dist/crypto.js +1 -179
  33. package/dist/daemon-write-endpoint.js +1 -290
  34. package/dist/daemon-writer.js +2 -406
  35. package/dist/database.js +43 -1110
  36. package/dist/deprecations.js +2 -162
  37. package/dist/design.js +13 -141
  38. package/dist/event-replication.js +1 -112
  39. package/dist/events-sse.js +7 -43
  40. package/dist/events.js +6 -238
  41. package/dist/failure-patterns.js +42 -659
  42. package/dist/federation.js +12 -236
  43. package/dist/goals.js +13 -101
  44. package/dist/golden.js +3 -355
  45. package/dist/handlers/agent.js +4 -165
  46. package/dist/handlers/alias-adapters.js +1 -129
  47. package/dist/handlers/aliases.js +1 -171
  48. package/dist/handlers/audit.js +1 -87
  49. package/dist/handlers/boundary.js +1 -221
  50. package/dist/handlers/capture.js +73 -1109
  51. package/dist/handlers/causality.js +7 -114
  52. package/dist/handlers/cloud.js +85 -382
  53. package/dist/handlers/companion.js +28 -459
  54. package/dist/handlers/datalake.js +7 -187
  55. package/dist/handlers/dispatch-context.js +0 -22
  56. package/dist/handlers/entity.js +25 -256
  57. package/dist/handlers/events.js +16 -335
  58. package/dist/handlers/failure.js +13 -340
  59. package/dist/handlers/goals.js +4 -296
  60. package/dist/handlers/intelligence.js +126 -674
  61. package/dist/handlers/invoicing.js +1 -70
  62. package/dist/handlers/mcpclient.js +6 -137
  63. package/dist/handlers/orchestration.js +40 -125
  64. package/dist/handlers/output-schemas.js +1 -24
  65. package/dist/handlers/presence.js +3 -99
  66. package/dist/handlers/project.js +28 -182
  67. package/dist/handlers/prompts.js +6 -157
  68. package/dist/handlers/quest.js +4 -224
  69. package/dist/handlers/recall.js +11 -218
  70. package/dist/handlers/registry.js +1 -167
  71. package/dist/handlers/resources.js +1 -288
  72. package/dist/handlers/review.js +11 -74
  73. package/dist/handlers/run.js +17 -487
  74. package/dist/handlers/search.js +15 -326
  75. package/dist/handlers/session.js +28 -615
  76. package/dist/handlers/share.js +8 -184
  77. package/dist/handlers/shims.js +1 -464
  78. package/dist/handlers/skill.js +67 -449
  79. package/dist/handlers/survivors.js +1 -120
  80. package/dist/handlers/symbols.js +8 -109
  81. package/dist/handlers/syncops.js +4 -302
  82. package/dist/handlers/types.js +1 -27
  83. package/dist/harvest.js +5 -191
  84. package/dist/hours.js +7 -156
  85. package/dist/http-auth.js +3 -321
  86. package/dist/http-fast.js +21 -1137
  87. package/dist/icons.js +1 -47
  88. package/dist/index.js +2 -924
  89. package/dist/indexer.js +4 -145
  90. package/dist/intelligence.js +31 -261
  91. package/dist/internal-dispatch.js +3 -212
  92. package/dist/keyset.js +1 -110
  93. package/dist/knowledge-graph.js +12 -176
  94. package/dist/license.js +2 -441
  95. package/dist/logger.js +2 -199
  96. package/dist/maintenance.js +2 -148
  97. package/dist/mcp-client.js +6 -262
  98. package/dist/memory-artifacts.js +30 -449
  99. package/dist/migrate-prompt.js +2 -124
  100. package/dist/migrations.js +40 -655
  101. package/dist/performance.js +1 -228
  102. package/dist/presence.js +11 -140
  103. package/dist/priority-embed.js +5 -164
  104. package/dist/providers/embedding-provider.js +1 -196
  105. package/dist/readonly-gate.js +1 -29
  106. package/dist/rehydration.js +9 -157
  107. package/dist/reindex.js +1 -88
  108. package/dist/render-target.js +21 -514
  109. package/dist/render.js +4 -280
  110. package/dist/repl-guard.js +1 -173
  111. package/dist/replication-daemon-entrypoint.js +1 -31
  112. package/dist/replication-daemon.js +2 -262
  113. package/dist/resilience.js +1 -591
  114. package/dist/reverse-bridge.js +5 -360
  115. package/dist/security.js +1 -244
  116. package/dist/session-seen.js +3 -51
  117. package/dist/setup.js +1 -260
  118. package/dist/skill-author.js +5 -168
  119. package/dist/spec-kit.js +1 -191
  120. package/dist/sqlite-busy.js +1 -154
  121. package/dist/statusline.js +11 -315
  122. package/dist/sub-agent.js +13 -262
  123. package/dist/summarizer.js +13 -139
  124. package/dist/symbols.js +7 -283
  125. package/dist/sync.js +5 -359
  126. package/dist/tasks-dispatch.js +1 -84
  127. package/dist/tasks.js +1 -282
  128. package/dist/token-budget.js +1 -143
  129. package/dist/tool-analytics.js +7 -129
  130. package/dist/tool-annotations.js +1 -365
  131. package/dist/tool-manifest-v2.json +1 -1
  132. package/dist/tool-manifest.json +1 -1
  133. package/dist/tool-profiles.js +1 -75
  134. package/dist/trace-harvest.js +6 -244
  135. package/dist/types.js +1 -30
  136. package/dist/ui-dashboard.js +41 -50
  137. package/dist/ulid.js +1 -81
  138. package/dist/validate.js +1 -129
  139. package/dist/vault.js +1 -534
  140. package/dist/vectors.js +3 -184
  141. package/dist/version-check.js +4 -136
  142. package/dist/visibility.js +19 -155
  143. package/dist/wyrm-cli.js +98 -2464
  144. package/dist/wyrm-guard.js +14 -424
  145. package/dist/wyrm-loop.js +3 -150
  146. package/dist/wyrm-manifest.json +1 -1
  147. package/dist/wyrm-statusline-daemon.js +1 -11
  148. package/dist/wyrm-statusline.js +4 -56
  149. package/dist/wyrm-ui.js +9 -77
  150. package/package.json +4 -2
package/dist/tasks.js CHANGED
@@ -1,282 +1 @@
1
- /**
2
- * MCP Tasks (T037) — capability-gated, SYNCHRONOUS-fallback task lifecycle for
3
- * the long ops of the tools that REMAIN on MCP (Article V: vendor-neutral,
4
- * capability-gated; Article VII: admin-gated long ops are off by default).
5
- *
6
- * WHY this exists. Two of Wyrm's retained tools have ops that can run long on a
7
- * large corpus and are the ones a curated client NEEDS on the MCP wire:
8
- * - wyrm_maintenance / wyrm_reindex / wyrm_vector_setup — the admin upkeep +
9
- * vector (re)build. This is the fix for the curated-client "can't reindex
10
- * the last 17% of vectors" wound — these stay on MCP precisely so a
11
- * resource-constrained client CAN drive a backfill from the agent.
12
- * - harvest / auto_capture extraction (modes of wyrm_capture) — corpus scans.
13
- *
14
- * HOW it composes with the SDK. The MCP Tasks RC's server-side `tools/call`
15
- * augmentation works like this (server/index.js): when the request carries a
16
- * `task` param, the SDK REQUIRES the CallTool handler to return a
17
- * `CreateTaskResult` (`{task:{taskId,status,…}}`) — NOT a CallToolResult — and
18
- * the actual payload is fetched later via `tasks/result`. That deferred-result
19
- * machinery is the part the spec flags as not-yet-stable. Wyrm's faithful,
20
- * "synchronous fallback until the RC stabilizes" implementation runs the op
21
- * IMMEDIATELY and never truly defers:
22
- * 1. Detect the client's `tasks` capability (negotiated at initialize) AND the
23
- * request's `task` augmentation param (`TaskAugmentedRequestParamsSchema`).
24
- * 2. When BOTH are present: run the op SYNCHRONOUSLY, store the completed
25
- * CallToolResult, and return a `CreateTaskResult` whose task is already
26
- * `completed` (or `failed`). The client fetches the payload via
27
- * `tasks/result` (served from the store). No work is deferred — the op
28
- * already ran — so a client that never polls still triggered the op.
29
- * 3. Otherwise (the default for every client today, and any client that omits
30
- * `task`): run the op inline and return the CallToolResult directly. This
31
- * is the synchronous fallback and the always-correct path; no data lost.
32
- *
33
- * The whole module is deterministic and no-network (Article III): the only
34
- * non-determinism is the task id + timestamps, confined to the task record and
35
- * the CreateTaskResult envelope — never the stored CallToolResult bytes.
36
- */
37
- import { randomUUID } from 'node:crypto';
38
- import { renderErrorResponse } from './render.js';
39
- /** Default retention for a completed task record (ms). */
40
- export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000; // 5 minutes
41
- /** Hard cap so a hostile/buggy `task.ttl` can never pin memory (Article VII). */
42
- export const MAX_TASK_TTL_MS = 60 * 60 * 1000; // 1 hour
43
- /** Suggested client poll cadence advertised on the task record (ms). */
44
- export const DEFAULT_POLL_INTERVAL_MS = 500;
45
- /** Hard cap on retained task records — bounds memory; oldest evicted first. */
46
- export const MAX_RETAINED_TASKS = 256;
47
- /**
48
- * True when the connected client advertised the `tasks` capability. Vendor-
49
- * neutral (Article V): reads the negotiated capability, never a client name.
50
- */
51
- export function clientSupportsTasks(tasksCapability) {
52
- return tasksCapability != null && typeof tasksCapability === 'object';
53
- }
54
- /**
55
- * The `task` augmentation a client attaches to a CallTool request when it wants
56
- * a task created (`TaskAugmentedRequestParamsSchema.task`). Reads it off the
57
- * raw request params; returns the requested ttl (clamped) or null when absent.
58
- * Total on adversarial input — never throws (an invalid ttl degrades to the
59
- * default, a missing augmentation yields null = synchronous path).
60
- */
61
- export function readTaskAugmentation(params) {
62
- const task = params && typeof params === 'object' ? params.task : undefined;
63
- if (task == null || typeof task !== 'object')
64
- return { requested: false };
65
- const rawTtl = task.ttl;
66
- let ttlMs = DEFAULT_TASK_TTL_MS;
67
- if (typeof rawTtl === 'number' && Number.isFinite(rawTtl) && rawTtl > 0) {
68
- ttlMs = Math.min(Math.floor(rawTtl), MAX_TASK_TTL_MS);
69
- }
70
- return { requested: true, ttlMs };
71
- }
72
- /**
73
- * In-memory task store. Single-process (the stdio server is one process); the
74
- * MCP RC has no persistence requirement and a restarted server starts clean.
75
- * Bounded by MAX_RETAINED_TASKS with oldest-first eviction.
76
- */
77
- export class TaskStore {
78
- tasks = new Map();
79
- /** Create a `working` task record and return its id. */
80
- create(tool, ttlMs, nowMs = Date.now()) {
81
- this.evictExpired(nowMs);
82
- if (this.tasks.size >= MAX_RETAINED_TASKS)
83
- this.evictOldest();
84
- const iso = new Date(nowMs).toISOString();
85
- const record = {
86
- taskId: randomUUID(),
87
- status: 'working',
88
- ttl: ttlMs,
89
- createdAt: iso,
90
- lastUpdatedAt: iso,
91
- pollInterval: DEFAULT_POLL_INTERVAL_MS,
92
- tool,
93
- _createdAtMs: nowMs,
94
- };
95
- this.tasks.set(record.taskId, record);
96
- return record;
97
- }
98
- /** Mark a task terminal (completed/failed/cancelled) with its result. */
99
- settle(taskId, status, result, statusMessage, nowMs = Date.now()) {
100
- const record = this.tasks.get(taskId);
101
- if (!record)
102
- return;
103
- record.status = status;
104
- record.lastUpdatedAt = new Date(nowMs).toISOString();
105
- if (result !== undefined)
106
- record.result = result;
107
- if (statusMessage !== undefined)
108
- record.statusMessage = statusMessage;
109
- }
110
- /** Read a task record (after evicting any whose ttl has elapsed). */
111
- get(taskId, nowMs = Date.now()) {
112
- this.evictExpired(nowMs);
113
- return this.tasks.get(taskId);
114
- }
115
- /** List the live task records, newest first (after eviction). */
116
- list(nowMs = Date.now()) {
117
- this.evictExpired(nowMs);
118
- return [...this.tasks.values()].sort((a, b) => b._createdAtMs - a._createdAtMs);
119
- }
120
- /** Cancel a still-working task; no-op if unknown or already terminal. */
121
- cancel(taskId, nowMs = Date.now()) {
122
- const record = this.tasks.get(taskId);
123
- if (!record || record.status !== 'working')
124
- return false;
125
- record.status = 'cancelled';
126
- record.lastUpdatedAt = new Date(nowMs).toISOString();
127
- return true;
128
- }
129
- size() {
130
- return this.tasks.size;
131
- }
132
- /** Drop records whose (createdAt + ttl) is in the past. */
133
- evictExpired(nowMs) {
134
- for (const [id, rec] of this.tasks) {
135
- if (rec.ttl != null && nowMs - rec._createdAtMs > rec.ttl)
136
- this.tasks.delete(id);
137
- }
138
- }
139
- /** Drop the single oldest record (Map preserves insertion order). */
140
- evictOldest() {
141
- const oldest = this.tasks.keys().next();
142
- if (!oldest.done)
143
- this.tasks.delete(oldest.value);
144
- }
145
- }
146
- /** Project a TaskRecord to the on-wire shape (strips Wyrm internals). */
147
- export function toWireTask(record) {
148
- const wire = {
149
- taskId: record.taskId,
150
- status: record.status,
151
- ttl: record.ttl,
152
- createdAt: record.createdAt,
153
- lastUpdatedAt: record.lastUpdatedAt,
154
- };
155
- if (record.pollInterval !== undefined)
156
- wire.pollInterval = record.pollInterval;
157
- if (record.statusMessage !== undefined)
158
- wire.statusMessage = record.statusMessage;
159
- return wire;
160
- }
161
- /**
162
- * Resolve whether a given CallTool should run as a task.
163
- *
164
- * The decisive signal is the request's `task` augmentation, because the MCP SDK
165
- * (server/index.js) UNCONDITIONALLY requires the CallTool handler to return a
166
- * `CreateTaskResult` whenever `params.task` is present — it does NOT consult the
167
- * negotiated client capability. So a request bearing `task` MUST run the task
168
- * path or the SDK rejects the response with -32602. The negotiated `tasks`
169
- * capability is recorded as advisory context (a well-behaved client only sends
170
- * `task` after negotiating it), not a suppressor — suppressing it here would
171
- * desync from the SDK and crash the very clients the gate is meant to serve.
172
- *
173
- * No `task` augmentation → the synchronous fallback (a normal CallToolResult),
174
- * the default for every client today.
175
- */
176
- export function resolveTaskGate(tasksCapability, params) {
177
- const capabilityNegotiated = clientSupportsTasks(tasksCapability);
178
- const aug = readTaskAugmentation(params);
179
- if (aug.requested)
180
- return { asTask: true, ttlMs: aug.ttlMs, capabilityNegotiated };
181
- return { asTask: false, ttlMs: DEFAULT_TASK_TTL_MS, capabilityNegotiated };
182
- }
183
- /**
184
- * Run a long op under the task gate. ALWAYS runs the op SYNCHRONOUSLY (no
185
- * deferral) regardless of the gate — the op having run is the spine.
186
- *
187
- * - gate.asTask=false → returns {kind:'inline'} with the CallToolResult; the
188
- * dispatcher returns it directly. This is the synchronous fallback.
189
- * - gate.asTask=true → stores the completed CallToolResult and returns
190
- * {kind:'task'} with a CreateTaskResult whose task is already `completed`
191
- * (or `failed`). The dispatcher returns the CreateTaskResult on the wire;
192
- * the client fetches the payload via tasks/result (served from the store).
193
- *
194
- * A throwing op records a `failed` task and re-throws (the dispatcher's error
195
- * boundary owns the wire error shape — we never swallow it).
196
- */
197
- export async function runLongOp(tool, gate, store, run, nowMs = Date.now()) {
198
- if (!gate.asTask) {
199
- return { kind: 'inline', result: await run() };
200
- }
201
- const record = store.create(tool, gate.ttlMs, nowMs);
202
- let result;
203
- try {
204
- result = await run();
205
- }
206
- catch (err) {
207
- store.settle(record.taskId, 'failed', undefined, String(err));
208
- throw err;
209
- }
210
- store.settle(record.taskId, result?.isError ? 'failed' : 'completed', result);
211
- // re-read so the wire task reflects the settled status/timestamp
212
- const settled = store.get(record.taskId, nowMs) ?? record;
213
- return { kind: 'task', createTask: { task: toWireTask(settled) } };
214
- }
215
- /**
216
- * Fetch the stored CallToolResult payload for a completed task (the
217
- * tasks/result backend). Returns undefined for an unknown/expired/unsettled id.
218
- */
219
- export function taskPayload(store, taskId, nowMs = Date.now()) {
220
- return store.get(taskId, nowMs)?.result;
221
- }
222
- // ── Admin gate (Article VII: destructive long ops off by default) ───────────
223
- //
224
- // `wyrm_maintenance` is destructiveHint:true and `wyrm_reindex`/
225
- // `wyrm_vector_setup` rebuild the embedding store — operations a stray fleet
226
- // agent should not be able to trigger over MCP unless the operator has opted
227
- // in. The gate is a single off-by-default env flag (`WYRM_ADMIN=1`), evaluated
228
- // per call so it can be flipped without a restart in tests. The tools STILL
229
- // remain on MCP (that is the whole point of T037 — the curated client must be
230
- // able to drive a reindex); the gate just requires the operator's intent.
231
- /** The admin-gated tools — the destructive/rebuild long ops on the wire. */
232
- export const ADMIN_GATED_TOOLS = new Set([
233
- 'wyrm_maintenance',
234
- 'wyrm_reindex',
235
- 'wyrm_vector_setup',
236
- ]);
237
- /**
238
- * The RESOLVED dispatcher names the MCP-Tasks affordance TARGETS — the long ops
239
- * of tools that REMAIN on MCP (spec T037): the admin upkeep/vector tools (also
240
- * WYRM_ADMIN-gated) + the extraction modes of wyrm_capture, which resolve
241
- * through the noun shim to wyrm_harvest (mode=artifacts) / wyrm_auto_capture
242
- * (mode=extract). Note: a task augmentation on ANY tool is still honored by the
243
- * dispatcher (the SDK forces a CreateTaskResult whenever params.task is set) —
244
- * this set documents where a task is actually USEFUL, not the only place the
245
- * shape is produced.
246
- */
247
- export const LONG_OP_TOOLS = new Set([
248
- ...ADMIN_GATED_TOOLS,
249
- 'wyrm_harvest',
250
- 'wyrm_auto_capture',
251
- ]);
252
- /** Machine-readable code for an admin-gate rejection (SEP-1303 family). */
253
- export const WYRM_ADMIN_REQUIRED_CODE = 'WYRM_ADMIN_REQUIRED';
254
- /** True when the operator has opted into admin-gated long ops. */
255
- export function adminGateOpen(env = process.env) {
256
- const v = env.WYRM_ADMIN;
257
- return v === '1' || v === 'true' || v === 'yes';
258
- }
259
- /** True when `tool` is admin-gated and the operator has NOT opted in. */
260
- export function adminGateBlocks(tool, env = process.env) {
261
- return ADMIN_GATED_TOOLS.has(tool) && !adminGateOpen(env);
262
- }
263
- /**
264
- * The structured (SEP-1303-shaped) response for an admin-gate rejection.
265
- * `isError:true` with a non-retryable {error,expected} body so a fleet subagent
266
- * does not loop — the operator must set WYRM_ADMIN, not the agent.
267
- */
268
- export function adminDeniedResponse(tool) {
269
- return renderErrorResponse({
270
- error: {
271
- code: WYRM_ADMIN_REQUIRED_CODE,
272
- message: `${tool} is an admin-gated maintenance op (it rebuilds or vacuums the local memory store) ` +
273
- `and is disabled by default.`,
274
- retryable: false,
275
- },
276
- expected: `Set the WYRM_ADMIN=1 environment variable on the Wyrm MCP server process to enable ${tool}, ` +
277
- `then call it again. This rejection is deterministic — do NOT retry unchanged. The operator ` +
278
- `controls this flag, not the agent: it exists so a stray fleet call cannot vacuum/reindex the ` +
279
- `store. The equivalent local CLI (\`wyrm maintenance\` / \`wyrm index rebuild\`) is always available.`,
280
- });
281
- }
282
- //# sourceMappingURL=tasks.js.map
1
+ import{randomUUID as u}from"node:crypto";import{renderErrorResponse as p}from"./render.js";const l=300*1e3,f=3600*1e3,k=500,_=256;function h(e){return e!=null&&typeof e=="object"}function m(e){const t=e&&typeof e=="object"?e.task:void 0;if(t==null||typeof t!="object")return{requested:!1};const s=t.ttl;let a=l;return typeof s=="number"&&Number.isFinite(s)&&s>0&&(a=Math.min(Math.floor(s),f)),{requested:!0,ttlMs:a}}class w{tasks=new Map;create(t,s,a=Date.now()){this.evictExpired(a),this.tasks.size>=_&&this.evictOldest();const i=new Date(a).toISOString(),n={taskId:u(),status:"working",ttl:s,createdAt:i,lastUpdatedAt:i,pollInterval:k,tool:t,_createdAtMs:a};return this.tasks.set(n.taskId,n),n}settle(t,s,a,i,n=Date.now()){const r=this.tasks.get(t);r&&(r.status=s,r.lastUpdatedAt=new Date(n).toISOString(),a!==void 0&&(r.result=a),i!==void 0&&(r.statusMessage=i))}get(t,s=Date.now()){return this.evictExpired(s),this.tasks.get(t)}list(t=Date.now()){return this.evictExpired(t),[...this.tasks.values()].sort((s,a)=>a._createdAtMs-s._createdAtMs)}cancel(t,s=Date.now()){const a=this.tasks.get(t);return!a||a.status!=="working"?!1:(a.status="cancelled",a.lastUpdatedAt=new Date(s).toISOString(),!0)}size(){return this.tasks.size}evictExpired(t){for(const[s,a]of this.tasks)a.ttl!=null&&t-a._createdAtMs>a.ttl&&this.tasks.delete(s)}evictOldest(){const t=this.tasks.keys().next();t.done||this.tasks.delete(t.value)}}function A(e){const t={taskId:e.taskId,status:e.status,ttl:e.ttl,createdAt:e.createdAt,lastUpdatedAt:e.lastUpdatedAt};return e.pollInterval!==void 0&&(t.pollInterval=e.pollInterval),e.statusMessage!==void 0&&(t.statusMessage=e.statusMessage),t}function I(e,t){const s=h(e),a=m(t);return a.requested?{asTask:!0,ttlMs:a.ttlMs,capabilityNegotiated:s}:{asTask:!1,ttlMs:l,capabilityNegotiated:s}}async function g(e,t,s,a,i=Date.now()){if(!t.asTask)return{kind:"inline",result:await a()};const n=s.create(e,t.ttlMs,i);let r;try{r=await a()}catch(o){throw s.settle(n.taskId,"failed",void 0,String(o)),o}s.settle(n.taskId,r?.isError?"failed":"completed",r);const d=s.get(n.taskId,i)??n;return{kind:"task",createTask:{task:A(d)}}}function D(e,t,s=Date.now()){return e.get(t,s)?.result}const c=new Set(["wyrm_maintenance","wyrm_reindex","wyrm_vector_setup"]),y=new Set([...c,"wyrm_harvest","wyrm_auto_capture"]),T="WYRM_ADMIN_REQUIRED";function x(e=process.env){const t=e.WYRM_ADMIN;return t==="1"||t==="true"||t==="yes"}function S(e,t=process.env){return c.has(e)&&!x(t)}function E(e){return p({error:{code:T,message:`${e} is an admin-gated maintenance op (it rebuilds or vacuums the local memory store) and is disabled by default.`,retryable:!1},expected:`Set the WYRM_ADMIN=1 environment variable on the Wyrm MCP server process to enable ${e}, then call it again. This rejection is deterministic \u2014 do NOT retry unchanged. The operator controls this flag, not the agent: it exists so a stray fleet call cannot vacuum/reindex the store. The equivalent local CLI (\`wyrm maintenance\` / \`wyrm index rebuild\`) is always available.`})}export{c as ADMIN_GATED_TOOLS,k as DEFAULT_POLL_INTERVAL_MS,l as DEFAULT_TASK_TTL_MS,y as LONG_OP_TOOLS,_ as MAX_RETAINED_TASKS,f as MAX_TASK_TTL_MS,w as TaskStore,T as WYRM_ADMIN_REQUIRED_CODE,E as adminDeniedResponse,S as adminGateBlocks,x as adminGateOpen,h as clientSupportsTasks,m as readTaskAugmentation,I as resolveTaskGate,g as runLongOp,D as taskPayload,A as toWireTask};
@@ -1,143 +1 @@
1
- /**
2
- * Token budget primitive (spec 014).
3
- *
4
- * Pure functions over typed items. No DB, no I/O. Two responsibilities:
5
- *
6
- * 1. Estimate the token cost of a string. Phase 1 uses a deterministic
7
- * 4-characters-per-token approximation (round up — slightly
8
- * over-counts so we stay under budget). A future spec replaces this
9
- * with `tiktoken` for exact `cl100k_base` counts.
10
- *
11
- * 2. Apply a token budget to a ranked list of items: rank-by-score,
12
- * fill until budget, elide the rest as stubs. Items already shown in
13
- * the session are demoted to a "seen" elision bucket.
14
- *
15
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
16
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
17
- */
18
- export function makeEstimator() {
19
- // Phase 1: 4-chars-per-token approximation. English averages ~4 cpt
20
- // in cl100k_base; code is closer to 3. Wyrm content is mixed; rounding
21
- // up keeps us under budget rather than over.
22
- return {
23
- source: 'approx-4cpt',
24
- count(text) {
25
- if (!text)
26
- return 0;
27
- return Math.ceil(text.length / 4);
28
- },
29
- };
30
- }
31
- /**
32
- * Apply a token budget to a list of candidate items.
33
- *
34
- * Algorithm:
35
- * 1. Sort by score desc (stable on tie).
36
- * 2. For each item:
37
- * - If key is in `alreadySeen`, elide with reason='seen', count stubCost.
38
- * - Else if inlineCost fits in remaining budget, render inline.
39
- * - Else elide with reason='budget', count stubCost.
40
- * 3. Return both lists + token totals.
41
- *
42
- * Stubs are always emitted (operator can deref via wyrm_recall). The
43
- * `reserved` knob lets callers carve out budget for a stable preamble
44
- * block before items start consuming.
45
- */
46
- export function applyBudget(items, opts) {
47
- const seen = opts.alreadySeen ?? new Set();
48
- const reserved = Math.max(0, opts.reserved ?? 0);
49
- const usable = Math.max(0, opts.budget - reserved);
50
- // Stable sort by score desc. JavaScript Array.prototype.sort is stable
51
- // since ES2019 — relied on here for tie-break determinism.
52
- const sorted = [...items].sort((a, b) => b.score - a.score);
53
- const inline = [];
54
- const elided = [];
55
- let tokensInline = 0;
56
- let tokensStubs = 0;
57
- for (const candidate of sorted) {
58
- if (seen.has(candidate.key)) {
59
- elided.push({
60
- item: candidate.item,
61
- key: candidate.key,
62
- reason: 'seen',
63
- stubCost: candidate.stubCost,
64
- });
65
- tokensStubs += candidate.stubCost;
66
- continue;
67
- }
68
- if (tokensInline + candidate.inlineCost <= usable) {
69
- inline.push(candidate.item);
70
- tokensInline += candidate.inlineCost;
71
- }
72
- else {
73
- elided.push({
74
- item: candidate.item,
75
- key: candidate.key,
76
- reason: 'budget',
77
- stubCost: candidate.stubCost,
78
- });
79
- tokensStubs += candidate.stubCost;
80
- }
81
- }
82
- return {
83
- inline,
84
- elided,
85
- tokensInline,
86
- tokensStubs,
87
- budget: opts.budget,
88
- };
89
- }
90
- // -----------------------------------------------------------------------------
91
- // Model-tier defaults (D1: auto-detect from MCP clientInfo.name; spec 014)
92
- // -----------------------------------------------------------------------------
93
- const FALLBACK_BUDGET = 4096;
94
- /**
95
- * Map a known MCP client name to its sensible default context budget.
96
- * Precedence at call site: call-arg > env > auto > fallback.
97
- */
98
- export function budgetForClient(clientName) {
99
- if (!clientName)
100
- return FALLBACK_BUDGET;
101
- const n = clientName.toLowerCase();
102
- // Anthropic surfaces
103
- if (n.includes('mythos'))
104
- return 4096; // future-proof — same as opus until we know
105
- if (n.includes('opus'))
106
- return 4096;
107
- if (n.includes('sonnet'))
108
- return 8192;
109
- if (n.includes('haiku'))
110
- return 12288;
111
- if (n === 'claude-code' || n.startsWith('claude-code'))
112
- return 4096; // assume opus
113
- if (n === 'claude-desktop' || n.startsWith('claude-desktop'))
114
- return 4096;
115
- // Other clients — assume mid-tier
116
- if (n === 'cursor' || n.includes('cursor'))
117
- return 8192;
118
- if (n === 'windsurf' || n.includes('windsurf'))
119
- return 8192;
120
- if (n === 'zed' || n.includes('zed'))
121
- return 8192;
122
- if (n === 'continue' || n.includes('continue'))
123
- return 8192;
124
- if (n === 'copilot' || n.includes('copilot'))
125
- return 8192;
126
- return FALLBACK_BUDGET;
127
- }
128
- /**
129
- * Resolve the effective budget for a context_build call.
130
- *
131
- * call-arg > WYRM_CONTEXT_TOKEN_BUDGET env > clientName-derived > FALLBACK
132
- */
133
- export function resolveBudget(opts) {
134
- if (opts.callArg && opts.callArg > 0)
135
- return opts.callArg;
136
- if (opts.envValue) {
137
- const n = parseInt(opts.envValue, 10);
138
- if (!Number.isNaN(n) && n > 0)
139
- return n;
140
- }
141
- return budgetForClient(opts.clientName);
142
- }
143
- //# sourceMappingURL=token-budget.js.map
1
+ function h(){return{source:"approx-4cpt",count(t){return t?Math.ceil(t.length/4):0}}}function g(t,e){const c=e.alreadySeen??new Set,l=Math.max(0,e.reserved??0),d=Math.max(0,e.budget-l),a=[...t].sort((n,f)=>f.score-n.score),i=[],r=[];let u=0,s=0;for(const n of a){if(c.has(n.key)){r.push({item:n.item,key:n.key,reason:"seen",stubCost:n.stubCost}),s+=n.stubCost;continue}u+n.inlineCost<=d?(i.push(n.item),u+=n.inlineCost):(r.push({item:n.item,key:n.key,reason:"budget",stubCost:n.stubCost}),s+=n.stubCost)}return{inline:i,elided:r,tokensInline:u,tokensStubs:s,budget:e.budget}}const o=4096;function b(t){if(!t)return o;const e=t.toLowerCase();return e.includes("mythos")||e.includes("opus")?4096:e.includes("sonnet")?8192:e.includes("haiku")?12288:e==="claude-code"||e.startsWith("claude-code")||e==="claude-desktop"||e.startsWith("claude-desktop")?4096:e==="cursor"||e.includes("cursor")||e==="windsurf"||e.includes("windsurf")||e==="zed"||e.includes("zed")||e==="continue"||e.includes("continue")||e==="copilot"||e.includes("copilot")?8192:o}function k(t){if(t.callArg&&t.callArg>0)return t.callArg;if(t.envValue){const e=parseInt(t.envValue,10);if(!Number.isNaN(e)&&e>0)return e}return b(t.clientName)}export{g as applyBudget,b as budgetForClient,h as makeEstimator,k as resolveBudget};
@@ -1,84 +1,8 @@
1
- /**
2
- * Tool Call Analytics — Wyrm watches Wyrm.
3
- *
4
- * Every wyrm_* tool call gets logged here: tool name, args summary,
5
- * success/failure, latency, optional project context. From this stream
6
- * we derive:
7
- *
8
- * - usage histogram (which tools are most-called)
9
- * - error rate per tool
10
- * - p50/p95 latency per tool
11
- * - args-pattern summarisation (what args do callers pass)
12
- * - recall→action correlation (was a returned memory followed by an action?)
13
- *
14
- * Operators see "Claude calls wyrm_recall 80% of the time but the result
15
- * is unused 60% of the time → ranking is bad → tune retrieval".
16
- *
17
- * Privacy: args_summary is truncated and stripped of values longer than
18
- * 100 chars per field — we keep keys and short values for pattern
19
- * analysis, never full payloads.
20
- *
21
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
22
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
23
- */
24
- export class ToolAnalytics {
25
- db;
26
- constructor(db) {
27
- this.db = db;
28
- }
29
- /** Truncate args for logging — drop long values, keep keys + short values. */
30
- static summarizeArgs(args) {
31
- if (args == null)
32
- return '';
33
- if (typeof args !== 'object')
34
- return String(args).slice(0, 100);
35
- try {
36
- const o = args;
37
- const out = {};
38
- for (const [k, v] of Object.entries(o)) {
39
- if (v == null) {
40
- out[k] = null;
41
- continue;
42
- }
43
- if (typeof v === 'string') {
44
- out[k] = v.length > 100 ? `<${v.length}ch>` : v;
45
- }
46
- else if (typeof v === 'number' || typeof v === 'boolean') {
47
- out[k] = v;
48
- }
49
- else if (Array.isArray(v)) {
50
- out[k] = `<array:${v.length}>`;
51
- }
52
- else if (typeof v === 'object') {
53
- out[k] = `<object>`;
54
- }
55
- }
56
- return JSON.stringify(out).slice(0, 500);
57
- }
58
- catch {
59
- return '<unserializable>';
60
- }
61
- }
62
- /** Log a single tool call. Designed to be fire-and-forget — swallows errors
63
- * so logging never breaks the tool's own response path. */
64
- log(input) {
65
- try {
66
- this.db.prepare(`
1
+ class _{db;constructor(t){this.db=t}static summarizeArgs(t){if(t==null)return"";if(typeof t!="object")return String(t).slice(0,100);try{const s=t,e={};for(const[a,o]of Object.entries(s)){if(o==null){e[a]=null;continue}typeof o=="string"?e[a]=o.length>100?`<${o.length}ch>`:o:typeof o=="number"||typeof o=="boolean"?e[a]=o:Array.isArray(o)?e[a]=`<array:${o.length}>`:typeof o=="object"&&(e[a]="<object>")}return JSON.stringify(e).slice(0,500)}catch{return"<unserializable>"}}log(t){try{this.db.prepare(`
67
2
  INSERT INTO tool_call_log
68
3
  (tool_name, project_id, args_summary, success, error_message, latency_ms)
69
4
  VALUES (?, ?, ?, ?, ?, ?)
70
- `).run(input.tool_name, input.project_id ?? null, input.args !== undefined ? ToolAnalytics.summarizeArgs(input.args) : null, input.success ? 1 : 0, input.error_message ?? null, Math.max(0, Math.round(input.latency_ms)));
71
- }
72
- catch {
73
- // Logging must never break the tool path.
74
- }
75
- }
76
- /** Per-tool usage statistics over the last `days` days (default 30). */
77
- usageStats(opts) {
78
- const days = opts?.days ?? 30;
79
- const limit = opts?.limit ?? 100;
80
- // Get aggregate stats per tool
81
- const aggregates = this.db.prepare(`
5
+ `).run(t.tool_name,t.project_id??null,t.args!==void 0?_.summarizeArgs(t.args):null,t.success?1:0,t.error_message??null,Math.max(0,Math.round(t.latency_ms)))}catch{}}usageStats(t){const s=t?.days??30,e=t?.limit??100,a=this.db.prepare(`
82
6
  SELECT
83
7
  tool_name,
84
8
  COUNT(*) AS total,
@@ -90,49 +14,18 @@ export class ToolAnalytics {
90
14
  GROUP BY tool_name
91
15
  ORDER BY total DESC
92
16
  LIMIT ?
93
- `).all(days, limit);
94
- // For each tool, compute p50/p95 latency (small enough to do in-process)
95
- const results = [];
96
- for (const a of aggregates) {
97
- const latencies = this.db.prepare(`
17
+ `).all(s,e),o=[];for(const l of a){const c=this.db.prepare(`
98
18
  SELECT latency_ms FROM tool_call_log
99
19
  WHERE tool_name = ?
100
20
  AND called_at >= datetime('now', '-' || ? || ' days')
101
21
  ORDER BY latency_ms
102
- `).all(a.tool_name, days).map(r => r.latency_ms);
103
- const p = (arr, pct) => {
104
- if (arr.length === 0)
105
- return 0;
106
- const idx = Math.min(arr.length - 1, Math.floor((arr.length - 1) * pct));
107
- return arr[idx];
108
- };
109
- results.push({
110
- tool_name: a.tool_name,
111
- total: a.total,
112
- success_count: a.success_count,
113
- error_count: a.error_count,
114
- error_rate: a.total > 0 ? Math.round((a.error_count / a.total) * 1000) / 1000 : 0,
115
- p50_ms: p(latencies, 0.5),
116
- p95_ms: p(latencies, 0.95),
117
- last_called: a.last_called,
118
- });
119
- }
120
- return results;
121
- }
122
- /** Recent error sample for a tool — useful when error_rate > 0. */
123
- recentErrors(toolName, limit = 10) {
124
- return this.db.prepare(`
22
+ `).all(l.tool_name,s).map(r=>r.latency_ms),n=(r,u)=>{if(r.length===0)return 0;const i=Math.min(r.length-1,Math.floor((r.length-1)*u));return r[i]};o.push({tool_name:l.tool_name,total:l.total,success_count:l.success_count,error_count:l.error_count,error_rate:l.total>0?Math.round(l.error_count/l.total*1e3)/1e3:0,p50_ms:n(c,.5),p95_ms:n(c,.95),last_called:l.last_called})}return o}recentErrors(t,s=10){return this.db.prepare(`
125
23
  SELECT called_at, error_message, args_summary
126
24
  FROM tool_call_log
127
25
  WHERE tool_name = ? AND success = 0
128
26
  ORDER BY called_at DESC
129
27
  LIMIT ?
130
- `).all(toolName, limit);
131
- }
132
- /** Total volume + success rate over the whole window. */
133
- overview(opts) {
134
- const days = opts?.days ?? 30;
135
- const row = this.db.prepare(`
28
+ `).all(t,s)}overview(t){const s=t?.days??30,e=this.db.prepare(`
136
29
  SELECT
137
30
  COUNT(*) AS total,
138
31
  SUM(success) AS success_count,
@@ -140,22 +33,7 @@ export class ToolAnalytics {
140
33
  COUNT(DISTINCT tool_name) AS distinct_tools
141
34
  FROM tool_call_log
142
35
  WHERE called_at >= datetime('now', '-' || ? || ' days')
143
- `).get(days);
144
- return {
145
- days,
146
- total_calls: row.total || 0,
147
- success_count: row.success_count || 0,
148
- error_count: row.error_count || 0,
149
- success_rate: row.total > 0 ? Math.round((row.success_count / row.total) * 1000) / 1000 : 0,
150
- distinct_tools: row.distinct_tools || 0,
151
- };
152
- }
153
- /** Prune rows older than N days. Returns count deleted. Run via maintenance. */
154
- prune(olderThanDays = 90) {
155
- return this.db.prepare(`
36
+ `).get(s);return{days:s,total_calls:e.total||0,success_count:e.success_count||0,error_count:e.error_count||0,success_rate:e.total>0?Math.round(e.success_count/e.total*1e3)/1e3:0,distinct_tools:e.distinct_tools||0}}prune(t=90){return this.db.prepare(`
156
37
  DELETE FROM tool_call_log
157
38
  WHERE called_at < datetime('now', '-' || ? || ' days')
158
- `).run(olderThanDays).changes;
159
- }
160
- }
161
- //# sourceMappingURL=tool-analytics.js.map
39
+ `).run(t).changes}}export{_ as ToolAnalytics};