opencode-irf 0.0.2 → 0.0.4

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 (46) hide show
  1. package/dist/irf.js +632 -0
  2. package/package.json +6 -6
  3. package/dist/index.d.ts +0 -4
  4. package/dist/index.d.ts.map +0 -1
  5. package/dist/index.js +0 -131
  6. package/dist/index.js.map +0 -1
  7. package/dist/src/discover.d.ts +0 -11
  8. package/dist/src/discover.d.ts.map +0 -1
  9. package/dist/src/discover.js +0 -92
  10. package/dist/src/discover.js.map +0 -1
  11. package/dist/src/process.d.ts +0 -26
  12. package/dist/src/process.d.ts.map +0 -1
  13. package/dist/src/process.js +0 -56
  14. package/dist/src/process.js.map +0 -1
  15. package/dist/src/prompt.d.ts +0 -7
  16. package/dist/src/prompt.d.ts.map +0 -1
  17. package/dist/src/prompt.js +0 -84
  18. package/dist/src/prompt.js.map +0 -1
  19. package/dist/src/schema.d.ts +0 -69
  20. package/dist/src/schema.d.ts.map +0 -1
  21. package/dist/src/schema.js +0 -48
  22. package/dist/src/schema.js.map +0 -1
  23. package/dist/src/session.d.ts +0 -23
  24. package/dist/src/session.d.ts.map +0 -1
  25. package/dist/src/session.js +0 -112
  26. package/dist/src/session.js.map +0 -1
  27. package/dist/src/utils/compare.d.ts +0 -18
  28. package/dist/src/utils/compare.d.ts.map +0 -1
  29. package/dist/src/utils/compare.js +0 -74
  30. package/dist/src/utils/compare.js.map +0 -1
  31. package/dist/src/utils/extractLlmError.d.ts +0 -13
  32. package/dist/src/utils/extractLlmError.d.ts.map +0 -1
  33. package/dist/src/utils/extractLlmError.js +0 -12
  34. package/dist/src/utils/extractLlmError.js.map +0 -1
  35. package/dist/src/utils/safe.d.ts +0 -12
  36. package/dist/src/utils/safe.d.ts.map +0 -1
  37. package/dist/src/utils/safe.js +0 -31
  38. package/dist/src/utils/safe.js.map +0 -1
  39. package/dist/src/utils/stripCodeFences.d.ts +0 -2
  40. package/dist/src/utils/stripCodeFences.d.ts.map +0 -1
  41. package/dist/src/utils/stripCodeFences.js +0 -9
  42. package/dist/src/utils/stripCodeFences.js.map +0 -1
  43. package/dist/src/utils/validate.d.ts +0 -19
  44. package/dist/src/utils/validate.d.ts.map +0 -1
  45. package/dist/src/utils/validate.js +0 -30
  46. package/dist/src/utils/validate.js.map +0 -1
package/dist/irf.js ADDED
@@ -0,0 +1,632 @@
1
+ // irf.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // src/discover.ts
5
+ import { glob, readFile } from "node:fs/promises";
6
+ import { join, resolve } from "node:path";
7
+
8
+ // src/utils/safe.ts
9
+ var safe = (fn) => {
10
+ try {
11
+ const data = fn();
12
+ return {
13
+ data,
14
+ error: null
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;
500
+ }
501
+ const cleaned = stripCodeFences(text);
502
+ const validation = validateJson(cleaned, schema);
503
+ if (validation.error) {
504
+ const errorMsg = formatValidationError(validation);
505
+ lastError = errorMsg + " | raw: " + cleaned.slice(0, 200);
506
+ prompt = buildRetryPrompt(errorMsg);
507
+ continue;
508
+ }
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
+ };
518
+ };
519
+
520
+ // irf.ts
521
+ var resolveFiles2 = async (directory, filesArg) => {
522
+ if (filesArg) {
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" };
526
+ }
527
+ return { data: await readFilePaths(directory, paths), error: null };
528
+ }
529
+ return await discover(directory);
530
+ };
531
+ var ERROR_LABELS = {
532
+ readError: "Read failed",
533
+ parseError: "Parse failed",
534
+ formatError: "Format failed",
535
+ writeError: "Write failed"
536
+ };
537
+ var formatFileResult = (result) => {
538
+ if (result.status === "success") {
539
+ return "**" + result.path + "**: " + result.rulesCount + " rules written";
540
+ }
541
+ return "**" + result.path + "**: " + ERROR_LABELS[result.status] + " - " + result.error;
542
+ };
543
+ var appendComparisonTable = (lines, comparisons) => {
544
+ if (comparisons.length === 0) {
545
+ return;
546
+ }
547
+ lines.push("");
548
+ lines.push("## Comparison");
549
+ lines.push("```");
550
+ lines.push(buildTable(comparisons));
551
+ lines.push("```");
552
+ lines.push("");
553
+ lines.push("IMPORTANT: Show the comparison table above to the user exactly as-is.");
554
+ };
555
+ var plugin = async ({ directory, client }) => {
556
+ return {
557
+ tool: {
558
+ "irf-rewrite": tool({
559
+ description: [
560
+ "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.",
561
+ "Accepts an optional mode: verbose (full Rule/Reason pairs), balanced (LLM decides which rules need reasons), or concise (bullet list, no reasons).",
562
+ "Defaults to balanced.",
563
+ "Accepts an optional files parameter to process specific files instead of running discovery."
564
+ ].join(" "),
565
+ args: {
566
+ mode: tool.schema.string().optional().describe("Output format: verbose, balanced, or concise (default: balanced)"),
567
+ files: tool.schema.string().optional().describe("Comma-separated file paths to process instead of discovering from opencode.json")
568
+ },
569
+ async execute(args, context) {
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
632
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-irf",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "OpenCode plugin that converts unstructured instructions into structured rules using speech act theory and deontic logic.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -25,19 +25,19 @@
25
25
  "type": "module",
26
26
  "exports": {
27
27
  ".": {
28
- "import": "./dist/index.js",
29
- "types": "./dist/index.d.ts"
28
+ "import": "./dist/irf.js",
29
+ "types": "./dist/irf.d.ts"
30
30
  }
31
31
  },
32
- "main": "./dist/index.js",
33
- "types": "./dist/index.d.ts",
32
+ "main": "./dist/irf.js",
33
+ "types": "./dist/irf.d.ts",
34
34
  "files": [
35
35
  "dist/",
36
36
  "README.md",
37
37
  "LICENSE"
38
38
  ],
39
39
  "scripts": {
40
- "build": "npm run clean && tsc",
40
+ "build": "npm run clean && bun build irf.ts --outfile dist/irf.js --target node --external @opencode-ai/plugin --external zod",
41
41
  "clean": "rm -rf dist",
42
42
  "prepublishOnly": "npm run build"
43
43
  },
package/dist/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- import type { Plugin } from '@opencode-ai/plugin';
2
- declare const plugin: Plugin;
3
- export default plugin;
4
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAsDjD,QAAA,MAAM,MAAM,EAAE,MAkGb,CAAA;AAED,eAAe,MAAM,CAAA"}