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.
- package/README.md +8 -14
- package/dist/activation.js +59 -1
- package/dist/agent-daemon.js +281 -4
- package/dist/agent-loop.js +332 -7
- package/dist/analytics.js +236 -13
- package/dist/attribution.js +49 -1
- package/dist/audit.js +457 -2
- package/dist/auto-capture.js +138 -3
- package/dist/auto-orchestrator.js +325 -1
- package/dist/autoconfig.d.ts +50 -0
- package/dist/autoconfig.d.ts.map +1 -1
- package/dist/autoconfig.js +1115 -39
- package/dist/autoconfig.js.map +1 -1
- package/dist/buddy-runner.js +109 -1
- package/dist/buddy.js +564 -14
- package/dist/build-flags.js +15 -1
- package/dist/capabilities.js +183 -3
- package/dist/capture.js +56 -1
- package/dist/causality.js +148 -8
- package/dist/cli.js +281 -20
- package/dist/cloud/cli.js +541 -5
- package/dist/cloud/client.js +221 -1
- package/dist/cloud/crypto.js +85 -1
- package/dist/cloud/machine-id.js +113 -2
- package/dist/cloud/recovery.js +60 -1
- package/dist/cloud/sync-engine.js +543 -7
- package/dist/cloud-backup.js +579 -5
- package/dist/cloud-profile.js +138 -1
- package/dist/cloud-sync-entrypoint.js +47 -1
- package/dist/cloud-sync.js +309 -2
- package/dist/connectors/bridge-source.d.ts +46 -0
- package/dist/connectors/bridge-source.d.ts.map +1 -0
- package/dist/connectors/bridge-source.js +77 -0
- package/dist/connectors/bridge-source.js.map +1 -0
- package/dist/connectors/index.d.ts +24 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +69 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/ingest.d.ts +16 -0
- package/dist/connectors/ingest.d.ts.map +1 -0
- package/dist/connectors/ingest.js +116 -0
- package/dist/connectors/ingest.js.map +1 -0
- package/dist/connectors/types.d.ts +99 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +17 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/constellation.js +168 -12
- package/dist/content-signature.js +45 -1
- package/dist/context-build-budgeted.js +144 -4
- package/dist/context-ranking.js +69 -1
- package/dist/crypto.js +179 -1
- package/dist/daemon-write-endpoint.js +290 -1
- package/dist/daemon-writer.js +406 -2
- package/dist/database.js +1278 -53
- package/dist/deprecations.js +162 -2
- package/dist/design.js +141 -13
- package/dist/event-replication.js +112 -1
- package/dist/events-sse.js +43 -7
- package/dist/events.js +238 -6
- package/dist/failure-patterns.d.ts +107 -0
- package/dist/failure-patterns.d.ts.map +1 -1
- package/dist/failure-patterns.js +924 -43
- package/dist/failure-patterns.js.map +1 -1
- package/dist/federation.js +236 -12
- package/dist/goals.js +101 -13
- package/dist/golden.js +355 -3
- package/dist/handlers/agent.js +165 -4
- package/dist/handlers/alias-adapters.js +129 -1
- package/dist/handlers/aliases.js +171 -1
- package/dist/handlers/audit.js +87 -1
- package/dist/handlers/boundary.js +221 -1
- package/dist/handlers/capture.js +1114 -73
- package/dist/handlers/causality.js +119 -9
- package/dist/handlers/cloud.js +382 -85
- package/dist/handlers/companion.js +459 -28
- package/dist/handlers/datalake.js +187 -7
- package/dist/handlers/dispatch-context.js +22 -0
- package/dist/handlers/entity.js +256 -25
- package/dist/handlers/events.js +335 -16
- package/dist/handlers/failure.d.ts.map +1 -1
- package/dist/handlers/failure.js +408 -13
- package/dist/handlers/failure.js.map +1 -1
- package/dist/handlers/goals.js +296 -4
- package/dist/handlers/intelligence.js +681 -126
- package/dist/handlers/invoicing.js +70 -1
- package/dist/handlers/mcpclient.js +137 -6
- package/dist/handlers/orchestration.js +125 -40
- package/dist/handlers/output-schemas.js +24 -1
- package/dist/handlers/presence.js +99 -3
- package/dist/handlers/project.js +182 -28
- package/dist/handlers/prompts.js +157 -6
- package/dist/handlers/quest.js +224 -4
- package/dist/handlers/recall.js +237 -13
- package/dist/handlers/registry.js +167 -1
- package/dist/handlers/resources.js +288 -1
- package/dist/handlers/review.js +74 -11
- package/dist/handlers/run.js +498 -16
- package/dist/handlers/search.js +338 -15
- package/dist/handlers/session.js +643 -31
- package/dist/handlers/share.js +184 -8
- package/dist/handlers/shims.js +464 -1
- package/dist/handlers/skill.js +449 -67
- package/dist/handlers/survivors.js +120 -1
- package/dist/handlers/symbols.js +109 -8
- package/dist/handlers/syncops.js +302 -4
- package/dist/handlers/types.js +27 -1
- package/dist/harvest.js +191 -5
- package/dist/hours.js +156 -7
- package/dist/http-auth.js +321 -3
- package/dist/http-fast.js +1302 -22
- package/dist/icons.js +47 -1
- package/dist/importers.js +268 -1
- package/dist/index.js +840 -2
- package/dist/indexer.js +145 -4
- package/dist/intelligence.js +261 -31
- package/dist/internal-dispatch.js +212 -3
- package/dist/keyset.js +110 -1
- package/dist/knowledge-graph.js +176 -12
- package/dist/license.js +441 -2
- package/dist/logger.js +199 -2
- package/dist/maintenance.js +148 -2
- package/dist/mcp-client.js +262 -6
- package/dist/memory-artifacts.js +596 -32
- package/dist/migrate-prompt.js +124 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +799 -42
- package/dist/migrations.js.map +1 -1
- package/dist/performance.js +228 -1
- package/dist/presence.js +140 -11
- package/dist/priority-embed.js +164 -5
- package/dist/providers/embedding-provider.js +196 -1
- package/dist/readonly-gate.js +29 -1
- package/dist/receipt.js +43 -1
- package/dist/rehydration.js +157 -9
- package/dist/reindex.js +88 -1
- package/dist/render-target.js +544 -21
- package/dist/render.js +280 -4
- package/dist/repl-guard.js +173 -1
- package/dist/replication-daemon-entrypoint.js +31 -1
- package/dist/replication-daemon.js +262 -2
- package/dist/rerank.js +142 -1
- package/dist/resilience.js +591 -1
- package/dist/reverse-bridge.js +360 -5
- package/dist/security.js +244 -1
- package/dist/session-seen.js +51 -3
- package/dist/setup.js +260 -1
- package/dist/skill-author.js +168 -5
- package/dist/spec-kit.js +191 -1
- package/dist/sqlite-busy.js +154 -1
- package/dist/statusline.js +315 -11
- package/dist/sub-agent.js +262 -13
- package/dist/summarizer.js +139 -13
- package/dist/symbols.js +283 -7
- package/dist/sync.js +359 -5
- package/dist/tasks-dispatch.js +84 -1
- package/dist/tasks.js +282 -1
- package/dist/token-budget.js +143 -1
- package/dist/tool-analytics.js +129 -7
- package/dist/tool-annotations.js +365 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +75 -1
- package/dist/trace-harvest.js +244 -6
- package/dist/types.js +30 -1
- package/dist/ui-dashboard.js +50 -41
- package/dist/ulid.js +81 -1
- package/dist/usage-tracker.js +66 -1
- package/dist/validate.js +129 -1
- package/dist/vault.js +534 -1
- package/dist/vector-init.js +67 -1
- package/dist/vectors.js +184 -3
- package/dist/version-check.js +136 -4
- package/dist/visibility.js +155 -19
- package/dist/wyrm-cli.js +2845 -101
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.d.ts.map +1 -1
- package/dist/wyrm-guard.js +475 -14
- package/dist/wyrm-guard.js.map +1 -1
- package/dist/wyrm-loop.js +150 -3
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +11 -1
- package/dist/wyrm-statusline.js +56 -4
- package/dist/wyrm-ui.js +77 -9
- package/package.json +1 -1
package/dist/deprecations.js
CHANGED
|
@@ -1,2 +1,162 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
24
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
`;
|
|
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
|
-
|
|
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
|
package/dist/events-sse.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
|
|
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
|