@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,452 @@
|
|
|
1
|
+
import { JSDOM } from "jsdom";
|
|
2
|
+
|
|
3
|
+
export const deterministic = true;
|
|
4
|
+
|
|
5
|
+
const cache = new Map();
|
|
6
|
+
|
|
7
|
+
// --- Detection ---
|
|
8
|
+
|
|
9
|
+
const XPATH_AXES = new Set([
|
|
10
|
+
"child",
|
|
11
|
+
"descendant",
|
|
12
|
+
"descendant-or-self",
|
|
13
|
+
"parent",
|
|
14
|
+
"ancestor",
|
|
15
|
+
"ancestor-or-self",
|
|
16
|
+
"following",
|
|
17
|
+
"following-sibling",
|
|
18
|
+
"preceding",
|
|
19
|
+
"preceding-sibling",
|
|
20
|
+
"self",
|
|
21
|
+
"attribute",
|
|
22
|
+
"namespace",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const XPATH_FUNCTIONS =
|
|
26
|
+
/\b(position|last|contains|starts-with|not|text|count|sum|name|local-name|string-length|normalize-space|concat|substring|translate|boolean|number|string|true|false|ceiling|floor|round|id|lang|comment|processing-instruction)\s*\(/;
|
|
27
|
+
|
|
28
|
+
function isPlausibleJsonPath(pattern) {
|
|
29
|
+
if (pattern.startsWith("$.")) {
|
|
30
|
+
const after = pattern[2];
|
|
31
|
+
if (!after) return false;
|
|
32
|
+
if (/[a-zA-Z_*@.[[]/.test(after)) return !pattern.includes("/");
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (pattern.startsWith("$[")) return !pattern.includes("/");
|
|
36
|
+
if (pattern === "$..") return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasXPathAxis(pattern) {
|
|
41
|
+
const matches = pattern.matchAll(/(\w[\w-]*)(?=::)/g);
|
|
42
|
+
for (const m of matches) {
|
|
43
|
+
if (XPATH_AXES.has(m[1])) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasXPathPredicate(pattern) {
|
|
49
|
+
const brackets = pattern.matchAll(/\[([^\]]+)\]/g);
|
|
50
|
+
for (const m of brackets) {
|
|
51
|
+
const content = m[1];
|
|
52
|
+
if (content.startsWith("@")) return true;
|
|
53
|
+
if (/^\d+$/.test(content.trim())) return true;
|
|
54
|
+
if (XPATH_FUNCTIONS.test(content)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseSed(pattern) {
|
|
60
|
+
// s/search/replace/ or s/search/replace/flags
|
|
61
|
+
if (!pattern.startsWith("s/")) return null;
|
|
62
|
+
const parts = [];
|
|
63
|
+
let i = 2;
|
|
64
|
+
let current = "";
|
|
65
|
+
while (i < pattern.length) {
|
|
66
|
+
if (pattern[i] === "/" && pattern[i - 1] !== "\\") {
|
|
67
|
+
parts.push(current);
|
|
68
|
+
current = "";
|
|
69
|
+
} else {
|
|
70
|
+
current += pattern[i];
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
parts.push(current);
|
|
75
|
+
if (parts.length < 2) return null;
|
|
76
|
+
const search = parts[0];
|
|
77
|
+
const replace = parts[1];
|
|
78
|
+
const flags = parts[2] || "";
|
|
79
|
+
return { search, replace, flags };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detect(pattern) {
|
|
83
|
+
// Sed-style: s/search/replace/flags
|
|
84
|
+
if (pattern.startsWith("s/")) {
|
|
85
|
+
const parsed = parseSed(pattern);
|
|
86
|
+
if (parsed) return "sed";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Explicit regex: /pattern/ or /pattern/flags
|
|
90
|
+
if (
|
|
91
|
+
pattern.startsWith("/") &&
|
|
92
|
+
pattern.length > 2 &&
|
|
93
|
+
/\/[gimsuy]*$/.test(pattern.slice(1))
|
|
94
|
+
) {
|
|
95
|
+
const lastSlash = pattern.lastIndexOf("/");
|
|
96
|
+
if (lastSlash > 0) return "regex";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// JSONPath
|
|
100
|
+
if (
|
|
101
|
+
(pattern.startsWith("$.") ||
|
|
102
|
+
pattern.startsWith("$[") ||
|
|
103
|
+
pattern.startsWith("$..")) &&
|
|
104
|
+
isPlausibleJsonPath(pattern)
|
|
105
|
+
) {
|
|
106
|
+
return "jsonpath";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// XPath
|
|
110
|
+
if (
|
|
111
|
+
pattern.startsWith("//") &&
|
|
112
|
+
pattern.length > 2 &&
|
|
113
|
+
/[a-zA-Z_*@.]/.test(pattern[2])
|
|
114
|
+
) {
|
|
115
|
+
return "xpath";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (pattern.startsWith("/")) {
|
|
119
|
+
if (hasXPathAxis(pattern)) return "xpath";
|
|
120
|
+
if (hasXPathPredicate(pattern)) return "xpath";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Glob: ** or unescaped * ? not after .
|
|
124
|
+
if (pattern.includes("**")) return "glob";
|
|
125
|
+
if (/(?:^|[^.\\])[*?]/.test(pattern)) return "glob";
|
|
126
|
+
|
|
127
|
+
// Everything else is literal
|
|
128
|
+
return "literal";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Compilation ---
|
|
132
|
+
|
|
133
|
+
function globToRegex(glob) {
|
|
134
|
+
let result = "";
|
|
135
|
+
for (let i = 0; i < glob.length; i++) {
|
|
136
|
+
const c = glob[i];
|
|
137
|
+
if (c === "*") result += ".*";
|
|
138
|
+
else if (c === "?") result += ".";
|
|
139
|
+
else if (c === "[") {
|
|
140
|
+
const close = glob.indexOf("]", i + 1);
|
|
141
|
+
if (close === -1) {
|
|
142
|
+
result += "\\[";
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
result += glob.slice(i, close + 1);
|
|
146
|
+
i = close;
|
|
147
|
+
} else if (/[.+^${}()|\\]/.test(c)) {
|
|
148
|
+
result += `\\${c}`;
|
|
149
|
+
} else result += c;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseRegex(pattern) {
|
|
155
|
+
const lastSlash = pattern.lastIndexOf("/");
|
|
156
|
+
const body = pattern.slice(1, lastSlash);
|
|
157
|
+
const flags = pattern.slice(lastSlash + 1);
|
|
158
|
+
return { body, flags };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseJsonPath(path) {
|
|
162
|
+
const segments = [];
|
|
163
|
+
let i = path.startsWith("$") ? 1 : 0;
|
|
164
|
+
|
|
165
|
+
while (i < path.length) {
|
|
166
|
+
if (path[i] === ".") {
|
|
167
|
+
if (path[i + 1] === ".") {
|
|
168
|
+
segments.push({ type: "recursive" });
|
|
169
|
+
i += 2;
|
|
170
|
+
const start = i;
|
|
171
|
+
while (i < path.length && path[i] !== "." && path[i] !== "[") i++;
|
|
172
|
+
const key = path.slice(start, i);
|
|
173
|
+
if (key === "*") segments.push({ type: "wildcard" });
|
|
174
|
+
else if (key) segments.push({ type: "key", value: key });
|
|
175
|
+
} else {
|
|
176
|
+
i++;
|
|
177
|
+
const start = i;
|
|
178
|
+
while (i < path.length && path[i] !== "." && path[i] !== "[") i++;
|
|
179
|
+
const key = path.slice(start, i);
|
|
180
|
+
if (key === "*") segments.push({ type: "wildcard" });
|
|
181
|
+
else if (key) segments.push({ type: "key", value: key });
|
|
182
|
+
}
|
|
183
|
+
} else if (path[i] === "[") {
|
|
184
|
+
i++;
|
|
185
|
+
if (path[i] === "*") {
|
|
186
|
+
segments.push({ type: "wildcard" });
|
|
187
|
+
i += 2;
|
|
188
|
+
} else if (path[i] === "'" || path[i] === '"') {
|
|
189
|
+
const quote = path[i];
|
|
190
|
+
i++;
|
|
191
|
+
const start = i;
|
|
192
|
+
while (i < path.length && path[i] !== quote) i++;
|
|
193
|
+
segments.push({ type: "key", value: path.slice(start, i) });
|
|
194
|
+
i += 2;
|
|
195
|
+
} else {
|
|
196
|
+
const start = i;
|
|
197
|
+
while (i < path.length && path[i] !== "]") i++;
|
|
198
|
+
segments.push({
|
|
199
|
+
type: "index",
|
|
200
|
+
value: parseInt(path.slice(start, i), 10),
|
|
201
|
+
});
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return segments;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function compile(pattern) {
|
|
212
|
+
const type = detect(pattern);
|
|
213
|
+
switch (type) {
|
|
214
|
+
case "literal":
|
|
215
|
+
return { type, pattern };
|
|
216
|
+
case "glob":
|
|
217
|
+
return {
|
|
218
|
+
type,
|
|
219
|
+
anchoredRe: new RegExp(`^${globToRegex(pattern)}$`),
|
|
220
|
+
searchRe: new RegExp(globToRegex(pattern)),
|
|
221
|
+
};
|
|
222
|
+
case "regex": {
|
|
223
|
+
const { body, flags } = parseRegex(pattern);
|
|
224
|
+
return {
|
|
225
|
+
type,
|
|
226
|
+
re: new RegExp(body, flags || undefined),
|
|
227
|
+
reGlobal: new RegExp(body, flags.includes("g") ? flags : `${flags}g`),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "sed": {
|
|
231
|
+
const parsed = parseSed(pattern);
|
|
232
|
+
const isRegex = parsed.flags.length > 0;
|
|
233
|
+
return {
|
|
234
|
+
type,
|
|
235
|
+
search: parsed.search,
|
|
236
|
+
replace: parsed.replace,
|
|
237
|
+
flags: parsed.flags,
|
|
238
|
+
isRegex,
|
|
239
|
+
re: isRegex ? new RegExp(parsed.search, parsed.flags) : null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
case "xpath":
|
|
243
|
+
return { type, expr: pattern };
|
|
244
|
+
case "jsonpath":
|
|
245
|
+
return { type, segments: parseJsonPath(pattern) };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- XPath evaluation ---
|
|
250
|
+
|
|
251
|
+
function evalXpath(expr, string) {
|
|
252
|
+
try {
|
|
253
|
+
const dom = new JSDOM(string, { contentType: "text/xml" });
|
|
254
|
+
const doc = dom.window.document;
|
|
255
|
+
const result = doc.evaluate(expr, doc, null, 0, null);
|
|
256
|
+
const node = result.iterateNext();
|
|
257
|
+
if (!node) return null;
|
|
258
|
+
return { match: node.textContent, node };
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- JSONPath evaluation ---
|
|
265
|
+
|
|
266
|
+
function evalJsonPath(segments, string) {
|
|
267
|
+
let current;
|
|
268
|
+
try {
|
|
269
|
+
current = [JSON.parse(string)];
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const seg of segments) {
|
|
275
|
+
const next = [];
|
|
276
|
+
for (const node of current) {
|
|
277
|
+
if (node === null || node === undefined) continue;
|
|
278
|
+
switch (seg.type) {
|
|
279
|
+
case "key":
|
|
280
|
+
if (typeof node === "object" && seg.value in node) {
|
|
281
|
+
next.push(node[seg.value]);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case "index":
|
|
285
|
+
if (Array.isArray(node) && seg.value < node.length) {
|
|
286
|
+
next.push(node[seg.value]);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
case "wildcard":
|
|
290
|
+
if (Array.isArray(node)) next.push(...node);
|
|
291
|
+
else if (typeof node === "object") next.push(...Object.values(node));
|
|
292
|
+
break;
|
|
293
|
+
case "recursive":
|
|
294
|
+
next.push(node);
|
|
295
|
+
collectDescendants(node, next);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (next.length === 0) return null;
|
|
300
|
+
current = next;
|
|
301
|
+
}
|
|
302
|
+
return current.length > 0 ? current : null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function collectDescendants(node, out) {
|
|
306
|
+
if (node === null || typeof node !== "object") return;
|
|
307
|
+
const values = Array.isArray(node) ? node : Object.values(node);
|
|
308
|
+
for (const v of values) {
|
|
309
|
+
if (v !== null && typeof v === "object") {
|
|
310
|
+
out.push(v);
|
|
311
|
+
collectDescendants(v, out);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- Public API ---
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* hedmatch — does the pattern match the ENTIRE string?
|
|
320
|
+
* For path matching, WHERE clauses, full-string comparison.
|
|
321
|
+
*/
|
|
322
|
+
export function hedmatch(pattern, string) {
|
|
323
|
+
if (string === null) return false;
|
|
324
|
+
|
|
325
|
+
let compiled = cache.get(pattern);
|
|
326
|
+
if (!compiled) {
|
|
327
|
+
compiled = compile(pattern);
|
|
328
|
+
cache.set(pattern, compiled);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
switch (compiled.type) {
|
|
332
|
+
case "literal":
|
|
333
|
+
return string === compiled.pattern;
|
|
334
|
+
case "glob":
|
|
335
|
+
return compiled.anchoredRe.test(string);
|
|
336
|
+
case "regex":
|
|
337
|
+
return compiled.re.test(string);
|
|
338
|
+
case "sed":
|
|
339
|
+
return compiled.isRegex
|
|
340
|
+
? compiled.re.test(string)
|
|
341
|
+
: string.includes(compiled.search);
|
|
342
|
+
case "xpath":
|
|
343
|
+
return evalXpath(compiled.expr, string) !== null;
|
|
344
|
+
case "jsonpath":
|
|
345
|
+
return evalJsonPath(compiled.segments, string) !== null;
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* hedsearch — find the pattern anywhere IN the string.
|
|
352
|
+
* For substring search, content filtering, "does this text contain...".
|
|
353
|
+
* Returns { found, match, index } or { found: false }.
|
|
354
|
+
*/
|
|
355
|
+
export function hedsearch(pattern, string) {
|
|
356
|
+
if (string === null) return { found: false };
|
|
357
|
+
|
|
358
|
+
let compiled = cache.get(pattern);
|
|
359
|
+
if (!compiled) {
|
|
360
|
+
compiled = compile(pattern);
|
|
361
|
+
cache.set(pattern, compiled);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
switch (compiled.type) {
|
|
365
|
+
case "literal": {
|
|
366
|
+
const idx = string.indexOf(compiled.pattern);
|
|
367
|
+
if (idx === -1) return { found: false };
|
|
368
|
+
return { found: true, match: compiled.pattern, index: idx };
|
|
369
|
+
}
|
|
370
|
+
case "glob": {
|
|
371
|
+
const m = compiled.searchRe.exec(string);
|
|
372
|
+
if (!m) return { found: false };
|
|
373
|
+
return { found: true, match: m[0], index: m.index };
|
|
374
|
+
}
|
|
375
|
+
case "regex": {
|
|
376
|
+
const m = compiled.re.exec(string);
|
|
377
|
+
if (!m) return { found: false };
|
|
378
|
+
return { found: true, match: m[0], index: m.index };
|
|
379
|
+
}
|
|
380
|
+
case "sed": {
|
|
381
|
+
if (compiled.isRegex) {
|
|
382
|
+
compiled.re.lastIndex = 0;
|
|
383
|
+
const m = compiled.re.exec(string);
|
|
384
|
+
if (!m) return { found: false };
|
|
385
|
+
return { found: true, match: m[0], index: m.index };
|
|
386
|
+
}
|
|
387
|
+
const idx = string.indexOf(compiled.search);
|
|
388
|
+
if (idx === -1) return { found: false };
|
|
389
|
+
return { found: true, match: compiled.search, index: idx };
|
|
390
|
+
}
|
|
391
|
+
case "xpath": {
|
|
392
|
+
const result = evalXpath(compiled.expr, string);
|
|
393
|
+
if (!result) return { found: false };
|
|
394
|
+
return { found: true, match: result.match, index: 0 };
|
|
395
|
+
}
|
|
396
|
+
case "jsonpath": {
|
|
397
|
+
const result = evalJsonPath(compiled.segments, string);
|
|
398
|
+
if (!result) return { found: false };
|
|
399
|
+
return { found: true, match: result[0], index: 0 };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return { found: false };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* hedreplace — find pattern in string, replace with replacement.
|
|
407
|
+
* Returns the new string, or null if pattern not found.
|
|
408
|
+
*/
|
|
409
|
+
export function hedreplace(pattern, replacement, string) {
|
|
410
|
+
if (string === null) return null;
|
|
411
|
+
|
|
412
|
+
let compiled = cache.get(pattern);
|
|
413
|
+
if (!compiled) {
|
|
414
|
+
compiled = compile(pattern);
|
|
415
|
+
cache.set(pattern, compiled);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
switch (compiled.type) {
|
|
419
|
+
case "literal": {
|
|
420
|
+
if (!string.includes(compiled.pattern)) return null;
|
|
421
|
+
return string.replaceAll(compiled.pattern, replacement);
|
|
422
|
+
}
|
|
423
|
+
case "glob": {
|
|
424
|
+
if (!compiled.searchRe.test(string)) return null;
|
|
425
|
+
return string.replace(compiled.searchRe, replacement);
|
|
426
|
+
}
|
|
427
|
+
case "regex": {
|
|
428
|
+
if (!compiled.re.test(string)) return null;
|
|
429
|
+
compiled.re.lastIndex = 0;
|
|
430
|
+
compiled.reGlobal.lastIndex = 0;
|
|
431
|
+
return string.replace(compiled.reGlobal, replacement);
|
|
432
|
+
}
|
|
433
|
+
case "sed": {
|
|
434
|
+
// For sed, replacement is embedded in the pattern. Ignore the argument.
|
|
435
|
+
if (compiled.isRegex) {
|
|
436
|
+
compiled.re.lastIndex = 0;
|
|
437
|
+
if (!compiled.re.test(string)) return null;
|
|
438
|
+
compiled.re.lastIndex = 0;
|
|
439
|
+
return string.replace(compiled.re, compiled.replace);
|
|
440
|
+
}
|
|
441
|
+
if (!string.includes(compiled.search)) return null;
|
|
442
|
+
return string.replaceAll(compiled.search, compiled.replace);
|
|
443
|
+
}
|
|
444
|
+
case "xpath":
|
|
445
|
+
case "jsonpath":
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// SQL functions are in separate files (hedmatch.js, hedsearch.js)
|
|
452
|
+
// that import from this library. Filename = SQL function name.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sed syntax parsing. Handles s/search/replace/flags with:
|
|
3
|
+
* - Escaped delimiters (\\/)
|
|
4
|
+
* - Chained commands (s/a/b/ s/c/d/)
|
|
5
|
+
* - Flag extraction (g, i, m, s, v)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function splitSed(str) {
|
|
9
|
+
const parts = [];
|
|
10
|
+
let current = "";
|
|
11
|
+
for (let i = 0; i < str.length; i++) {
|
|
12
|
+
if (str[i] === "\\" && i + 1 < str.length) {
|
|
13
|
+
current += str[i] + str[i + 1];
|
|
14
|
+
i++;
|
|
15
|
+
} else if (str[i] === "/") {
|
|
16
|
+
parts.push(current);
|
|
17
|
+
current = "";
|
|
18
|
+
} else {
|
|
19
|
+
current += str[i];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
parts.push(current);
|
|
23
|
+
return parts;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseSed(input) {
|
|
27
|
+
if (!input.startsWith("s/")) return null;
|
|
28
|
+
|
|
29
|
+
const blocks = [];
|
|
30
|
+
let remaining = input;
|
|
31
|
+
while (remaining.startsWith("s/")) {
|
|
32
|
+
const parts = splitSed(remaining.slice(2));
|
|
33
|
+
if (parts.length < 2) break;
|
|
34
|
+
const flags = (parts[2] || "").match(/^[gimsv]*/)?.[0] || "";
|
|
35
|
+
blocks.push({
|
|
36
|
+
search: parts[0].replaceAll("\\/", "/"),
|
|
37
|
+
replace: parts[1].replaceAll("\\/", "/"),
|
|
38
|
+
flags,
|
|
39
|
+
sed: true,
|
|
40
|
+
});
|
|
41
|
+
const rest = parts.slice(2).join("/");
|
|
42
|
+
const next = rest.indexOf("s/");
|
|
43
|
+
remaining = next >= 0 ? rest.slice(next) : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (blocks.length === 0) return null;
|
|
47
|
+
return blocks;
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for pattern-based tool results.
|
|
3
|
+
* Used by get, set, store, and rm tools.
|
|
4
|
+
*/
|
|
5
|
+
export async function storePatternResult(
|
|
6
|
+
store,
|
|
7
|
+
runId,
|
|
8
|
+
turn,
|
|
9
|
+
scheme,
|
|
10
|
+
path,
|
|
11
|
+
bodyFilter,
|
|
12
|
+
matches,
|
|
13
|
+
preview = false,
|
|
14
|
+
) {
|
|
15
|
+
const slug = await store.slugPath(runId, scheme, path);
|
|
16
|
+
const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
|
|
17
|
+
const total = matches.reduce((s, m) => s + m.tokens_full, 0);
|
|
18
|
+
const listing = matches.map((m) => `${m.path} (${m.tokens_full})`).join("\n");
|
|
19
|
+
const prefix = preview ? "PREVIEW " : "";
|
|
20
|
+
const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
|
|
21
|
+
await store.upsert(runId, turn, slug, body, "pattern");
|
|
22
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import PluginContext from "../hooks/PluginContext.js";
|
|
6
|
+
|
|
7
|
+
const instances = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dynamically loads and registers plugins from provided directories
|
|
11
|
+
* and RUMMY_PLUGIN_* env vars.
|
|
12
|
+
*/
|
|
13
|
+
export async function registerPlugins(dirs = [], hooks) {
|
|
14
|
+
const uniqueDirs = [...new Set(dirs.map((d) => join(d)))];
|
|
15
|
+
|
|
16
|
+
for (const dir of uniqueDirs) {
|
|
17
|
+
await scanDir(dir, hooks, true);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await loadEnvPlugins(hooks);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const AUDIT_SCHEMES = [
|
|
24
|
+
"instructions",
|
|
25
|
+
"system",
|
|
26
|
+
"prompt",
|
|
27
|
+
"ask",
|
|
28
|
+
"act",
|
|
29
|
+
"progress",
|
|
30
|
+
"reasoning",
|
|
31
|
+
"model",
|
|
32
|
+
"error",
|
|
33
|
+
"user",
|
|
34
|
+
"assistant",
|
|
35
|
+
"content",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* After DB is ready, inject db and store into all PluginContext instances,
|
|
40
|
+
* upsert declared schemes, and bootstrap audit schemes.
|
|
41
|
+
*/
|
|
42
|
+
export async function initPlugins(db, store, hooks) {
|
|
43
|
+
for (const name of AUDIT_SCHEMES) {
|
|
44
|
+
const scheme = {
|
|
45
|
+
name,
|
|
46
|
+
fidelity: ["ask", "act", "progress"].includes(name) ? "full" : "null",
|
|
47
|
+
model_visible: ["ask", "act", "progress"].includes(name) ? 1 : 0,
|
|
48
|
+
valid_states: JSON.stringify(["info"]),
|
|
49
|
+
category: "audit",
|
|
50
|
+
};
|
|
51
|
+
await db.upsert_scheme.run(scheme);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const ctx of instances.values()) {
|
|
55
|
+
ctx.db = db;
|
|
56
|
+
ctx.entries = store;
|
|
57
|
+
for (const scheme of ctx.schemes) {
|
|
58
|
+
await db.upsert_scheme.run(scheme);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Register default schemes for tools that plugins ensured but didn't registerScheme for
|
|
63
|
+
if (hooks) {
|
|
64
|
+
const registered = new Set();
|
|
65
|
+
for (const ctx of instances.values()) {
|
|
66
|
+
for (const s of ctx.schemes) registered.add(s.name);
|
|
67
|
+
}
|
|
68
|
+
for (const name of AUDIT_SCHEMES) registered.add(name);
|
|
69
|
+
|
|
70
|
+
for (const toolName of hooks.tools.names) {
|
|
71
|
+
if (registered.has(toolName)) continue;
|
|
72
|
+
await db.upsert_scheme.run({
|
|
73
|
+
name: toolName,
|
|
74
|
+
fidelity: "full",
|
|
75
|
+
model_visible: 1,
|
|
76
|
+
valid_states: JSON.stringify([
|
|
77
|
+
"full",
|
|
78
|
+
"proposed",
|
|
79
|
+
"pass",
|
|
80
|
+
"rejected",
|
|
81
|
+
"error",
|
|
82
|
+
"info",
|
|
83
|
+
]),
|
|
84
|
+
category: "result",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadEnvPlugins(hooks) {
|
|
91
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
92
|
+
if (!key.startsWith("RUMMY_PLUGIN_") || !value) continue;
|
|
93
|
+
const name = key.replace("RUMMY_PLUGIN_", "").toLowerCase();
|
|
94
|
+
try {
|
|
95
|
+
const { default: Plugin } = await import(value);
|
|
96
|
+
if (typeof Plugin?.register === "function") {
|
|
97
|
+
await Plugin.register(hooks);
|
|
98
|
+
} else if (typeof Plugin === "function") {
|
|
99
|
+
const ctx = new PluginContext(name, hooks);
|
|
100
|
+
new Plugin(ctx);
|
|
101
|
+
instances.set(name, ctx);
|
|
102
|
+
}
|
|
103
|
+
console.log(`[RUMMY] Plugin ${name}: ${value}`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.warn(`[RUMMY] Plugin ${name} (${value}): ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function scanDir(dir, hooks, isRoot = false) {
|
|
111
|
+
if (!existsSync(dir)) return;
|
|
112
|
+
|
|
113
|
+
let dirStats;
|
|
114
|
+
try {
|
|
115
|
+
dirStats = await stat(dir);
|
|
116
|
+
} catch (_err) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!dirStats.isDirectory()) {
|
|
121
|
+
if (process.env.RUMMY_DEBUG === "true") {
|
|
122
|
+
console.error(
|
|
123
|
+
`[RUMMY] Cannot scan plugin directory (not a directory): ${dir}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(dir);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (process.env.RUMMY_DEBUG === "true") {
|
|
134
|
+
console.error(`[RUMMY] Failed to read directory ${dir}:`, err.message);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const name of entries) {
|
|
140
|
+
if (name.endsWith(".test.js")) continue;
|
|
141
|
+
|
|
142
|
+
const fullPath = join(dir, name);
|
|
143
|
+
let stats;
|
|
144
|
+
try {
|
|
145
|
+
stats = await stat(fullPath);
|
|
146
|
+
} catch (_err) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (stats.isFile() && name.endsWith(".js")) {
|
|
151
|
+
if (name === "index.js" || name === `${basename(dir)}.js`) {
|
|
152
|
+
await loadPlugin(fullPath, hooks);
|
|
153
|
+
} else if (isRoot && name !== "index.js") {
|
|
154
|
+
await loadPlugin(fullPath, hooks);
|
|
155
|
+
}
|
|
156
|
+
} else if (stats.isDirectory()) {
|
|
157
|
+
await scanDir(fullPath, hooks, false);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadPlugin(filePath, hooks) {
|
|
163
|
+
try {
|
|
164
|
+
const url = pathToFileURL(filePath).href;
|
|
165
|
+
const { default: Plugin } = await import(url);
|
|
166
|
+
|
|
167
|
+
if (typeof Plugin?.register === "function") {
|
|
168
|
+
await Plugin.register(hooks);
|
|
169
|
+
} else if (typeof Plugin === "function") {
|
|
170
|
+
const name = basename(filePath, ".js");
|
|
171
|
+
const ctx = new PluginContext(name, hooks);
|
|
172
|
+
const _instance = new Plugin(ctx);
|
|
173
|
+
instances.set(name, ctx);
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (process.env.RUMMY_DEBUG === "true") {
|
|
177
|
+
console.error(`[RUMMY] Plugin load failed at ${filePath}:`, err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# instructions
|
|
2
|
+
|
|
3
|
+
Projects the system prompt instructions into model context.
|
|
4
|
+
|
|
5
|
+
## Registration
|
|
6
|
+
|
|
7
|
+
- **Projection**: `onProject("instructions", ...)` — no tool handler.
|
|
8
|
+
|
|
9
|
+
## Behavior
|
|
10
|
+
|
|
11
|
+
Replaces the `[%TOOLS%]` placeholder in the prompt body with the `tools` attribute. Appends tool descriptions and persona text when present in attributes.
|