oh-my-opencode-slim 2.0.0-beta.0 → 2.0.0-beta.3
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/README.md +2 -2
- package/dist/cli/index.js +6 -0
- package/dist/config/constants.d.ts +1 -1
- package/dist/hooks/deepwork/index.d.ts +13 -0
- package/dist/hooks/{session-goal → goal}/index.d.ts +1 -1
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/phase-reminder/index.d.ts +1 -1
- package/dist/index.js +517 -918
- package/dist/tools/index.d.ts +0 -2
- package/dist/tui.js +7 -3
- package/package.json +3 -2
- package/src/skills/codemap.md +3 -2
- package/src/skills/deepwork/SKILL.md +92 -0
- package/dist/agents/council-master.d.ts +0 -2
- package/dist/background/background-manager.d.ts +0 -203
- package/dist/background/index.d.ts +0 -3
- package/dist/background/multiplexer-session-manager.d.ts +0 -70
- package/dist/background/subagent-depth.d.ts +0 -35
- package/dist/cli/divoom.d.ts +0 -23
- package/dist/goal/index.d.ts +0 -3
- package/dist/goal/manager.d.ts +0 -41
- package/dist/goal/prompts.d.ts +0 -4
- package/dist/goal/store.d.ts +0 -15
- package/dist/goal/types.d.ts +0 -28
- package/dist/integrations/divoom/index.d.ts +0 -3
- package/dist/integrations/divoom/status-manager.d.ts +0 -31
- package/dist/integrations/divoom/swift-helper-source.d.ts +0 -1
- package/dist/integrations/divoom/swift-transport.d.ts +0 -26
- package/dist/integrations/divoom/types.d.ts +0 -41
- package/dist/tools/background.d.ts +0 -13
- package/dist/tools/fork/command.d.ts +0 -28
- package/dist/tools/fork/files.d.ts +0 -33
- package/dist/tools/fork/index.d.ts +0 -10
- package/dist/tools/fork/state.d.ts +0 -7
- package/dist/tools/fork/tools.d.ts +0 -23
- package/dist/tools/fork/vendor.d.ts +0 -28
- package/dist/tools/handoff/command.d.ts +0 -29
- package/dist/tools/handoff/files.d.ts +0 -33
- package/dist/tools/handoff/index.d.ts +0 -10
- package/dist/tools/handoff/state.d.ts +0 -7
- package/dist/tools/handoff/tools.d.ts +0 -23
- package/dist/tools/handoff/vendor.d.ts +0 -28
- package/dist/tools/lsp/client.d.ts +0 -81
- package/dist/tools/lsp/config-store.d.ts +0 -29
- package/dist/tools/lsp/config.d.ts +0 -5
- package/dist/tools/lsp/constants.d.ts +0 -24
- package/dist/tools/lsp/index.d.ts +0 -4
- package/dist/tools/lsp/tools.d.ts +0 -5
- package/dist/tools/lsp/types.d.ts +0 -45
- package/dist/tools/lsp/utils.d.ts +0 -34
- package/dist/tools/subtask/command.d.ts +0 -30
- package/dist/tools/subtask/files.d.ts +0 -34
- package/dist/tools/subtask/index.d.ts +0 -11
- package/dist/tools/subtask/state.d.ts +0 -7
- package/dist/tools/subtask/tools.d.ts +0 -23
- package/dist/tools/subtask/vendor.d.ts +0 -27
- package/dist/utils/tmux-debug-log.d.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -18226,6 +18226,12 @@ var CUSTOM_SKILLS = [
|
|
|
18226
18226
|
description: "Clone important dependency source for local inspection",
|
|
18227
18227
|
allowedAgents: ["orchestrator"],
|
|
18228
18228
|
sourcePath: "src/skills/clonedeps"
|
|
18229
|
+
},
|
|
18230
|
+
{
|
|
18231
|
+
name: "deepwork",
|
|
18232
|
+
description: "Heavy/complex coding sessions and large modifications workflow",
|
|
18233
|
+
allowedAgents: ["orchestrator"],
|
|
18234
|
+
sourcePath: "src/skills/deepwork"
|
|
18229
18235
|
}
|
|
18230
18236
|
];
|
|
18231
18237
|
|
|
@@ -18302,9 +18308,7 @@ var POLL_INTERVAL_BACKGROUND_MS = 2000;
|
|
|
18302
18308
|
var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
18303
18309
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
18304
18310
|
var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
18305
|
-
var PHASE_REMINDER_TEXT = `!IMPORTANT!
|
|
18306
|
-
Understand → build a short work graph with independent lanes, dependencies, and advisory ownership → dispatch independent specialists as background tasks → record task/session IDs → continue orchestration → poll task_status for terminal results → reconcile → verify.
|
|
18307
|
-
Only consume outputs or advance dependent work when background results are terminal. !END!`;
|
|
18311
|
+
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → poll task_status → reconcile terminal results → verify. Do not consume running-job output or advance dependent work. !END!`;
|
|
18308
18312
|
var TMUX_SPAWN_DELAY_MS = 500;
|
|
18309
18313
|
var COUNCILLOR_STAGGER_MS = 250;
|
|
18310
18314
|
var DEFAULT_DISABLED_AGENTS = ["observer"];
|
|
@@ -19103,27 +19107,6 @@ ${enabledParallelExamples}
|
|
|
19103
19107
|
|
|
19104
19108
|
Balance: respect dependencies, avoid parallelizing what must be sequential, and avoid overlapping write ownership.
|
|
19105
19109
|
|
|
19106
|
-
### Context Isolation
|
|
19107
|
-
If no specialist delegation is needed, consider \`subtask\` before doing
|
|
19108
|
-
context-heavy work directly.
|
|
19109
|
-
|
|
19110
|
-
Ask whether the parent context needs the details or only the result. Use
|
|
19111
|
-
\`subtask\` when the work is bounded, context-heavy, and the parent only needs a
|
|
19112
|
-
compact outcome.
|
|
19113
|
-
|
|
19114
|
-
Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
|
|
19115
|
-
verification across files/logs/messages.
|
|
19116
|
-
|
|
19117
|
-
Prefer native background \`task(..., background: true)\` plus \`task_status\` for independent specialist lanes. Use \`subtask\` only for bounded parent-local context isolation when native background specialist scheduling is not the right fit.
|
|
19118
|
-
|
|
19119
|
-
Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
|
|
19120
|
-
work better handled by a named specialist, or cases where the parent must reason
|
|
19121
|
-
over the details.
|
|
19122
|
-
|
|
19123
|
-
When calling \`subtask\`, give a self-contained prompt with objective,
|
|
19124
|
-
constraints, relevant context, deliverable, and validation. Pass only clearly
|
|
19125
|
-
relevant files. Wait for the summary, then integrate and verify it.
|
|
19126
|
-
|
|
19127
19110
|
### OpenCode scheduler model
|
|
19128
19111
|
- Delegated specialists should be launched as background tasks whenever work can run independently: use \`task(..., background: true)\`.
|
|
19129
19112
|
- A dispatch returns a task/session ID immediately; it does not mean completion.
|
|
@@ -22724,6 +22707,9 @@ class BackgroundJobBoard {
|
|
|
22724
22707
|
const existing = this.jobs.get(input.taskID);
|
|
22725
22708
|
if (!existing)
|
|
22726
22709
|
return;
|
|
22710
|
+
if (existing.state === "reconciled") {
|
|
22711
|
+
return existing;
|
|
22712
|
+
}
|
|
22727
22713
|
const now = input.now ?? Date.now();
|
|
22728
22714
|
const terminal = TERMINAL_STATES.has(input.state);
|
|
22729
22715
|
const updated = {
|
|
@@ -23008,7 +22994,7 @@ class SessionManager {
|
|
|
23008
22994
|
return;
|
|
23009
22995
|
return [
|
|
23010
22996
|
"### Resumable Sessions",
|
|
23011
|
-
"Reuse only
|
|
22997
|
+
"Reuse only completed/reconciled threads. Poll running jobs from Background Job Board.",
|
|
23012
22998
|
"",
|
|
23013
22999
|
...lines
|
|
23014
23000
|
].join(`
|
|
@@ -23206,6 +23192,53 @@ function createChatHeadersHook(ctx) {
|
|
|
23206
23192
|
}
|
|
23207
23193
|
};
|
|
23208
23194
|
}
|
|
23195
|
+
// src/hooks/deepwork/index.ts
|
|
23196
|
+
var COMMAND_NAME = "deepwork";
|
|
23197
|
+
function activationPrompt(task2) {
|
|
23198
|
+
return [
|
|
23199
|
+
"Use the deepwork skill for this task. Treat it as a heavy coding session.",
|
|
23200
|
+
"",
|
|
23201
|
+
"Deepwork requirements:",
|
|
23202
|
+
"- create/update a `.slim/deepwork/` progress file;",
|
|
23203
|
+
"- keep OpenCode todos synced with the current phase;",
|
|
23204
|
+
"- draft a plan and get `@oracle` review before implementation;",
|
|
23205
|
+
"- create and review a phased implementation/delegation plan;",
|
|
23206
|
+
"- execute phase by phase with background specialists where useful;",
|
|
23207
|
+
"- poll `task_status`, reconcile results, validate, and ask `@oracle` to review each phase;",
|
|
23208
|
+
"- ask `@oracle` to include simplify/readability feedback in phase reviews;",
|
|
23209
|
+
"- fix actionable review issues before continuing.",
|
|
23210
|
+
"",
|
|
23211
|
+
"Task:",
|
|
23212
|
+
task2
|
|
23213
|
+
].join(`
|
|
23214
|
+
`);
|
|
23215
|
+
}
|
|
23216
|
+
function createDeepworkCommandHook() {
|
|
23217
|
+
return {
|
|
23218
|
+
registerCommand: (opencodeConfig) => {
|
|
23219
|
+
const commandConfig = opencodeConfig.command;
|
|
23220
|
+
if (commandConfig?.[COMMAND_NAME])
|
|
23221
|
+
return;
|
|
23222
|
+
if (!opencodeConfig.command)
|
|
23223
|
+
opencodeConfig.command = {};
|
|
23224
|
+
opencodeConfig.command[COMMAND_NAME] = {
|
|
23225
|
+
template: "Start a deepwork session for a complex coding task",
|
|
23226
|
+
description: "Use the deepwork workflow for heavy multi-phase coding work"
|
|
23227
|
+
};
|
|
23228
|
+
},
|
|
23229
|
+
handleCommandExecuteBefore: async (input, output) => {
|
|
23230
|
+
if (input.command !== COMMAND_NAME)
|
|
23231
|
+
return;
|
|
23232
|
+
output.parts.length = 0;
|
|
23233
|
+
const task2 = input.arguments.trim();
|
|
23234
|
+
if (!task2) {
|
|
23235
|
+
output.parts.push(createInternalAgentTextPart("What task should deepwork manage? Run `/deepwork <task>`."));
|
|
23236
|
+
return;
|
|
23237
|
+
}
|
|
23238
|
+
output.parts.push({ type: "text", text: activationPrompt(task2) });
|
|
23239
|
+
}
|
|
23240
|
+
};
|
|
23241
|
+
}
|
|
23209
23242
|
// src/hooks/delegate-task-retry/patterns.ts
|
|
23210
23243
|
var DELEGATE_TASK_ERROR_PATTERNS = [
|
|
23211
23244
|
{
|
|
@@ -23619,370 +23652,58 @@ class ForegroundFallbackManager {
|
|
|
23619
23652
|
return all;
|
|
23620
23653
|
}
|
|
23621
23654
|
}
|
|
23622
|
-
// src/hooks/
|
|
23623
|
-
import
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
import { basename as basename2, extname, join as join7 } from "node:path";
|
|
23634
|
-
var lastCleanupByDir = new Map;
|
|
23635
|
-
var CLEANUP_INTERVAL = 10 * 60 * 1000;
|
|
23636
|
-
function isImagePart(p) {
|
|
23637
|
-
if (p.type === "image")
|
|
23638
|
-
return true;
|
|
23639
|
-
if (p.type === "file") {
|
|
23640
|
-
const mime = p.mime;
|
|
23641
|
-
if (mime?.startsWith("image/"))
|
|
23642
|
-
return true;
|
|
23643
|
-
const filename = p.filename;
|
|
23644
|
-
const name = p.name;
|
|
23645
|
-
const fileName = filename ?? name;
|
|
23646
|
-
if (fileName && /\.(png|jpg|jpeg|gif|bmp|webp|svg|ico|tiff?|heic)$/i.test(fileName))
|
|
23647
|
-
return true;
|
|
23648
|
-
}
|
|
23649
|
-
return false;
|
|
23650
|
-
}
|
|
23651
|
-
function decodeDataUrl(url) {
|
|
23652
|
-
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
23653
|
-
if (!match)
|
|
23654
|
-
return null;
|
|
23655
|
-
return { mime: match[1], data: Buffer.from(match[2], "base64") };
|
|
23655
|
+
// src/hooks/goal/index.ts
|
|
23656
|
+
import * as fs7 from "node:fs/promises";
|
|
23657
|
+
|
|
23658
|
+
// src/interview/document.ts
|
|
23659
|
+
import * as fsSync from "node:fs";
|
|
23660
|
+
import * as fs6 from "node:fs/promises";
|
|
23661
|
+
import * as path9 from "node:path";
|
|
23662
|
+
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
23663
|
+
function normalizeOutputFolder(outputFolder) {
|
|
23664
|
+
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
23665
|
+
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
23656
23666
|
}
|
|
23657
|
-
function
|
|
23658
|
-
|
|
23659
|
-
"image/png": ".png",
|
|
23660
|
-
"image/jpeg": ".jpg",
|
|
23661
|
-
"image/gif": ".gif",
|
|
23662
|
-
"image/webp": ".webp",
|
|
23663
|
-
"image/svg+xml": ".svg",
|
|
23664
|
-
"image/bmp": ".bmp"
|
|
23665
|
-
};
|
|
23666
|
-
return map[mime] ?? ".png";
|
|
23667
|
+
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
23668
|
+
return path9.join(directory, normalizeOutputFolder(outputFolder));
|
|
23667
23669
|
}
|
|
23668
|
-
function
|
|
23669
|
-
|
|
23670
|
+
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
23671
|
+
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
23672
|
+
return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
23670
23673
|
}
|
|
23671
|
-
function
|
|
23672
|
-
|
|
23673
|
-
const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
|
|
23674
|
-
if (now - lastCleanup < CLEANUP_INTERVAL)
|
|
23675
|
-
return;
|
|
23676
|
-
lastCleanupByDir.set(saveDir, now);
|
|
23677
|
-
const maxAge = 60 * 60 * 1000;
|
|
23678
|
-
const dirsToScan = [];
|
|
23679
|
-
try {
|
|
23680
|
-
for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
|
|
23681
|
-
const fp = join7(saveDir, entry.name);
|
|
23682
|
-
if (entry.isDirectory()) {
|
|
23683
|
-
dirsToScan.push(fp);
|
|
23684
|
-
} else {
|
|
23685
|
-
try {
|
|
23686
|
-
if (now - statSync3(fp).mtimeMs > maxAge)
|
|
23687
|
-
unlinkSync2(fp);
|
|
23688
|
-
} catch {}
|
|
23689
|
-
}
|
|
23690
|
-
}
|
|
23691
|
-
} catch {}
|
|
23692
|
-
for (const dir of dirsToScan) {
|
|
23693
|
-
try {
|
|
23694
|
-
let isEmpty = true;
|
|
23695
|
-
let allRemoved = true;
|
|
23696
|
-
for (const f of readdirSync2(dir)) {
|
|
23697
|
-
isEmpty = false;
|
|
23698
|
-
const fp = join7(dir, f);
|
|
23699
|
-
try {
|
|
23700
|
-
if (now - statSync3(fp).mtimeMs > maxAge) {
|
|
23701
|
-
unlinkSync2(fp);
|
|
23702
|
-
} else {
|
|
23703
|
-
allRemoved = false;
|
|
23704
|
-
}
|
|
23705
|
-
} catch {
|
|
23706
|
-
allRemoved = false;
|
|
23707
|
-
}
|
|
23708
|
-
}
|
|
23709
|
-
if (!isEmpty && allRemoved) {
|
|
23710
|
-
try {
|
|
23711
|
-
rmdirSync(dir);
|
|
23712
|
-
} catch {}
|
|
23713
|
-
}
|
|
23714
|
-
} catch {}
|
|
23715
|
-
}
|
|
23674
|
+
function relativeInterviewPath(directory, filePath) {
|
|
23675
|
+
return path9.relative(directory, filePath) || path9.basename(filePath);
|
|
23716
23676
|
}
|
|
23717
|
-
function
|
|
23718
|
-
const
|
|
23719
|
-
|
|
23720
|
-
|
|
23721
|
-
if (existsSync5(candidate)) {
|
|
23722
|
-
return candidate;
|
|
23677
|
+
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
23678
|
+
const trimmed = value.trim();
|
|
23679
|
+
if (!trimmed) {
|
|
23680
|
+
return null;
|
|
23723
23681
|
}
|
|
23724
|
-
|
|
23725
|
-
const
|
|
23726
|
-
|
|
23727
|
-
|
|
23728
|
-
|
|
23729
|
-
|
|
23730
|
-
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
continue;
|
|
23735
|
-
}
|
|
23736
|
-
log2(`[image-hook] failed to save image: ${e}`);
|
|
23737
|
-
return null;
|
|
23682
|
+
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
23683
|
+
const candidates = new Set;
|
|
23684
|
+
const resolvedRoot = path9.resolve(directory);
|
|
23685
|
+
if (path9.isAbsolute(trimmed)) {
|
|
23686
|
+
candidates.add(trimmed);
|
|
23687
|
+
} else {
|
|
23688
|
+
candidates.add(path9.resolve(directory, trimmed));
|
|
23689
|
+
candidates.add(path9.join(outputDir, trimmed));
|
|
23690
|
+
if (!trimmed.endsWith(".md")) {
|
|
23691
|
+
candidates.add(path9.join(outputDir, `${trimmed}.md`));
|
|
23738
23692
|
}
|
|
23739
23693
|
}
|
|
23740
|
-
|
|
23741
|
-
|
|
23742
|
-
}
|
|
23743
|
-
function processImageAttachments(args) {
|
|
23744
|
-
const { messages, workDir, disabledAgents, log: log2 } = args;
|
|
23745
|
-
const observerEnabled = !disabledAgents.has("observer");
|
|
23746
|
-
if (!observerEnabled)
|
|
23747
|
-
return;
|
|
23748
|
-
const messagesWithImages = [];
|
|
23749
|
-
for (const msg of messages) {
|
|
23750
|
-
if (msg.info.role !== "user")
|
|
23694
|
+
for (const candidate of candidates) {
|
|
23695
|
+
if (path9.extname(candidate) !== ".md") {
|
|
23751
23696
|
continue;
|
|
23752
|
-
const imageParts = msg.parts.filter(isImagePart);
|
|
23753
|
-
if (imageParts.length > 0) {
|
|
23754
|
-
messagesWithImages.push({ msg, imageParts });
|
|
23755
23697
|
}
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
if (existsSync5(saveDir))
|
|
23760
|
-
cleanupAllSessions(saveDir);
|
|
23761
|
-
return;
|
|
23762
|
-
}
|
|
23763
|
-
const gitignorePath = join7(workDir, ".opencode", ".gitignore");
|
|
23764
|
-
try {
|
|
23765
|
-
mkdirSync3(saveDir, { recursive: true });
|
|
23766
|
-
if (!existsSync5(gitignorePath))
|
|
23767
|
-
writeFileSync3(gitignorePath, `*
|
|
23768
|
-
`);
|
|
23769
|
-
} catch (e) {
|
|
23770
|
-
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
23771
|
-
}
|
|
23772
|
-
cleanupAllSessions(saveDir);
|
|
23773
|
-
for (const { msg, imageParts } of messagesWithImages) {
|
|
23774
|
-
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
23775
|
-
const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
|
|
23776
|
-
try {
|
|
23777
|
-
mkdirSync3(targetDir, { recursive: true });
|
|
23778
|
-
} catch (e) {
|
|
23779
|
-
log2(`[image-hook] failed to create target image directory: ${e}`);
|
|
23698
|
+
const resolved = path9.resolve(candidate);
|
|
23699
|
+
if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
|
|
23700
|
+
continue;
|
|
23780
23701
|
}
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
const url = p.url;
|
|
23784
|
-
const filename = p.filename ?? p.name;
|
|
23785
|
-
if (url) {
|
|
23786
|
-
const decoded = decodeDataUrl(url);
|
|
23787
|
-
if (decoded) {
|
|
23788
|
-
const hash = createHash("sha1").update(decoded.data).digest("hex").slice(0, 8);
|
|
23789
|
-
const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
|
|
23790
|
-
const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
|
|
23791
|
-
const ext = sanitizedFilename ? extname(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
|
|
23792
|
-
const name = `${baseName}-${hash}${ext}`;
|
|
23793
|
-
const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
|
|
23794
|
-
if (filePath)
|
|
23795
|
-
savedPaths.push(filePath);
|
|
23796
|
-
}
|
|
23797
|
-
}
|
|
23702
|
+
if (fsSync.existsSync(candidate)) {
|
|
23703
|
+
return candidate;
|
|
23798
23704
|
}
|
|
23799
|
-
const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
|
|
23800
|
-
log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
|
|
23801
|
-
msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
|
|
23802
|
-
{
|
|
23803
|
-
type: "text",
|
|
23804
|
-
text: `[Image attachment detected.${pathsText} Your model may not support image input. Delegate to @observer with the file path(s) above so it can read the file with its read tool.]`
|
|
23805
|
-
}
|
|
23806
|
-
]);
|
|
23807
23705
|
}
|
|
23808
|
-
|
|
23809
|
-
// src/hooks/json-error-recovery/hook.ts
|
|
23810
|
-
var JSON_ERROR_TOOL_EXCLUDE_LIST = [
|
|
23811
|
-
"bash",
|
|
23812
|
-
"read",
|
|
23813
|
-
"glob",
|
|
23814
|
-
"webfetch",
|
|
23815
|
-
"grep_app_searchgithub",
|
|
23816
|
-
"websearch_web_search_exa"
|
|
23817
|
-
];
|
|
23818
|
-
var JSON_ERROR_PATTERNS = [
|
|
23819
|
-
/json parse error/i,
|
|
23820
|
-
/failed to parse json/i,
|
|
23821
|
-
/invalid json/i,
|
|
23822
|
-
/malformed json/i,
|
|
23823
|
-
/unexpected end of json input/i,
|
|
23824
|
-
/syntaxerror:\s*unexpected token.*json/i,
|
|
23825
|
-
/json[^\n]*expected '\}'/i,
|
|
23826
|
-
/json[^\n]*unexpected eof/i
|
|
23827
|
-
];
|
|
23828
|
-
var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
|
|
23829
|
-
var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
|
|
23830
|
-
var JSON_ERROR_REMINDER = `
|
|
23831
|
-
[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
|
|
23832
|
-
|
|
23833
|
-
You sent invalid JSON arguments. The system could not parse your tool call.
|
|
23834
|
-
STOP and do this NOW:
|
|
23835
|
-
|
|
23836
|
-
1. LOOK at the error message above to see what was expected vs what you sent.
|
|
23837
|
-
2. CORRECT your JSON syntax (missing braces, unescaped quotes, trailing commas, etc).
|
|
23838
|
-
3. RETRY the tool call with valid JSON.
|
|
23839
|
-
|
|
23840
|
-
DO NOT repeat the exact same invalid call.
|
|
23841
|
-
`;
|
|
23842
|
-
function createJsonErrorRecoveryHook(_ctx) {
|
|
23843
|
-
return {
|
|
23844
|
-
"tool.execute.after": async (input, output) => {
|
|
23845
|
-
if (JSON_ERROR_EXCLUDED_TOOLS.has(input.tool.toLowerCase()))
|
|
23846
|
-
return;
|
|
23847
|
-
if (typeof output.output !== "string")
|
|
23848
|
-
return;
|
|
23849
|
-
if (output.output.includes(JSON_ERROR_REMINDER_MARKER))
|
|
23850
|
-
return;
|
|
23851
|
-
const outputText = output.output;
|
|
23852
|
-
const hasJsonError = JSON_ERROR_PATTERNS.some((pattern) => pattern.test(outputText));
|
|
23853
|
-
if (hasJsonError) {
|
|
23854
|
-
output.output += `
|
|
23855
|
-
${JSON_ERROR_REMINDER}`;
|
|
23856
|
-
}
|
|
23857
|
-
}
|
|
23858
|
-
};
|
|
23859
|
-
}
|
|
23860
|
-
// src/hooks/phase-reminder/index.ts
|
|
23861
|
-
var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
|
|
23862
|
-
function createPhaseReminderHook() {
|
|
23863
|
-
return {
|
|
23864
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
23865
|
-
const { messages } = output;
|
|
23866
|
-
if (messages.length === 0) {
|
|
23867
|
-
return;
|
|
23868
|
-
}
|
|
23869
|
-
let lastUserMessageIndex = -1;
|
|
23870
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
23871
|
-
if (messages[i].info.role === "user") {
|
|
23872
|
-
lastUserMessageIndex = i;
|
|
23873
|
-
break;
|
|
23874
|
-
}
|
|
23875
|
-
}
|
|
23876
|
-
if (lastUserMessageIndex === -1) {
|
|
23877
|
-
return;
|
|
23878
|
-
}
|
|
23879
|
-
const lastUserMessage = messages[lastUserMessageIndex];
|
|
23880
|
-
const agent = lastUserMessage.info.agent;
|
|
23881
|
-
if (agent && agent !== "orchestrator") {
|
|
23882
|
-
return;
|
|
23883
|
-
}
|
|
23884
|
-
const textPartIndex = lastUserMessage.parts.findIndex((p) => p.type === "text" && p.text !== undefined);
|
|
23885
|
-
if (textPartIndex === -1) {
|
|
23886
|
-
return;
|
|
23887
|
-
}
|
|
23888
|
-
const originalText = lastUserMessage.parts[textPartIndex].text ?? "";
|
|
23889
|
-
if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
|
|
23890
|
-
return;
|
|
23891
|
-
}
|
|
23892
|
-
if (lastUserMessage.parts.some((p) => p.text?.includes(PHASE_REMINDER))) {
|
|
23893
|
-
return;
|
|
23894
|
-
}
|
|
23895
|
-
lastUserMessage.parts.push({
|
|
23896
|
-
type: "text",
|
|
23897
|
-
text: PHASE_REMINDER
|
|
23898
|
-
});
|
|
23899
|
-
}
|
|
23900
|
-
};
|
|
23901
|
-
}
|
|
23902
|
-
// src/hooks/post-file-tool-nudge/index.ts
|
|
23903
|
-
var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
|
|
23904
|
-
var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
|
|
23905
|
-
function createPostFileToolNudgeHook(options = {}) {
|
|
23906
|
-
function appendReminder(output) {
|
|
23907
|
-
if (typeof output.output !== "string") {
|
|
23908
|
-
return;
|
|
23909
|
-
}
|
|
23910
|
-
if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
|
|
23911
|
-
return;
|
|
23912
|
-
}
|
|
23913
|
-
output.output = [
|
|
23914
|
-
output.output,
|
|
23915
|
-
"",
|
|
23916
|
-
"<internal_reminder>",
|
|
23917
|
-
POST_FILE_TOOL_NUDGE,
|
|
23918
|
-
"</internal_reminder>"
|
|
23919
|
-
].join(`
|
|
23920
|
-
`);
|
|
23921
|
-
}
|
|
23922
|
-
return {
|
|
23923
|
-
"tool.execute.after": async (input, output) => {
|
|
23924
|
-
if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
|
|
23925
|
-
return;
|
|
23926
|
-
}
|
|
23927
|
-
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
23928
|
-
return;
|
|
23929
|
-
}
|
|
23930
|
-
appendReminder(output);
|
|
23931
|
-
}
|
|
23932
|
-
};
|
|
23933
|
-
}
|
|
23934
|
-
// src/hooks/session-goal/index.ts
|
|
23935
|
-
import * as fs7 from "node:fs/promises";
|
|
23936
|
-
|
|
23937
|
-
// src/interview/document.ts
|
|
23938
|
-
import * as fsSync from "node:fs";
|
|
23939
|
-
import * as fs6 from "node:fs/promises";
|
|
23940
|
-
import * as path9 from "node:path";
|
|
23941
|
-
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
23942
|
-
function normalizeOutputFolder(outputFolder) {
|
|
23943
|
-
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
23944
|
-
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
23945
|
-
}
|
|
23946
|
-
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
23947
|
-
return path9.join(directory, normalizeOutputFolder(outputFolder));
|
|
23948
|
-
}
|
|
23949
|
-
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
23950
|
-
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
23951
|
-
return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
23952
|
-
}
|
|
23953
|
-
function relativeInterviewPath(directory, filePath) {
|
|
23954
|
-
return path9.relative(directory, filePath) || path9.basename(filePath);
|
|
23955
|
-
}
|
|
23956
|
-
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
23957
|
-
const trimmed = value.trim();
|
|
23958
|
-
if (!trimmed) {
|
|
23959
|
-
return null;
|
|
23960
|
-
}
|
|
23961
|
-
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
23962
|
-
const candidates = new Set;
|
|
23963
|
-
const resolvedRoot = path9.resolve(directory);
|
|
23964
|
-
if (path9.isAbsolute(trimmed)) {
|
|
23965
|
-
candidates.add(trimmed);
|
|
23966
|
-
} else {
|
|
23967
|
-
candidates.add(path9.resolve(directory, trimmed));
|
|
23968
|
-
candidates.add(path9.join(outputDir, trimmed));
|
|
23969
|
-
if (!trimmed.endsWith(".md")) {
|
|
23970
|
-
candidates.add(path9.join(outputDir, `${trimmed}.md`));
|
|
23971
|
-
}
|
|
23972
|
-
}
|
|
23973
|
-
for (const candidate of candidates) {
|
|
23974
|
-
if (path9.extname(candidate) !== ".md") {
|
|
23975
|
-
continue;
|
|
23976
|
-
}
|
|
23977
|
-
const resolved = path9.resolve(candidate);
|
|
23978
|
-
if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
|
|
23979
|
-
continue;
|
|
23980
|
-
}
|
|
23981
|
-
if (fsSync.existsSync(candidate)) {
|
|
23982
|
-
return candidate;
|
|
23983
|
-
}
|
|
23984
|
-
}
|
|
23985
|
-
return null;
|
|
23706
|
+
return null;
|
|
23986
23707
|
}
|
|
23987
23708
|
function slugify(value) {
|
|
23988
23709
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
@@ -24103,8 +23824,8 @@ A: ${answer.answer.trim()}` : null;
|
|
|
24103
23824
|
}), "utf8");
|
|
24104
23825
|
}
|
|
24105
23826
|
|
|
24106
|
-
// src/hooks/
|
|
24107
|
-
var
|
|
23827
|
+
// src/hooks/goal/index.ts
|
|
23828
|
+
var COMMAND_NAME2 = "goal";
|
|
24108
23829
|
var MAX_GOAL_LENGTH = 4000;
|
|
24109
23830
|
function normalizeGoalText(text) {
|
|
24110
23831
|
return text.trim().replace(/\s+/g, " ").slice(0, MAX_GOAL_LENGTH);
|
|
@@ -24161,23 +23882,23 @@ function resolveGoal(goals, sessionID) {
|
|
|
24161
23882
|
currentSessionID = goal.inheritedFrom;
|
|
24162
23883
|
}
|
|
24163
23884
|
}
|
|
24164
|
-
function
|
|
23885
|
+
function createGoalHook(ctx, config, options) {
|
|
24165
23886
|
const goals = new Map;
|
|
24166
23887
|
const outputFolder = config.interview?.outputFolder ?? "interview";
|
|
24167
23888
|
return {
|
|
24168
23889
|
registerCommand: (opencodeConfig) => {
|
|
24169
23890
|
const commandConfig = opencodeConfig.command;
|
|
24170
|
-
if (commandConfig?.[
|
|
23891
|
+
if (commandConfig?.[COMMAND_NAME2])
|
|
24171
23892
|
return;
|
|
24172
23893
|
if (!opencodeConfig.command)
|
|
24173
23894
|
opencodeConfig.command = {};
|
|
24174
|
-
opencodeConfig.command[
|
|
24175
|
-
template: "Set or show the current
|
|
23895
|
+
opencodeConfig.command[COMMAND_NAME2] = {
|
|
23896
|
+
template: "Set or show the current goal",
|
|
24176
23897
|
description: "Pin a session objective that keeps todos, delegation, and verification aligned"
|
|
24177
23898
|
};
|
|
24178
23899
|
},
|
|
24179
23900
|
handleCommandExecuteBefore: async (input, output) => {
|
|
24180
|
-
if (input.command !==
|
|
23901
|
+
if (input.command !== COMMAND_NAME2)
|
|
24181
23902
|
return;
|
|
24182
23903
|
output.parts.length = 0;
|
|
24183
23904
|
const args = input.arguments.trim();
|
|
@@ -24201,64 +23922,376 @@ Use todos for execution steps. Auto-continuation continues only while todos rema
|
|
|
24201
23922
|
pushText(output, `Could not find a readable interview spec for "${value}".`);
|
|
24202
23923
|
return;
|
|
24203
23924
|
}
|
|
24204
|
-
goals.set(input.sessionID, {
|
|
24205
|
-
text: interviewGoal.text,
|
|
24206
|
-
source: "interview",
|
|
24207
|
-
sourcePath: interviewGoal.sourcePath,
|
|
24208
|
-
createdAt: Date.now()
|
|
24209
|
-
});
|
|
24210
|
-
pushText(output, `Set active goal from interview:
|
|
24211
|
-
${interviewGoal.text}`);
|
|
23925
|
+
goals.set(input.sessionID, {
|
|
23926
|
+
text: interviewGoal.text,
|
|
23927
|
+
source: "interview",
|
|
23928
|
+
sourcePath: interviewGoal.sourcePath,
|
|
23929
|
+
createdAt: Date.now()
|
|
23930
|
+
});
|
|
23931
|
+
pushText(output, `Set active goal from interview:
|
|
23932
|
+
${interviewGoal.text}`);
|
|
23933
|
+
return;
|
|
23934
|
+
}
|
|
23935
|
+
const text = normalizeGoalText(args);
|
|
23936
|
+
goals.set(input.sessionID, {
|
|
23937
|
+
text,
|
|
23938
|
+
source: "manual",
|
|
23939
|
+
createdAt: Date.now()
|
|
23940
|
+
});
|
|
23941
|
+
pushText(output, `Set active goal:
|
|
23942
|
+
${text}`);
|
|
23943
|
+
},
|
|
23944
|
+
handleEvent: (input) => {
|
|
23945
|
+
const event = input.event;
|
|
23946
|
+
if (event.type === "session.created") {
|
|
23947
|
+
const info = event.properties?.info;
|
|
23948
|
+
if (!info?.id || !info.parentID)
|
|
23949
|
+
return;
|
|
23950
|
+
const parentGoal = goals.get(info.parentID);
|
|
23951
|
+
if (!parentGoal)
|
|
23952
|
+
return;
|
|
23953
|
+
goals.set(info.id, {
|
|
23954
|
+
inheritedFrom: info.parentID,
|
|
23955
|
+
createdAt: Date.now(),
|
|
23956
|
+
text: ""
|
|
23957
|
+
});
|
|
23958
|
+
return;
|
|
23959
|
+
}
|
|
23960
|
+
if (event.type === "session.deleted") {
|
|
23961
|
+
const props = event.properties;
|
|
23962
|
+
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
23963
|
+
if (sessionID)
|
|
23964
|
+
goals.delete(sessionID);
|
|
23965
|
+
}
|
|
23966
|
+
},
|
|
23967
|
+
handleSystemTransform: (input, output) => {
|
|
23968
|
+
if (!input.sessionID)
|
|
23969
|
+
return;
|
|
23970
|
+
const resolved = resolveGoal(goals, input.sessionID);
|
|
23971
|
+
if (!resolved)
|
|
23972
|
+
return;
|
|
23973
|
+
const agentName = options?.getAgentName?.(input.sessionID);
|
|
23974
|
+
const { goal, inherited } = resolved;
|
|
23975
|
+
if (!inherited && agentName && agentName !== "orchestrator")
|
|
23976
|
+
return;
|
|
23977
|
+
const block = formatGoal(goal, inherited);
|
|
23978
|
+
if (output.system.some((entry) => entry.includes(block)))
|
|
23979
|
+
return;
|
|
23980
|
+
output.system.push(block);
|
|
23981
|
+
},
|
|
23982
|
+
getGoal: (sessionID) => resolveGoal(goals, sessionID)?.goal
|
|
23983
|
+
};
|
|
23984
|
+
}
|
|
23985
|
+
// src/hooks/image-hook.ts
|
|
23986
|
+
import { createHash } from "node:crypto";
|
|
23987
|
+
import {
|
|
23988
|
+
existsSync as existsSync6,
|
|
23989
|
+
mkdirSync as mkdirSync3,
|
|
23990
|
+
readdirSync as readdirSync2,
|
|
23991
|
+
rmdirSync,
|
|
23992
|
+
statSync as statSync3,
|
|
23993
|
+
unlinkSync as unlinkSync2,
|
|
23994
|
+
writeFileSync as writeFileSync3
|
|
23995
|
+
} from "node:fs";
|
|
23996
|
+
import { basename as basename3, extname as extname2, join as join8 } from "node:path";
|
|
23997
|
+
var lastCleanupByDir = new Map;
|
|
23998
|
+
var CLEANUP_INTERVAL = 10 * 60 * 1000;
|
|
23999
|
+
function isImagePart(p) {
|
|
24000
|
+
if (p.type === "image")
|
|
24001
|
+
return true;
|
|
24002
|
+
if (p.type === "file") {
|
|
24003
|
+
const mime = p.mime;
|
|
24004
|
+
if (mime?.startsWith("image/"))
|
|
24005
|
+
return true;
|
|
24006
|
+
const filename = p.filename;
|
|
24007
|
+
const name = p.name;
|
|
24008
|
+
const fileName = filename ?? name;
|
|
24009
|
+
if (fileName && /\.(png|jpg|jpeg|gif|bmp|webp|svg|ico|tiff?|heic)$/i.test(fileName))
|
|
24010
|
+
return true;
|
|
24011
|
+
}
|
|
24012
|
+
return false;
|
|
24013
|
+
}
|
|
24014
|
+
function decodeDataUrl(url) {
|
|
24015
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
24016
|
+
if (!match)
|
|
24017
|
+
return null;
|
|
24018
|
+
return { mime: match[1], data: Buffer.from(match[2], "base64") };
|
|
24019
|
+
}
|
|
24020
|
+
function extFromMime(mime) {
|
|
24021
|
+
const map = {
|
|
24022
|
+
"image/png": ".png",
|
|
24023
|
+
"image/jpeg": ".jpg",
|
|
24024
|
+
"image/gif": ".gif",
|
|
24025
|
+
"image/webp": ".webp",
|
|
24026
|
+
"image/svg+xml": ".svg",
|
|
24027
|
+
"image/bmp": ".bmp"
|
|
24028
|
+
};
|
|
24029
|
+
return map[mime] ?? ".png";
|
|
24030
|
+
}
|
|
24031
|
+
function sanitizeFilename(name) {
|
|
24032
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
24033
|
+
}
|
|
24034
|
+
function cleanupAllSessions(saveDir) {
|
|
24035
|
+
const now = Date.now();
|
|
24036
|
+
const lastCleanup = lastCleanupByDir.get(saveDir) ?? 0;
|
|
24037
|
+
if (now - lastCleanup < CLEANUP_INTERVAL)
|
|
24038
|
+
return;
|
|
24039
|
+
lastCleanupByDir.set(saveDir, now);
|
|
24040
|
+
const maxAge = 60 * 60 * 1000;
|
|
24041
|
+
const dirsToScan = [];
|
|
24042
|
+
try {
|
|
24043
|
+
for (const entry of readdirSync2(saveDir, { withFileTypes: true })) {
|
|
24044
|
+
const fp = join8(saveDir, entry.name);
|
|
24045
|
+
if (entry.isDirectory()) {
|
|
24046
|
+
dirsToScan.push(fp);
|
|
24047
|
+
} else {
|
|
24048
|
+
try {
|
|
24049
|
+
if (now - statSync3(fp).mtimeMs > maxAge)
|
|
24050
|
+
unlinkSync2(fp);
|
|
24051
|
+
} catch {}
|
|
24052
|
+
}
|
|
24053
|
+
}
|
|
24054
|
+
} catch {}
|
|
24055
|
+
for (const dir of dirsToScan) {
|
|
24056
|
+
try {
|
|
24057
|
+
let isEmpty = true;
|
|
24058
|
+
let allRemoved = true;
|
|
24059
|
+
for (const f of readdirSync2(dir)) {
|
|
24060
|
+
isEmpty = false;
|
|
24061
|
+
const fp = join8(dir, f);
|
|
24062
|
+
try {
|
|
24063
|
+
if (now - statSync3(fp).mtimeMs > maxAge) {
|
|
24064
|
+
unlinkSync2(fp);
|
|
24065
|
+
} else {
|
|
24066
|
+
allRemoved = false;
|
|
24067
|
+
}
|
|
24068
|
+
} catch {
|
|
24069
|
+
allRemoved = false;
|
|
24070
|
+
}
|
|
24071
|
+
}
|
|
24072
|
+
if (!isEmpty && allRemoved) {
|
|
24073
|
+
try {
|
|
24074
|
+
rmdirSync(dir);
|
|
24075
|
+
} catch {}
|
|
24076
|
+
}
|
|
24077
|
+
} catch {}
|
|
24078
|
+
}
|
|
24079
|
+
}
|
|
24080
|
+
function writeUniqueFile(dir, name, data, log2) {
|
|
24081
|
+
const ext = extname2(name);
|
|
24082
|
+
const base = basename3(name, ext) || name;
|
|
24083
|
+
let candidate = join8(dir, name);
|
|
24084
|
+
if (existsSync6(candidate)) {
|
|
24085
|
+
return candidate;
|
|
24086
|
+
}
|
|
24087
|
+
let counter = 0;
|
|
24088
|
+
const MAX_ATTEMPTS = 1000;
|
|
24089
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
24090
|
+
try {
|
|
24091
|
+
writeFileSync3(candidate, data, { flag: "wx" });
|
|
24092
|
+
return candidate;
|
|
24093
|
+
} catch (e) {
|
|
24094
|
+
if (e instanceof Error && e.code === "EEXIST") {
|
|
24095
|
+
counter += 1;
|
|
24096
|
+
candidate = join8(dir, `${base}-${counter}${ext}`);
|
|
24097
|
+
continue;
|
|
24098
|
+
}
|
|
24099
|
+
log2(`[image-hook] failed to save image: ${e}`);
|
|
24100
|
+
return null;
|
|
24101
|
+
}
|
|
24102
|
+
}
|
|
24103
|
+
log2(`[image-hook] failed to save image: max attempts (${MAX_ATTEMPTS}) reached`);
|
|
24104
|
+
return null;
|
|
24105
|
+
}
|
|
24106
|
+
function processImageAttachments(args) {
|
|
24107
|
+
const { messages, workDir, disabledAgents, log: log2 } = args;
|
|
24108
|
+
const observerEnabled = !disabledAgents.has("observer");
|
|
24109
|
+
if (!observerEnabled)
|
|
24110
|
+
return;
|
|
24111
|
+
const messagesWithImages = [];
|
|
24112
|
+
for (const msg of messages) {
|
|
24113
|
+
if (msg.info.role !== "user")
|
|
24114
|
+
continue;
|
|
24115
|
+
const imageParts = msg.parts.filter(isImagePart);
|
|
24116
|
+
if (imageParts.length > 0) {
|
|
24117
|
+
messagesWithImages.push({ msg, imageParts });
|
|
24118
|
+
}
|
|
24119
|
+
}
|
|
24120
|
+
const saveDir = join8(workDir, ".opencode", "images");
|
|
24121
|
+
if (messagesWithImages.length === 0) {
|
|
24122
|
+
if (existsSync6(saveDir))
|
|
24123
|
+
cleanupAllSessions(saveDir);
|
|
24124
|
+
return;
|
|
24125
|
+
}
|
|
24126
|
+
const gitignorePath = join8(workDir, ".opencode", ".gitignore");
|
|
24127
|
+
try {
|
|
24128
|
+
mkdirSync3(saveDir, { recursive: true });
|
|
24129
|
+
if (!existsSync6(gitignorePath))
|
|
24130
|
+
writeFileSync3(gitignorePath, `*
|
|
24131
|
+
`);
|
|
24132
|
+
} catch (e) {
|
|
24133
|
+
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
24134
|
+
}
|
|
24135
|
+
cleanupAllSessions(saveDir);
|
|
24136
|
+
for (const { msg, imageParts } of messagesWithImages) {
|
|
24137
|
+
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
24138
|
+
const targetDir = sessionSubdir ? join8(saveDir, sessionSubdir) : saveDir;
|
|
24139
|
+
try {
|
|
24140
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
24141
|
+
} catch (e) {
|
|
24142
|
+
log2(`[image-hook] failed to create target image directory: ${e}`);
|
|
24143
|
+
}
|
|
24144
|
+
const savedPaths = [];
|
|
24145
|
+
for (const p of imageParts) {
|
|
24146
|
+
const url = p.url;
|
|
24147
|
+
const filename = p.filename ?? p.name;
|
|
24148
|
+
if (url) {
|
|
24149
|
+
const decoded = decodeDataUrl(url);
|
|
24150
|
+
if (decoded) {
|
|
24151
|
+
const hash = createHash("sha1").update(decoded.data).digest("hex").slice(0, 8);
|
|
24152
|
+
const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
|
|
24153
|
+
const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
|
|
24154
|
+
const ext = sanitizedFilename ? extname2(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
|
|
24155
|
+
const name = `${baseName}-${hash}${ext}`;
|
|
24156
|
+
const filePath = writeUniqueFile(targetDir, name, decoded.data, log2);
|
|
24157
|
+
if (filePath)
|
|
24158
|
+
savedPaths.push(filePath);
|
|
24159
|
+
}
|
|
24160
|
+
}
|
|
24161
|
+
}
|
|
24162
|
+
const pathsText = savedPaths.length > 0 ? ` Saved to: ${savedPaths.join(", ")}` : "";
|
|
24163
|
+
log2(`[image-hook] stripping image/file parts, saving to disk${pathsText}`);
|
|
24164
|
+
msg.parts = msg.parts.filter((p) => !isImagePart(p)).concat([
|
|
24165
|
+
{
|
|
24166
|
+
type: "text",
|
|
24167
|
+
text: `[Image attachment detected.${pathsText} Your model may not support image input. Delegate to @observer with the file path(s) above so it can read the file with its read tool.]`
|
|
24168
|
+
}
|
|
24169
|
+
]);
|
|
24170
|
+
}
|
|
24171
|
+
}
|
|
24172
|
+
// src/hooks/json-error-recovery/hook.ts
|
|
24173
|
+
var JSON_ERROR_TOOL_EXCLUDE_LIST = [
|
|
24174
|
+
"bash",
|
|
24175
|
+
"read",
|
|
24176
|
+
"glob",
|
|
24177
|
+
"webfetch",
|
|
24178
|
+
"grep_app_searchgithub",
|
|
24179
|
+
"websearch_web_search_exa"
|
|
24180
|
+
];
|
|
24181
|
+
var JSON_ERROR_PATTERNS = [
|
|
24182
|
+
/json parse error/i,
|
|
24183
|
+
/failed to parse json/i,
|
|
24184
|
+
/invalid json/i,
|
|
24185
|
+
/malformed json/i,
|
|
24186
|
+
/unexpected end of json input/i,
|
|
24187
|
+
/syntaxerror:\s*unexpected token.*json/i,
|
|
24188
|
+
/json[^\n]*expected '\}'/i,
|
|
24189
|
+
/json[^\n]*unexpected eof/i
|
|
24190
|
+
];
|
|
24191
|
+
var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
|
|
24192
|
+
var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
|
|
24193
|
+
var JSON_ERROR_REMINDER = `
|
|
24194
|
+
[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
|
|
24195
|
+
|
|
24196
|
+
You sent invalid JSON arguments. The system could not parse your tool call.
|
|
24197
|
+
STOP and do this NOW:
|
|
24198
|
+
|
|
24199
|
+
1. LOOK at the error message above to see what was expected vs what you sent.
|
|
24200
|
+
2. CORRECT your JSON syntax (missing braces, unescaped quotes, trailing commas, etc).
|
|
24201
|
+
3. RETRY the tool call with valid JSON.
|
|
24202
|
+
|
|
24203
|
+
DO NOT repeat the exact same invalid call.
|
|
24204
|
+
`;
|
|
24205
|
+
function createJsonErrorRecoveryHook(_ctx) {
|
|
24206
|
+
return {
|
|
24207
|
+
"tool.execute.after": async (input, output) => {
|
|
24208
|
+
if (JSON_ERROR_EXCLUDED_TOOLS.has(input.tool.toLowerCase()))
|
|
24209
|
+
return;
|
|
24210
|
+
if (typeof output.output !== "string")
|
|
24211
|
+
return;
|
|
24212
|
+
if (output.output.includes(JSON_ERROR_REMINDER_MARKER))
|
|
24213
|
+
return;
|
|
24214
|
+
const outputText = output.output;
|
|
24215
|
+
const hasJsonError = JSON_ERROR_PATTERNS.some((pattern) => pattern.test(outputText));
|
|
24216
|
+
if (hasJsonError) {
|
|
24217
|
+
output.output += `
|
|
24218
|
+
${JSON_ERROR_REMINDER}`;
|
|
24219
|
+
}
|
|
24220
|
+
}
|
|
24221
|
+
};
|
|
24222
|
+
}
|
|
24223
|
+
// src/hooks/phase-reminder/index.ts
|
|
24224
|
+
var PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
|
|
24225
|
+
function createPhaseReminderHook() {
|
|
24226
|
+
return {
|
|
24227
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
24228
|
+
const { messages } = output;
|
|
24229
|
+
if (messages.length === 0) {
|
|
24230
|
+
return;
|
|
24231
|
+
}
|
|
24232
|
+
let lastUserMessageIndex = -1;
|
|
24233
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
24234
|
+
if (messages[i].info.role === "user") {
|
|
24235
|
+
lastUserMessageIndex = i;
|
|
24236
|
+
break;
|
|
24237
|
+
}
|
|
24238
|
+
}
|
|
24239
|
+
if (lastUserMessageIndex === -1) {
|
|
24212
24240
|
return;
|
|
24213
24241
|
}
|
|
24214
|
-
const
|
|
24215
|
-
|
|
24216
|
-
|
|
24217
|
-
source: "manual",
|
|
24218
|
-
createdAt: Date.now()
|
|
24219
|
-
});
|
|
24220
|
-
pushText(output, `Set active goal:
|
|
24221
|
-
${text}`);
|
|
24222
|
-
},
|
|
24223
|
-
handleEvent: (input) => {
|
|
24224
|
-
const event = input.event;
|
|
24225
|
-
if (event.type === "session.created") {
|
|
24226
|
-
const info = event.properties?.info;
|
|
24227
|
-
if (!info?.id || !info.parentID)
|
|
24228
|
-
return;
|
|
24229
|
-
const parentGoal = goals.get(info.parentID);
|
|
24230
|
-
if (!parentGoal)
|
|
24231
|
-
return;
|
|
24232
|
-
goals.set(info.id, {
|
|
24233
|
-
inheritedFrom: info.parentID,
|
|
24234
|
-
createdAt: Date.now(),
|
|
24235
|
-
text: ""
|
|
24236
|
-
});
|
|
24242
|
+
const lastUserMessage = messages[lastUserMessageIndex];
|
|
24243
|
+
const agent = lastUserMessage.info.agent;
|
|
24244
|
+
if (agent && agent !== "orchestrator") {
|
|
24237
24245
|
return;
|
|
24238
24246
|
}
|
|
24239
|
-
|
|
24240
|
-
|
|
24241
|
-
|
|
24242
|
-
if (sessionID)
|
|
24243
|
-
goals.delete(sessionID);
|
|
24247
|
+
const textPartIndex = lastUserMessage.parts.findIndex((p) => p.type === "text" && p.text !== undefined);
|
|
24248
|
+
if (textPartIndex === -1) {
|
|
24249
|
+
return;
|
|
24244
24250
|
}
|
|
24245
|
-
|
|
24246
|
-
|
|
24247
|
-
if (!input.sessionID)
|
|
24251
|
+
const originalText = lastUserMessage.parts[textPartIndex].text ?? "";
|
|
24252
|
+
if (originalText.includes(SLIM_INTERNAL_INITIATOR_MARKER)) {
|
|
24248
24253
|
return;
|
|
24249
|
-
|
|
24250
|
-
if (
|
|
24254
|
+
}
|
|
24255
|
+
if (lastUserMessage.parts.some((p) => p.text?.includes(PHASE_REMINDER))) {
|
|
24251
24256
|
return;
|
|
24252
|
-
|
|
24253
|
-
|
|
24254
|
-
|
|
24257
|
+
}
|
|
24258
|
+
lastUserMessage.parts.push({
|
|
24259
|
+
type: "text",
|
|
24260
|
+
text: PHASE_REMINDER
|
|
24261
|
+
});
|
|
24262
|
+
}
|
|
24263
|
+
};
|
|
24264
|
+
}
|
|
24265
|
+
// src/hooks/post-file-tool-nudge/index.ts
|
|
24266
|
+
var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
|
|
24267
|
+
var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
|
|
24268
|
+
function createPostFileToolNudgeHook(options = {}) {
|
|
24269
|
+
function appendReminder(output) {
|
|
24270
|
+
if (typeof output.output !== "string") {
|
|
24271
|
+
return;
|
|
24272
|
+
}
|
|
24273
|
+
if (output.output.includes(POST_FILE_TOOL_NUDGE)) {
|
|
24274
|
+
return;
|
|
24275
|
+
}
|
|
24276
|
+
output.output = [
|
|
24277
|
+
output.output,
|
|
24278
|
+
"",
|
|
24279
|
+
"<internal_reminder>",
|
|
24280
|
+
POST_FILE_TOOL_NUDGE,
|
|
24281
|
+
"</internal_reminder>"
|
|
24282
|
+
].join(`
|
|
24283
|
+
`);
|
|
24284
|
+
}
|
|
24285
|
+
return {
|
|
24286
|
+
"tool.execute.after": async (input, output) => {
|
|
24287
|
+
if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
|
|
24255
24288
|
return;
|
|
24256
|
-
|
|
24257
|
-
if (
|
|
24289
|
+
}
|
|
24290
|
+
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
24258
24291
|
return;
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24292
|
+
}
|
|
24293
|
+
appendReminder(output);
|
|
24294
|
+
}
|
|
24262
24295
|
};
|
|
24263
24296
|
}
|
|
24264
24297
|
// src/hooks/task-session-manager/index.ts
|
|
@@ -24280,6 +24313,31 @@ var RESUMABLE_SESSIONS_END = "</resumable_sessions>";
|
|
|
24280
24313
|
var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
|
|
24281
24314
|
var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
|
|
24282
24315
|
var MAX_PROCESSED_INJECTED_COMPLETIONS = 500;
|
|
24316
|
+
function djb2Hash(str) {
|
|
24317
|
+
let hash = 5381;
|
|
24318
|
+
for (let i = 0;i < str.length; i++) {
|
|
24319
|
+
hash = (hash << 5) + hash + str.charCodeAt(i);
|
|
24320
|
+
}
|
|
24321
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
24322
|
+
}
|
|
24323
|
+
function createOccurrenceId(part, message, partIndex) {
|
|
24324
|
+
if (typeof part.id === "string") {
|
|
24325
|
+
return part.id;
|
|
24326
|
+
}
|
|
24327
|
+
if (typeof message.info.id === "string") {
|
|
24328
|
+
return `${message.info.id}:${partIndex}`;
|
|
24329
|
+
}
|
|
24330
|
+
const sessionID = message.info.sessionID ?? "unknown";
|
|
24331
|
+
const content = typeof part.text === "string" ? part.text : "";
|
|
24332
|
+
const status = parseTaskStatusOutput(content);
|
|
24333
|
+
if (status) {
|
|
24334
|
+
const stableKey = `${sessionID}:${status.taskID}:${status.state}:${status.result ?? ""}`;
|
|
24335
|
+
const hash2 = djb2Hash(stableKey);
|
|
24336
|
+
return `anon:${hash2}`;
|
|
24337
|
+
}
|
|
24338
|
+
const hash = djb2Hash(`${sessionID}:${content}`);
|
|
24339
|
+
return `anon:${hash}`;
|
|
24340
|
+
}
|
|
24283
24341
|
function isAgentName(value) {
|
|
24284
24342
|
return typeof value === "string" && AGENT_NAME_SET.has(value);
|
|
24285
24343
|
}
|
|
@@ -24395,7 +24453,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24395
24453
|
}
|
|
24396
24454
|
return updated;
|
|
24397
24455
|
}
|
|
24398
|
-
function updateFromInjectedCompletion(part, message,
|
|
24456
|
+
function updateFromInjectedCompletion(part, message, _messageIndex, partIndex) {
|
|
24399
24457
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
24400
24458
|
return;
|
|
24401
24459
|
}
|
|
@@ -24411,7 +24469,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24411
24469
|
return;
|
|
24412
24470
|
if (isFailed && status.state !== "error")
|
|
24413
24471
|
return;
|
|
24414
|
-
const occurrenceId =
|
|
24472
|
+
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
24415
24473
|
if (processedInjectedCompletions.has(occurrenceId))
|
|
24416
24474
|
return;
|
|
24417
24475
|
const updated = updateBackgroundJobFromOutput(part.text);
|
|
@@ -24661,7 +24719,6 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24661
24719
|
if (!sessionId)
|
|
24662
24720
|
return;
|
|
24663
24721
|
sessionManager.dropTask(sessionId);
|
|
24664
|
-
backgroundJobBoard.drop(sessionId);
|
|
24665
24722
|
sessionManager.clearParent(sessionId);
|
|
24666
24723
|
backgroundJobBoard.clearParent(sessionId);
|
|
24667
24724
|
terminalJobsInjectedByParent.delete(sessionId);
|
|
@@ -24817,7 +24874,7 @@ function createTodoHygiene(options) {
|
|
|
24817
24874
|
|
|
24818
24875
|
// src/hooks/todo-continuation/index.ts
|
|
24819
24876
|
var HOOK_NAME = "todo-continuation";
|
|
24820
|
-
var
|
|
24877
|
+
var COMMAND_NAME3 = "auto-continue";
|
|
24821
24878
|
var TODO_STATE_TIMEOUT_MS = 500;
|
|
24822
24879
|
var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
|
|
24823
24880
|
var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
|
|
@@ -25335,7 +25392,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
25335
25392
|
}
|
|
25336
25393
|
}
|
|
25337
25394
|
async function handleCommandExecuteBefore(input, output) {
|
|
25338
|
-
if (input.command !==
|
|
25395
|
+
if (input.command !== COMMAND_NAME3) {
|
|
25339
25396
|
return;
|
|
25340
25397
|
}
|
|
25341
25398
|
registerOrchestratorSession(input.sessionID);
|
|
@@ -25354,11 +25411,11 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
25354
25411
|
if (!newEnabled) {
|
|
25355
25412
|
cancelPendingTimer(state);
|
|
25356
25413
|
output.parts.push(createInternalAgentTextPart("[Auto-continue: disabled by user command.]"));
|
|
25357
|
-
log(`[${HOOK_NAME}] Disabled via /${
|
|
25414
|
+
log(`[${HOOK_NAME}] Disabled via /${COMMAND_NAME3} command`);
|
|
25358
25415
|
return;
|
|
25359
25416
|
}
|
|
25360
25417
|
state.suppressUntil = 0;
|
|
25361
|
-
log(`[${HOOK_NAME}] Enabled via /${
|
|
25418
|
+
log(`[${HOOK_NAME}] Enabled via /${COMMAND_NAME3} command`, {
|
|
25362
25419
|
maxContinuations
|
|
25363
25420
|
});
|
|
25364
25421
|
let hasIncompleteTodos = false;
|
|
@@ -28204,7 +28261,7 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
28204
28261
|
}
|
|
28205
28262
|
|
|
28206
28263
|
// src/interview/service.ts
|
|
28207
|
-
var
|
|
28264
|
+
var COMMAND_NAME4 = "interview";
|
|
28208
28265
|
var DEFAULT_MAX_QUESTIONS = 2;
|
|
28209
28266
|
function isTruthyEnvFlag(value) {
|
|
28210
28267
|
if (!value) {
|
|
@@ -28451,11 +28508,11 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28451
28508
|
}
|
|
28452
28509
|
function registerCommand(opencodeConfig) {
|
|
28453
28510
|
const configCommand = opencodeConfig.command;
|
|
28454
|
-
if (!configCommand?.[
|
|
28511
|
+
if (!configCommand?.[COMMAND_NAME4]) {
|
|
28455
28512
|
if (!opencodeConfig.command) {
|
|
28456
28513
|
opencodeConfig.command = {};
|
|
28457
28514
|
}
|
|
28458
|
-
opencodeConfig.command[
|
|
28515
|
+
opencodeConfig.command[COMMAND_NAME4] = {
|
|
28459
28516
|
template: "Start an interview and write a live markdown spec",
|
|
28460
28517
|
description: "Open a localhost interview UI linked to the current OpenCode session"
|
|
28461
28518
|
};
|
|
@@ -28529,7 +28586,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
28529
28586
|
}
|
|
28530
28587
|
}
|
|
28531
28588
|
async function handleCommandExecuteBefore(input, output) {
|
|
28532
|
-
if (input.command !==
|
|
28589
|
+
if (input.command !== COMMAND_NAME4) {
|
|
28533
28590
|
return;
|
|
28534
28591
|
}
|
|
28535
28592
|
const idea = input.arguments.trim();
|
|
@@ -30783,11 +30840,11 @@ function recordTuiAgentModel(input) {
|
|
|
30783
30840
|
}
|
|
30784
30841
|
|
|
30785
30842
|
// src/tools/preset-manager.ts
|
|
30786
|
-
var
|
|
30843
|
+
var COMMAND_NAME5 = "preset";
|
|
30787
30844
|
function createPresetManager(ctx, config) {
|
|
30788
30845
|
let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
|
|
30789
30846
|
async function handleCommandExecuteBefore(input, output) {
|
|
30790
|
-
if (input.command !==
|
|
30847
|
+
if (input.command !== COMMAND_NAME5) {
|
|
30791
30848
|
return;
|
|
30792
30849
|
}
|
|
30793
30850
|
output.parts.length = 0;
|
|
@@ -30806,11 +30863,11 @@ function createPresetManager(ctx, config) {
|
|
|
30806
30863
|
}
|
|
30807
30864
|
function registerCommand(opencodeConfig) {
|
|
30808
30865
|
const configCommand = opencodeConfig.command;
|
|
30809
|
-
if (!configCommand?.[
|
|
30866
|
+
if (!configCommand?.[COMMAND_NAME5]) {
|
|
30810
30867
|
if (!opencodeConfig.command) {
|
|
30811
30868
|
opencodeConfig.command = {};
|
|
30812
30869
|
}
|
|
30813
|
-
opencodeConfig.command[
|
|
30870
|
+
opencodeConfig.command[COMMAND_NAME5] = {
|
|
30814
30871
|
template: "List available presets and switch between them",
|
|
30815
30872
|
description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
|
|
30816
30873
|
};
|
|
@@ -33244,460 +33301,6 @@ function createWebfetchTool(pluginCtx, options = {}) {
|
|
|
33244
33301
|
}
|
|
33245
33302
|
});
|
|
33246
33303
|
}
|
|
33247
|
-
// src/tools/subtask/command.ts
|
|
33248
|
-
var COMMAND_NAME5 = "subtask";
|
|
33249
|
-
var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
|
|
33250
|
-
|
|
33251
|
-
The user's request below is the full scope for the worker. Do not broaden it.
|
|
33252
|
-
Create a self-contained worker prompt that includes:
|
|
33253
|
-
- the exact objective
|
|
33254
|
-
- relevant context from this conversation
|
|
33255
|
-
- specific files/paths that matter
|
|
33256
|
-
- expected deliverables
|
|
33257
|
-
- validation the worker should run, if applicable
|
|
33258
|
-
|
|
33259
|
-
USER REQUEST:
|
|
33260
|
-
$ARGUMENTS
|
|
33261
|
-
|
|
33262
|
-
Then call the subtask tool:
|
|
33263
|
-
\`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
|
|
33264
|
-
|
|
33265
|
-
Only include files that are clearly relevant. If no files are needed, omit files.`;
|
|
33266
|
-
function createSubtaskCommandManager(_ctx, state) {
|
|
33267
|
-
function registerCommand(opencodeConfig) {
|
|
33268
|
-
const configCommand = opencodeConfig.command;
|
|
33269
|
-
if (!configCommand?.[COMMAND_NAME5]) {
|
|
33270
|
-
if (!opencodeConfig.command) {
|
|
33271
|
-
opencodeConfig.command = {};
|
|
33272
|
-
}
|
|
33273
|
-
opencodeConfig.command[COMMAND_NAME5] = {
|
|
33274
|
-
description: "Create a focused subtask prompt for a new session",
|
|
33275
|
-
template: SUBTASK_COMMAND_TEMPLATE
|
|
33276
|
-
};
|
|
33277
|
-
}
|
|
33278
|
-
}
|
|
33279
|
-
return {
|
|
33280
|
-
registerCommand,
|
|
33281
|
-
handleEvent(input) {
|
|
33282
|
-
if (input.event.type === "session.created") {
|
|
33283
|
-
const info = input.event.properties?.info;
|
|
33284
|
-
if (!info?.id || !info.parentID)
|
|
33285
|
-
return;
|
|
33286
|
-
const source = state.sourceFor(info.parentID);
|
|
33287
|
-
if (source)
|
|
33288
|
-
state.markSession(info.id, source);
|
|
33289
|
-
return;
|
|
33290
|
-
}
|
|
33291
|
-
if (input.event.type !== "session.deleted")
|
|
33292
|
-
return;
|
|
33293
|
-
const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
33294
|
-
if (sessionID)
|
|
33295
|
-
state.unmarkSession(sessionID);
|
|
33296
|
-
}
|
|
33297
|
-
};
|
|
33298
|
-
}
|
|
33299
|
-
// src/tools/subtask/files.ts
|
|
33300
|
-
import * as fs12 from "node:fs/promises";
|
|
33301
|
-
import * as path20 from "node:path";
|
|
33302
|
-
|
|
33303
|
-
// src/tools/subtask/vendor.ts
|
|
33304
|
-
import * as fs11 from "node:fs/promises";
|
|
33305
|
-
import * as path19 from "node:path";
|
|
33306
|
-
var DEFAULT_READ_LIMIT = 2000;
|
|
33307
|
-
var MAX_LINE_LENGTH = 2000;
|
|
33308
|
-
var MAX_BYTES = 50 * 1024;
|
|
33309
|
-
var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
|
|
33310
|
-
var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
|
|
33311
|
-
var SAMPLE_BYTES = 4096;
|
|
33312
|
-
var BINARY_EXTENSIONS = new Set([
|
|
33313
|
-
".zip",
|
|
33314
|
-
".tar",
|
|
33315
|
-
".gz",
|
|
33316
|
-
".exe",
|
|
33317
|
-
".dll",
|
|
33318
|
-
".so",
|
|
33319
|
-
".class",
|
|
33320
|
-
".jar",
|
|
33321
|
-
".war",
|
|
33322
|
-
".7z",
|
|
33323
|
-
".doc",
|
|
33324
|
-
".docx",
|
|
33325
|
-
".xls",
|
|
33326
|
-
".xlsx",
|
|
33327
|
-
".ppt",
|
|
33328
|
-
".pptx",
|
|
33329
|
-
".odt",
|
|
33330
|
-
".ods",
|
|
33331
|
-
".odp",
|
|
33332
|
-
".bin",
|
|
33333
|
-
".dat",
|
|
33334
|
-
".obj",
|
|
33335
|
-
".o",
|
|
33336
|
-
".a",
|
|
33337
|
-
".lib",
|
|
33338
|
-
".wasm",
|
|
33339
|
-
".pyc",
|
|
33340
|
-
".pyo"
|
|
33341
|
-
]);
|
|
33342
|
-
async function isBinaryFile(filepath) {
|
|
33343
|
-
const ext = path19.extname(filepath).toLowerCase();
|
|
33344
|
-
if (BINARY_EXTENSIONS.has(ext)) {
|
|
33345
|
-
return true;
|
|
33346
|
-
}
|
|
33347
|
-
try {
|
|
33348
|
-
const file = await fs11.open(filepath, "r");
|
|
33349
|
-
try {
|
|
33350
|
-
const buffer = Buffer.alloc(SAMPLE_BYTES);
|
|
33351
|
-
const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
|
|
33352
|
-
if (result.bytesRead === 0)
|
|
33353
|
-
return false;
|
|
33354
|
-
const bytes = buffer.subarray(0, result.bytesRead);
|
|
33355
|
-
let nonPrintableCount = 0;
|
|
33356
|
-
for (let i = 0;i < bytes.length; i++) {
|
|
33357
|
-
const byte = bytes[i];
|
|
33358
|
-
if (byte === undefined)
|
|
33359
|
-
continue;
|
|
33360
|
-
if (byte === 0)
|
|
33361
|
-
return true;
|
|
33362
|
-
if (byte < 9 || byte > 13 && byte < 32) {
|
|
33363
|
-
nonPrintableCount++;
|
|
33364
|
-
}
|
|
33365
|
-
}
|
|
33366
|
-
return nonPrintableCount / bytes.length > 0.3;
|
|
33367
|
-
} finally {
|
|
33368
|
-
await file.close();
|
|
33369
|
-
}
|
|
33370
|
-
} catch {
|
|
33371
|
-
return false;
|
|
33372
|
-
}
|
|
33373
|
-
}
|
|
33374
|
-
function formatFileContent(_filepath, content) {
|
|
33375
|
-
const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
|
|
33376
|
-
const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
|
|
33377
|
-
const lines = contentToFormat.split(`
|
|
33378
|
-
`);
|
|
33379
|
-
const limit = DEFAULT_READ_LIMIT;
|
|
33380
|
-
const offset = 0;
|
|
33381
|
-
const raw = lines.slice(offset, offset + limit).map((line) => {
|
|
33382
|
-
return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
|
|
33383
|
-
});
|
|
33384
|
-
const formatted = raw.map((line, index) => {
|
|
33385
|
-
return `${index + offset + 1}: ${line}`;
|
|
33386
|
-
});
|
|
33387
|
-
let output = [
|
|
33388
|
-
`<path>${_filepath}</path>`,
|
|
33389
|
-
"<type>file</type>",
|
|
33390
|
-
`<content>
|
|
33391
|
-
`
|
|
33392
|
-
].join(`
|
|
33393
|
-
`);
|
|
33394
|
-
output += formatted.join(`
|
|
33395
|
-
`);
|
|
33396
|
-
const totalLines = lines.length;
|
|
33397
|
-
const lastReadLine = offset + formatted.length;
|
|
33398
|
-
const hasMoreLines = totalLines > lastReadLine;
|
|
33399
|
-
if (cappedContent) {
|
|
33400
|
-
output += `
|
|
33401
|
-
|
|
33402
|
-
(Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
|
|
33403
|
-
} else if (hasMoreLines) {
|
|
33404
|
-
output += `
|
|
33405
|
-
|
|
33406
|
-
(Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
|
|
33407
|
-
} else {
|
|
33408
|
-
output += `
|
|
33409
|
-
|
|
33410
|
-
(End of file - total ${totalLines} lines)`;
|
|
33411
|
-
}
|
|
33412
|
-
output += `
|
|
33413
|
-
</content>`;
|
|
33414
|
-
return output;
|
|
33415
|
-
}
|
|
33416
|
-
|
|
33417
|
-
// src/tools/subtask/files.ts
|
|
33418
|
-
var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
|
|
33419
|
-
var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
|
|
33420
|
-
function cleanFileReference(ref) {
|
|
33421
|
-
return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
|
|
33422
|
-
}
|
|
33423
|
-
function parseFileReferences(text) {
|
|
33424
|
-
const fileRefs = new Set;
|
|
33425
|
-
for (const match of text.matchAll(FILE_REGEX)) {
|
|
33426
|
-
if (match[1]) {
|
|
33427
|
-
fileRefs.add(cleanFileReference(match[1]));
|
|
33428
|
-
}
|
|
33429
|
-
}
|
|
33430
|
-
return fileRefs;
|
|
33431
|
-
}
|
|
33432
|
-
async function buildSyntheticFileParts(directory, refs) {
|
|
33433
|
-
const parts = [];
|
|
33434
|
-
const realDirectory = await fs12.realpath(directory);
|
|
33435
|
-
for (const ref of refs) {
|
|
33436
|
-
const filepath = path20.resolve(directory, ref);
|
|
33437
|
-
const relative3 = path20.relative(directory, filepath);
|
|
33438
|
-
if (relative3.startsWith("..") || path20.isAbsolute(relative3))
|
|
33439
|
-
continue;
|
|
33440
|
-
try {
|
|
33441
|
-
const realFilepath = await fs12.realpath(filepath);
|
|
33442
|
-
const realRelative = path20.relative(realDirectory, realFilepath);
|
|
33443
|
-
if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
|
|
33444
|
-
continue;
|
|
33445
|
-
}
|
|
33446
|
-
const stats = await fs12.stat(realFilepath);
|
|
33447
|
-
if (!stats.isFile())
|
|
33448
|
-
continue;
|
|
33449
|
-
if (await isBinaryFile(realFilepath))
|
|
33450
|
-
continue;
|
|
33451
|
-
const content = await fs12.readFile(realFilepath, "utf-8");
|
|
33452
|
-
parts.push({
|
|
33453
|
-
type: "text",
|
|
33454
|
-
synthetic: true,
|
|
33455
|
-
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
|
|
33456
|
-
});
|
|
33457
|
-
parts.push({
|
|
33458
|
-
type: "text",
|
|
33459
|
-
synthetic: true,
|
|
33460
|
-
text: formatFileContent(realFilepath, content)
|
|
33461
|
-
});
|
|
33462
|
-
} catch {}
|
|
33463
|
-
}
|
|
33464
|
-
return parts;
|
|
33465
|
-
}
|
|
33466
|
-
// src/tools/subtask/state.ts
|
|
33467
|
-
function createSubtaskState() {
|
|
33468
|
-
const sourceBySession = new Map;
|
|
33469
|
-
return {
|
|
33470
|
-
markSession(sessionID, sourceSessionID) {
|
|
33471
|
-
sourceBySession.set(sessionID, sourceSessionID);
|
|
33472
|
-
},
|
|
33473
|
-
unmarkSession(sessionID) {
|
|
33474
|
-
sourceBySession.delete(sessionID);
|
|
33475
|
-
},
|
|
33476
|
-
isSubtaskSession(sessionID) {
|
|
33477
|
-
return sourceBySession.has(sessionID);
|
|
33478
|
-
},
|
|
33479
|
-
sourceFor(sessionID) {
|
|
33480
|
-
return sourceBySession.get(sessionID);
|
|
33481
|
-
}
|
|
33482
|
-
};
|
|
33483
|
-
}
|
|
33484
|
-
// src/tools/subtask/tools.ts
|
|
33485
|
-
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
33486
|
-
var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
33487
|
-
var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
|
|
33488
|
-
function normalizeSubtaskSummary(text) {
|
|
33489
|
-
return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
|
|
33490
|
-
}
|
|
33491
|
-
function getAbortSignal(context) {
|
|
33492
|
-
if (!context || typeof context !== "object" || !("abort" in context)) {
|
|
33493
|
-
return;
|
|
33494
|
-
}
|
|
33495
|
-
const signal = context.abort;
|
|
33496
|
-
return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
|
|
33497
|
-
}
|
|
33498
|
-
function createSubtaskTool(ctx, state, depthTracker) {
|
|
33499
|
-
const client = ctx.client;
|
|
33500
|
-
return tool5({
|
|
33501
|
-
description: "Run a child worker session and return its completion summary to the caller",
|
|
33502
|
-
args: {
|
|
33503
|
-
prompt: tool5.schema.string().describe("The generated subtask prompt"),
|
|
33504
|
-
files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
|
|
33505
|
-
},
|
|
33506
|
-
async execute(args, context) {
|
|
33507
|
-
const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
|
|
33508
|
-
const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
|
|
33509
|
-
const abortSignal = getAbortSignal(context);
|
|
33510
|
-
if (state.isSubtaskSession(sessionID)) {
|
|
33511
|
-
return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
|
|
33512
|
-
}
|
|
33513
|
-
if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
|
|
33514
|
-
return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
|
|
33515
|
-
}
|
|
33516
|
-
const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
|
|
33517
|
-
|
|
33518
|
-
Your job is bounded: complete only the task below. Do not expand scope.
|
|
33519
|
-
If needed context is missing, use read_session to inspect the parent session.
|
|
33520
|
-
Do not spawn another subtask.`;
|
|
33521
|
-
const files = new Set([
|
|
33522
|
-
...parseFileReferences(args.prompt),
|
|
33523
|
-
...(args.files ?? []).map(cleanFileReference)
|
|
33524
|
-
]);
|
|
33525
|
-
const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
|
|
33526
|
-
const fullPrompt = fileRefs ? `${sessionReference}
|
|
33527
|
-
|
|
33528
|
-
TASK:
|
|
33529
|
-
${args.prompt}
|
|
33530
|
-
|
|
33531
|
-
FILES PROVIDED:
|
|
33532
|
-
${fileRefs}` : `${sessionReference}
|
|
33533
|
-
|
|
33534
|
-
TASK:
|
|
33535
|
-
${args.prompt}`;
|
|
33536
|
-
let childSessionID;
|
|
33537
|
-
try {
|
|
33538
|
-
const session2 = await client.session.create({
|
|
33539
|
-
responseStyle: "data",
|
|
33540
|
-
throwOnError: true,
|
|
33541
|
-
query: { directory },
|
|
33542
|
-
body: {
|
|
33543
|
-
parentID: sessionID === "unknown" ? undefined : sessionID,
|
|
33544
|
-
title: `Subtask worker from ${sessionID}`
|
|
33545
|
-
}
|
|
33546
|
-
});
|
|
33547
|
-
childSessionID = session2?.data?.id ?? session2?.id;
|
|
33548
|
-
if (!childSessionID) {
|
|
33549
|
-
throw new Error("Subtask worker session did not return an id");
|
|
33550
|
-
}
|
|
33551
|
-
if (sessionID !== "unknown" && depthTracker) {
|
|
33552
|
-
const registered = depthTracker.registerChild(sessionID, childSessionID);
|
|
33553
|
-
if (!registered) {
|
|
33554
|
-
throw new Error("Subtask worker blocked: max subagent depth exceeded");
|
|
33555
|
-
}
|
|
33556
|
-
}
|
|
33557
|
-
state.markSession(childSessionID, sessionID);
|
|
33558
|
-
await promptWithTimeout(client, {
|
|
33559
|
-
responseStyle: "data",
|
|
33560
|
-
throwOnError: true,
|
|
33561
|
-
query: { directory },
|
|
33562
|
-
path: { id: childSessionID },
|
|
33563
|
-
body: {
|
|
33564
|
-
agent: "orchestrator",
|
|
33565
|
-
parts: [
|
|
33566
|
-
{
|
|
33567
|
-
type: "text",
|
|
33568
|
-
text: `${fullPrompt}
|
|
33569
|
-
|
|
33570
|
-
Instructions:
|
|
33571
|
-
1. Understand the task and relevant file context.
|
|
33572
|
-
2. Make only necessary changes.
|
|
33573
|
-
3. Run the most relevant validation checks when practical.
|
|
33574
|
-
4. Stop when the requested task is done.
|
|
33575
|
-
|
|
33576
|
-
Return your final response in this format:
|
|
33577
|
-
|
|
33578
|
-
<subtask_summary>
|
|
33579
|
-
Status: completed | blocked | partial
|
|
33580
|
-
|
|
33581
|
-
What changed:
|
|
33582
|
-
- ...
|
|
33583
|
-
|
|
33584
|
-
Files touched:
|
|
33585
|
-
- ...
|
|
33586
|
-
|
|
33587
|
-
Validation:
|
|
33588
|
-
- ...
|
|
33589
|
-
|
|
33590
|
-
Risks / follow-up:
|
|
33591
|
-
- ...
|
|
33592
|
-
</subtask_summary>`
|
|
33593
|
-
},
|
|
33594
|
-
...await buildSyntheticFileParts(directory, files)
|
|
33595
|
-
]
|
|
33596
|
-
}
|
|
33597
|
-
}, SUBTASK_TIMEOUT_MS, abortSignal);
|
|
33598
|
-
const extraction = await extractSessionResult(client, childSessionID, {
|
|
33599
|
-
directory,
|
|
33600
|
-
includeReasoning: false
|
|
33601
|
-
});
|
|
33602
|
-
if (extraction.empty) {
|
|
33603
|
-
throw new Error("Subtask worker returned no summary");
|
|
33604
|
-
}
|
|
33605
|
-
const summary = normalizeSubtaskSummary(extraction.text);
|
|
33606
|
-
return [
|
|
33607
|
-
`task_id: ${childSessionID}`,
|
|
33608
|
-
"",
|
|
33609
|
-
"<subtask_summary>",
|
|
33610
|
-
summary,
|
|
33611
|
-
"</subtask_summary>"
|
|
33612
|
-
].join(`
|
|
33613
|
-
`);
|
|
33614
|
-
} finally {
|
|
33615
|
-
if (childSessionID) {
|
|
33616
|
-
try {
|
|
33617
|
-
await client.session.abort({
|
|
33618
|
-
path: { id: childSessionID },
|
|
33619
|
-
query: { directory }
|
|
33620
|
-
});
|
|
33621
|
-
state.unmarkSession(childSessionID);
|
|
33622
|
-
} catch {}
|
|
33623
|
-
}
|
|
33624
|
-
}
|
|
33625
|
-
}
|
|
33626
|
-
});
|
|
33627
|
-
}
|
|
33628
|
-
function formatTranscript(messages, limit) {
|
|
33629
|
-
const lines = [];
|
|
33630
|
-
for (const msg of messages) {
|
|
33631
|
-
const role = msg.info?.role;
|
|
33632
|
-
const parts = msg.parts;
|
|
33633
|
-
if (role === "user") {
|
|
33634
|
-
lines.push("## User");
|
|
33635
|
-
for (const part of parts) {
|
|
33636
|
-
if (part.type === "text" && !part.ignored && typeof part.text === "string") {
|
|
33637
|
-
lines.push(part.text);
|
|
33638
|
-
}
|
|
33639
|
-
if (part.type === "file") {
|
|
33640
|
-
lines.push(`[Attached: ${part.filename || "file"}]`);
|
|
33641
|
-
}
|
|
33642
|
-
}
|
|
33643
|
-
lines.push("");
|
|
33644
|
-
}
|
|
33645
|
-
if (role === "assistant") {
|
|
33646
|
-
lines.push("## Assistant");
|
|
33647
|
-
for (const part of parts) {
|
|
33648
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
33649
|
-
lines.push(part.text);
|
|
33650
|
-
}
|
|
33651
|
-
if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
|
|
33652
|
-
lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
|
|
33653
|
-
}
|
|
33654
|
-
}
|
|
33655
|
-
lines.push("");
|
|
33656
|
-
}
|
|
33657
|
-
}
|
|
33658
|
-
const output = lines.join(`
|
|
33659
|
-
`).trim();
|
|
33660
|
-
if (messages.length >= (limit ?? 100)) {
|
|
33661
|
-
return output + `
|
|
33662
|
-
|
|
33663
|
-
(Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
|
|
33664
|
-
}
|
|
33665
|
-
return `${output}
|
|
33666
|
-
|
|
33667
|
-
(End of session - ${messages.length} messages)`;
|
|
33668
|
-
}
|
|
33669
|
-
function createReadSessionTool(client, state) {
|
|
33670
|
-
return tool5({
|
|
33671
|
-
description: "Read the conversation transcript from a previous session. Use this when you need specific information from the source session that wasn't included in the subtask summary.",
|
|
33672
|
-
args: {
|
|
33673
|
-
sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
|
|
33674
|
-
limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
|
|
33675
|
-
},
|
|
33676
|
-
async execute(args, context) {
|
|
33677
|
-
const limit = Math.min(args.limit ?? 100, 500);
|
|
33678
|
-
const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
|
|
33679
|
-
const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
|
|
33680
|
-
if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
|
|
33681
|
-
return "read_session is only available from subtask worker sessions.";
|
|
33682
|
-
}
|
|
33683
|
-
if (state.sourceFor(callerSessionID) !== args.sessionID) {
|
|
33684
|
-
return "read_session can only read the source session for this subtask worker.";
|
|
33685
|
-
}
|
|
33686
|
-
try {
|
|
33687
|
-
const response = await client.session.messages({
|
|
33688
|
-
path: { id: args.sessionID },
|
|
33689
|
-
query: { limit, ...directory ? { directory } : {} }
|
|
33690
|
-
});
|
|
33691
|
-
if (!response.data || response.data.length === 0) {
|
|
33692
|
-
return "Session has no messages or does not exist.";
|
|
33693
|
-
}
|
|
33694
|
-
return formatTranscript(response.data, limit);
|
|
33695
|
-
} catch (error) {
|
|
33696
|
-
return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
33697
|
-
}
|
|
33698
|
-
}
|
|
33699
|
-
});
|
|
33700
|
-
}
|
|
33701
33304
|
// src/utils/subagent-depth.ts
|
|
33702
33305
|
class SubagentDepthTracker {
|
|
33703
33306
|
depthBySession = new Map;
|
|
@@ -33810,7 +33413,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33810
33413
|
let jsonErrorRecoveryHook;
|
|
33811
33414
|
let foregroundFallback;
|
|
33812
33415
|
let todoContinuationHook;
|
|
33813
|
-
let
|
|
33416
|
+
let deepworkCommandHook;
|
|
33417
|
+
let goalHook;
|
|
33814
33418
|
let taskSessionManagerHook;
|
|
33815
33419
|
let backgroundJobBoard;
|
|
33816
33420
|
let interviewManager;
|
|
@@ -33819,8 +33423,6 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33819
33423
|
let councilTools;
|
|
33820
33424
|
let webfetch;
|
|
33821
33425
|
let rewriteDisplayNameMentions;
|
|
33822
|
-
let subtaskCommandManager;
|
|
33823
|
-
let subtaskState;
|
|
33824
33426
|
let toolCount = 0;
|
|
33825
33427
|
try {
|
|
33826
33428
|
config = loadPluginConfig(ctx.directory);
|
|
@@ -33906,7 +33508,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33906
33508
|
autoEnableThreshold: config.todoContinuation?.autoEnableThreshold ?? 4,
|
|
33907
33509
|
backgroundJobBoard
|
|
33908
33510
|
});
|
|
33909
|
-
|
|
33511
|
+
deepworkCommandHook = createDeepworkCommandHook();
|
|
33512
|
+
goalHook = createGoalHook(ctx, config, {
|
|
33910
33513
|
getAgentName: (sessionID) => sessionAgentMap.get(sessionID)
|
|
33911
33514
|
});
|
|
33912
33515
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
@@ -33919,9 +33522,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33919
33522
|
interviewManager = createInterviewManager(ctx, config);
|
|
33920
33523
|
presetManager = createPresetManager(ctx, config);
|
|
33921
33524
|
divoomManager = createDivoomManager(config.divoom);
|
|
33922
|
-
|
|
33923
|
-
subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
|
|
33924
|
-
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
|
|
33525
|
+
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2;
|
|
33925
33526
|
} catch (err) {
|
|
33926
33527
|
log("[plugin] FATAL: init failed", String(err));
|
|
33927
33528
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -33966,9 +33567,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
33966
33567
|
webfetch,
|
|
33967
33568
|
...todoContinuationHook.tool,
|
|
33968
33569
|
ast_grep_search,
|
|
33969
|
-
ast_grep_replace
|
|
33970
|
-
subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
|
|
33971
|
-
read_session: createReadSessionTool(ctx.client, subtaskState)
|
|
33570
|
+
ast_grep_replace
|
|
33972
33571
|
},
|
|
33973
33572
|
mcp: mcps,
|
|
33974
33573
|
config: async (opencodeConfig) => {
|
|
@@ -34164,9 +33763,9 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34164
33763
|
};
|
|
34165
33764
|
}
|
|
34166
33765
|
interviewManager.registerCommand(opencodeConfig);
|
|
34167
|
-
|
|
33766
|
+
goalHook.registerCommand(opencodeConfig);
|
|
33767
|
+
deepworkCommandHook.registerCommand(opencodeConfig);
|
|
34168
33768
|
presetManager.registerCommand(opencodeConfig);
|
|
34169
|
-
subtaskCommandManager.registerCommand(opencodeConfig);
|
|
34170
33769
|
},
|
|
34171
33770
|
event: async (input) => {
|
|
34172
33771
|
const event = input.event;
|
|
@@ -34191,11 +33790,10 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34191
33790
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
34192
33791
|
await foregroundFallback.handleEvent(input.event);
|
|
34193
33792
|
await todoContinuationHook.handleEvent(input);
|
|
34194
|
-
|
|
33793
|
+
goalHook.handleEvent(input);
|
|
34195
33794
|
await autoUpdateChecker.event(input);
|
|
34196
33795
|
await interviewManager.handleEvent(input);
|
|
34197
33796
|
await taskSessionManagerHook.event(input);
|
|
34198
|
-
subtaskCommandManager.handleEvent(input);
|
|
34199
33797
|
if (event.type === "permission.asked" || event.type === "question.asked") {
|
|
34200
33798
|
const props = event.properties;
|
|
34201
33799
|
divoomManager.onUserInputRequired({
|
|
@@ -34253,7 +33851,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34253
33851
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
34254
33852
|
await interviewManager.handleCommandExecuteBefore(input, output);
|
|
34255
33853
|
await presetManager.handleCommandExecuteBefore(input, output);
|
|
34256
|
-
await
|
|
33854
|
+
await goalHook.handleCommandExecuteBefore(input, output);
|
|
33855
|
+
await deepworkCommandHook.handleCommandExecuteBefore(input, output);
|
|
34257
33856
|
},
|
|
34258
33857
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
34259
33858
|
"chat.message": async (input, output) => {
|
|
@@ -34282,7 +33881,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
34282
33881
|
${output.system[0]}` : "");
|
|
34283
33882
|
}
|
|
34284
33883
|
}
|
|
34285
|
-
|
|
33884
|
+
goalHook.handleSystemTransform(input, output);
|
|
34286
33885
|
collapseSystemInPlace(output.system);
|
|
34287
33886
|
},
|
|
34288
33887
|
"experimental.chat.messages.transform": async (input, output) => {
|