@possumtech/rummy 0.2.1

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 (120) hide show
  1. package/.env.example +55 -0
  2. package/LICENSE +21 -0
  3. package/PLUGINS.md +302 -0
  4. package/README.md +41 -0
  5. package/SPEC.md +524 -0
  6. package/lang/en.json +34 -0
  7. package/migrations/001_initial_schema.sql +226 -0
  8. package/package.json +54 -0
  9. package/service.js +143 -0
  10. package/src/agent/AgentLoop.js +553 -0
  11. package/src/agent/ContextAssembler.js +29 -0
  12. package/src/agent/KnownStore.js +254 -0
  13. package/src/agent/ProjectAgent.js +101 -0
  14. package/src/agent/ResponseHealer.js +134 -0
  15. package/src/agent/TurnExecutor.js +457 -0
  16. package/src/agent/XmlParser.js +247 -0
  17. package/src/agent/known_checks.sql +42 -0
  18. package/src/agent/known_queries.sql +80 -0
  19. package/src/agent/known_store.sql +161 -0
  20. package/src/agent/messages.js +17 -0
  21. package/src/agent/prompt_queue.sql +39 -0
  22. package/src/agent/runs.sql +114 -0
  23. package/src/agent/schemes.sql +3 -0
  24. package/src/agent/sessions.sql +51 -0
  25. package/src/agent/tokens.js +28 -0
  26. package/src/agent/turns.sql +36 -0
  27. package/src/hooks/HookRegistry.js +72 -0
  28. package/src/hooks/Hooks.js +115 -0
  29. package/src/hooks/PluginContext.js +116 -0
  30. package/src/hooks/RummyContext.js +181 -0
  31. package/src/hooks/ToolRegistry.js +83 -0
  32. package/src/llm/LlmProvider.js +107 -0
  33. package/src/llm/OllamaClient.js +88 -0
  34. package/src/llm/OpenAiClient.js +80 -0
  35. package/src/llm/OpenRouterClient.js +78 -0
  36. package/src/llm/XaiClient.js +113 -0
  37. package/src/plugins/ask_user/README.md +18 -0
  38. package/src/plugins/ask_user/ask_user.js +48 -0
  39. package/src/plugins/ask_user/docs.md +2 -0
  40. package/src/plugins/cp/README.md +18 -0
  41. package/src/plugins/cp/cp.js +55 -0
  42. package/src/plugins/cp/docs.md +2 -0
  43. package/src/plugins/current/README.md +14 -0
  44. package/src/plugins/current/current.js +48 -0
  45. package/src/plugins/engine/README.md +12 -0
  46. package/src/plugins/engine/engine.sql +18 -0
  47. package/src/plugins/engine/turn_context.sql +51 -0
  48. package/src/plugins/env/README.md +14 -0
  49. package/src/plugins/env/docs.md +2 -0
  50. package/src/plugins/env/env.js +32 -0
  51. package/src/plugins/file/README.md +25 -0
  52. package/src/plugins/file/file.js +85 -0
  53. package/src/plugins/get/README.md +19 -0
  54. package/src/plugins/get/docs.md +6 -0
  55. package/src/plugins/get/get.js +53 -0
  56. package/src/plugins/hedberg/README.md +72 -0
  57. package/src/plugins/hedberg/docs.md +9 -0
  58. package/src/plugins/hedberg/edits.js +65 -0
  59. package/src/plugins/hedberg/hedberg.js +89 -0
  60. package/src/plugins/hedberg/matcher.js +181 -0
  61. package/src/plugins/hedberg/normalize.js +41 -0
  62. package/src/plugins/hedberg/patterns.js +452 -0
  63. package/src/plugins/hedberg/sed.js +48 -0
  64. package/src/plugins/helpers.js +22 -0
  65. package/src/plugins/index.js +180 -0
  66. package/src/plugins/instructions/README.md +11 -0
  67. package/src/plugins/instructions/instructions.js +37 -0
  68. package/src/plugins/instructions/preamble.md +12 -0
  69. package/src/plugins/known/README.md +18 -0
  70. package/src/plugins/known/docs.md +3 -0
  71. package/src/plugins/known/known.js +57 -0
  72. package/src/plugins/mv/README.md +18 -0
  73. package/src/plugins/mv/docs.md +2 -0
  74. package/src/plugins/mv/mv.js +56 -0
  75. package/src/plugins/previous/README.md +15 -0
  76. package/src/plugins/previous/previous.js +50 -0
  77. package/src/plugins/progress/README.md +17 -0
  78. package/src/plugins/progress/progress.js +44 -0
  79. package/src/plugins/prompt/README.md +16 -0
  80. package/src/plugins/prompt/prompt.js +45 -0
  81. package/src/plugins/rm/README.md +18 -0
  82. package/src/plugins/rm/docs.md +4 -0
  83. package/src/plugins/rm/rm.js +51 -0
  84. package/src/plugins/rpc/README.md +45 -0
  85. package/src/plugins/rpc/rpc.js +587 -0
  86. package/src/plugins/set/README.md +32 -0
  87. package/src/plugins/set/docs.md +4 -0
  88. package/src/plugins/set/set.js +268 -0
  89. package/src/plugins/sh/README.md +18 -0
  90. package/src/plugins/sh/docs.md +2 -0
  91. package/src/plugins/sh/sh.js +32 -0
  92. package/src/plugins/skills/README.md +25 -0
  93. package/src/plugins/skills/skills.js +175 -0
  94. package/src/plugins/store/README.md +20 -0
  95. package/src/plugins/store/docs.md +5 -0
  96. package/src/plugins/store/store.js +52 -0
  97. package/src/plugins/summarize/README.md +18 -0
  98. package/src/plugins/summarize/docs.md +4 -0
  99. package/src/plugins/summarize/summarize.js +24 -0
  100. package/src/plugins/telemetry/README.md +19 -0
  101. package/src/plugins/telemetry/rpc_log.sql +28 -0
  102. package/src/plugins/telemetry/telemetry.js +186 -0
  103. package/src/plugins/unknown/README.md +23 -0
  104. package/src/plugins/unknown/docs.md +5 -0
  105. package/src/plugins/unknown/unknown.js +31 -0
  106. package/src/plugins/update/README.md +18 -0
  107. package/src/plugins/update/docs.md +4 -0
  108. package/src/plugins/update/update.js +24 -0
  109. package/src/server/ClientConnection.js +228 -0
  110. package/src/server/RpcRegistry.js +52 -0
  111. package/src/server/SocketServer.js +43 -0
  112. package/src/sql/file_constraints.sql +15 -0
  113. package/src/sql/functions/countTokens.js +7 -0
  114. package/src/sql/functions/hedmatch.js +8 -0
  115. package/src/sql/functions/hedreplace.js +8 -0
  116. package/src/sql/functions/hedsearch.js +8 -0
  117. package/src/sql/functions/schemeOf.js +7 -0
  118. package/src/sql/functions/slugify.js +6 -0
  119. package/src/sql/v_model_context.sql +101 -0
  120. package/src/sql/v_run_log.sql +23 -0
