@objctp/opencode-better-prompt 0.8.0
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/LICENSE +21 -0
- package/README.md +161 -0
- package/agents/prompt-correction.md +121 -0
- package/agents/prompt-enhancement.md +91 -0
- package/agents/prompt-summarisation.md +68 -0
- package/agents/prompt-translation.md +54 -0
- package/config/better-prompt.local.md.example +88 -0
- package/opencode.json +6 -0
- package/package.json +48 -0
- package/plugins/better-prompt/agents.ts +238 -0
- package/plugins/better-prompt/catalog.ts +271 -0
- package/plugins/better-prompt/config.ts +104 -0
- package/plugins/better-prompt/format.ts +26 -0
- package/plugins/better-prompt/pipeline.ts +190 -0
- package/plugins/better-prompt/state.ts +21 -0
- package/plugins/better-prompt/tui/format.ts +42 -0
- package/plugins/better-prompt/tui/routes.tsx +310 -0
- package/plugins/better-prompt/tui/select-view.tsx +58 -0
- package/plugins/better-prompt/tui/sidebar-panel.tsx +220 -0
- package/plugins/better-prompt/types.ts +137 -0
- package/plugins/better-prompt-tui.tsx +129 -0
- package/plugins/better-prompt.ts +298 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
4
|
+
import type { Event, Part } from "@opencode-ai/sdk";
|
|
5
|
+
import { CONFIG_PATH, parseConfig } from "./better-prompt/config";
|
|
6
|
+
import { runPipeline } from "./better-prompt/pipeline";
|
|
7
|
+
import { SUB_AGENTS } from "./better-prompt/agents";
|
|
8
|
+
import { clearState, writeAudit, writeState } from "./better-prompt/state";
|
|
9
|
+
import type {
|
|
10
|
+
AuditEntry,
|
|
11
|
+
PipelineDeps,
|
|
12
|
+
PipelineResult,
|
|
13
|
+
PipelineState,
|
|
14
|
+
SessionContext,
|
|
15
|
+
StageNotifier,
|
|
16
|
+
StageState,
|
|
17
|
+
ToastVariant,
|
|
18
|
+
Usage,
|
|
19
|
+
} from "./better-prompt/types";
|
|
20
|
+
|
|
21
|
+
// :::: Plugin :::: ////////////////////////////////////////////
|
|
22
|
+
|
|
23
|
+
export const BetterPromptPlugin: Plugin = async (ctx) => {
|
|
24
|
+
const { client, directory } = ctx;
|
|
25
|
+
|
|
26
|
+
const AUDIT_DIR = join(directory, ".opencode", "better-prompt");
|
|
27
|
+
const AUDIT_PATH = join(AUDIT_DIR, "audit.json");
|
|
28
|
+
const STATE_PATH = join(homedir(), ".local", "state", "opencode", "better-prompt", "state.json");
|
|
29
|
+
|
|
30
|
+
// :::: Error/warn logging via opencode :::: ///////////////
|
|
31
|
+
|
|
32
|
+
async function logError(message: string, error?: unknown): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const errStr = error instanceof Error ? error.message : error ? String(error) : "";
|
|
35
|
+
await client.app.log({
|
|
36
|
+
body: {
|
|
37
|
+
service: "better-prompt",
|
|
38
|
+
level: "error",
|
|
39
|
+
message: errStr ? `${message} — ${errStr}` : message,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
// opencode log may be unavailable
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function logWarn(message: string, detail?: string): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
await client.app.log({
|
|
50
|
+
body: {
|
|
51
|
+
service: "better-prompt",
|
|
52
|
+
level: "warn",
|
|
53
|
+
message: detail ? `${message} — ${detail}` : message,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
// opencode log may be unavailable
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// :::: Toast :::: /////////////////////////////////////////
|
|
62
|
+
|
|
63
|
+
async function toast(
|
|
64
|
+
message: string,
|
|
65
|
+
variant: ToastVariant = "info",
|
|
66
|
+
duration = 4000,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
await client.tui.showToast({
|
|
70
|
+
body: { message: `[bp] ${message}`, variant, duration },
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
// TUI may not be available in headless/CLI mode
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// :::: Session state :::: /////////////////////////////////
|
|
78
|
+
|
|
79
|
+
const sessionCost = {
|
|
80
|
+
cost: 0,
|
|
81
|
+
inputTokens: 0,
|
|
82
|
+
outputTokens: 0,
|
|
83
|
+
cacheWriteTokens: 0,
|
|
84
|
+
cacheReadTokens: 0,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const sessionContexts = new Map<string, SessionContext>();
|
|
88
|
+
|
|
89
|
+
const deps: PipelineDeps = { client, logError, logWarn, toast, sessionContexts };
|
|
90
|
+
|
|
91
|
+
// :::: Hooks :::: /////////////////////////////////////////
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
event: async ({ event }: { event: Event }) => {
|
|
95
|
+
if (event.type === "session.deleted") {
|
|
96
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
97
|
+
const sid = (props?.sessionID ??
|
|
98
|
+
(props?.info as Record<string, unknown> | undefined)?.id) as string | undefined;
|
|
99
|
+
if (sid) {
|
|
100
|
+
sessionContexts.delete(sid);
|
|
101
|
+
clearState(STATE_PATH);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
"chat.message": async (
|
|
107
|
+
input: { sessionID: string; agent?: string; messageID?: string },
|
|
108
|
+
output: { parts: Part[] },
|
|
109
|
+
) => {
|
|
110
|
+
if (input.agent && SUB_AGENTS.has(input.agent)) return;
|
|
111
|
+
|
|
112
|
+
const textPart = output.parts?.find((p: Part) => p.type === "text" && "text" in p);
|
|
113
|
+
if (!textPart || !("text" in textPart)) return;
|
|
114
|
+
|
|
115
|
+
const originalText = textPart.text;
|
|
116
|
+
if (!originalText?.trim()) return;
|
|
117
|
+
|
|
118
|
+
const config = parseConfig(CONFIG_PATH);
|
|
119
|
+
if (!config.enabled) return;
|
|
120
|
+
|
|
121
|
+
// Show initial toast to fix "frozen" UX
|
|
122
|
+
await toast("Processing prompt...", "info", 15000);
|
|
123
|
+
|
|
124
|
+
// Stage tracker for timing and live sidebar updates
|
|
125
|
+
const stageTracker: Record<
|
|
126
|
+
string,
|
|
127
|
+
{
|
|
128
|
+
status: string;
|
|
129
|
+
startTime?: number;
|
|
130
|
+
durationMs: number | null;
|
|
131
|
+
error?: string;
|
|
132
|
+
}
|
|
133
|
+
> = {};
|
|
134
|
+
|
|
135
|
+
function buildStageStates(): PipelineState["stages"] {
|
|
136
|
+
const names = ["correction", "translation", "context", "enhancement"] as const;
|
|
137
|
+
const result = {} as PipelineState["stages"];
|
|
138
|
+
for (const s of names) {
|
|
139
|
+
const t = stageTracker[s];
|
|
140
|
+
result[s] = {
|
|
141
|
+
status: (t?.status as StageState["status"]) || "pending",
|
|
142
|
+
durationMs: t?.durationMs ?? null,
|
|
143
|
+
...(t?.error && { error: t.error }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeRunningState() {
|
|
150
|
+
writeState(STATE_PATH, {
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
status: "running",
|
|
153
|
+
language: null,
|
|
154
|
+
mistakes: 0,
|
|
155
|
+
stages: buildStageStates(),
|
|
156
|
+
cost: 0,
|
|
157
|
+
inputTokens: 0,
|
|
158
|
+
outputTokens: 0,
|
|
159
|
+
cacheWriteTokens: 0,
|
|
160
|
+
cacheReadTokens: 0,
|
|
161
|
+
sessionCost: sessionCost.cost,
|
|
162
|
+
sessionInputTokens: sessionCost.inputTokens,
|
|
163
|
+
sessionOutputTokens: sessionCost.outputTokens,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const notify: StageNotifier = (stage, status, detail) => {
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
if (status === "starting") {
|
|
170
|
+
stageTracker[stage] = {
|
|
171
|
+
status: "active",
|
|
172
|
+
startTime: now,
|
|
173
|
+
durationMs: null,
|
|
174
|
+
};
|
|
175
|
+
} else {
|
|
176
|
+
const prev = stageTracker[stage];
|
|
177
|
+
const durationMs = prev?.startTime ? now - prev.startTime : null;
|
|
178
|
+
stageTracker[stage] = {
|
|
179
|
+
status,
|
|
180
|
+
durationMs,
|
|
181
|
+
...(detail && { error: detail }),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
writeRunningState();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let result: string;
|
|
188
|
+
let corrected: string | null;
|
|
189
|
+
let detectedLanguage: string | null;
|
|
190
|
+
let mistakes: PipelineResult["mistakes"];
|
|
191
|
+
let usage: Usage = {
|
|
192
|
+
cost: 0,
|
|
193
|
+
inputTokens: 0,
|
|
194
|
+
outputTokens: 0,
|
|
195
|
+
cacheWriteTokens: 0,
|
|
196
|
+
cacheReadTokens: 0,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const pipelineResult = await runPipeline(
|
|
201
|
+
deps,
|
|
202
|
+
originalText,
|
|
203
|
+
input.sessionID,
|
|
204
|
+
config,
|
|
205
|
+
notify,
|
|
206
|
+
input.messageID,
|
|
207
|
+
);
|
|
208
|
+
result = pipelineResult.result;
|
|
209
|
+
corrected = pipelineResult.corrected;
|
|
210
|
+
detectedLanguage = pipelineResult.detectedLanguage;
|
|
211
|
+
mistakes = pipelineResult.mistakes;
|
|
212
|
+
usage = pipelineResult.usage;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
void logError("pipeline failed", err);
|
|
215
|
+
writeState(STATE_PATH, {
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
status: "error",
|
|
218
|
+
language: null,
|
|
219
|
+
mistakes: 0,
|
|
220
|
+
stages: buildStageStates(),
|
|
221
|
+
cost: 0,
|
|
222
|
+
inputTokens: 0,
|
|
223
|
+
outputTokens: 0,
|
|
224
|
+
cacheWriteTokens: 0,
|
|
225
|
+
cacheReadTokens: 0,
|
|
226
|
+
sessionCost: sessionCost.cost,
|
|
227
|
+
sessionInputTokens: sessionCost.inputTokens,
|
|
228
|
+
sessionOutputTokens: sessionCost.outputTokens,
|
|
229
|
+
});
|
|
230
|
+
await toast("Pipeline error — original prompt sent", "error", 5000);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Completion toast
|
|
235
|
+
const changed = result !== originalText;
|
|
236
|
+
await toast(changed ? "Prompt modified" : "No changes", changed ? "success" : "info", 3000);
|
|
237
|
+
|
|
238
|
+
// Replace text
|
|
239
|
+
textPart.text = result;
|
|
240
|
+
|
|
241
|
+
// Audit
|
|
242
|
+
if (config.audit) {
|
|
243
|
+
const mistakeNature = [...new Set(mistakes.map((m) => m.type))];
|
|
244
|
+
const entry: AuditEntry = {
|
|
245
|
+
date: new Date().toISOString(),
|
|
246
|
+
prompt: originalText,
|
|
247
|
+
language: detectedLanguage,
|
|
248
|
+
corrected: config.correction ? corrected : null,
|
|
249
|
+
enhanced: config.enhancement ? result : null,
|
|
250
|
+
"mistake-nature": mistakeNature,
|
|
251
|
+
mistakes,
|
|
252
|
+
models: {
|
|
253
|
+
correction: config.correction ? config.correction_model : null,
|
|
254
|
+
translation: config.translation ? config.translation_model : null,
|
|
255
|
+
enhancement: config.enhancement ? config.enhancement_model : null,
|
|
256
|
+
context: config.enhancement ? config.correction_model : null,
|
|
257
|
+
},
|
|
258
|
+
usage,
|
|
259
|
+
};
|
|
260
|
+
writeAudit(AUDIT_PATH, entry);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Accumulate session cost
|
|
264
|
+
sessionCost.cost += usage.cost;
|
|
265
|
+
sessionCost.inputTokens += usage.inputTokens;
|
|
266
|
+
sessionCost.outputTokens += usage.outputTokens;
|
|
267
|
+
sessionCost.cacheWriteTokens += usage.cacheWriteTokens;
|
|
268
|
+
sessionCost.cacheReadTokens += usage.cacheReadTokens;
|
|
269
|
+
|
|
270
|
+
// Write final state for TUI sidebar
|
|
271
|
+
writeState(STATE_PATH, {
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
status: changed ? "modified" : "no_changes",
|
|
274
|
+
language: detectedLanguage,
|
|
275
|
+
mistakes: mistakes.length,
|
|
276
|
+
stages: buildStageStates(),
|
|
277
|
+
cost: usage.cost,
|
|
278
|
+
inputTokens: usage.inputTokens,
|
|
279
|
+
outputTokens: usage.outputTokens,
|
|
280
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
281
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
282
|
+
sessionCost: sessionCost.cost,
|
|
283
|
+
sessionInputTokens: sessionCost.inputTokens,
|
|
284
|
+
sessionOutputTokens: sessionCost.outputTokens,
|
|
285
|
+
preview: {
|
|
286
|
+
original: originalText,
|
|
287
|
+
processed: result,
|
|
288
|
+
mistakeDetails: mistakes,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export default {
|
|
296
|
+
id: "better-prompt",
|
|
297
|
+
server: BetterPromptPlugin,
|
|
298
|
+
};
|