opencode-irf 0.0.5 → 0.0.6
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/dist/irf.d.ts +4 -0
- package/dist/irf.d.ts.map +1 -0
- package/dist/irf.js +125 -626
- package/dist/irf.js.map +1 -0
- package/dist/src/discover.d.ts +11 -0
- package/dist/src/discover.d.ts.map +1 -0
- package/dist/src/discover.js +92 -0
- package/dist/src/discover.js.map +1 -0
- package/dist/src/process.d.ts +26 -0
- package/dist/src/process.d.ts.map +1 -0
- package/dist/src/process.js +56 -0
- package/dist/src/process.js.map +1 -0
- package/dist/src/prompt.d.ts +7 -0
- package/dist/src/prompt.d.ts.map +1 -0
- package/dist/src/prompt.js +84 -0
- package/dist/src/prompt.js.map +1 -0
- package/dist/src/schema.d.ts +69 -0
- package/dist/src/schema.d.ts.map +1 -0
- package/dist/src/schema.js +48 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/session.d.ts +23 -0
- package/dist/src/session.d.ts.map +1 -0
- package/dist/src/session.js +112 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/utils/compare.d.ts +18 -0
- package/dist/src/utils/compare.d.ts.map +1 -0
- package/dist/src/utils/compare.js +74 -0
- package/dist/src/utils/compare.js.map +1 -0
- package/dist/src/utils/extractLlmError.d.ts +13 -0
- package/dist/src/utils/extractLlmError.d.ts.map +1 -0
- package/dist/src/utils/extractLlmError.js +12 -0
- package/dist/src/utils/extractLlmError.js.map +1 -0
- package/dist/src/utils/safe.d.ts +12 -0
- package/dist/src/utils/safe.d.ts.map +1 -0
- package/dist/src/utils/safe.js +31 -0
- package/dist/src/utils/safe.js.map +1 -0
- package/dist/src/utils/stripCodeFences.d.ts +2 -0
- package/dist/src/utils/stripCodeFences.d.ts.map +1 -0
- package/dist/src/utils/stripCodeFences.js +9 -0
- package/dist/src/utils/stripCodeFences.js.map +1 -0
- package/dist/src/utils/validate.d.ts +19 -0
- package/dist/src/utils/validate.d.ts.map +1 -0
- package/dist/src/utils/validate.js +30 -0
- package/dist/src/utils/validate.js.map +1 -0
- package/package.json +5 -3
package/dist/irf.js
CHANGED
|
@@ -1,632 +1,131 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} catch (error) {
|
|
17
|
-
return {
|
|
18
|
-
data: null,
|
|
19
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
var safeAsync = async (fn) => {
|
|
24
|
-
try {
|
|
25
|
-
const data = await fn();
|
|
26
|
-
return {
|
|
27
|
-
data,
|
|
28
|
-
error: null
|
|
29
|
-
};
|
|
30
|
-
} catch (error) {
|
|
31
|
-
return {
|
|
32
|
-
data: null,
|
|
33
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// src/discover.ts
|
|
39
|
-
var readConfig = async (directory) => {
|
|
40
|
-
const configPath = join(directory, "opencode.json");
|
|
41
|
-
const { data, error } = await safeAsync(() => readFile(configPath, "utf-8"));
|
|
42
|
-
if (error) {
|
|
43
|
-
return {
|
|
44
|
-
data: null,
|
|
45
|
-
error: "Could not read " + configPath + ": " + error.message
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const { data: parsed, error: parseError } = safe(() => JSON.parse(data));
|
|
49
|
-
if (parseError) {
|
|
50
|
-
return {
|
|
51
|
-
data: null,
|
|
52
|
-
error: "Invalid JSON in " + configPath + ": " + parseError.message
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
const instructions = parsed.instructions;
|
|
56
|
-
if (!instructions || !Array.isArray(instructions) || instructions.length === 0) {
|
|
57
|
-
return {
|
|
58
|
-
data: null,
|
|
59
|
-
error: 'No "instructions" array found in ' + configPath
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const patterns = instructions.filter((entry) => typeof entry === "string");
|
|
63
|
-
if (patterns.length === 0) {
|
|
64
|
-
return {
|
|
65
|
-
data: null,
|
|
66
|
-
error: 'No valid string patterns in "instructions" in ' + configPath
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
data: patterns,
|
|
71
|
-
error: null
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
var resolveFiles = async (directory, patterns) => {
|
|
75
|
-
const seen = new Set;
|
|
76
|
-
const files = [];
|
|
77
|
-
for (const pattern of patterns) {
|
|
78
|
-
for await (const path of glob(pattern, { cwd: directory })) {
|
|
79
|
-
const full = join(directory, path);
|
|
80
|
-
if (!seen.has(full)) {
|
|
81
|
-
seen.add(full);
|
|
82
|
-
files.push(full);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return files;
|
|
87
|
-
};
|
|
88
|
-
var readFiles = async (files) => {
|
|
89
|
-
const results = [];
|
|
90
|
-
for (const file of files) {
|
|
91
|
-
const { data, error } = await safeAsync(() => readFile(file, "utf-8"));
|
|
92
|
-
if (error) {
|
|
93
|
-
results.push({ path: file, content: "", error: error.message });
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
results.push({ path: file, content: data });
|
|
97
|
-
}
|
|
98
|
-
return results;
|
|
99
|
-
};
|
|
100
|
-
var readFilePaths = async (directory, paths) => {
|
|
101
|
-
const resolved = paths.map((p) => resolve(directory, p));
|
|
102
|
-
return await readFiles(resolved);
|
|
103
|
-
};
|
|
104
|
-
var discover = async (directory) => {
|
|
105
|
-
const config = await readConfig(directory);
|
|
106
|
-
if (config.error !== null) {
|
|
107
|
-
return {
|
|
108
|
-
data: null,
|
|
109
|
-
error: config.error
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
const patterns = config.data;
|
|
113
|
-
const files = await resolveFiles(directory, patterns);
|
|
114
|
-
if (files.length === 0) {
|
|
115
|
-
return {
|
|
116
|
-
data: null,
|
|
117
|
-
error: "No instruction files found matching patterns: " + patterns.join(", ")
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const results = await readFiles(files);
|
|
121
|
-
return {
|
|
122
|
-
data: results,
|
|
123
|
-
error: null
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// src/process.ts
|
|
128
|
-
import { writeFile } from "node:fs/promises";
|
|
129
|
-
import { basename } from "node:path";
|
|
130
|
-
|
|
131
|
-
// src/schema.ts
|
|
132
|
-
import { z } from "zod";
|
|
133
|
-
var StrengthSchema = z.enum([
|
|
134
|
-
"obligatory",
|
|
135
|
-
"permissible",
|
|
136
|
-
"forbidden",
|
|
137
|
-
"optional",
|
|
138
|
-
"supererogatory",
|
|
139
|
-
"indifferent",
|
|
140
|
-
"omissible"
|
|
141
|
-
]).describe("Deontic modality expressing enforcement strength");
|
|
142
|
-
var ActionSchema = z.string().describe("Imperative verb describing the action to take");
|
|
143
|
-
var TargetSchema = z.string().describe("Object or subject the action applies to");
|
|
144
|
-
var ContextSchema = z.string().describe("Condition, scope, or circumstance when the rule applies");
|
|
145
|
-
var ReasonSchema = z.string().describe("Justification for why the rule exists");
|
|
146
|
-
var ParsedRuleSchema = z.object({
|
|
147
|
-
strength: StrengthSchema,
|
|
148
|
-
action: ActionSchema,
|
|
149
|
-
target: TargetSchema,
|
|
150
|
-
context: ContextSchema.optional(),
|
|
151
|
-
reason: ReasonSchema
|
|
152
|
-
}).describe("Single instruction decomposed into deontic components");
|
|
153
|
-
var RuleSchema = z.string().describe("Human-readable rule derived from parsed components");
|
|
154
|
-
var ParsedSchema = z.array(ParsedRuleSchema).describe("Array of parsed rules");
|
|
155
|
-
var ParseResponseSchema = z.object({
|
|
156
|
-
rules: ParsedSchema
|
|
157
|
-
});
|
|
158
|
-
var FormatResponseSchema = z.object({
|
|
159
|
-
rules: z.array(RuleSchema)
|
|
160
|
-
});
|
|
161
|
-
var parseSchemaExample = JSON.stringify({
|
|
162
|
-
rules: [{
|
|
163
|
-
strength: StrengthSchema.options.join("/"),
|
|
164
|
-
action: ActionSchema.description || "verb",
|
|
165
|
-
target: TargetSchema.description || "object",
|
|
166
|
-
context: ContextSchema.description || "optional condition",
|
|
167
|
-
reason: ReasonSchema.description || "justification"
|
|
168
|
-
}]
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// src/prompt.ts
|
|
172
|
-
var FORMAT_MODES = ["verbose", "balanced", "concise"];
|
|
173
|
-
var isFormatMode = (v) => typeof v === "string" && FORMAT_MODES.includes(v);
|
|
174
|
-
var buildParsePrompt = (input) => {
|
|
175
|
-
const instructions = [
|
|
176
|
-
"You are a rule parser that converts raw instructions into structured parsed rules.",
|
|
177
|
-
"Take the provided instructions and break them down into structured components.",
|
|
178
|
-
"Each rule should have: strength, action (verb), target (object), context (optional condition/scope), and reason (justification).",
|
|
179
|
-
"Focus on extracting the core components without adding extra details.",
|
|
180
|
-
"",
|
|
181
|
-
"Return ONLY valid JSON matching this exact schema:",
|
|
182
|
-
parseSchemaExample,
|
|
183
|
-
"",
|
|
184
|
-
"Do not include any text outside the JSON object. Do not wrap it in markdown code fences."
|
|
185
|
-
].join(`
|
|
186
|
-
`);
|
|
187
|
-
return [instructions, "Instructions to parse:", input].join(`
|
|
188
|
-
|
|
189
|
-
`);
|
|
190
|
-
};
|
|
191
|
-
var verboseInstructions = [
|
|
192
|
-
"You are a rule formatter that converts structured parsed rules into human-readable rules.",
|
|
193
|
-
"Take the provided parsed rule components and create natural language versions.",
|
|
194
|
-
"Every rule must include both a Rule line and a Reason line.",
|
|
195
|
-
"Each rule must follow this exact format:",
|
|
196
|
-
"",
|
|
197
|
-
"Rule: <clear, concise, actionable statement>",
|
|
198
|
-
"Reason: <justification from the parsed rule>",
|
|
199
|
-
"",
|
|
200
|
-
"Each human-readable rule should directly correspond to the parsed components without adding extra details.",
|
|
201
|
-
"Make the rules clear, concise, and actionable.",
|
|
202
|
-
"",
|
|
203
|
-
"Return ONLY valid JSON matching this exact schema:",
|
|
204
|
-
'{"rules": ["Rule: ...\\nReason: ...", "Rule: ...\\nReason: ..."]}',
|
|
205
|
-
"",
|
|
206
|
-
"Do not include any text outside the JSON object. Do not wrap it in markdown code fences."
|
|
207
|
-
];
|
|
208
|
-
var balancedInstructions = [
|
|
209
|
-
"You are a rule formatter that converts structured parsed rules into human-readable rules.",
|
|
210
|
-
"Take the provided parsed rule components and create natural language versions.",
|
|
211
|
-
"Use your judgment for each rule:",
|
|
212
|
-
"- If the rule is non-obvious or counterintuitive, include both the Rule and Reason lines.",
|
|
213
|
-
"- If the rule is self-explanatory, include only the Rule line and omit the Reason.",
|
|
214
|
-
"",
|
|
215
|
-
"Format rules that include a reason:",
|
|
216
|
-
"Rule: <clear, concise, actionable statement>",
|
|
217
|
-
"Reason: <justification from the parsed rule>",
|
|
218
|
-
"",
|
|
219
|
-
"Format rules that are self-explanatory:",
|
|
220
|
-
"Rule: <clear, concise, actionable statement>",
|
|
221
|
-
"",
|
|
222
|
-
"Each human-readable rule should directly correspond to the parsed components without adding extra details.",
|
|
223
|
-
"Make the rules clear, concise, and actionable.",
|
|
224
|
-
"",
|
|
225
|
-
"Return ONLY valid JSON matching this exact schema:",
|
|
226
|
-
'{"rules": ["Rule: ...\\nReason: ...", "Rule: ..."]}',
|
|
227
|
-
"",
|
|
228
|
-
"Do not include any text outside the JSON object. Do not wrap it in markdown code fences."
|
|
229
|
-
];
|
|
230
|
-
var conciseInstructions = [
|
|
231
|
-
"You are a rule formatter that converts structured parsed rules into concise directives.",
|
|
232
|
-
"Take the provided parsed rule components and create a bullet list of clear directives.",
|
|
233
|
-
"Do not include reasons or justifications. Output only the actionable statement.",
|
|
234
|
-
'Each rule must be a single line starting with "- " (dash space).',
|
|
235
|
-
"",
|
|
236
|
-
"Each directive should directly correspond to the parsed components without adding extra details.",
|
|
237
|
-
"Make the directives clear, concise, and actionable.",
|
|
238
|
-
"",
|
|
239
|
-
"Return ONLY valid JSON matching this exact schema:",
|
|
240
|
-
'{"rules": ["- ...", "- ..."]}',
|
|
241
|
-
"",
|
|
242
|
-
"Do not include any text outside the JSON object. Do not wrap it in markdown code fences."
|
|
243
|
-
];
|
|
244
|
-
var formatInstructions = {
|
|
245
|
-
verbose: verboseInstructions,
|
|
246
|
-
balanced: balancedInstructions,
|
|
247
|
-
concise: conciseInstructions
|
|
248
|
-
};
|
|
249
|
-
var buildFormatPrompt = (parsedRulesJson, mode = "balanced") => {
|
|
250
|
-
const instructions = formatInstructions[mode].join(`
|
|
251
|
-
`);
|
|
252
|
-
return [instructions, "Parsed rules to convert:", parsedRulesJson].join(`
|
|
253
|
-
|
|
254
|
-
`);
|
|
255
|
-
};
|
|
256
|
-
var buildRetryPrompt = (errorMessage) => {
|
|
257
|
-
return "Your previous response was invalid. " + errorMessage + `
|
|
258
|
-
|
|
259
|
-
Return ONLY valid JSON. Do not include any text outside the JSON object.`;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
// src/utils/compare.ts
|
|
263
|
-
var compareBytes = (file, original, generated) => {
|
|
264
|
-
const originalBytes = new TextEncoder().encode(original).length;
|
|
265
|
-
const generatedBytes = new TextEncoder().encode(generated).length;
|
|
266
|
-
const difference = originalBytes - generatedBytes;
|
|
267
|
-
const percentChange = originalBytes === 0 ? 0 : difference / originalBytes * 100;
|
|
268
|
-
return {
|
|
269
|
-
file,
|
|
270
|
-
originalBytes,
|
|
271
|
-
generatedBytes,
|
|
272
|
-
difference,
|
|
273
|
-
percentChange
|
|
274
|
-
};
|
|
275
|
-
};
|
|
276
|
-
var formatChange = (difference, percentChange) => {
|
|
277
|
-
if (difference > 0) {
|
|
278
|
-
return "−" + percentChange.toFixed(1) + "%";
|
|
279
|
-
}
|
|
280
|
-
return "+" + Math.abs(percentChange).toFixed(1) + "%";
|
|
281
|
-
};
|
|
282
|
-
var formatRow = (result) => {
|
|
283
|
-
const changeStr = formatChange(result.difference, result.percentChange);
|
|
284
|
-
return result.file.padEnd(25) + result.originalBytes.toString().padStart(10) + result.generatedBytes.toString().padStart(12) + result.difference.toString().padStart(8) + changeStr.padStart(10);
|
|
285
|
-
};
|
|
286
|
-
var summarize = (results) => {
|
|
287
|
-
const totalOriginal = results.reduce((sum, r) => sum + r.originalBytes, 0);
|
|
288
|
-
const totalGenerated = results.reduce((sum, r) => sum + r.generatedBytes, 0);
|
|
289
|
-
const totalDifference = totalOriginal - totalGenerated;
|
|
290
|
-
const totalPercentChange = totalOriginal === 0 ? 0 : totalDifference / totalOriginal * 100;
|
|
291
|
-
return {
|
|
292
|
-
totalOriginal,
|
|
293
|
-
totalGenerated,
|
|
294
|
-
totalDifference,
|
|
295
|
-
totalPercentChange
|
|
296
|
-
};
|
|
297
|
-
};
|
|
298
|
-
var buildTable = (results) => {
|
|
299
|
-
if (results.length === 0) {
|
|
300
|
-
return "";
|
|
301
|
-
}
|
|
302
|
-
const lines = [];
|
|
303
|
-
const header = "File".padEnd(25) + "Original".padStart(10) + "Generated".padStart(12) + "Diff".padStart(8) + "Change".padStart(10);
|
|
304
|
-
const separator = "─".repeat(65);
|
|
305
|
-
lines.push(header);
|
|
306
|
-
lines.push(separator);
|
|
307
|
-
const sorted = [...results].sort((a, b) => Math.abs(b.difference) - Math.abs(a.difference));
|
|
308
|
-
for (const result of sorted) {
|
|
309
|
-
lines.push(formatRow(result));
|
|
310
|
-
}
|
|
311
|
-
const totals = summarize(results);
|
|
312
|
-
lines.push(separator);
|
|
313
|
-
const totalChangeStr = formatChange(totals.totalDifference, totals.totalPercentChange);
|
|
314
|
-
lines.push("TOTAL".padEnd(25) + totals.totalOriginal.toString().padStart(10) + totals.totalGenerated.toString().padStart(12) + totals.totalDifference.toString().padStart(8) + totalChangeStr.padStart(10));
|
|
315
|
-
lines.push("");
|
|
316
|
-
lines.push((totals.totalDifference > 0 ? "SAVED " : "INCREASED ") + Math.abs(totals.totalDifference) + " bytes (" + Math.abs(totals.totalPercentChange).toFixed(1) + "%)");
|
|
317
|
-
return lines.join(`
|
|
318
|
-
`);
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
// src/process.ts
|
|
322
|
-
var processFile = async (options) => {
|
|
323
|
-
const { file, prompt, mode = "balanced" } = options;
|
|
324
|
-
if (file.error) {
|
|
325
|
-
return {
|
|
326
|
-
status: "readError",
|
|
327
|
-
path: file.path,
|
|
328
|
-
error: file.error
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
const parseResult = await prompt(buildParsePrompt(file.content), ParseResponseSchema);
|
|
332
|
-
if (parseResult.error !== null) {
|
|
333
|
-
return {
|
|
334
|
-
status: "parseError",
|
|
335
|
-
path: file.path,
|
|
336
|
-
error: String(parseResult.error)
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
const formatResult = await prompt(buildFormatPrompt(JSON.stringify(parseResult.data), mode), FormatResponseSchema);
|
|
340
|
-
if (formatResult.error !== null) {
|
|
341
|
-
return {
|
|
342
|
-
status: "formatError",
|
|
343
|
-
path: file.path,
|
|
344
|
-
error: String(formatResult.error)
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
const formattedRules = formatResult.data.rules;
|
|
348
|
-
const joiner = mode === "concise" ? `
|
|
349
|
-
` : `
|
|
350
|
-
|
|
351
|
-
`;
|
|
352
|
-
const content = formattedRules.join(joiner) + `
|
|
353
|
-
`;
|
|
354
|
-
const { error: writeError } = await safeAsync(() => writeFile(file.path, content, "utf-8"));
|
|
355
|
-
if (writeError) {
|
|
356
|
-
return {
|
|
357
|
-
status: "writeError",
|
|
358
|
-
path: file.path,
|
|
359
|
-
error: writeError.message
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
const comparison = compareBytes(basename(file.path), file.content, content);
|
|
363
|
-
return {
|
|
364
|
-
status: "success",
|
|
365
|
-
path: file.path,
|
|
366
|
-
rulesCount: formattedRules.length,
|
|
367
|
-
comparison
|
|
368
|
-
};
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
// src/utils/extractLlmError.ts
|
|
372
|
-
var extractLlmError = (info) => {
|
|
373
|
-
if (!info.error) {
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
const err = info.error;
|
|
377
|
-
if (err.data && err.data.message) {
|
|
378
|
-
return err.data.message;
|
|
379
|
-
}
|
|
380
|
-
return err.name || "Unknown LLM error";
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// src/utils/stripCodeFences.ts
|
|
384
|
-
var stripCodeFences = (text) => {
|
|
385
|
-
return text.replace(/^.*```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
// src/utils/validate.ts
|
|
389
|
-
var validateJson = (json, schema) => {
|
|
390
|
-
const { data: parsed, error } = safe(() => JSON.parse(json));
|
|
391
|
-
if (error) {
|
|
392
|
-
return {
|
|
393
|
-
data: null,
|
|
394
|
-
error: "parse"
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
const result = schema.safeParse(parsed);
|
|
398
|
-
if (!result.success) {
|
|
399
|
-
return {
|
|
400
|
-
data: null,
|
|
401
|
-
error: "schema",
|
|
402
|
-
issues: result.error.issues
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
return {
|
|
406
|
-
data: result.data,
|
|
407
|
-
error: null
|
|
408
|
-
};
|
|
409
|
-
};
|
|
410
|
-
var formatValidationError = (result) => {
|
|
411
|
-
if (result.error === "parse") {
|
|
412
|
-
return "Invalid JSON. Return valid JSON.";
|
|
413
|
-
}
|
|
414
|
-
const issues = result.issues.map((i) => " - " + i.path.join(".") + ": " + i.message).join(`
|
|
415
|
-
`);
|
|
416
|
-
return `Schema validation failed:
|
|
417
|
-
` + issues + `
|
|
418
|
-
|
|
419
|
-
Fix the issues and try again.`;
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
// src/session.ts
|
|
423
|
-
var MAX_RETRIES = 3;
|
|
424
|
-
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
425
|
-
var isPart = (v) => isRecord(v) && typeof v.type === "string";
|
|
426
|
-
var isPartArray = (v) => Array.isArray(v) && v.every(isPart);
|
|
427
|
-
var isMessageInfo = (v) => isRecord(v) && typeof v.role === "string";
|
|
428
|
-
var isMessageEntry = (v) => isRecord(v) && isMessageInfo(v.info) && isPartArray(v.parts);
|
|
429
|
-
var isMessageEntryArray = (v) => Array.isArray(v) && v.every(isMessageEntry);
|
|
430
|
-
var extractText = (parts) => {
|
|
431
|
-
return parts.filter((p) => p.type === "text" && p.text).map((p) => p.text || "").join("");
|
|
432
|
-
};
|
|
433
|
-
var detectModel = async (client, sessionId) => {
|
|
434
|
-
const messagesResult = await client.session.messages({
|
|
435
|
-
path: { id: sessionId }
|
|
436
|
-
});
|
|
437
|
-
if (!messagesResult.data) {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
const messages = messagesResult.data;
|
|
441
|
-
if (!isMessageEntryArray(messages)) {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
445
|
-
const info = messages[i].info;
|
|
446
|
-
if (info.role === "assistant" && info.providerID && info.modelID) {
|
|
447
|
-
return {
|
|
448
|
-
providerID: info.providerID,
|
|
449
|
-
modelID: info.modelID
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return null;
|
|
454
|
-
};
|
|
455
|
-
var promptWithRetry = async (options) => {
|
|
456
|
-
const { client, sessionId, initialPrompt, schema, model } = options;
|
|
457
|
-
let prompt = initialPrompt;
|
|
458
|
-
let lastError = "";
|
|
459
|
-
for (let attempt = 0;attempt < MAX_RETRIES; attempt++) {
|
|
460
|
-
const response = await client.session.prompt({
|
|
461
|
-
path: { id: sessionId },
|
|
462
|
-
body: {
|
|
463
|
-
parts: [{ type: "text", text: prompt }],
|
|
464
|
-
tools: {},
|
|
465
|
-
model
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
if (!response.data) {
|
|
469
|
-
return {
|
|
470
|
-
data: null,
|
|
471
|
-
error: "No response from LLM (attempt " + (attempt + 1) + ")"
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
const info = response.data.info;
|
|
475
|
-
if (!isMessageInfo(info)) {
|
|
476
|
-
return {
|
|
477
|
-
data: null,
|
|
478
|
-
error: "Unexpected response shape: missing info (attempt " + (attempt + 1) + ")"
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
const llmError = extractLlmError(info);
|
|
482
|
-
if (llmError) {
|
|
483
|
-
return {
|
|
484
|
-
data: null,
|
|
485
|
-
error: llmError
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
const parts = response.data.parts;
|
|
489
|
-
if (!isPartArray(parts)) {
|
|
490
|
-
return {
|
|
491
|
-
data: null,
|
|
492
|
-
error: "Unexpected response shape: missing parts (attempt " + (attempt + 1) + ")"
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
const text = extractText(parts);
|
|
496
|
-
if (!text) {
|
|
497
|
-
lastError = "Empty response";
|
|
498
|
-
prompt = buildRetryPrompt("Empty response. Return valid JSON.");
|
|
499
|
-
continue;
|
|
1
|
+
import { tool } from '@opencode-ai/plugin';
|
|
2
|
+
import { discover, readFilePaths } from './src/discover';
|
|
3
|
+
import { processFile } from './src/process';
|
|
4
|
+
import { isFormatMode } from './src/prompt';
|
|
5
|
+
import { detectModel, promptWithRetry } from './src/session';
|
|
6
|
+
import { buildTable } from './src/utils/compare';
|
|
7
|
+
import { safeAsync } from './src/utils/safe';
|
|
8
|
+
// resolve instruction files from explicit paths or opencode.json discovery
|
|
9
|
+
const resolveFiles = async (directory, filesArg) => {
|
|
10
|
+
if (filesArg) {
|
|
11
|
+
const paths = filesArg.split(',').map((p) => p.trim()).filter((p) => p.length > 0);
|
|
12
|
+
if (paths.length === 0) {
|
|
13
|
+
return { data: null, error: 'No valid file paths provided' };
|
|
14
|
+
}
|
|
15
|
+
return { data: await readFilePaths(directory, paths), error: null };
|
|
500
16
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
17
|
+
return await discover(directory);
|
|
18
|
+
};
|
|
19
|
+
const ERROR_LABELS = {
|
|
20
|
+
readError: 'Read failed',
|
|
21
|
+
parseError: 'Parse failed',
|
|
22
|
+
formatError: 'Format failed',
|
|
23
|
+
writeError: 'Write failed',
|
|
24
|
+
};
|
|
25
|
+
// format a file result into a markdown status line
|
|
26
|
+
const formatFileResult = (result) => {
|
|
27
|
+
if (result.status === 'success') {
|
|
28
|
+
return '**' + result.path + '**: ' + result.rulesCount + ' rules written';
|
|
508
29
|
}
|
|
509
|
-
return
|
|
510
|
-
data: validation.data,
|
|
511
|
-
error: null
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
return {
|
|
515
|
-
data: null,
|
|
516
|
-
error: "Failed after " + MAX_RETRIES + " attempts. Last error: " + lastError
|
|
517
|
-
};
|
|
30
|
+
return '**' + result.path + '**: ' + ERROR_LABELS[result.status] + ' - ' + result.error;
|
|
518
31
|
};
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const paths = filesArg.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
524
|
-
if (paths.length === 0) {
|
|
525
|
-
return { data: null, error: "No valid file paths provided" };
|
|
32
|
+
// append comparison table section to output lines
|
|
33
|
+
const appendComparisonTable = (lines, comparisons) => {
|
|
34
|
+
if (comparisons.length === 0) {
|
|
35
|
+
return;
|
|
526
36
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push('## Comparison');
|
|
39
|
+
lines.push('```');
|
|
40
|
+
lines.push(buildTable(comparisons));
|
|
41
|
+
lines.push('```');
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push('IMPORTANT: Show the comparison table above to the user exactly as-is.');
|
|
44
|
+
};
|
|
45
|
+
// deno-lint-ignore require-await
|
|
46
|
+
const plugin = async ({ directory, client }) => {
|
|
47
|
+
return {
|
|
48
|
+
tool: {
|
|
49
|
+
'irf-rewrite': tool({
|
|
50
|
+
description: [
|
|
51
|
+
'Discover instruction files from opencode.json, parse them into structured rules, format them into human-readable rules, and write the formatted rules back to the original files.',
|
|
52
|
+
'Accepts an optional mode: verbose (full Rule/Reason pairs), balanced (LLM decides which rules need reasons), or concise (bullet list, no reasons).',
|
|
53
|
+
'Defaults to balanced.',
|
|
54
|
+
'Accepts an optional files parameter to process specific files instead of running discovery.',
|
|
55
|
+
].join(' '),
|
|
56
|
+
args: {
|
|
57
|
+
mode: tool.schema.string().optional().describe('Output format: verbose, balanced, or concise (default: balanced)'),
|
|
58
|
+
files: tool.schema.string().optional().describe('Comma-separated file paths to process instead of discovering from opencode.json'),
|
|
59
|
+
},
|
|
60
|
+
async execute(args, context) {
|
|
61
|
+
// validate mode argument
|
|
62
|
+
const mode = isFormatMode(args.mode) ? args.mode : 'balanced';
|
|
63
|
+
try {
|
|
64
|
+
// resolve files: explicit paths or discovery
|
|
65
|
+
const resolved = await resolveFiles(directory, args.files);
|
|
66
|
+
if (resolved.error !== null) {
|
|
67
|
+
return resolved.error;
|
|
68
|
+
}
|
|
69
|
+
const files = resolved.data;
|
|
70
|
+
// detect model from current session
|
|
71
|
+
const model = await detectModel(client, context.sessionID);
|
|
72
|
+
if (!model) {
|
|
73
|
+
return 'Could not detect current model. Send a message first, then call irf-rewrite.';
|
|
74
|
+
}
|
|
75
|
+
// create a session for internal LLM calls
|
|
76
|
+
const sessionResult = await client.session.create({
|
|
77
|
+
body: {
|
|
78
|
+
title: 'IRF Parse',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (!sessionResult.data) {
|
|
82
|
+
return 'Failed to create internal session';
|
|
83
|
+
}
|
|
84
|
+
const sessionId = sessionResult.data.id;
|
|
85
|
+
// close over session details so processFile only needs a prompt callback
|
|
86
|
+
const prompt = (text, schema) => promptWithRetry({
|
|
87
|
+
client,
|
|
88
|
+
sessionId,
|
|
89
|
+
initialPrompt: text,
|
|
90
|
+
schema,
|
|
91
|
+
model,
|
|
92
|
+
});
|
|
93
|
+
// process files sequentially; parallel prompting through a shared
|
|
94
|
+
// session may cause ordering issues depending on SDK behavior
|
|
95
|
+
const results = [];
|
|
96
|
+
const comparisons = [];
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
// bail if the tool call was cancelled
|
|
99
|
+
if (context.abort.aborted) {
|
|
100
|
+
results.push('Cancelled');
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const fileResult = await processFile({
|
|
104
|
+
file,
|
|
105
|
+
prompt,
|
|
106
|
+
mode,
|
|
107
|
+
});
|
|
108
|
+
if (fileResult.status === 'success') {
|
|
109
|
+
comparisons.push(fileResult.comparison);
|
|
110
|
+
}
|
|
111
|
+
results.push(formatFileResult(fileResult));
|
|
112
|
+
}
|
|
113
|
+
// clean up the internal session
|
|
114
|
+
await safeAsync(() => client.session.delete({
|
|
115
|
+
path: { id: sessionId },
|
|
116
|
+
}));
|
|
117
|
+
// build comparison table
|
|
118
|
+
appendComparisonTable(results, comparisons);
|
|
119
|
+
return results.join('\n');
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
123
|
+
return 'irf-rewrite error: ' + msg;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
568
127
|
},
|
|
569
|
-
|
|
570
|
-
const mode = isFormatMode(args.mode) ? args.mode : "balanced";
|
|
571
|
-
try {
|
|
572
|
-
const resolved = await resolveFiles2(directory, args.files);
|
|
573
|
-
if (resolved.error !== null) {
|
|
574
|
-
return resolved.error;
|
|
575
|
-
}
|
|
576
|
-
const files = resolved.data;
|
|
577
|
-
const model = await detectModel(client, context.sessionID);
|
|
578
|
-
if (!model) {
|
|
579
|
-
return "Could not detect current model. Send a message first, then call irf-rewrite.";
|
|
580
|
-
}
|
|
581
|
-
const sessionResult = await client.session.create({
|
|
582
|
-
body: {
|
|
583
|
-
title: "IRF Parse"
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
if (!sessionResult.data) {
|
|
587
|
-
return "Failed to create internal session";
|
|
588
|
-
}
|
|
589
|
-
const sessionId = sessionResult.data.id;
|
|
590
|
-
const prompt = (text, schema) => promptWithRetry({
|
|
591
|
-
client,
|
|
592
|
-
sessionId,
|
|
593
|
-
initialPrompt: text,
|
|
594
|
-
schema,
|
|
595
|
-
model
|
|
596
|
-
});
|
|
597
|
-
const results = [];
|
|
598
|
-
const comparisons = [];
|
|
599
|
-
for (const file of files) {
|
|
600
|
-
if (context.abort.aborted) {
|
|
601
|
-
results.push("Cancelled");
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
const fileResult = await processFile({
|
|
605
|
-
file,
|
|
606
|
-
prompt,
|
|
607
|
-
mode
|
|
608
|
-
});
|
|
609
|
-
if (fileResult.status === "success") {
|
|
610
|
-
comparisons.push(fileResult.comparison);
|
|
611
|
-
}
|
|
612
|
-
results.push(formatFileResult(fileResult));
|
|
613
|
-
}
|
|
614
|
-
await safeAsync(() => client.session.delete({
|
|
615
|
-
path: { id: sessionId }
|
|
616
|
-
}));
|
|
617
|
-
appendComparisonTable(results, comparisons);
|
|
618
|
-
return results.join(`
|
|
619
|
-
`);
|
|
620
|
-
} catch (err) {
|
|
621
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
622
|
-
return "irf-rewrite error: " + msg;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
})
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
};
|
|
629
|
-
var irf_default = plugin;
|
|
630
|
-
export {
|
|
631
|
-
irf_default as default
|
|
128
|
+
};
|
|
632
129
|
};
|
|
130
|
+
export default plugin;
|
|
131
|
+
//# sourceMappingURL=irf.js.map
|