@@ -0,0 +1,28 @@
1
+ -- PREP: log_rpc_call
2
+ INSERT INTO rpc_log (project_id, method, rpc_id, params)
3
+ VALUES (:project_id, :method, :rpc_id, :params)
4
+ RETURNING id;
5
+
6
+ -- PREP: log_rpc_result
7
+ UPDATE rpc_log
8
+ SET result = :result
9
+ WHERE id = :id;
10
+
11
+ -- PREP: log_rpc_error
12
+ UPDATE rpc_log
13
+ SET error = :error
14
+ WHERE id = :id;
15
+
16
+ -- PREP: get_rpc_log
17
+ SELECT id, project_id, method, rpc_id, params, result, error, created_at
18
+ FROM rpc_log
19
+ WHERE project_id = :project_id
20
+ ORDER BY id DESC
21
+ LIMIT :limit;
22
+
23
+ -- PREP: get_rpc_log_by_method
24
+ SELECT id, project_id, method, rpc_id, params, result, error, created_at
25
+ FROM rpc_log
26
+ WHERE project_id = :project_id AND method = :method
27
+ ORDER BY id DESC
28
+ LIMIT :limit;
@@ -0,0 +1,186 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export default class Telemetry {
5
+ #core;
6
+ #starts = new Map();
7
+ #lastRunPath = null;
8
+ #turnLog = [];
9
+
10
+ constructor(core) {
11
+ this.#core = core;
12
+
13
+ const home = process.env.RUMMY_HOME;
14
+ if (home) this.#lastRunPath = join(home, "last_run.txt");
15
+
16
+ core.on("rpc.started", this.#onRpcStarted.bind(this));
17
+ core.on("rpc.completed", this.#onRpcCompleted.bind(this));
18
+ core.on("rpc.error", this.#onRpcError.bind(this));
19
+ core.on("run.step.completed", this.#onStepCompleted.bind(this));
20
+ core.on("turn.response", this.#onTurnResponse.bind(this));
21
+ core.filter("llm.messages", this.#logMessages.bind(this), 999);
22
+ core.filter("llm.response", this.#logResponse.bind(this), 999);
23
+ }
24
+
25
+ async #onRpcStarted({ method, id, params }) {
26
+ this.#starts.set(id, Date.now());
27
+ const summary =
28
+ method === "ask" || method === "act"
29
+ ? `prompt="${(params?.prompt || "").slice(0, 60)}"`
30
+ : method === "run/abort"
31
+ ? `run=${params?.run}`
32
+ : method === "run/resolve"
33
+ ? `run=${params?.run} action=${params?.resolution?.action}`
34
+ : "";
35
+ console.log(`[RPC] → ${method}(${id})${summary ? ` ${summary}` : ""}`);
36
+
37
+ if (method === "ask" || method === "act") {
38
+ this.#turnLog = [];
39
+ }
40
+ }
41
+
42
+ async #onRpcCompleted({ method, id, result }) {
43
+ const elapsed = this.#starts.has(id)
44
+ ? `${((Date.now() - this.#starts.get(id)) / 1000).toFixed(1)}s`
45
+ : "";
46
+ this.#starts.delete(id);
47
+ const summary = result?.run
48
+ ? `run=${result.run} status=${result.status || "ok"}`
49
+ : result?.status
50
+ ? `status=${result.status}`
51
+ : "";
52
+ console.log(
53
+ `[RPC] ← ${method}(${id}) ${elapsed}${summary ? ` ${summary}` : ""}`,
54
+ );
55
+ }
56
+
57
+ async #onRpcError({ id, error }) {
58
+ const elapsed = this.#starts.has(id)
59
+ ? `${((Date.now() - this.#starts.get(id)) / 1000).toFixed(1)}s`
60
+ : "";
61
+ this.#starts.delete(id);
62
+ console.error(`[RPC] ✗ (${id}) ${elapsed} ${error?.message || error}`);
63
+ }
64
+
65
+ async #onStepCompleted(payload) {
66
+ if (process.env.RUMMY_DEBUG !== "true") return;
67
+ console.log(
68
+ `[DEBUG] Turn ${payload.turn} completed for run ${payload.run}`,
69
+ );
70
+ }
71
+
72
+ async #onTurnResponse({
73
+ rummy,
74
+ turn,
75
+ result,
76
+ responseMessage,
77
+ content,
78
+ commands,
79
+ unparsed,
80
+ systemMsg,
81
+ userMsg,
82
+ }) {
83
+ const { entries: store, runId } = rummy;
84
+
85
+ // assistant://N — the model's raw response
86
+ await store.upsert(runId, turn, `assistant://${turn}`, content, "info");
87
+
88
+ // system://N, user://N — assembled messages as audit
89
+ if (systemMsg) {
90
+ await store.upsert(runId, turn, `system://${turn}`, systemMsg, "info");
91
+ }
92
+ if (userMsg) {
93
+ await store.upsert(runId, turn, `user://${turn}`, userMsg, "info");
94
+ }
95
+
96
+ // model://N — raw API response diagnostics
97
+ await store.upsert(
98
+ runId,
99
+ turn,
100
+ `model://${turn}`,
101
+ JSON.stringify({
102
+ keys: responseMessage ? Object.keys(responseMessage) : [],
103
+ reasoning_content: responseMessage?.reasoning_content || null,
104
+ content: content.slice(0, 4096),
105
+ usage: result.usage || null,
106
+ model: result.model || null,
107
+ }),
108
+ "info",
109
+ );
110
+
111
+ // reasoning://N
112
+ if (responseMessage?.reasoning_content) {
113
+ await store.upsert(
114
+ runId,
115
+ turn,
116
+ `reasoning://${turn}`,
117
+ responseMessage.reasoning_content,
118
+ "info",
119
+ );
120
+ }
121
+
122
+ // content://N — unparsed text
123
+ if (unparsed) {
124
+ await store.upsert(runId, turn, `content://${turn}`, unparsed, "info");
125
+ }
126
+
127
+ // Commit usage stats
128
+ const usage = result.usage || {};
129
+ const cachedTokens =
130
+ usage.cached_tokens ||
131
+ usage.prompt_tokens_details?.cached_tokens ||
132
+ usage.input_tokens_details?.cached_tokens ||
133
+ usage.cache_read_input_tokens ||
134
+ 0;
135
+ const reasoningTokens =
136
+ usage.reasoning_tokens ||
137
+ usage.completion_tokens_details?.reasoning_tokens ||
138
+ usage.output_tokens_details?.reasoning_tokens ||
139
+ 0;
140
+ await rummy.db.update_turn_stats.run({
141
+ id: rummy.turnId,
142
+ prompt_tokens: usage.prompt_tokens ?? 0,
143
+ cached_tokens: cachedTokens ?? 0,
144
+ completion_tokens: usage.completion_tokens ?? 0,
145
+ reasoning_tokens: reasoningTokens ?? 0,
146
+ total_tokens: usage.total_tokens ?? 0,
147
+ cost: usage.cost ?? 0,
148
+ });
149
+ }
150
+
151
+ async #logMessages(messages, context) {
152
+ this.#turnLog.push(
153
+ `\n${"=".repeat(60)}\nTURN — model=${context.model} run=${context.runId}\n${"=".repeat(60)}`,
154
+ );
155
+ for (const msg of messages) {
156
+ const label = msg.role.toUpperCase();
157
+ const body =
158
+ typeof msg.content === "string"
159
+ ? msg.content
160
+ : JSON.stringify(msg.content);
161
+ this.#turnLog.push(`\n--- ${label} ---\n${body}`);
162
+ }
163
+ return messages;
164
+ }
165
+
166
+ async #logResponse(response) {
167
+ const msg = response.choices?.[0]?.message;
168
+ this.#turnLog.push(`\n--- ASSISTANT ---\n${msg?.content || "(empty)"}`);
169
+ if (msg?.reasoning_content) {
170
+ this.#turnLog.push(`\n--- REASONING ---\n${msg.reasoning_content}`);
171
+ }
172
+ const usage = response.usage || {};
173
+ this.#turnLog.push(`\n--- USAGE ---\n${JSON.stringify(usage)}`);
174
+ this.#flush();
175
+ return response;
176
+ }
177
+
178
+ #flush() {
179
+ if (!this.#lastRunPath || this.#turnLog.length === 0) return;
180
+ try {
181
+ writeFileSync(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`);
182
+ } catch {
183
+ // RUMMY_HOME may not exist yet
184
+ }
185
+ }
186
+ }
@@ -0,0 +1,23 @@
1
+ # unknown
2
+
3
+ The Rumsfeld mechanism. The model registers what it doesn't know before acting.
4
+
5
+ `<unknown>which database adapter is configured</unknown>`
6
+
7
+ ## Registration
8
+
9
+ - **Tool**: `unknown`
10
+ - **Modes**: ask, act
11
+ - **Category**: structural
12
+ - **Handler**: None — recorded by TurnExecutor, deduplicated against existing unknowns.
13
+
14
+ ## Projection
15
+
16
+ `# unknown\n{body}`
17
+
18
+ ## Behavior
19
+
20
+ Unknowns are sticky — they persist across turns until the model explicitly
21
+ stores or removes them. The model investigates unknowns using `<get>`,
22
+ `<env>`, or `<ask_user>`, then removes resolved ones with `<rm>`.
23
+ Server deduplicates on insert.
@@ -0,0 +1,5 @@
1
+ ## <unknown>[what you need to learn]</unknown> - Track open questions
2
+ Example: <unknown>contents of answer.txt</unknown>
3
+ Example: <unknown>which database adapter is configured</unknown>
4
+ * Use get, env, or ask_user to investigate unknowns
5
+ * When irrelevant or resolved, use <rm/> to remove from context.
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export default class Unknown {
4
+ #core;
5
+
6
+ constructor(core) {
7
+ this.#core = core;
8
+ core.registerScheme({
9
+ validStates: ["full", "stored"],
10
+ category: "knowledge",
11
+ });
12
+ core.on("full", this.full.bind(this));
13
+ core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
14
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
15
+ core.filter("instructions.toolDocs", async (content) =>
16
+ content ? `${content}\n\n${docs}` : docs,
17
+ );
18
+ }
19
+
20
+ full(entry) {
21
+ return `# unknown\n${entry.body}`;
22
+ }
23
+
24
+ async assembleUnknowns(content, ctx) {
25
+ const entries = ctx.rows.filter((r) => r.category === "unknown");
26
+ if (entries.length === 0) return content;
27
+
28
+ const lines = entries.map((u) => `<unknown>${u.body}</unknown>`);
29
+ return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
30
+ }
31
+ }
@@ -0,0 +1,18 @@
1
+ # update
2
+
3
+ Structural tool for model-generated progress updates.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `update`
8
+ - **Modes**: ask, act
9
+ - **Category**: structural
10
+ - **Handler**: None — projection only.
11
+
12
+ ## Projection
13
+
14
+ Shows `update` followed by the entry body.
15
+
16
+ ## Behavior
17
+
18
+ No handler logic. Allows the model to emit progress/status entries that appear in context via projection.
@@ -0,0 +1,4 @@
1
+ ## <update>[Brief update]</update>
2
+ * Describe the current state
3
+ * DO NOT use if done
4
+ * Keep brief (<= 80 characters)
@@ -0,0 +1,24 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export default class Update {
4
+ #core;
5
+
6
+ constructor(core) {
7
+ this.#core = core;
8
+ core.registerScheme({ validStates: ["info"], category: "structural" });
9
+ core.on("full", this.full.bind(this));
10
+ core.on("summary", this.summary.bind(this));
11
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
12
+ core.filter("instructions.toolDocs", async (content) =>
13
+ content ? `${content}\n\n${docs}` : docs,
14
+ );
15
+ }
16
+
17
+ full(entry) {
18
+ return `# update\n${entry.body}`;
19
+ }
20
+
21
+ summary(entry) {
22
+ return this.full(entry);
23
+ }
24
+ }
@@ -0,0 +1,228 @@
1
+ import msg from "../agent/messages.js";
2
+ import ProjectAgent from "../agent/ProjectAgent.js";
3
+
4
+ export default class ClientConnection {
5
+ #ws;
6
+ #db;
7
+ #projectAgent;
8
+ #hooks;
9
+ #rpcRegistry;
10
+ #rpcLogPending = new Map();
11
+ #context = {
12
+ projectId: null,
13
+ projectRoot: null,
14
+ };
15
+
16
+ constructor(ws, db, hooks) {
17
+ this.#ws = ws;
18
+ this.#db = db;
19
+ this.#hooks = hooks;
20
+ this.#rpcRegistry = hooks.rpc.registry;
21
+ this.#projectAgent = new ProjectAgent(db, hooks);
22
+
23
+ this.#ws.on("message", (data) => this.#handleMessage(data));
24
+
25
+ this.#setupNotifications();
26
+ }
27
+
28
+ #setupNotifications() {
29
+ this.#hooks.run.progress.on((payload) => {
30
+ if (payload.projectId === this.#context.projectId) {
31
+ this.#sendNotification("run/progress", {
32
+ run: payload.run,
33
+ turn: payload.turn,
34
+ status: payload.status,
35
+ });
36
+ }
37
+ });
38
+
39
+ this.#hooks.ui.render.on((payload) => {
40
+ if (payload.projectId === this.#context.projectId) {
41
+ this.#sendNotification("ui/render", {
42
+ text: payload.text,
43
+ append: payload.append,
44
+ });
45
+ }
46
+ });
47
+
48
+ this.#hooks.ui.notify.on((payload) => {
49
+ if (payload.projectId === this.#context.projectId) {
50
+ this.#sendNotification("ui/notify", {
51
+ text: payload.text,
52
+ level: payload.level,
53
+ });
54
+ }
55
+ });
56
+
57
+ this.#hooks.run.state.on((payload) => {
58
+ if (payload.projectId === this.#context.projectId) {
59
+ this.#sendNotification("run/state", {
60
+ run: payload.run,
61
+ turn: payload.turn,
62
+ status: payload.status,
63
+ summary: payload.summary,
64
+ history: payload.history,
65
+ unknowns: payload.unknowns,
66
+ proposed: payload.proposed,
67
+ telemetry: payload.telemetry,
68
+ });
69
+ }
70
+ });
71
+ }
72
+
73
+ #buildHandlerContext() {
74
+ return {
75
+ projectAgent: this.#projectAgent,
76
+ db: this.#db,
77
+ rpcRegistry: this.#rpcRegistry,
78
+ projectId: this.#context.projectId,
79
+ projectRoot: this.#context.projectRoot,
80
+ setContext: (projectId, projectRoot) => {
81
+ this.#context.projectId = projectId;
82
+ this.#context.projectRoot = projectRoot;
83
+ },
84
+ };
85
+ }
86
+
87
+ async handleMessageForTest(data) {
88
+ return this.#handleMessage(data);
89
+ }
90
+
91
+ async #handleMessage(data) {
92
+ let id = null;
93
+ const debug = process.env.RUMMY_DEBUG === "true";
94
+ try {
95
+ const rawMessage = await this.#hooks.socket.message.raw.filter(data);
96
+ if (debug) console.log(`[SOCKET] IN: ${rawMessage.toString()}`);
97
+
98
+ const message = JSON.parse(rawMessage.toString());
99
+
100
+ const filteredRequest = await this.#hooks.rpc.request.filter(message);
101
+ const { method, params, id: msgId } = filteredRequest;
102
+ id = msgId;
103
+
104
+ await this.#hooks.rpc.started.emit({
105
+ method,
106
+ params,
107
+ id,
108
+ projectId: this.#context.projectId,
109
+ });
110
+
111
+ try {
112
+ const logRow = await this.#db.log_rpc_call.get({
113
+ project_id: this.#context.projectId || null,
114
+ method,
115
+ rpc_id: id,
116
+ params: params ? JSON.stringify(params) : null,
117
+ });
118
+ if (logRow) this.#rpcLogPending.set(id, logRow.id);
119
+ } catch {}
120
+
121
+ const resolvedMethod = method === "rpc/discover" ? "discover" : method;
122
+ const registration = this.#rpcRegistry.get(resolvedMethod);
123
+ if (!registration)
124
+ throw new Error(msg("error.method_not_found", { method }));
125
+
126
+ if (registration.requiresInit && !this.#context.projectId) {
127
+ throw new Error(msg("error.not_initialized"));
128
+ }
129
+
130
+ let result;
131
+ if (registration.longRunning) {
132
+ result = await registration.handler(
133
+ params || {},
134
+ this.#buildHandlerContext(),
135
+ );
136
+ } else {
137
+ const timeout = Number(process.env.RUMMY_RPC_TIMEOUT) || 10_000;
138
+ result = await Promise.race([
139
+ registration.handler(params || {}, this.#buildHandlerContext()),
140
+ new Promise((_, reject) =>
141
+ setTimeout(
142
+ () =>
143
+ reject(
144
+ new Error(
145
+ msg("error.rpc_timeout", {
146
+ method: resolvedMethod,
147
+ timeout,
148
+ }),
149
+ ),
150
+ ),
151
+ timeout,
152
+ ),
153
+ ),
154
+ ]);
155
+ }
156
+
157
+ const finalResult = await this.#hooks.rpc.response.result.filter(result, {
158
+ method,
159
+ id,
160
+ });
161
+
162
+ this.#send({
163
+ jsonrpc: "2.0",
164
+ result: finalResult,
165
+ id,
166
+ });
167
+
168
+ await this.#hooks.rpc.completed.emit({
169
+ method,
170
+ id,
171
+ result: finalResult,
172
+ });
173
+
174
+ const logId = this.#rpcLogPending.get(id);
175
+ if (logId) {
176
+ this.#rpcLogPending.delete(id);
177
+ try {
178
+ await this.#db.log_rpc_result.run({
179
+ id: logId,
180
+ result: finalResult
181
+ ? JSON.stringify(finalResult).slice(0, 4096)
182
+ : null,
183
+ });
184
+ } catch {}
185
+ }
186
+ } catch (error) {
187
+ if (debug) {
188
+ console.error(`[SOCKET] ERR: ${error.message}`);
189
+ console.error(`[DEBUG] Stack: ${error.stack}`);
190
+ }
191
+ this.#send({
192
+ jsonrpc: "2.0",
193
+ error: { code: -32603, message: error.message },
194
+ id: id || null,
195
+ });
196
+ await this.#hooks.rpc.error.emit({ id, error });
197
+
198
+ const errLogId = this.#rpcLogPending.get(id);
199
+ if (errLogId) {
200
+ this.#rpcLogPending.delete(id);
201
+ try {
202
+ await this.#db.log_rpc_error.run({
203
+ id: errLogId,
204
+ error: error.message,
205
+ });
206
+ } catch {}
207
+ }
208
+ }
209
+ }
210
+
211
+ #send(payload) {
212
+ const debug = process.env.RUMMY_DEBUG === "true";
213
+ if (debug) {
214
+ console.log(`[SOCKET] OUT: ${JSON.stringify(payload, null, 2)}`);
215
+ }
216
+ if (this.#ws.readyState === 1) {
217
+ this.#ws.send(JSON.stringify(payload));
218
+ }
219
+ }
220
+
221
+ #sendNotification(method, params) {
222
+ this.#send({
223
+ jsonrpc: "2.0",
224
+ method,
225
+ params,
226
+ });
227
+ }
228
+ }
@@ -0,0 +1,52 @@
1
+ export default class RpcRegistry {
2
+ #methods = new Map();
3
+ #notifications = new Map();
4
+
5
+ register(
6
+ name,
7
+ {
8
+ handler,
9
+ description = "",
10
+ params = {},
11
+ requiresInit = false,
12
+ longRunning = false,
13
+ },
14
+ ) {
15
+ if (this.#methods.has(name))
16
+ throw new Error(`RPC method '${name}' already registered.`);
17
+ this.#methods.set(
18
+ name,
19
+ Object.freeze({
20
+ handler,
21
+ description,
22
+ params,
23
+ requiresInit,
24
+ longRunning,
25
+ }),
26
+ );
27
+ }
28
+
29
+ registerNotification(name, description = "") {
30
+ this.#notifications.set(name, Object.freeze({ description }));
31
+ }
32
+
33
+ get(name) {
34
+ return this.#methods.get(name);
35
+ }
36
+
37
+ has(name) {
38
+ return this.#methods.has(name);
39
+ }
40
+
41
+ discover() {
42
+ const methods = {};
43
+ for (const [name, def] of this.#methods) {
44
+ methods[name] = { description: def.description, params: def.params };
45
+ }
46
+ const notifications = {};
47
+ for (const [name, def] of this.#notifications) {
48
+ notifications[name] = { description: def.description };
49
+ }
50
+ return { methods, notifications };
51
+ }
52
+ }
@@ -0,0 +1,43 @@
1
+ import { WebSocketServer } from "ws";
2
+ import ClientConnection from "./ClientConnection.js";
3
+
4
+ export default class SocketServer {
5
+ #db;
6
+ #wss;
7
+ #hooks;
8
+
9
+ constructor(db, options) {
10
+ this.#db = db;
11
+ this.#hooks = options.hooks;
12
+ this.#wss = new WebSocketServer(options);
13
+
14
+ this.#wss.on("connection", (ws, req) => {
15
+ if (process.env.RUMMY_DEBUG === "true") {
16
+ console.log(`[SOCKET] New connection from ${req.socket.remoteAddress}`);
17
+ }
18
+ new ClientConnection(ws, this.#db, this.#hooks);
19
+ });
20
+
21
+ this.#wss.on("error", (_err) => {
22
+ // Proxy to registry or handle locally
23
+ });
24
+ }
25
+
26
+ address() {
27
+ return this.#wss.address();
28
+ }
29
+
30
+ on(event, handler) {
31
+ this.#wss.on(event, handler);
32
+ }
33
+
34
+ close() {
35
+ return new Promise((resolve) => {
36
+ if (!this.#wss) return resolve();
37
+ for (const client of this.#wss.clients) {
38
+ client.terminate();
39
+ }
40
+ this.#wss.close(resolve);
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,15 @@
1
+ -- PREP: upsert_file_constraint
2
+ INSERT INTO file_constraints (project_id, pattern, visibility)
3
+ VALUES (:project_id, :pattern, :visibility)
4
+ ON CONFLICT (project_id, pattern) DO UPDATE SET
5
+ visibility = excluded.visibility;
6
+
7
+ -- PREP: delete_file_constraint
8
+ DELETE FROM file_constraints
9
+ WHERE project_id = :project_id AND pattern = :pattern;
10
+
11
+ -- PREP: get_file_constraints
12
+ SELECT pattern, visibility
13
+ FROM file_constraints
14
+ WHERE project_id = :project_id
15
+ ORDER BY pattern;
@@ -0,0 +1,7 @@
1
+ import { countTokens } from "../../agent/tokens.js";
2
+
3
+ export const deterministic = true;
4
+
5
+ export default function (text) {
6
+ return text ? countTokens(text) : 0;
7
+ }
@@ -0,0 +1,8 @@
1
+ import { hedmatch } from "../../plugins/hedberg/patterns.js";
2
+
3
+ export const deterministic = true;
4
+
5
+ export default function (pattern, string) {
6
+ if (string === null) return 0;
7
+ return hedmatch(pattern, string) ? 1 : 0;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { hedreplace } from "../../plugins/hedberg/patterns.js";
2
+
3
+ export const deterministic = true;
4
+
5
+ export default function (pattern, replacement, string) {
6
+ if (string === null) return null;
7
+ return hedreplace(pattern, replacement, string);
8
+ }
@@ -0,0 +1,8 @@
1
+ import { hedsearch } from "../../plugins/hedberg/patterns.js";
2
+
3
+ export const deterministic = true;
4
+
5
+ export default function (pattern, string) {
6
+ if (string === null) return 0;
7
+ return hedsearch(pattern, string).found ? 1 : 0;
8
+ }