wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
package/dist/tasks.js CHANGED
@@ -1 +1,282 @@
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
+ /**
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 +1,143 @@
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
+ /**
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 Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. 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,8 +1,84 @@
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(`
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 Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. 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(`
2
67
  INSERT INTO tool_call_log
3
68
  (tool_name, project_id, args_summary, success, error_message, latency_ms)
4
69
  VALUES (?, ?, ?, ?, ?, ?)
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(`
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(`
6
82
  SELECT
7
83
  tool_name,
8
84
  COUNT(*) AS total,
@@ -14,18 +90,49 @@ class _{db;constructor(t){this.db=t}static summarizeArgs(t){if(t==null)return"";
14
90
  GROUP BY tool_name
15
91
  ORDER BY total DESC
16
92
  LIMIT ?
17
- `).all(s,e),o=[];for(const l of a){const c=this.db.prepare(`
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(`
18
98
  SELECT latency_ms FROM tool_call_log
19
99
  WHERE tool_name = ?
20
100
  AND called_at >= datetime('now', '-' || ? || ' days')
21
101
  ORDER BY latency_ms
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(`
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(`
23
125
  SELECT called_at, error_message, args_summary
24
126
  FROM tool_call_log
25
127
  WHERE tool_name = ? AND success = 0
26
128
  ORDER BY called_at DESC
27
129
  LIMIT ?
28
- `).all(t,s)}overview(t){const s=t?.days??30,e=this.db.prepare(`
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(`
29
136
  SELECT
30
137
  COUNT(*) AS total,
31
138
  SUM(success) AS success_count,
@@ -33,7 +140,22 @@ class _{db;constructor(t){this.db=t}static summarizeArgs(t){if(t==null)return"";
33
140
  COUNT(DISTINCT tool_name) AS distinct_tools
34
141
  FROM tool_call_log
35
142
  WHERE called_at >= datetime('now', '-' || ? || ' days')
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(`
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(`
37
156
  DELETE FROM tool_call_log
38
157
  WHERE called_at < datetime('now', '-' || ? || ' days')
39
- `).run(t).changes}}export{_ as ToolAnalytics};
158
+ `).run(olderThanDays).changes;
159
+ }
160
+ }
161
+ //# sourceMappingURL=tool-analytics.js.map