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
@@ -1,2 +1,162 @@
1
- import{renderErrorResponse as c}from"./render.js";const i={wyrm_setup:{cli:"wyrm setup"},wyrm_embed:{cli:"wyrm embed"},wyrm_inject_prompt:{cli:"wyrm prompt inject"},wyrm_migrate_prompt:{cli:"wyrm prompt migrate"},wyrm_check_update:{cli:"wyrm update --check"},wyrm_self_update:{cli:"wyrm update"},wyrm_vector_setup:{cli:"wyrm index setup"},wyrm_reindex:{cli:"wyrm index rebuild"},wyrm_encrypt_setup:{cli:"wyrm setup --encrypt"},wyrm_license:{cli:"wyrm license"},wyrm_activate:{cli:"wyrm activate <key>"},wyrm_hours_report:{cli:"wyrm hours report"},wyrm_invoice_generate:{cli:"wyrm invoice generate"},wyrm_agent_init:{cli:"wyrm agent init"},wyrm_agent_status:{cli:"wyrm agent status"},wyrm_agent_stop:{cli:"wyrm agent stop"},wyrm_agent_restart:{cli:"wyrm agent restart"},wyrm_intro:{cli:"wyrm intro"},wyrm_sync_export:{cli:"wyrm sync export --out <path>",functionalThrough:"7.0.x"},wyrm_sync_import:{cli:"wyrm sync import --from <path>"},wyrm_cloud_backup:{cli:"wyrm cloud backup",functionalThrough:"7.0.x"},wyrm_cloud_sync:{cli:"wyrm cloud sync"}};function o(r){const e=i[r];if(!e)return"";const t=e.functionalThrough?` \u2014 remains functional through ${e.functionalThrough}`:"";return` [Deprecated in v7 \u2192 use the wyrm CLI: ${e.cli}${t}]`}function y(r){return r.map(e=>{const t=o(e.name);return t?{...e,description:`${e.description??""}${t}`}:e})}const n=new Set;function m(){n.clear()}function a(r){try{const e=i[r];if(!e||n.has(r))return;n.add(r);const t=e.functionalThrough?` This name stays functional through ${e.functionalThrough} (published sunset) so scheduled jobs never silently stop.`:"";process.stderr.write(`[wyrm] DEPRECATION: '${r}' moves off MCP to the wyrm CLI in 7.0 \u2014 use: ${e.cli}. Behavior is unchanged in 6.18.${t}
2
- `)}catch{}}const s="WYRM_CLI_EXILE";function p(r){const e=i[r];return e!==void 0&&e.functionalThrough===void 0}function l(r){const e=i[r];if(!e)throw new Error(`cliExileRedirectBody: '${r}' is not a CLI-exile tool`);return{error:{code:s,message:`${r} moved off MCP in 7.0 \u2014 run: ${e.cli}`,retryable:!1},cli:e.cli,tool:r,expected:`This operator/egress tool is CLI-exiled (spec FR-4): its MCP alias never executes in 7.0. Run \`${e.cli}\` from a shell instead \u2014 do not retry this MCP call (the redirect is deterministic). The wyrm CLI ships the full replacement; \`wyrm --help\` lists it.`}}function w(r){return c(l(r))}export{i as CLI_EXILE_TOOLS,s as WYRM_CLI_EXILE_CODE,m as _resetDeprecationWarningsForTests,y as applyDeprecationNotices,l as cliExileRedirectBody,w as cliExileRedirectResponse,o as deprecationSuffix,p as isHardCliExile,a as noteDeprecatedToolInvocation};
1
+ /**
2
+ * v7 deprecation notices Wyrm 6.18 bridge release (BROOD Phase F1, T007).
3
+ *
4
+ * 100% ADDITIVE on the 6.x surface: nothing here changes tool behavior,
5
+ * names, schemas, or profiles. This module is the single registry for the
6
+ * 22 CLI-exile candidates (spec FR-4: 18 operator tools + the device-egress
7
+ * quartet) and emits three kinds of forward-compat warnings:
8
+ *
9
+ * 1. A description SUFFIX merged into tools/list at build time
10
+ * (`applyDeprecationNotices`, wired in the ListToolsRequestSchema
11
+ * handler next to `annotateTools`).
12
+ * 2. A ONE-TIME-per-tool stderr note when a deprecated tool is invoked via
13
+ * MCP (`noteDeprecatedToolInvocation`, wired at the top of the CallTool
14
+ * handler). Failure-isolated: it can never block or break a call.
15
+ * (The 6.18 bridge also carried a ONE-TIME `WYRM_PROFILE=full` notice —
16
+ * removed at T022 when `full` became the permanent synonym of `legacy` it
17
+ * announced; see tool-profiles.ts.)
18
+ *
19
+ * (wyrm-http — the legacy HTTP server the 6.18 bridge warned about — was
20
+ * DELETED at 7.0.0/T033; its T005 access log recorded zero traffic. `wyrm
21
+ * serve` (http-fast) is the maintained HTTP surface.)
22
+ *
23
+ * CLI commands follow the spec FR-4 / T023 disposition: `wyrm
24
+ * setup|embed|sync|cloud` already exist; `wyrm
25
+ * license|activate|index|update|prompt|hours|invoice|agent` ship with T023.
26
+ * In 7.0 the MCP aliases for these names return a structured redirect to the
27
+ * exact command below — EXCEPT `wyrm_cloud_backup` and `wyrm_sync_export`,
28
+ * which stay FUNCTIONAL through 7.0.x with a published sunset so scheduled
29
+ * backups never silently stop.
30
+ *
31
+ * All output goes to STDERR only (stdout is the MCP wire on stdio).
32
+ */
33
+ // T023: the structured redirect rides the single T019 error dual-emit, so
34
+ // text/structured can never drift (render.ts imports nothing from here — no cycle).
35
+ import { renderErrorResponse } from './render.js';
36
+ /** The 22 CLI-exile candidates (spec FR-4): 18 operator + 4 device-egress. */
37
+ export const CLI_EXILE_TOOLS = {
38
+ // ── 18 operator tools ──────────────────────────────────────────────────
39
+ wyrm_setup: { cli: "wyrm setup" },
40
+ wyrm_embed: { cli: "wyrm embed" },
41
+ wyrm_inject_prompt: { cli: "wyrm prompt inject" },
42
+ wyrm_migrate_prompt: { cli: "wyrm prompt migrate" },
43
+ wyrm_check_update: { cli: "wyrm update --check" },
44
+ wyrm_self_update: { cli: "wyrm update" },
45
+ wyrm_vector_setup: { cli: "wyrm index setup" },
46
+ // reindex's long-running form also survives inside wyrm_maintenance (FR-3).
47
+ wyrm_reindex: { cli: "wyrm index rebuild" },
48
+ wyrm_encrypt_setup: { cli: "wyrm setup --encrypt" },
49
+ wyrm_license: { cli: "wyrm license" },
50
+ wyrm_activate: { cli: "wyrm activate <key>" },
51
+ wyrm_hours_report: { cli: "wyrm hours report" },
52
+ wyrm_invoice_generate: { cli: "wyrm invoice generate" },
53
+ wyrm_agent_init: { cli: "wyrm agent init" },
54
+ wyrm_agent_status: { cli: "wyrm agent status" },
55
+ wyrm_agent_stop: { cli: "wyrm agent stop" },
56
+ wyrm_agent_restart: { cli: "wyrm agent restart" },
57
+ wyrm_intro: { cli: "wyrm intro" },
58
+ // ── device-egress quartet ──────────────────────────────────────────────
59
+ wyrm_sync_export: { cli: "wyrm sync export --out <path>", functionalThrough: "7.0.x" },
60
+ wyrm_sync_import: { cli: "wyrm sync import --from <path>" },
61
+ wyrm_cloud_backup: { cli: "wyrm cloud backup", functionalThrough: "7.0.x" },
62
+ wyrm_cloud_sync: { cli: "wyrm cloud sync" },
63
+ };
64
+ /**
65
+ * The description suffix advertised on a deprecated tool, or '' for
66
+ * non-deprecated tools. Exact format pinned by tests.
67
+ */
68
+ export function deprecationSuffix(name) {
69
+ const entry = CLI_EXILE_TOOLS[name];
70
+ if (!entry)
71
+ return "";
72
+ const grace = entry.functionalThrough
73
+ ? ` — remains functional through ${entry.functionalThrough}`
74
+ : "";
75
+ return ` [Deprecated in v7 → use the wyrm CLI: ${entry.cli}${grace}]`;
76
+ }
77
+ /**
78
+ * Append the deprecation suffix to each CLI-exile tool's description.
79
+ * Non-mutating; unknown tools pass through untouched. Never throws.
80
+ */
81
+ export function applyDeprecationNotices(tools) {
82
+ return tools.map((tool) => {
83
+ const suffix = deprecationSuffix(tool.name);
84
+ if (!suffix)
85
+ return tool;
86
+ return { ...tool, description: `${tool.description ?? ""}${suffix}` };
87
+ });
88
+ }
89
+ // One-time-per-process warning state (per tool name).
90
+ const warnedTools = new Set();
91
+ /** Test hook: reset the one-time warning state. */
92
+ export function _resetDeprecationWarningsForTests() {
93
+ warnedTools.clear();
94
+ }
95
+ /**
96
+ * One-time stderr note when a CLI-exile tool is invoked via MCP.
97
+ * Failure-isolated: never throws, never blocks the call.
98
+ */
99
+ export function noteDeprecatedToolInvocation(name) {
100
+ try {
101
+ const entry = CLI_EXILE_TOOLS[name];
102
+ if (!entry || warnedTools.has(name))
103
+ return;
104
+ warnedTools.add(name);
105
+ const grace = entry.functionalThrough
106
+ ? ` This name stays functional through ${entry.functionalThrough} (published sunset) so scheduled jobs never silently stop.`
107
+ : "";
108
+ process.stderr.write(`[wyrm] DEPRECATION: '${name}' moves off MCP to the wyrm CLI in 7.0 — use: ${entry.cli}.` +
109
+ ` Behavior is unchanged in 6.18.${grace}\n`);
110
+ }
111
+ catch {
112
+ /* warnings must never break a tool call */
113
+ }
114
+ }
115
+ // v7 F3 (T022): noteFullProfileDeprecation() (the 6.18 bridge notice) is
116
+ // GONE — WYRM_PROFILE=full is now the permanent synonym of 'legacy' it
117
+ // announced (tool-profiles.ts resolveProfile), and full has always meant
118
+ // "every tool", so there is nothing left to warn about.
119
+ // ── v7 F3 (T023): the structured CLI-exile redirect ─────────────────────────
120
+ //
121
+ // In 7.0 the MCP aliases for the names above stop executing and return this
122
+ // structured redirect instead (spec FR-4 / §6: "~22 operator/egress names
123
+ // return a structured redirect to their exact `wyrm` CLI replacement").
124
+ // EXCEPTION: the functionalThrough pair (cloud_backup / sync_export) keeps
125
+ // EXECUTING through 7.0.x with the published sunset — scheduled backups never
126
+ // silently stop. isHardCliExile() is the dispatcher's gate.
127
+ /** Machine-readable code for a CLI-exile redirect. Deliberately distinct from
128
+ * WYRM_BUSY (retry as-is) and WYRM_VALIDATION (correct an argument): a CLI
129
+ * exile is NEVER satisfiable over MCP — the contract is "run this command". */
130
+ export const WYRM_CLI_EXILE_CODE = 'WYRM_CLI_EXILE';
131
+ /** True iff `name` is CLI-exiled AND past its grace window — i.e. the MCP
132
+ * alias returns the structured redirect instead of executing. The
133
+ * functionalThrough pair (cloud_backup/sync_export) returns false: they stay
134
+ * functional through 7.0.x (published sunset). */
135
+ export function isHardCliExile(name) {
136
+ const entry = CLI_EXILE_TOOLS[name];
137
+ return entry !== undefined && entry.functionalThrough === undefined;
138
+ }
139
+ /** Build the redirect body. Deterministic (Article VIII: same name, same bytes). */
140
+ export function cliExileRedirectBody(name) {
141
+ const entry = CLI_EXILE_TOOLS[name];
142
+ if (!entry)
143
+ throw new Error(`cliExileRedirectBody: '${name}' is not a CLI-exile tool`);
144
+ return {
145
+ error: {
146
+ code: WYRM_CLI_EXILE_CODE,
147
+ message: `${name} moved off MCP in 7.0 — run: ${entry.cli}`,
148
+ retryable: false,
149
+ },
150
+ cli: entry.cli,
151
+ tool: name,
152
+ expected: `This operator/egress tool is CLI-exiled (spec FR-4): its MCP alias never executes in 7.0. ` +
153
+ `Run \`${entry.cli}\` from a shell instead — do not retry this MCP call (the redirect is ` +
154
+ `deterministic). The wyrm CLI ships the full replacement; \`wyrm --help\` lists it.`,
155
+ };
156
+ }
157
+ /** The full dual-emit MCP response for a CLI-exile redirect — `isError:true`,
158
+ * text = JSON of the SAME body riding structuredContent (the T019 renderer). */
159
+ export function cliExileRedirectResponse(name) {
160
+ return renderErrorResponse(cliExileRedirectBody(name));
161
+ }
162
+ //# sourceMappingURL=deprecations.js.map
package/dist/design.js CHANGED
@@ -1,4 +1,38 @@
1
- const a=new Set(["color","type","spacing","motion","shadow","radius","breakpoint","custom"]),d=new Set(["url","image","palette","snippet"]);class E{db;constructor(e){this.db=e}setToken(e){if(!a.has(e.category))throw new Error(`invalid category: ${e.category} (must be one of ${[...a].join(", ")})`);if(!e.key||!e.value)throw new Error("key and value are required");return this.db.prepare(`
1
+ /**
2
+ * Design tokens + references (5.9.0 — creative workflow).
3
+ *
4
+ * Design tokens store a project's design system primitives (colour /
5
+ * type / spacing / motion / etc.) as first-class data so AI helpers
6
+ * working on creative tasks don't have to re-derive the system from
7
+ * scattered CSS or memory artifacts on every call.
8
+ *
9
+ * Design references store inspiration / mood-board URLs and image paths
10
+ * with tags, so a designer accreting visual references over time has a
11
+ * searchable library.
12
+ *
13
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
14
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
15
+ */
16
+ const VALID_CATEGORIES = new Set([
17
+ 'color', 'type', 'spacing', 'motion', 'shadow', 'radius', 'breakpoint', 'custom',
18
+ ]);
19
+ const VALID_REF_KINDS = new Set([
20
+ 'url', 'image', 'palette', 'snippet',
21
+ ]);
22
+ export class DesignSystem {
23
+ db;
24
+ constructor(db) {
25
+ this.db = db;
26
+ }
27
+ // ---------- Tokens ----------
28
+ setToken(input) {
29
+ if (!VALID_CATEGORIES.has(input.category)) {
30
+ throw new Error(`invalid category: ${input.category} (must be one of ${[...VALID_CATEGORIES].join(', ')})`);
31
+ }
32
+ if (!input.key || !input.value) {
33
+ throw new Error('key and value are required');
34
+ }
35
+ this.db.prepare(`
2
36
  INSERT INTO design_tokens (project_id, category, key, value, notes, source)
3
37
  VALUES (?, ?, ?, ?, ?, ?)
4
38
  ON CONFLICT(project_id, category, key) DO UPDATE SET
@@ -6,41 +40,135 @@ const a=new Set(["color","type","spacing","motion","shadow","radius","breakpoint
6
40
  notes = excluded.notes,
7
41
  source = excluded.source,
8
42
  updated_at = datetime('now')
9
- `).run(e.projectId,e.category,e.key,e.value,e.notes??null,e.source??"user"),this.getToken(e.projectId,e.category,e.key)}getToken(e,r,t){return this.db.prepare(`
43
+ `).run(input.projectId, input.category, input.key, input.value, input.notes ?? null, input.source ?? 'user');
44
+ return this.getToken(input.projectId, input.category, input.key);
45
+ }
46
+ getToken(projectId, category, key) {
47
+ const row = this.db.prepare(`
10
48
  SELECT * FROM design_tokens
11
49
  WHERE project_id = ? AND category = ? AND key = ?
12
- `).get(e,r,t)??null}listTokens(e,r){return r?this.db.prepare(`
50
+ `).get(projectId, category, key);
51
+ return row ?? null;
52
+ }
53
+ listTokens(projectId, category) {
54
+ if (category) {
55
+ return this.db.prepare(`
13
56
  SELECT * FROM design_tokens
14
57
  WHERE project_id = ? AND category = ?
15
58
  ORDER BY key
16
- `).all(e,r):this.db.prepare(`
59
+ `).all(projectId, category);
60
+ }
61
+ return this.db.prepare(`
17
62
  SELECT * FROM design_tokens
18
63
  WHERE project_id = ?
19
64
  ORDER BY category, key
20
- `).all(e)}deleteToken(e,r,t){return(this.db.prepare(`
65
+ `).all(projectId);
66
+ }
67
+ deleteToken(projectId, category, key) {
68
+ const result = this.db.prepare(`
21
69
  DELETE FROM design_tokens
22
70
  WHERE project_id = ? AND category = ? AND key = ?
23
- `).run(e,r,t).changes??0)>0}formatForContext(e){const r=this.listTokens(e);if(r.length===0)return"";const t=new Map;for(const s of r){const o=t.get(s.category)??[];o.push(s),t.set(s.category,o)}const n=[];n.push("### \u{1F3A8} Design tokens");for(const[s,o]of t){n.push(""),n.push(`**${s}:**`);for(const c of o)n.push(`- \`${c.key}\` = \`${c.value}\`${c.notes?` \u2014 ${c.notes}`:""}`)}return n.join(`
24
- `)}addReference(e){if(!d.has(e.kind))throw new Error(`invalid kind: ${e.kind} (must be one of ${[...d].join(", ")})`);if(!e.location)throw new Error("location is required");const r=this.db.prepare(`
71
+ `).run(projectId, category, key);
72
+ return (result.changes ?? 0) > 0;
73
+ }
74
+ /**
75
+ * Render the token set as a markdown table grouped by category — handy
76
+ * for folding into context briefs.
77
+ */
78
+ formatForContext(projectId) {
79
+ const tokens = this.listTokens(projectId);
80
+ if (tokens.length === 0)
81
+ return '';
82
+ const byCategory = new Map();
83
+ for (const t of tokens) {
84
+ const arr = byCategory.get(t.category) ?? [];
85
+ arr.push(t);
86
+ byCategory.set(t.category, arr);
87
+ }
88
+ const lines = [];
89
+ lines.push('### 🎨 Design tokens');
90
+ for (const [cat, items] of byCategory) {
91
+ lines.push('');
92
+ lines.push(`**${cat}:**`);
93
+ for (const t of items) {
94
+ lines.push(`- \`${t.key}\` = \`${t.value}\`${t.notes ? ` — ${t.notes}` : ''}`);
95
+ }
96
+ }
97
+ return lines.join('\n');
98
+ }
99
+ // ---------- References ----------
100
+ addReference(input) {
101
+ if (!VALID_REF_KINDS.has(input.kind)) {
102
+ throw new Error(`invalid kind: ${input.kind} (must be one of ${[...VALID_REF_KINDS].join(', ')})`);
103
+ }
104
+ if (!input.location) {
105
+ throw new Error('location is required');
106
+ }
107
+ const result = this.db.prepare(`
25
108
  INSERT INTO design_references (project_id, kind, location, title, notes, tags)
26
109
  VALUES (?, ?, ?, ?, ?, ?)
27
- `).run(e.projectId??null,e.kind,e.location,e.title??null,e.notes??null,e.tags??null);return this.getReference(Number(r.lastInsertRowid))}getReference(e){return this.db.prepare("SELECT * FROM design_references WHERE id = ?").get(e)??null}listReferences(e={}){const r=Math.min(e.limit??50,200);if(e.tag){const t=`%${e.tag}%`;return e.projectId!=null?this.db.prepare(`
110
+ `).run(input.projectId ?? null, input.kind, input.location, input.title ?? null, input.notes ?? null, input.tags ?? null);
111
+ return this.getReference(Number(result.lastInsertRowid));
112
+ }
113
+ getReference(id) {
114
+ const row = this.db.prepare(`SELECT * FROM design_references WHERE id = ?`).get(id);
115
+ return row ?? null;
116
+ }
117
+ listReferences(opts = {}) {
118
+ const limit = Math.min(opts.limit ?? 50, 200);
119
+ if (opts.tag) {
120
+ const tagPattern = `%${opts.tag}%`;
121
+ if (opts.projectId != null) {
122
+ return this.db.prepare(`
28
123
  SELECT * FROM design_references
29
124
  WHERE project_id = ? AND tags LIKE ?
30
125
  ORDER BY captured_at DESC LIMIT ?
31
- `).all(e.projectId,t,r):this.db.prepare(`
126
+ `).all(opts.projectId, tagPattern, limit);
127
+ }
128
+ return this.db.prepare(`
32
129
  SELECT * FROM design_references
33
130
  WHERE tags LIKE ?
34
131
  ORDER BY captured_at DESC LIMIT ?
35
- `).all(t,r)}return e.projectId!=null?this.db.prepare(`
132
+ `).all(tagPattern, limit);
133
+ }
134
+ if (opts.projectId != null) {
135
+ return this.db.prepare(`
36
136
  SELECT * FROM design_references
37
137
  WHERE project_id = ?
38
138
  ORDER BY captured_at DESC LIMIT ?
39
- `).all(e.projectId,r):this.db.prepare(`
139
+ `).all(opts.projectId, limit);
140
+ }
141
+ return this.db.prepare(`
40
142
  SELECT * FROM design_references
41
143
  ORDER BY captured_at DESC LIMIT ?
42
- `).all(r)}searchReferences(e,r={}){const t=Math.min(r.limit??20,100),n=`
144
+ `).all(limit);
145
+ }
146
+ searchReferences(query, opts = {}) {
147
+ const limit = Math.min(opts.limit ?? 20, 100);
148
+ // FTS5 lookup, joined back to design_references for the full rows.
149
+ const baseQuery = `
43
150
  SELECT dr.* FROM design_references_fts ft
44
151
  JOIN design_references dr ON dr.id = ft.rowid
45
152
  WHERE design_references_fts MATCH ?
46
- `;return r.projectId!=null?this.db.prepare(`${n} AND dr.project_id = ? ORDER BY dr.captured_at DESC LIMIT ?`).all(e,r.projectId,t):this.db.prepare(`${n} ORDER BY dr.captured_at DESC LIMIT ?`).all(e,t)}deleteReference(e){return(this.db.prepare("DELETE FROM design_references WHERE id = ?").run(e).changes??0)>0}countReferences(e){return e!=null?this.db.prepare("SELECT COUNT(*) AS n FROM design_references WHERE project_id = ?").get(e).n:this.db.prepare("SELECT COUNT(*) AS n FROM design_references").get().n}}export{E as DesignSystem};
153
+ `;
154
+ if (opts.projectId != null) {
155
+ return this.db.prepare(`${baseQuery} AND dr.project_id = ? ORDER BY dr.captured_at DESC LIMIT ?`)
156
+ .all(query, opts.projectId, limit);
157
+ }
158
+ return this.db.prepare(`${baseQuery} ORDER BY dr.captured_at DESC LIMIT ?`)
159
+ .all(query, limit);
160
+ }
161
+ deleteReference(id) {
162
+ const result = this.db.prepare(`DELETE FROM design_references WHERE id = ?`).run(id);
163
+ return (result.changes ?? 0) > 0;
164
+ }
165
+ countReferences(projectId) {
166
+ if (projectId != null) {
167
+ const row = this.db.prepare(`SELECT COUNT(*) AS n FROM design_references WHERE project_id = ?`).get(projectId);
168
+ return row.n;
169
+ }
170
+ const row = this.db.prepare(`SELECT COUNT(*) AS n FROM design_references`).get();
171
+ return row.n;
172
+ }
173
+ }
174
+ //# sourceMappingURL=design.js.map
@@ -1 +1,112 @@
1
- import{logger as f}from"./logger.js";import{guardedFetchJson as m,checkPeerUrl as P}from"./repl-guard.js";const u=500,g=e=>`repl:push:${e.baseUrl}:${e.localProjectId}`,y=e=>`repl:pull:${e.baseUrl}:${e.localProjectId}`,_=e=>Number.parseInt(e||"0",10)||0;function v(e){const r={"Content-Type":"application/json"};return e&&(r.Authorization=`Bearer ${e}`),r}function j(e){return{origin_device:e.origin_device,origin_seq:e.origin_seq,cloud_cursor:e.cursor,kind:e.kind,ref_table:e.ref_table,ref_id:e.ref_id,actor:e.actor,is_shared:e.is_shared,payload_json:e.payload_json,created_at:e.created_at}}async function $(e,r,a){const o=P(r.baseUrl);if(!o.ok)return{pushed:0,error:`push: ${o.reason}`};const s=_(e.getMeta(g(r))),t=e.eventsForPush(r.localProjectId,s,u);if(t.length===0)return{pushed:0};try{return await m(a,r.baseUrl,"/events",{method:"POST",headers:v(r.token),body:JSON.stringify({project:r.remoteProject,events:t.map(j)})}),e.setMeta(g(r),String(t[t.length-1].cursor)),{pushed:t.length}}catch(c){const i=c instanceof Error?c.message:String(c);return f.warn("live-memory: push failed",{peer:r.baseUrl,err:i}),{pushed:0,error:`push: ${i}`}}}async function U(e,r,a){const o=_(e.getMeta(y(r)));try{const s=`/events?project=${encodeURIComponent(r.remoteProject)}&since=${o}&limit=${u}&shared=1`,t=await m(a,r.baseUrl,s,{method:"GET",headers:v(r.token)}),c=(Array.isArray(t.events)?t.events:[]).filter(n=>!!n&&typeof n=="object"&&Number.isFinite(n.cursor)).slice(0,u);let i=0,d=0,h=0,l=o;for(const n of c){const p=e.ingestRemoteEvent(r.localProjectId,{...j(n),cloud_cursor:n.cursor});p==="inserted"?i++:p==="echo"?h++:d++,n.cursor>l&&(l=n.cursor)}return e.setMeta(y(r),String(l)),{pulled:i,duplicates:d,echoes:h}}catch(s){const t=s instanceof Error?s.message:String(s);return f.warn("live-memory: pull failed",{peer:r.baseUrl,err:t}),{pulled:0,duplicates:0,echoes:0,error:`pull: ${t}`}}}async function S(e,r,a){const o=await U(e,r,a),s=await $(e,r,a),t=[o.error,s.error].filter(c=>!!c);return{pulled:o.pulled,pushed:s.pushed,duplicates:o.duplicates,echoes:o.echoes,errors:t}}export{U as pullPeerEvents,$ as pushSharedEvents,S as replicateOnce};
1
+ /**
2
+ * Live Memory v6.4 (Phase 3) — cross-device event replication.
3
+ *
4
+ * A Wyrm node syncs with a PEER node over the Phase-2 HTTP surface — no separate
5
+ * cloud relay required, the peer is just another Wyrm HTTP server (`wyrm serve`):
6
+ * - PULL: GET peer `/events?project=…&since=<pull-watermark>` → ingest each.
7
+ * - PUSH: POST peer `/events` with our shared events newer than <push-watermark>.
8
+ *
9
+ * Privacy: only `is_shared = 1` events are ever collected for push, and
10
+ * reference-only rows keep `payload_json = NULL` — protected data never leaves
11
+ * the box. Dedup + echo-suppression happen on the receiving side
12
+ * (`ingestRemoteEvent`), so re-delivery and round-trips are no-ops. Events are
13
+ * append-only, so there are no merge conflicts.
14
+ *
15
+ * The DB dependency is a structural interface (`ReplDb`) and `fetch` is injected,
16
+ * so this whole module is unit-testable with a fake peer and no real sockets.
17
+ *
18
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
19
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
20
+ */
21
+ import { logger } from './logger.js';
22
+ import { guardedFetchJson, checkPeerUrl } from './repl-guard.js';
23
+ const PUSH_LIMIT = 500;
24
+ const pushKey = (p) => `repl:push:${p.baseUrl}:${p.localProjectId}`;
25
+ const pullKey = (p) => `repl:pull:${p.baseUrl}:${p.localProjectId}`;
26
+ const toInt = (v) => Number.parseInt(v || '0', 10) || 0;
27
+ function authHeaders(token) {
28
+ const h = { 'Content-Type': 'application/json' };
29
+ if (token)
30
+ h['Authorization'] = `Bearer ${token}`;
31
+ return h;
32
+ }
33
+ /** Map a stored event to the wire shape; the peer records OUR cursor as its `cloud_cursor`. */
34
+ function toRemote(e) {
35
+ return {
36
+ origin_device: e.origin_device, origin_seq: e.origin_seq, cloud_cursor: e.cursor,
37
+ kind: e.kind, ref_table: e.ref_table, ref_id: e.ref_id, actor: e.actor,
38
+ is_shared: e.is_shared, payload_json: e.payload_json, created_at: e.created_at,
39
+ };
40
+ }
41
+ /** PUSH local shared events newer than our push-watermark to the peer. */
42
+ export async function pushSharedEvents(db, peer, fetchImpl) {
43
+ const pre = checkPeerUrl(peer.baseUrl);
44
+ if (!pre.ok)
45
+ return { pushed: 0, error: `push: ${pre.reason}` };
46
+ const since = toInt(db.getMeta(pushKey(peer)));
47
+ const batch = db.eventsForPush(peer.localProjectId, since, PUSH_LIMIT);
48
+ if (batch.length === 0)
49
+ return { pushed: 0 };
50
+ try {
51
+ // guardedFetchJson enforces scheme/host allowlist, no-redirect, timeout, bounded body.
52
+ await guardedFetchJson(fetchImpl, peer.baseUrl, '/events', {
53
+ method: 'POST', headers: authHeaders(peer.token),
54
+ body: JSON.stringify({ project: peer.remoteProject, events: batch.map(toRemote) }),
55
+ });
56
+ // Advance the watermark only on a confirmed 2xx (ingest is idempotent, so a
57
+ // retried push that double-sends is harmless on the peer).
58
+ db.setMeta(pushKey(peer), String(batch[batch.length - 1].cursor));
59
+ return { pushed: batch.length };
60
+ }
61
+ catch (err) {
62
+ const msg = err instanceof Error ? err.message : String(err);
63
+ logger.warn('live-memory: push failed', { peer: peer.baseUrl, err: msg });
64
+ return { pushed: 0, error: `push: ${msg}` };
65
+ }
66
+ }
67
+ /** PULL peer events newer than our pull-watermark and ingest them. */
68
+ export async function pullPeerEvents(db, peer, fetchImpl) {
69
+ const since = toInt(db.getMeta(pullKey(peer)));
70
+ try {
71
+ // `shared=1`: only the peer's shareable events — its private events stay on its box.
72
+ const path = `/events?project=${encodeURIComponent(peer.remoteProject)}&since=${since}&limit=${PUSH_LIMIT}&shared=1`;
73
+ const data = (await guardedFetchJson(fetchImpl, peer.baseUrl, path, {
74
+ method: 'GET', headers: authHeaders(peer.token),
75
+ }));
76
+ // Defend against a hostile/buggy peer: keep only well-shaped events with a
77
+ // finite numeric cursor, and never ingest more than we asked for.
78
+ const events = (Array.isArray(data.events) ? data.events : [])
79
+ .filter((e) => !!e && typeof e === 'object' && Number.isFinite(e.cursor))
80
+ .slice(0, PUSH_LIMIT);
81
+ let pulled = 0, duplicates = 0, echoes = 0;
82
+ let maxCursor = since;
83
+ for (const e of events) {
84
+ const r = db.ingestRemoteEvent(peer.localProjectId, { ...toRemote(e), cloud_cursor: e.cursor });
85
+ if (r === 'inserted')
86
+ pulled++;
87
+ else if (r === 'echo')
88
+ echoes++;
89
+ else
90
+ duplicates++;
91
+ if (e.cursor > maxCursor)
92
+ maxCursor = e.cursor;
93
+ }
94
+ // Advance the watermark ONLY from cursors we actually received — NEVER from a
95
+ // peer-claimed `cursor` scalar. An empty or forged response can't skip us forward.
96
+ db.setMeta(pullKey(peer), String(maxCursor));
97
+ return { pulled, duplicates, echoes };
98
+ }
99
+ catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ logger.warn('live-memory: pull failed', { peer: peer.baseUrl, err: msg });
102
+ return { pulled: 0, duplicates: 0, echoes: 0, error: `pull: ${msg}` };
103
+ }
104
+ }
105
+ /** One-shot replication with a peer: pull, then push. Never throws. */
106
+ export async function replicateOnce(db, peer, fetchImpl) {
107
+ const pull = await pullPeerEvents(db, peer, fetchImpl);
108
+ const push = await pushSharedEvents(db, peer, fetchImpl);
109
+ const errors = [pull.error, push.error].filter((e) => Boolean(e));
110
+ return { pulled: pull.pulled, pushed: push.pushed, duplicates: pull.duplicates, echoes: pull.echoes, errors };
111
+ }
112
+ //# sourceMappingURL=event-replication.js.map
@@ -1,7 +1,43 @@
1
- function o(r){const e=String(r.kind).replace(/[\r\n]/g," ");return`id: ${r.cursor}
2
- event: ${e}
3
- data: ${JSON.stringify(r)}
4
-
5
- `}const s=`: ka
6
-
7
- `;function u(r,e){const i=Array.isArray(r)?r[0]:r;for(const n of[i,e]){if(n==null||n==="")continue;const t=Number.parseInt(String(n),10);if(Number.isFinite(t)&&t>=0)return t}return 0}export{s as SSE_KEEPALIVE,u as resolveStartCursor,o as sseFrame};
1
+ /**
2
+ * Live Memory v6.4 (Phase 2) — SSE framing + cursor-resume helpers.
3
+ *
4
+ * Pure functions, no DB/HTTP imports. Kept in their own module so they can be
5
+ * unit-tested in isolation. (Historical: importing `http-fast.ts` used to run
6
+ * a top-level `new WyrmDB()` and open the real ~/.wyrm/wyrm.db at module
7
+ * load; v7 F2 T012 made that open lazy, but small pure modules remain the
8
+ * right unit-test surface.)
9
+ *
10
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
11
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
12
+ */
13
+ /**
14
+ * Serialize one event as an SSE frame. The `id:` line is the event cursor, so a
15
+ * dropped client reconnects with `Last-Event-ID: <cursor>` and resumes exactly
16
+ * where it left off. `kind` becomes the `event:` type; newlines are stripped so
17
+ * a malformed kind can't inject extra SSE fields.
18
+ */
19
+ export function sseFrame(ev) {
20
+ const kind = String(ev.kind).replace(/[\r\n]/g, ' ');
21
+ return `id: ${ev.cursor}\nevent: ${kind}\ndata: ${JSON.stringify(ev)}\n\n`;
22
+ }
23
+ /** An SSE keep-alive comment line (ignored by clients, keeps the socket warm). */
24
+ export const SSE_KEEPALIVE = ': ka\n\n';
25
+ /**
26
+ * Resolve the cursor an SSE / pull client should start from.
27
+ * Precedence: SSE reconnect `Last-Event-ID` header → `?since=` query param → 0.
28
+ * Always returns a finite integer ≥ 0 (defends against negative, NaN, and the
29
+ * `string[]` shape Node uses for repeated headers). A present-but-garbage
30
+ * Last-Event-ID falls through to `since` rather than silently resetting to 0.
31
+ */
32
+ export function resolveStartCursor(lastEventId, sinceParam) {
33
+ const fromHeader = Array.isArray(lastEventId) ? lastEventId[0] : lastEventId;
34
+ for (const raw of [fromHeader, sinceParam]) {
35
+ if (raw === undefined || raw === null || raw === '')
36
+ continue;
37
+ const n = Number.parseInt(String(raw), 10);
38
+ if (Number.isFinite(n) && n >= 0)
39
+ return n;
40
+ }
41
+ return 0;
42
+ }
43
+ //# sourceMappingURL=events-sse.js.map