@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.
- package/.env.example +55 -0
- package/LICENSE +21 -0
- package/PLUGINS.md +302 -0
- package/README.md +41 -0
- package/SPEC.md +524 -0
- package/lang/en.json +34 -0
- package/migrations/001_initial_schema.sql +226 -0
- package/package.json +54 -0
- package/service.js +143 -0
- package/src/agent/AgentLoop.js +553 -0
- package/src/agent/ContextAssembler.js +29 -0
- package/src/agent/KnownStore.js +254 -0
- package/src/agent/ProjectAgent.js +101 -0
- package/src/agent/ResponseHealer.js +134 -0
- package/src/agent/TurnExecutor.js +457 -0
- package/src/agent/XmlParser.js +247 -0
- package/src/agent/known_checks.sql +42 -0
- package/src/agent/known_queries.sql +80 -0
- package/src/agent/known_store.sql +161 -0
- package/src/agent/messages.js +17 -0
- package/src/agent/prompt_queue.sql +39 -0
- package/src/agent/runs.sql +114 -0
- package/src/agent/schemes.sql +3 -0
- package/src/agent/sessions.sql +51 -0
- package/src/agent/tokens.js +28 -0
- package/src/agent/turns.sql +36 -0
- package/src/hooks/HookRegistry.js +72 -0
- package/src/hooks/Hooks.js +115 -0
- package/src/hooks/PluginContext.js +116 -0
- package/src/hooks/RummyContext.js +181 -0
- package/src/hooks/ToolRegistry.js +83 -0
- package/src/llm/LlmProvider.js +107 -0
- package/src/llm/OllamaClient.js +88 -0
- package/src/llm/OpenAiClient.js +80 -0
- package/src/llm/OpenRouterClient.js +78 -0
- package/src/llm/XaiClient.js +113 -0
- package/src/plugins/ask_user/README.md +18 -0
- package/src/plugins/ask_user/ask_user.js +48 -0
- package/src/plugins/ask_user/docs.md +2 -0
- package/src/plugins/cp/README.md +18 -0
- package/src/plugins/cp/cp.js +55 -0
- package/src/plugins/cp/docs.md +2 -0
- package/src/plugins/current/README.md +14 -0
- package/src/plugins/current/current.js +48 -0
- package/src/plugins/engine/README.md +12 -0
- package/src/plugins/engine/engine.sql +18 -0
- package/src/plugins/engine/turn_context.sql +51 -0
- package/src/plugins/env/README.md +14 -0
- package/src/plugins/env/docs.md +2 -0
- package/src/plugins/env/env.js +32 -0
- package/src/plugins/file/README.md +25 -0
- package/src/plugins/file/file.js +85 -0
- package/src/plugins/get/README.md +19 -0
- package/src/plugins/get/docs.md +6 -0
- package/src/plugins/get/get.js +53 -0
- package/src/plugins/hedberg/README.md +72 -0
- package/src/plugins/hedberg/docs.md +9 -0
- package/src/plugins/hedberg/edits.js +65 -0
- package/src/plugins/hedberg/hedberg.js +89 -0
- package/src/plugins/hedberg/matcher.js +181 -0
- package/src/plugins/hedberg/normalize.js +41 -0
- package/src/plugins/hedberg/patterns.js +452 -0
- package/src/plugins/hedberg/sed.js +48 -0
- package/src/plugins/helpers.js +22 -0
- package/src/plugins/index.js +180 -0
- package/src/plugins/instructions/README.md +11 -0
- package/src/plugins/instructions/instructions.js +37 -0
- package/src/plugins/instructions/preamble.md +12 -0
- package/src/plugins/known/README.md +18 -0
- package/src/plugins/known/docs.md +3 -0
- package/src/plugins/known/known.js +57 -0
- package/src/plugins/mv/README.md +18 -0
- package/src/plugins/mv/docs.md +2 -0
- package/src/plugins/mv/mv.js +56 -0
- package/src/plugins/previous/README.md +15 -0
- package/src/plugins/previous/previous.js +50 -0
- package/src/plugins/progress/README.md +17 -0
- package/src/plugins/progress/progress.js +44 -0
- package/src/plugins/prompt/README.md +16 -0
- package/src/plugins/prompt/prompt.js +45 -0
- package/src/plugins/rm/README.md +18 -0
- package/src/plugins/rm/docs.md +4 -0
- package/src/plugins/rm/rm.js +51 -0
- package/src/plugins/rpc/README.md +45 -0
- package/src/plugins/rpc/rpc.js +587 -0
- package/src/plugins/set/README.md +32 -0
- package/src/plugins/set/docs.md +4 -0
- package/src/plugins/set/set.js +268 -0
- package/src/plugins/sh/README.md +18 -0
- package/src/plugins/sh/docs.md +2 -0
- package/src/plugins/sh/sh.js +32 -0
- package/src/plugins/skills/README.md +25 -0
- package/src/plugins/skills/skills.js +175 -0
- package/src/plugins/store/README.md +20 -0
- package/src/plugins/store/docs.md +5 -0
- package/src/plugins/store/store.js +52 -0
- package/src/plugins/summarize/README.md +18 -0
- package/src/plugins/summarize/docs.md +4 -0
- package/src/plugins/summarize/summarize.js +24 -0
- package/src/plugins/telemetry/README.md +19 -0
- package/src/plugins/telemetry/rpc_log.sql +28 -0
- package/src/plugins/telemetry/telemetry.js +186 -0
- package/src/plugins/unknown/README.md +23 -0
- package/src/plugins/unknown/docs.md +5 -0
- package/src/plugins/unknown/unknown.js +31 -0
- package/src/plugins/update/README.md +18 -0
- package/src/plugins/update/docs.md +4 -0
- package/src/plugins/update/update.js +24 -0
- package/src/server/ClientConnection.js +228 -0
- package/src/server/RpcRegistry.js +52 -0
- package/src/server/SocketServer.js +43 -0
- package/src/sql/file_constraints.sql +15 -0
- package/src/sql/functions/countTokens.js +7 -0
- package/src/sql/functions/hedmatch.js +8 -0
- package/src/sql/functions/hedreplace.js +8 -0
- package/src/sql/functions/hedsearch.js +8 -0
- package/src/sql/functions/schemeOf.js +7 -0
- package/src/sql/functions/slugify.js +6 -0
- package/src/sql/v_model_context.sql +101 -0
- 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,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,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,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`.
|