@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,268 @@
1
+ import { readFileSync } from "node:fs";
2
+ import KnownStore from "../../agent/KnownStore.js";
3
+ import Hedberg, { generatePatch } from "../hedberg/hedberg.js";
4
+ import { storePatternResult } from "../helpers.js";
5
+
6
+ // biome-ignore lint/suspicious/noShadowRestrictedNames: tool name is "set"
7
+ export default class Set {
8
+ #core;
9
+
10
+ constructor(core) {
11
+ this.#core = core;
12
+ core.registerScheme({
13
+ validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
14
+ });
15
+ core.on("handler", this.handler.bind(this));
16
+ core.on("full", this.full.bind(this));
17
+ core.on("summary", this.summary.bind(this));
18
+ core.on("turn.proposing", this.#materializeRevisions.bind(this));
19
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
20
+ core.filter("instructions.toolDocs", async (content) =>
21
+ content ? `${content}\n\n${docs}` : docs,
22
+ );
23
+ }
24
+
25
+ async handler(entry, rummy) {
26
+ const { entries: store, sequence: turn, runId } = rummy;
27
+ const attrs = entry.attributes;
28
+
29
+ if (attrs.blocks || attrs.search != null) {
30
+ await this.#processEdit(rummy, entry, attrs);
31
+ return;
32
+ }
33
+
34
+ if (attrs.preview && attrs.path) {
35
+ const matches = await store.getEntriesByPattern(
36
+ runId,
37
+ attrs.path,
38
+ attrs.body,
39
+ );
40
+ await storePatternResult(
41
+ store,
42
+ runId,
43
+ turn,
44
+ "set",
45
+ attrs.path,
46
+ attrs.body,
47
+ matches,
48
+ true,
49
+ );
50
+ return;
51
+ }
52
+
53
+ const target = attrs.path;
54
+ if (!target) return;
55
+
56
+ const scheme = KnownStore.scheme(target);
57
+ if (scheme === null) {
58
+ const udiff = generatePatch(target, "", entry.body || "");
59
+ const merge = `<<<<<<< SEARCH\n=======\n${entry.body || ""}\n>>>>>>> REPLACE`;
60
+ await store.upsert(runId, turn, entry.resultPath, "", "proposed", {
61
+ attributes: { file: target, patch: udiff, merge },
62
+ });
63
+ } else if (attrs.filter || target.includes("*")) {
64
+ const matches = await store.getEntriesByPattern(
65
+ runId,
66
+ target,
67
+ attrs.filter,
68
+ );
69
+ await store.updateBodyByPattern(
70
+ runId,
71
+ target,
72
+ attrs.filter || null,
73
+ entry.body,
74
+ );
75
+ await storePatternResult(
76
+ store,
77
+ runId,
78
+ turn,
79
+ "set",
80
+ target,
81
+ attrs.filter,
82
+ matches,
83
+ );
84
+ } else {
85
+ await store.upsert(runId, turn, target, entry.body, "full");
86
+ }
87
+ }
88
+
89
+ full(entry) {
90
+ const attrs = entry.attributes;
91
+ const file = attrs.file || entry.path;
92
+ if (attrs.error) return `# set ${file}\n${attrs.error}`;
93
+ const tokens =
94
+ attrs.beforeTokens != null
95
+ ? ` ${attrs.beforeTokens}→${attrs.afterTokens} tokens`
96
+ : "";
97
+ if (!attrs.merge) return `# set ${file}${tokens}`;
98
+ return `# set ${file}${tokens}\n${attrs.merge}`;
99
+ }
100
+
101
+ summary(entry) {
102
+ return entry.attributes.merge || "";
103
+ }
104
+
105
+ async #processEdit(rummy, entry, attrs) {
106
+ const { entries: store, sequence: turn, runId } = rummy;
107
+ const target = attrs.path;
108
+ const matches = await store.getEntriesByPattern(runId, target, attrs.body);
109
+
110
+ if (matches.length === 0) {
111
+ await store.upsert(runId, turn, entry.resultPath, "", "error", {
112
+ attributes: { file: target, error: `${target} not found in context` },
113
+ });
114
+ return;
115
+ }
116
+
117
+ for (const match of matches) {
118
+ if (match.scheme === null) {
119
+ const canonicalPath = `set://${match.path}`;
120
+ const revision = Set.#buildRevision(attrs);
121
+ const existingAttrs = await rummy.getAttributes(canonicalPath);
122
+ const revisions = existingAttrs?.revisions || [];
123
+ revisions.push(revision);
124
+ await store.upsert(runId, turn, canonicalPath, "", "full", {
125
+ attributes: { file: match.path, revisions },
126
+ });
127
+ if (KnownStore.normalizePath(entry.resultPath) !== canonicalPath) {
128
+ await store.remove(runId, entry.resultPath);
129
+ }
130
+ return;
131
+ }
132
+
133
+ const { patch, searchText, replaceText, warning, error } =
134
+ Set.#applyRevision(match.body, attrs);
135
+
136
+ const state = error ? "error" : "pass";
137
+ const resultPath = `set://${match.path}`;
138
+ const udiff = patch ? generatePatch(match.path, match.body, patch) : null;
139
+ const merge =
140
+ searchText != null
141
+ ? `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`
142
+ : null;
143
+ const beforeTokens = match.tokens_full || 0;
144
+ const afterTokens = patch ? (patch.length / 4) | 0 : beforeTokens;
145
+
146
+ await store.upsert(runId, turn, resultPath, match.body, state, {
147
+ attributes: {
148
+ file: match.path,
149
+ patch: udiff,
150
+ merge,
151
+ beforeTokens,
152
+ afterTokens,
153
+ warning,
154
+ error,
155
+ },
156
+ });
157
+
158
+ if (state === "pass" && patch) {
159
+ await store.upsert(runId, turn, match.path, patch, match.state);
160
+ }
161
+ }
162
+ }
163
+
164
+ async #materializeRevisions({ rummy }) {
165
+ const { entries: store, sequence: turn, runId } = rummy;
166
+ const setEntries = await store.getEntriesByPattern(runId, "set://*");
167
+
168
+ for (const entry of setEntries) {
169
+ const attrs =
170
+ typeof entry.attributes === "string"
171
+ ? JSON.parse(entry.attributes)
172
+ : entry.attributes;
173
+ if (!attrs?.revisions?.length) continue;
174
+
175
+ const filePath = attrs.file;
176
+ const fileEntry = await store.getEntriesByPattern(runId, filePath);
177
+ if (fileEntry.length === 0) continue;
178
+
179
+ const original = fileEntry[0].body;
180
+ let current = original;
181
+ const mergeBlocks = [];
182
+ let lastError = null;
183
+ let lastWarning = null;
184
+
185
+ for (const rev of attrs.revisions) {
186
+ if (!rev) continue;
187
+ const { patch, searchText, replaceText, warning, error } =
188
+ Set.#applyRevision(current, rev);
189
+
190
+ if (error) lastError = error;
191
+ else if (patch) current = patch;
192
+ if (warning) lastWarning = warning;
193
+
194
+ if (searchText != null) {
195
+ mergeBlocks.push(
196
+ `<<<<<<< SEARCH\n${searchText}\n=======\n${replaceText}\n>>>>>>> REPLACE`,
197
+ );
198
+ }
199
+ }
200
+
201
+ const state = lastError ? "error" : "proposed";
202
+ const udiff =
203
+ current !== original
204
+ ? generatePatch(filePath, original, current)
205
+ : null;
206
+ const merge = mergeBlocks.length > 0 ? mergeBlocks.join("\n") : null;
207
+ const beforeTokens = fileEntry[0].tokens_full || 0;
208
+ const afterTokens = current ? (current.length / 4) | 0 : beforeTokens;
209
+
210
+ await store.upsert(runId, turn, entry.path, original, state, {
211
+ attributes: {
212
+ file: filePath,
213
+ patch: udiff,
214
+ merge,
215
+ beforeTokens,
216
+ afterTokens,
217
+ warning: lastWarning,
218
+ error: lastError,
219
+ },
220
+ });
221
+ }
222
+ }
223
+
224
+ static #buildRevision(attrs) {
225
+ if (attrs.search != null) {
226
+ return { search: attrs.search, replace: attrs.replace ?? "" };
227
+ }
228
+ if (attrs.blocks?.length > 0) {
229
+ return {
230
+ search: attrs.blocks[0].search,
231
+ replace: attrs.blocks[0].replace,
232
+ };
233
+ }
234
+ return null;
235
+ }
236
+
237
+ static #applyRevision(body, attrs) {
238
+ if (attrs.search != null) {
239
+ return Hedberg.replace(body, attrs.search, attrs.replace ?? "", {
240
+ sed: attrs.sed,
241
+ flags: attrs.flags,
242
+ });
243
+ }
244
+ if (attrs.blocks?.length > 0 && attrs.blocks[0].search === null) {
245
+ return {
246
+ patch: attrs.blocks[0].replace,
247
+ searchText: null,
248
+ replaceText: attrs.blocks[0].replace,
249
+ warning: null,
250
+ error: null,
251
+ };
252
+ }
253
+ if (body && attrs.blocks?.length > 0) {
254
+ const block = attrs.blocks[0];
255
+ return Hedberg.replace(body, block.search, block.replace, {
256
+ sed: block.sed,
257
+ flags: block.flags,
258
+ });
259
+ }
260
+ return {
261
+ patch: null,
262
+ searchText: null,
263
+ replaceText: null,
264
+ warning: null,
265
+ error: null,
266
+ };
267
+ }
268
+ }
@@ -0,0 +1,18 @@
1
+ # sh
2
+
3
+ Proposes shell command execution for client approval.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `sh`
8
+ - **Modes**: act only
9
+ - **Category**: act
10
+ - **Handler**: Upserts the entry as `proposed` state. The client must approve execution.
11
+
12
+ ## Projection
13
+
14
+ Shows `sh {command}` followed by the entry body.
15
+
16
+ ## Behavior
17
+
18
+ All shell commands require client-side approval — nothing executes server-side. Act mode only; blocked in ask mode.
@@ -0,0 +1,2 @@
1
+ ## <sh>[command]</sh> - Run a shell command with side effects
2
+ Example: <sh>npm install</sh>
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export default class Sh {
4
+ #core;
5
+
6
+ constructor(core) {
7
+ this.#core = core;
8
+ core.registerScheme();
9
+ core.on("handler", this.handler.bind(this));
10
+ core.on("full", this.full.bind(this));
11
+ core.on("summary", this.summary.bind(this));
12
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
13
+ core.filter("instructions.toolDocs", async (content) =>
14
+ content ? `${content}\n\n${docs}` : docs,
15
+ );
16
+ }
17
+
18
+ async handler(entry, rummy) {
19
+ const { entries: store, sequence: turn, runId } = rummy;
20
+ await store.upsert(runId, turn, entry.resultPath, entry.body, "proposed", {
21
+ attributes: entry.attributes,
22
+ });
23
+ }
24
+
25
+ full(entry) {
26
+ return `# sh ${entry.attributes.command || ""}\n${entry.body}`;
27
+ }
28
+
29
+ summary(entry) {
30
+ return entry.attributes.command || "";
31
+ }
32
+ }
@@ -0,0 +1,25 @@
1
+ # skills
2
+
3
+ Manages skills and personas via RPC methods. Skills are stackable per-run entries; personas are exclusive per-run configuration.
4
+
5
+ ## Registration
6
+
7
+ - **No tool handler** — registers RPC methods on `hooks.rpc.registry`.
8
+
9
+ ## RPC Methods
10
+
11
+ ### Skills
12
+ - `skill/add` — Load a skill from `config/skills/{name}.md` into the run as a `skill://` entry at full state.
13
+ - `skill/remove` — Remove a skill entry from a run.
14
+ - `getSkills` — List active skills on a run.
15
+ - `listSkills` — List available skill files from disk.
16
+
17
+ ### Personas
18
+ - `persona/set` — Set persona on a run. Load from `config/personas/{name}.md` by name, pass raw text, or clear by omitting both.
19
+ - `listPersonas` — List available persona files from disk.
20
+
21
+ ## Behavior
22
+
23
+ - Skills stack: multiple skills can be active on a run simultaneously as separate `skill://` entries.
24
+ - Personas are exclusive: setting a persona replaces the previous one (stored as a run column, not an entry).
25
+ - File paths resolve from `RUMMY_HOME` environment variable.
@@ -0,0 +1,175 @@
1
+ import fs from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export default class Skills {
5
+ #core;
6
+
7
+ constructor(core) {
8
+ this.#core = core;
9
+ core.registerScheme({
10
+ name: "skill",
11
+ validStates: ["full", "stored"],
12
+ category: "knowledge",
13
+ });
14
+ const r = core.hooks.rpc.registry;
15
+
16
+ r.register("skill/add", {
17
+ handler: async (params, ctx) => {
18
+ if (!params.name) throw new Error("name is required");
19
+ if (!params.run) throw new Error("run is required");
20
+
21
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
22
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
23
+
24
+ const body = await loadFile("skills", params.name);
25
+ const store = ctx.projectAgent.entries;
26
+ await store.upsert(
27
+ runRow.id,
28
+ runRow.next_turn,
29
+ `skill://${params.name}`,
30
+ body,
31
+ "full",
32
+ {
33
+ attributes: {
34
+ name: params.name,
35
+ source: filePath("skills", params.name),
36
+ },
37
+ },
38
+ );
39
+
40
+ return { status: "ok", skill: params.name };
41
+ },
42
+ description:
43
+ "Add a skill to a run. Reads from RUMMY_HOME/skills/{name}.md.",
44
+ params: {
45
+ run: "string — run alias",
46
+ name: "string — skill name (filename without .md)",
47
+ },
48
+ requiresInit: true,
49
+ });
50
+
51
+ r.register("skill/remove", {
52
+ handler: async (params, ctx) => {
53
+ if (!params.name) throw new Error("name is required");
54
+ if (!params.run) throw new Error("run is required");
55
+
56
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
57
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
58
+
59
+ const store = ctx.projectAgent.entries;
60
+ await store.remove(runRow.id, `skill://${params.name}`);
61
+
62
+ return { status: "ok" };
63
+ },
64
+ description: "Remove a skill from a run.",
65
+ params: {
66
+ run: "string — run alias",
67
+ name: "string — skill name",
68
+ },
69
+ requiresInit: true,
70
+ });
71
+
72
+ r.register("getSkills", {
73
+ handler: async (params, ctx) => {
74
+ if (!params.run) throw new Error("run is required");
75
+
76
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
77
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
78
+
79
+ const store = ctx.projectAgent.entries;
80
+ const entries = await store.getEntriesByPattern(
81
+ runRow.id,
82
+ "skill://*",
83
+ null,
84
+ );
85
+ return entries.map((e) => ({
86
+ name: e.path.replace("skill://", ""),
87
+ state: e.state,
88
+ }));
89
+ },
90
+ description: "List skills active on a run. Returns [{ name, state }].",
91
+ params: { run: "string — run alias" },
92
+ requiresInit: true,
93
+ });
94
+
95
+ r.register("listSkills", {
96
+ handler: async () => listAvailable("skills"),
97
+ description: "List available skill files. Returns [{ name, path }].",
98
+ requiresInit: true,
99
+ });
100
+
101
+ r.register("persona/set", {
102
+ handler: async (params, ctx) => {
103
+ if (!params.run) throw new Error("run is required");
104
+
105
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
106
+ if (!runRow) throw new Error(`Run not found: ${params.run}`);
107
+
108
+ let text = params.text;
109
+ if (params.name && !text) {
110
+ text = await loadFile("personas", params.name);
111
+ }
112
+
113
+ await ctx.db.update_run_config.run({
114
+ id: runRow.id,
115
+ temperature: null,
116
+ persona: text || null,
117
+ context_limit: null,
118
+ model: null,
119
+ });
120
+
121
+ return { status: "ok" };
122
+ },
123
+ description:
124
+ "Set persona on a run. Pass name or text. Pass neither to clear.",
125
+ params: {
126
+ run: "string — run alias",
127
+ name: "string? — persona filename (without .md)",
128
+ text: "string? — raw persona text (overrides name)",
129
+ },
130
+ requiresInit: true,
131
+ });
132
+
133
+ r.register("listPersonas", {
134
+ handler: async () => listAvailable("personas"),
135
+ description: "List available persona files. Returns [{ name, path }].",
136
+ requiresInit: true,
137
+ });
138
+ }
139
+ }
140
+
141
+ function configDir(subfolder) {
142
+ const home = process.env.RUMMY_HOME;
143
+ if (home) return join(home, subfolder);
144
+ return null;
145
+ }
146
+
147
+ function filePath(subfolder, name) {
148
+ const dir = configDir(subfolder);
149
+ if (!dir) return null;
150
+ return join(dir, `${name}.md`);
151
+ }
152
+
153
+ async function loadFile(subfolder, name) {
154
+ const path = filePath(subfolder, name);
155
+ if (!path) throw new Error("RUMMY_HOME not configured");
156
+ try {
157
+ return await fs.readFile(path, "utf8");
158
+ } catch (err) {
159
+ if (err.code === "ENOENT") throw new Error(`Not found: ${path}`);
160
+ throw err;
161
+ }
162
+ }
163
+
164
+ async function listAvailable(subfolder) {
165
+ const dir = configDir(subfolder);
166
+ if (!dir) return [];
167
+ try {
168
+ const files = await fs.readdir(dir);
169
+ return files
170
+ .filter((f) => f.endsWith(".md"))
171
+ .map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
172
+ } catch {
173
+ return [];
174
+ }
175
+ }
@@ -0,0 +1,20 @@
1
+ # store
2
+
3
+ Demotes entries from active context to stored (background) state.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `store`
8
+ - **Modes**: ask, act
9
+ - **Category**: ask
10
+ - **Handler**: Matches entries by pattern, demotes them via `demoteByPattern`, and records the result.
11
+
12
+ ## Projection
13
+
14
+ Shows `store {path}`.
15
+
16
+ ## Behavior
17
+
18
+ - Pattern queries (globs or body filters) produce a summary of matched paths.
19
+ - Exact path queries report "{path} stored" or "{path} not found".
20
+ - Stored entries remain in the database but are excluded from model context.
@@ -0,0 +1,5 @@
1
+ ## <store path="[path/to/file]"/> - Store a file or entry
2
+ Example: <store path="src/config.js"/>
3
+ Example: <store path="unknown://42"/>
4
+ * <store/> removes the file or entry from context, but does not delete it
5
+ * A stored file or entry can be restored with <get/>
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { storePatternResult } from "../helpers.js";
3
+
4
+ export default class Store {
5
+ #core;
6
+
7
+ constructor(core) {
8
+ this.#core = core;
9
+ core.registerScheme({ validStates: ["full", "stored", "pattern"] });
10
+ core.on("handler", this.handler.bind(this));
11
+ core.on("full", this.full.bind(this));
12
+ core.on("summary", this.summary.bind(this));
13
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
14
+ core.filter("instructions.toolDocs", async (content) =>
15
+ content ? `${content}\n\n${docs}` : docs,
16
+ );
17
+ }
18
+
19
+ async handler(entry, rummy) {
20
+ const { entries: store, sequence: turn, runId } = rummy;
21
+ const target = entry.attributes.path;
22
+ const bodyFilter = entry.attributes.body || null;
23
+ const isPattern = bodyFilter || target.includes("*");
24
+ const matches = await store.getEntriesByPattern(runId, target, bodyFilter);
25
+ await store.demoteByPattern(runId, target, bodyFilter);
26
+
27
+ if (isPattern) {
28
+ await storePatternResult(
29
+ store,
30
+ runId,
31
+ turn,
32
+ "store",
33
+ target,
34
+ bodyFilter,
35
+ matches,
36
+ );
37
+ } else {
38
+ const paths = matches.map((m) => m.path).join(", ");
39
+ const body =
40
+ matches.length > 0 ? `${paths} stored` : `${target} not found`;
41
+ await store.upsert(runId, turn, entry.resultPath, body, "stored");
42
+ }
43
+ }
44
+
45
+ full(entry) {
46
+ return `# store ${entry.attributes.path || entry.path}`;
47
+ }
48
+
49
+ summary(entry) {
50
+ return this.full(entry);
51
+ }
52
+ }
@@ -0,0 +1,18 @@
1
+ # summarize
2
+
3
+ Structural tool for model-generated summaries.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `summarize`
8
+ - **Modes**: ask, act
9
+ - **Category**: structural
10
+ - **Handler**: None — projection only.
11
+
12
+ ## Projection
13
+
14
+ Shows `summarize` followed by the entry body.
15
+
16
+ ## Behavior
17
+
18
+ No handler logic. The tool registration exists so the model can emit summary entries that appear in context via projection.
@@ -0,0 +1,4 @@
1
+ ## <summarize>[Answer or summary]</summarize>
2
+ * Describe the final state
3
+ * ONLY use if done
4
+ * Keep brief (<= 80 characters)
@@ -0,0 +1,24 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export default class Summarize {
4
+ #core;
5
+
6
+ constructor(core) {
7
+ this.#core = core;
8
+ core.registerScheme({ validStates: ["summary"], 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 `# summarize\n${entry.body}`;
19
+ }
20
+
21
+ summary(entry) {
22
+ return this.full(entry);
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ # telemetry
2
+
3
+ Console logging for RPC lifecycle and turn events.
4
+
5
+ ## Files
6
+
7
+ - **telemetry.js** — Plugin registration. Hooks into `rpc.started`, `rpc.completed`, `rpc.error`, and `run.step.completed`.
8
+ - **RunDumper.js** — Dumps a run's complete exchange to a readable text file. Active when `RUMMY_DEBUG=true`.
9
+ - **rpc_log.sql** — SQL for RPC audit logging.
10
+
11
+ ## Registration
12
+
13
+ - **No tool handler** — hooks into RPC and run lifecycle events.
14
+
15
+ ## Behavior
16
+
17
+ - Logs RPC method calls with timing (elapsed seconds) and contextual summaries (prompt text, run alias, resolution action).
18
+ - Errors are logged with their message.
19
+ - Turn completion debug logging is gated behind `RUMMY_DEBUG=true`.