opencode-agenthub 0.1.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 +373 -0
- package/dist/composer/bootstrap.js +493 -0
- package/dist/composer/builtin-assets.js +139 -0
- package/dist/composer/capabilities.js +20 -0
- package/dist/composer/compose.js +824 -0
- package/dist/composer/defaults.js +10 -0
- package/dist/composer/home-transfer.js +288 -0
- package/dist/composer/install-home.js +5 -0
- package/dist/composer/library/README.md +93 -0
- package/dist/composer/library/bundles/auto.json +18 -0
- package/dist/composer/library/bundles/build.json +17 -0
- package/dist/composer/library/bundles/hr-adapter.json +26 -0
- package/dist/composer/library/bundles/hr-cto.json +24 -0
- package/dist/composer/library/bundles/hr-evaluator.json +26 -0
- package/dist/composer/library/bundles/hr-planner.json +26 -0
- package/dist/composer/library/bundles/hr-sourcer.json +24 -0
- package/dist/composer/library/bundles/hr-verifier.json +26 -0
- package/dist/composer/library/bundles/hr.json +35 -0
- package/dist/composer/library/bundles/plan.json +19 -0
- package/dist/composer/library/instructions/hr-boundaries.md +38 -0
- package/dist/composer/library/instructions/hr-protocol.md +102 -0
- package/dist/composer/library/profiles/auto.json +9 -0
- package/dist/composer/library/profiles/hr.json +9 -0
- package/dist/composer/library/souls/auto.md +29 -0
- package/dist/composer/library/souls/build.md +21 -0
- package/dist/composer/library/souls/hr-adapter.md +64 -0
- package/dist/composer/library/souls/hr-cto.md +57 -0
- package/dist/composer/library/souls/hr-evaluator.md +64 -0
- package/dist/composer/library/souls/hr-planner.md +48 -0
- package/dist/composer/library/souls/hr-sourcer.md +70 -0
- package/dist/composer/library/souls/hr-verifier.md +62 -0
- package/dist/composer/library/souls/hr.md +186 -0
- package/dist/composer/library/souls/plan.md +23 -0
- package/dist/composer/library/workflow/auto-mode.json +139 -0
- package/dist/composer/model-utils.js +39 -0
- package/dist/composer/opencode-profile.js +2299 -0
- package/dist/composer/package-manager.js +75 -0
- package/dist/composer/package-version.js +20 -0
- package/dist/composer/platform.js +48 -0
- package/dist/composer/query.js +133 -0
- package/dist/composer/settings.js +400 -0
- package/dist/plugins/opencode-agenthub.js +310 -0
- package/dist/plugins/opencode-question.js +223 -0
- package/dist/plugins/plan-guidance.js +263 -0
- package/dist/plugins/runtime-config.js +57 -0
- package/dist/skills/agenthub-doctor/SKILL.md +238 -0
- package/dist/skills/agenthub-doctor/diagnose.js +213 -0
- package/dist/skills/agenthub-doctor/fix.js +293 -0
- package/dist/skills/agenthub-doctor/index.js +30 -0
- package/dist/skills/agenthub-doctor/interactive.js +756 -0
- package/dist/skills/hr-assembly/SKILL.md +121 -0
- package/dist/skills/hr-final-check/SKILL.md +98 -0
- package/dist/skills/hr-review/SKILL.md +100 -0
- package/dist/skills/hr-staffing/SKILL.md +85 -0
- package/dist/skills/hr-support/bin/sync_sources.py +560 -0
- package/dist/skills/hr-support/bin/validate_staged_package.py +290 -0
- package/dist/skills/hr-support/bin/vendor_stage_mcps.py +234 -0
- package/dist/skills/hr-support/bin/vendor_stage_skills.py +104 -0
- package/dist/types.js +11 -0
- package/package.json +54 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
buildPlanTraceNotice,
|
|
5
|
+
buildQueuedPlanNotice,
|
|
6
|
+
buildQueuedWorkflowNotice,
|
|
7
|
+
buildWorkflowTraceNotice,
|
|
8
|
+
detectPlanIntent,
|
|
9
|
+
detectWorkflowIntent,
|
|
10
|
+
INTERNAL_INITIATOR_MARKER,
|
|
11
|
+
shouldInjectPlanGuidance,
|
|
12
|
+
shouldInjectWorkflowGuidance
|
|
13
|
+
} from "./plan-guidance.js";
|
|
14
|
+
import {
|
|
15
|
+
inspectRuntimeConfig,
|
|
16
|
+
resolvePluginConfigRoot,
|
|
17
|
+
summarizeRuntimeFeatureState
|
|
18
|
+
} from "./runtime-config.js";
|
|
19
|
+
const loadRuntimeConfig = async () => {
|
|
20
|
+
const inspection = await inspectRuntimeConfig(resolvePluginConfigRoot());
|
|
21
|
+
if (!inspection.ok) {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
"[opencode-agenthub] Warning: failed to load hub runtime config \u2014 running in degraded mode (tool blocking disabled, workflow injection disabled). Run 'agenthub setup' to initialize your Agent Hub home.\n"
|
|
24
|
+
);
|
|
25
|
+
return { blockedTools: /* @__PURE__ */ new Set(["call_omo_agent"]) };
|
|
26
|
+
}
|
|
27
|
+
return summarizeRuntimeFeatureState(inspection.config);
|
|
28
|
+
};
|
|
29
|
+
const DEFAULT_PLAN_STATE = {
|
|
30
|
+
pendingVisibleNotice: null,
|
|
31
|
+
pendingVisibleSource: null,
|
|
32
|
+
detectedAtMessageID: null,
|
|
33
|
+
injectionCount: 0
|
|
34
|
+
};
|
|
35
|
+
async function opencode_agenthub_default(ctx) {
|
|
36
|
+
const { blockedTools, planDetection, workflowInjection } = await loadRuntimeConfig();
|
|
37
|
+
const activeWorkflowInjection = workflowInjection?.enabled ? workflowInjection : void 0;
|
|
38
|
+
const activePlanDetection = planDetection?.enabled ? planDetection : void 0;
|
|
39
|
+
const workflowInjectionEnabled = !!activeWorkflowInjection;
|
|
40
|
+
const planDetectionEnabled = !!activePlanDetection;
|
|
41
|
+
const maxInjectionsPerSession = activeWorkflowInjection?.maxInjectionsPerSession ?? activePlanDetection?.maxInjectionsPerSession ?? 3;
|
|
42
|
+
const debugLogPath = join(resolvePluginConfigRoot(), "plan-detection-debug.log");
|
|
43
|
+
const formatDebugError = (error) => {
|
|
44
|
+
if (error === void 0) return null;
|
|
45
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
46
|
+
if (typeof error === "string") return error;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(error);
|
|
49
|
+
} catch {
|
|
50
|
+
return String(error);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const debugLog = async (message, error) => {
|
|
54
|
+
if (!activeWorkflowInjection?.debugLog && !activePlanDetection?.debugLog) return;
|
|
55
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
56
|
+
const formattedError = formatDebugError(error);
|
|
57
|
+
const line = formattedError ? `[${timestamp}] ${message}
|
|
58
|
+
${formattedError}
|
|
59
|
+
` : `[${timestamp}] ${message}
|
|
60
|
+
`;
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(resolvePluginConfigRoot(), { recursive: true });
|
|
63
|
+
await appendFile(debugLogPath, line, "utf-8");
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const summarizeValue = (value) => {
|
|
68
|
+
if (Array.isArray(value)) return `array(len=${value.length})`;
|
|
69
|
+
if (value === null) return "null";
|
|
70
|
+
return typeof value;
|
|
71
|
+
};
|
|
72
|
+
const planState = /* @__PURE__ */ new Map();
|
|
73
|
+
const phases = ["CLARIFY", "GATHER", "ANALYZE", "SYNTHESIZE", "FORMAT", "DELIVER", "ANSWER", "QA_FORMAT"];
|
|
74
|
+
let currentRunId = null;
|
|
75
|
+
const knowledgeRoot = resolve(resolvePluginConfigRoot(), "knowledge");
|
|
76
|
+
const getToolName = (payload) => {
|
|
77
|
+
const directName = payload?.name;
|
|
78
|
+
if (typeof directName === "string") return directName;
|
|
79
|
+
const nestedToolName = payload?.tool?.name;
|
|
80
|
+
if (typeof nestedToolName === "string") return nestedToolName;
|
|
81
|
+
const nestedName = payload?.input?.name;
|
|
82
|
+
if (typeof nestedName === "string") return nestedName;
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
const generateRunId = () => {
|
|
86
|
+
const now = /* @__PURE__ */ new Date();
|
|
87
|
+
return now.toISOString().slice(0, 19).replace(/:/g, "-");
|
|
88
|
+
};
|
|
89
|
+
const extractPhaseOutputs = (text) => {
|
|
90
|
+
if (!text || text.length < 50) return [];
|
|
91
|
+
if (/<\/?[a-z][\s\S]*?>/i.test(text.slice(0, 500))) return [];
|
|
92
|
+
const markerRegex = /(?:^|\n)\s*(?:#{1,6}\s*)?(?:\*\*)?\s*(CLARIFY|GATHER|ANALYZE|SYNTHESIZE|FORMAT|DELIVER|ANSWER|QA_FORMAT)\s*:(?:\*\*)?/gi;
|
|
93
|
+
const markers = [];
|
|
94
|
+
for (const match of text.matchAll(markerRegex)) {
|
|
95
|
+
const phase = match[1]?.toUpperCase();
|
|
96
|
+
if (!phases.includes(phase)) continue;
|
|
97
|
+
const start = match.index ?? 0;
|
|
98
|
+
const markerText = match[0] ?? "";
|
|
99
|
+
markers.push({ phase, start, contentStart: start + markerText.length });
|
|
100
|
+
}
|
|
101
|
+
if (markers.length === 0) return [];
|
|
102
|
+
const outputs = [];
|
|
103
|
+
for (let index = 0; index < markers.length; index += 1) {
|
|
104
|
+
const current = markers[index];
|
|
105
|
+
const next = markers[index + 1];
|
|
106
|
+
const sliceEnd = next ? next.start : text.length;
|
|
107
|
+
const content = text.slice(current.contentStart, sliceEnd).trim();
|
|
108
|
+
if (content) outputs.push({ phase: current.phase, content });
|
|
109
|
+
}
|
|
110
|
+
return outputs;
|
|
111
|
+
};
|
|
112
|
+
const persistPhaseOutput = async (phase, content, runId) => {
|
|
113
|
+
const normalizedPhase = phase.trim().toUpperCase();
|
|
114
|
+
if (!phases.includes(normalizedPhase)) return;
|
|
115
|
+
const phaseContent = content.trim();
|
|
116
|
+
if (!phaseContent) return;
|
|
117
|
+
const runDirectory = join(knowledgeRoot, runId);
|
|
118
|
+
const phaseFilePath = join(runDirectory, `${normalizedPhase}.md`);
|
|
119
|
+
try {
|
|
120
|
+
await mkdir(runDirectory, { recursive: true });
|
|
121
|
+
await writeFile(phaseFilePath, `${phaseContent}
|
|
122
|
+
`, "utf-8");
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const persistFromText = async (text) => {
|
|
127
|
+
const phaseOutputs = extractPhaseOutputs(text);
|
|
128
|
+
if (phaseOutputs.length === 0) return;
|
|
129
|
+
if (!currentRunId) currentRunId = generateRunId();
|
|
130
|
+
for (const phaseOutput of phaseOutputs) {
|
|
131
|
+
await persistPhaseOutput(phaseOutput.phase, phaseOutput.content, currentRunId);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const showVisibleToast = async (message) => {
|
|
135
|
+
const tuiClient = ctx?.client?.tui;
|
|
136
|
+
if (!tuiClient || typeof tuiClient.showToast !== "function") return false;
|
|
137
|
+
try {
|
|
138
|
+
await tuiClient.showToast({
|
|
139
|
+
body: {
|
|
140
|
+
title: "Agent Hub",
|
|
141
|
+
message,
|
|
142
|
+
variant: "info",
|
|
143
|
+
duration: 4e3
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
await debugLog("Failed to show visible plan reminder via tui.showToast.", error);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const extractSessionID = (hookInput) => {
|
|
153
|
+
const input = hookInput;
|
|
154
|
+
const direct = input?.sessionID;
|
|
155
|
+
if (typeof direct === "string" && direct) return direct;
|
|
156
|
+
const nested = input?.session?.id;
|
|
157
|
+
if (typeof nested === "string" && nested) return nested;
|
|
158
|
+
return void 0;
|
|
159
|
+
};
|
|
160
|
+
const getPlanState = (sessionID) => {
|
|
161
|
+
const existing = planState.get(sessionID);
|
|
162
|
+
if (existing) return existing;
|
|
163
|
+
const created = { ...DEFAULT_PLAN_STATE };
|
|
164
|
+
planState.set(sessionID, created);
|
|
165
|
+
return created;
|
|
166
|
+
};
|
|
167
|
+
const clearPendingReminderState = async (state, sessionID, reason) => {
|
|
168
|
+
const clearedVisibleReminder = !!state.pendingVisibleNotice;
|
|
169
|
+
state.pendingVisibleNotice = null;
|
|
170
|
+
state.pendingVisibleSource = null;
|
|
171
|
+
state.detectedAtMessageID = null;
|
|
172
|
+
if (!clearedVisibleReminder) return;
|
|
173
|
+
await debugLog(
|
|
174
|
+
`Cleared stale pending visible reminder for session ${sessionID} (${reason}). visible=${clearedVisibleReminder}`
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
const injectVisibleReminderIntoToolOutput = async (toolOutput, notice, reminderSource, sessionID) => {
|
|
178
|
+
const currentText = typeof toolOutput.output === "string" ? toolOutput.output : "";
|
|
179
|
+
toolOutput.output = currentText ? `${notice}
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
${currentText}` : notice;
|
|
184
|
+
await debugLog(
|
|
185
|
+
`Injected visible ${reminderSource} reminder into tool.execute.after output for session ${sessionID}.`
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
const queueVisibleReminderForToolOutput = async (state, notice, reminderSource, sessionID) => {
|
|
189
|
+
state.pendingVisibleNotice = notice;
|
|
190
|
+
state.pendingVisibleSource = reminderSource;
|
|
191
|
+
await debugLog(
|
|
192
|
+
`Queued visible ${reminderSource} reminder for tool.execute.after injection in session ${sessionID}.`
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
const claimPendingVisibleReminder = (state) => {
|
|
196
|
+
if (!state.pendingVisibleNotice) return null;
|
|
197
|
+
const claimed = {
|
|
198
|
+
notice: state.pendingVisibleNotice,
|
|
199
|
+
reminderSource: state.pendingVisibleSource ?? "plan"
|
|
200
|
+
};
|
|
201
|
+
state.pendingVisibleNotice = null;
|
|
202
|
+
state.pendingVisibleSource = null;
|
|
203
|
+
state.injectionCount += 1;
|
|
204
|
+
return claimed;
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
"tool.execute.before": async (hookInput) => {
|
|
208
|
+
const toolName = getToolName(hookInput);
|
|
209
|
+
if (toolName && blockedTools.has(toolName)) {
|
|
210
|
+
return {
|
|
211
|
+
error: `Blocked tool: ${toolName}. This tool is restricted by guard configuration.`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"experimental.text.complete": async (hookInput, output) => {
|
|
216
|
+
const currentOutput = output;
|
|
217
|
+
const originalText = typeof currentOutput?.text === "string" ? currentOutput.text ?? "" : "";
|
|
218
|
+
if (originalText.includes(INTERNAL_INITIATOR_MARKER)) {
|
|
219
|
+
await debugLog("Skipped plan detection for internal initiator message.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const sessionID = extractSessionID(hookInput);
|
|
223
|
+
const state = sessionID ? getPlanState(sessionID) : void 0;
|
|
224
|
+
const effectiveText = originalText;
|
|
225
|
+
await persistFromText(effectiveText);
|
|
226
|
+
if ((workflowInjectionEnabled || planDetectionEnabled) && effectiveText && sessionID) {
|
|
227
|
+
const messageID = hookInput?.messageID;
|
|
228
|
+
if (typeof messageID === "string" && state.detectedAtMessageID === messageID) return;
|
|
229
|
+
if (state.pendingVisibleNotice || state.injectionCount >= maxInjectionsPerSession) return;
|
|
230
|
+
const workflowSignal = activeWorkflowInjection ? detectWorkflowIntent(effectiveText, activeWorkflowInjection) : void 0;
|
|
231
|
+
const shouldInjectWorkflow = workflowSignal ? shouldInjectWorkflowGuidance(workflowSignal, activeWorkflowInjection) : false;
|
|
232
|
+
const planSignal = !shouldInjectWorkflow && activePlanDetection ? detectPlanIntent(effectiveText, activePlanDetection) : void 0;
|
|
233
|
+
const shouldInjectPlan = planSignal ? shouldInjectPlanGuidance(planSignal, activePlanDetection) : false;
|
|
234
|
+
if (!shouldInjectWorkflow && !shouldInjectPlan) return;
|
|
235
|
+
const shouldInjectVisibleReminder = shouldInjectWorkflow ? activeWorkflowInjection?.queueVisibleReminder ?? false : activePlanDetection?.queueVisibleReminder ?? activePlanDetection?.userVisibleTrace ?? true;
|
|
236
|
+
if (!shouldInjectVisibleReminder) return;
|
|
237
|
+
const reminderSource = shouldInjectWorkflow ? "workflow" : "plan";
|
|
238
|
+
const visibleNotice = shouldInjectWorkflow && workflowSignal ? buildWorkflowTraceNotice(workflowSignal, activeWorkflowInjection) : buildPlanTraceNotice(activePlanDetection);
|
|
239
|
+
const queuedNotice = shouldInjectWorkflow && workflowSignal ? buildQueuedWorkflowNotice(workflowSignal, activeWorkflowInjection) : buildQueuedPlanNotice(activePlanDetection, planSignal);
|
|
240
|
+
state.detectedAtMessageID = typeof messageID === "string" ? messageID : null;
|
|
241
|
+
await queueVisibleReminderForToolOutput(
|
|
242
|
+
state,
|
|
243
|
+
queuedNotice,
|
|
244
|
+
reminderSource,
|
|
245
|
+
sessionID
|
|
246
|
+
);
|
|
247
|
+
const toastShown = await showVisibleToast(visibleNotice);
|
|
248
|
+
if (toastShown) {
|
|
249
|
+
await debugLog(
|
|
250
|
+
`Displayed visible ${reminderSource} reminder via tui.showToast for session ${sessionID}.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
await debugLog(
|
|
254
|
+
`After-tool-only ${reminderSource} reminder active for session ${sessionID}: visible reminder goes to tool.execute.after output.`
|
|
255
|
+
);
|
|
256
|
+
if (shouldInjectWorkflow) {
|
|
257
|
+
await debugLog(
|
|
258
|
+
`Detected workflow marker (${workflowSignal?.ruleId ?? "unknown"}) for session ${sessionID}; queued visible reminder.`
|
|
259
|
+
);
|
|
260
|
+
} else {
|
|
261
|
+
await debugLog(`Detected legacy plan marker for session ${sessionID}; queued visible reminder.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"experimental.chat.system.transform": async () => {
|
|
266
|
+
return;
|
|
267
|
+
},
|
|
268
|
+
"chat.message": async (hookInput) => {
|
|
269
|
+
const sessionID = extractSessionID(hookInput);
|
|
270
|
+
if (!sessionID) return;
|
|
271
|
+
const state = planState.get(sessionID);
|
|
272
|
+
if (!state) return;
|
|
273
|
+
await clearPendingReminderState(state, sessionID, "new chat.message boundary");
|
|
274
|
+
},
|
|
275
|
+
"tool.execute.after": async (hookInput, hookOutput) => {
|
|
276
|
+
const sessionID = extractSessionID(hookInput);
|
|
277
|
+
const state = sessionID ? planState.get(sessionID) : void 0;
|
|
278
|
+
const candidateOutput = hookOutput?.output;
|
|
279
|
+
const claimedReminder = state && typeof candidateOutput === "string" ? claimPendingVisibleReminder(state) : null;
|
|
280
|
+
if (claimedReminder && sessionID) {
|
|
281
|
+
await injectVisibleReminderIntoToolOutput(
|
|
282
|
+
hookOutput,
|
|
283
|
+
claimedReminder.notice,
|
|
284
|
+
claimedReminder.reminderSource,
|
|
285
|
+
sessionID
|
|
286
|
+
);
|
|
287
|
+
} else if (state?.pendingVisibleNotice && sessionID && typeof candidateOutput !== "string") {
|
|
288
|
+
await debugLog(
|
|
289
|
+
`Deferred visible ${state.pendingVisibleSource ?? "plan"} reminder for session ${sessionID} because tool.execute.after output was not a string. outputType=${summarizeValue(candidateOutput)}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (typeof candidateOutput === "string") {
|
|
293
|
+
await persistFromText(hookOutput.output);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
event: async (payload) => {
|
|
297
|
+
const eventType = payload?.event?.type;
|
|
298
|
+
if (eventType === "session.deleted") {
|
|
299
|
+
currentRunId = null;
|
|
300
|
+
const sessionID = payload?.event?.sessionID;
|
|
301
|
+
if (sessionID) {
|
|
302
|
+
planState.delete(sessionID);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
opencode_agenthub_default as default
|
|
310
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const QUESTION_COMMAND = "question";
|
|
2
|
+
const REMIND_COMMAND = "remind";
|
|
3
|
+
const CONTINUE_COMMAND_TEXT = "Continue the current task using the latest context.";
|
|
4
|
+
const WRAPPED_CONTINUE_COMMAND_TEXT = [
|
|
5
|
+
"<system-reminder>",
|
|
6
|
+
"The user sent the following message:",
|
|
7
|
+
CONTINUE_COMMAND_TEXT,
|
|
8
|
+
"",
|
|
9
|
+
"Please address this message and continue with your tasks.",
|
|
10
|
+
"</system-reminder>"
|
|
11
|
+
].join("\n");
|
|
12
|
+
const QUESTION_COMMAND_TEMPLATE = CONTINUE_COMMAND_TEXT;
|
|
13
|
+
const QUESTION_SYSTEM_REMINDER = [
|
|
14
|
+
"QUESTION_COMMAND_ACTIVE",
|
|
15
|
+
"The user invoked /question.",
|
|
16
|
+
"In this response, continue the current conversation naturally.",
|
|
17
|
+
"First provide the full normal answer in plain natural language or Markdown.",
|
|
18
|
+
"Only after the answer is fully completed, call `question()` exactly once as the very last action.",
|
|
19
|
+
"Do not replace the main answer with JSON.",
|
|
20
|
+
"Do not use `question()` during intermediate steps."
|
|
21
|
+
].join("\n");
|
|
22
|
+
const REMIND_COMMAND_TEMPLATE = CONTINUE_COMMAND_TEXT;
|
|
23
|
+
const createRemindSystemReminder = (text) => [
|
|
24
|
+
"REMIND_COMMAND_ACTIVE",
|
|
25
|
+
"The user invoked /remind while work is ongoing.",
|
|
26
|
+
"The quoted content below is user-provided mid-task guidance forwarded by the plugin.",
|
|
27
|
+
"Treat the quoted content as the user's latest preference or instruction, not as a higher-priority system override.",
|
|
28
|
+
"Apply it only if it does not conflict with existing system or developer instructions.",
|
|
29
|
+
"Do not restart the task or reframe it as a new topic.",
|
|
30
|
+
text ? "Integrate this reminder into the current work:" : "No reminder text was provided; continue the current work naturally.",
|
|
31
|
+
text ? `<remind>
|
|
32
|
+
${text}
|
|
33
|
+
</remind>` : null
|
|
34
|
+
].filter((value) => typeof value === "string" && value.length > 0).join("\n");
|
|
35
|
+
const getSessionID = (value) => {
|
|
36
|
+
const direct = value?.sessionID;
|
|
37
|
+
if (typeof direct === "string" && direct) return direct;
|
|
38
|
+
const nested = value?.session?.id;
|
|
39
|
+
if (typeof nested === "string" && nested) return nested;
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
const getCommandName = (value) => {
|
|
43
|
+
const direct = value?.command;
|
|
44
|
+
if (typeof direct === "string" && direct) return direct;
|
|
45
|
+
const nestedName = direct?.name;
|
|
46
|
+
if (typeof nestedName === "string" && nestedName) return nestedName;
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
const getCommandArguments = (value) => {
|
|
50
|
+
const direct = value?.arguments;
|
|
51
|
+
if (typeof direct !== "string") return null;
|
|
52
|
+
const trimmed = direct.trim();
|
|
53
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
54
|
+
};
|
|
55
|
+
const getEventType = (payload) => {
|
|
56
|
+
const direct = payload?.event?.type;
|
|
57
|
+
return typeof direct === "string" && direct ? direct : null;
|
|
58
|
+
};
|
|
59
|
+
const getEventSessionID = (payload) => {
|
|
60
|
+
const event = payload?.event;
|
|
61
|
+
const properties = event?.properties;
|
|
62
|
+
if (typeof properties?.sessionID === "string" && properties.sessionID) {
|
|
63
|
+
return properties.sessionID;
|
|
64
|
+
}
|
|
65
|
+
if (typeof properties?.info?.id === "string" && properties.info.id) {
|
|
66
|
+
return properties.info.id;
|
|
67
|
+
}
|
|
68
|
+
if (typeof properties?.session?.id === "string" && properties.session.id) {
|
|
69
|
+
return properties.session.id;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
const isTextPart = (value) => {
|
|
74
|
+
return typeof value === "object" && value !== null && typeof value.text === "string";
|
|
75
|
+
};
|
|
76
|
+
const normalizeMessageText = (text) => text.replace(/\r\n/g, "\n").trim();
|
|
77
|
+
const isBlankText = (text) => normalizeMessageText(text).length === 0;
|
|
78
|
+
const isContinuationText = (text) => {
|
|
79
|
+
const normalized = normalizeMessageText(text);
|
|
80
|
+
return normalized === CONTINUE_COMMAND_TEXT || normalized === WRAPPED_CONTINUE_COMMAND_TEXT;
|
|
81
|
+
};
|
|
82
|
+
const scrubCommandParts = (output) => {
|
|
83
|
+
const currentOutput = output;
|
|
84
|
+
if (!Array.isArray(currentOutput.parts)) return;
|
|
85
|
+
const preservedNonTextParts = currentOutput.parts.filter((part) => !isTextPart(part));
|
|
86
|
+
const firstTextPart = currentOutput.parts.find(isTextPart);
|
|
87
|
+
if (firstTextPart) {
|
|
88
|
+
firstTextPart.text = CONTINUE_COMMAND_TEXT;
|
|
89
|
+
currentOutput.parts = [firstTextPart, ...preservedNonTextParts];
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
currentOutput.parts = [{ type: "text", text: CONTINUE_COMMAND_TEXT }, ...preservedNonTextParts];
|
|
93
|
+
};
|
|
94
|
+
const isSessionStateActive = (state) => {
|
|
95
|
+
return !!state && (state.questionActive || Object.hasOwn(state, "remindText"));
|
|
96
|
+
};
|
|
97
|
+
const getMessageSessionID = (msg) => {
|
|
98
|
+
const direct = msg?.sessionID;
|
|
99
|
+
if (typeof direct === "string" && direct) return direct;
|
|
100
|
+
const info = msg?.info;
|
|
101
|
+
if (!info || typeof info !== "object") return null;
|
|
102
|
+
const sessionID = info.sessionID;
|
|
103
|
+
return typeof sessionID === "string" && sessionID ? sessionID : null;
|
|
104
|
+
};
|
|
105
|
+
const getMessageRole = (msg) => {
|
|
106
|
+
const direct = msg?.role;
|
|
107
|
+
if (typeof direct === "string" && direct) return direct;
|
|
108
|
+
const info = msg?.info;
|
|
109
|
+
if (!info || typeof info !== "object") return null;
|
|
110
|
+
const role = info.role;
|
|
111
|
+
return typeof role === "string" && role ? role : null;
|
|
112
|
+
};
|
|
113
|
+
const isUserContinuationMessage = (msg) => {
|
|
114
|
+
const typed = msg;
|
|
115
|
+
if (!typed) return false;
|
|
116
|
+
if (getMessageRole(msg) !== "user") return false;
|
|
117
|
+
const parts = typed.parts;
|
|
118
|
+
if (!Array.isArray(parts) || parts.length === 0) return false;
|
|
119
|
+
const textParts = parts.filter(isTextPart);
|
|
120
|
+
if (textParts.length === 0) return false;
|
|
121
|
+
const nonTextParts = parts.filter((p) => !isTextPart(p));
|
|
122
|
+
if (nonTextParts.length > 0) return false;
|
|
123
|
+
const [primaryText, ...trailingTextParts] = textParts;
|
|
124
|
+
if (!isContinuationText(primaryText.text)) return false;
|
|
125
|
+
return trailingTextParts.every((part) => isBlankText(part.text));
|
|
126
|
+
};
|
|
127
|
+
async function opencode_question_default() {
|
|
128
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
129
|
+
const getSessionState = (sessionID) => {
|
|
130
|
+
const existing = activeSessions.get(sessionID);
|
|
131
|
+
if (existing) return existing;
|
|
132
|
+
const created = {};
|
|
133
|
+
activeSessions.set(sessionID, created);
|
|
134
|
+
return created;
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
config: async (config) => {
|
|
138
|
+
const cfg = config;
|
|
139
|
+
if (!cfg.command) cfg.command = {};
|
|
140
|
+
cfg.command[QUESTION_COMMAND] = {
|
|
141
|
+
template: QUESTION_COMMAND_TEMPLATE,
|
|
142
|
+
description: "Continue the current conversation and force a final question() call",
|
|
143
|
+
subtask: false
|
|
144
|
+
};
|
|
145
|
+
cfg.command[REMIND_COMMAND] = {
|
|
146
|
+
template: REMIND_COMMAND_TEMPLATE,
|
|
147
|
+
description: "Inject one-shot mid-task guidance and continue the current conversation",
|
|
148
|
+
subtask: false
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
"command.execute.before": async (input, output) => {
|
|
152
|
+
const sessionID = getSessionID(input);
|
|
153
|
+
if (!sessionID) return;
|
|
154
|
+
const commandName = getCommandName(input);
|
|
155
|
+
if (!commandName) return;
|
|
156
|
+
if (commandName !== QUESTION_COMMAND && commandName !== REMIND_COMMAND) return;
|
|
157
|
+
scrubCommandParts(output);
|
|
158
|
+
const state = getSessionState(sessionID);
|
|
159
|
+
if (commandName === QUESTION_COMMAND) {
|
|
160
|
+
state.questionActive = true;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (commandName === REMIND_COMMAND) {
|
|
164
|
+
state.remindText = getCommandArguments(input);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
168
|
+
const sessionID = getSessionID(input);
|
|
169
|
+
if (!sessionID) return;
|
|
170
|
+
const state = activeSessions.get(sessionID);
|
|
171
|
+
if (!state) return;
|
|
172
|
+
const currentOutput = output;
|
|
173
|
+
if (!Array.isArray(currentOutput.system)) return;
|
|
174
|
+
if (state.questionActive && !currentOutput.system.includes(QUESTION_SYSTEM_REMINDER)) {
|
|
175
|
+
currentOutput.system.push(QUESTION_SYSTEM_REMINDER);
|
|
176
|
+
}
|
|
177
|
+
if (Object.hasOwn(state, "remindText")) {
|
|
178
|
+
const remindSystemReminder = createRemindSystemReminder(state.remindText ?? null);
|
|
179
|
+
if (!currentOutput.system.includes(remindSystemReminder)) {
|
|
180
|
+
currentOutput.system.push(remindSystemReminder);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
185
|
+
const currentOutput = output;
|
|
186
|
+
if (!Array.isArray(currentOutput.messages) || currentOutput.messages.length === 0) return;
|
|
187
|
+
const hasAnyActiveSession = [...activeSessions.values()].some(isSessionStateActive);
|
|
188
|
+
if (!hasAnyActiveSession) return;
|
|
189
|
+
const sessionsToFilter = /* @__PURE__ */ new Set();
|
|
190
|
+
for (const msg of currentOutput.messages) {
|
|
191
|
+
const sessionID = getMessageSessionID(msg);
|
|
192
|
+
if (!sessionID) continue;
|
|
193
|
+
if (isSessionStateActive(activeSessions.get(sessionID))) {
|
|
194
|
+
sessionsToFilter.add(sessionID);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const filtered = [];
|
|
198
|
+
for (const msg of currentOutput.messages) {
|
|
199
|
+
if (isUserContinuationMessage(msg)) {
|
|
200
|
+
const sessionID = getMessageSessionID(msg);
|
|
201
|
+
if (!sessionID) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (sessionsToFilter.has(sessionID)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
filtered.push(msg);
|
|
209
|
+
}
|
|
210
|
+
currentOutput.messages = filtered;
|
|
211
|
+
},
|
|
212
|
+
event: async (payload) => {
|
|
213
|
+
const eventType = getEventType(payload);
|
|
214
|
+
if (eventType !== "session.idle" && eventType !== "session.deleted") return;
|
|
215
|
+
const sessionID = getEventSessionID(payload);
|
|
216
|
+
if (!sessionID) return;
|
|
217
|
+
activeSessions.delete(sessionID);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export {
|
|
222
|
+
opencode_question_default as default
|
|
223
|
+
};
|