oh-my-opencode-slim 1.0.6 → 1.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/README.md +30 -17
- package/dist/cli/config-io.d.ts +1 -0
- package/dist/cli/divoom.d.ts +23 -0
- package/dist/cli/doctor.d.ts +38 -0
- package/dist/cli/index.js +469 -58
- package/dist/cli/providers.d.ts +3 -0
- package/dist/config/council-schema.d.ts +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.d.ts +46 -1
- package/dist/config/schema.d.ts +23 -0
- package/dist/divoom/council.gif +0 -0
- package/dist/divoom/designer.gif +0 -0
- package/dist/divoom/explorer.gif +0 -0
- package/dist/divoom/fixer.gif +0 -0
- package/dist/divoom/input.gif +0 -0
- package/dist/divoom/intro.gif +0 -0
- package/dist/divoom/librarian.gif +0 -0
- package/dist/divoom/manager.d.ts +57 -0
- package/dist/divoom/oracle.gif +0 -0
- package/dist/divoom/orchestrator.gif +0 -0
- package/dist/index.js +1304 -291
- package/dist/integrations/divoom/index.d.ts +3 -0
- package/dist/integrations/divoom/status-manager.d.ts +31 -0
- package/dist/integrations/divoom/swift-helper-source.d.ts +1 -0
- package/dist/integrations/divoom/swift-transport.d.ts +26 -0
- package/dist/integrations/divoom/types.d.ts +41 -0
- package/dist/multiplexer/tmux/index.d.ts +5 -0
- package/dist/tools/council.d.ts +2 -2
- package/dist/tools/fork/command.d.ts +28 -0
- package/dist/tools/fork/files.d.ts +33 -0
- package/dist/tools/fork/index.d.ts +10 -0
- package/dist/tools/fork/state.d.ts +7 -0
- package/dist/tools/fork/tools.d.ts +23 -0
- package/dist/tools/fork/vendor.d.ts +28 -0
- package/dist/tools/handoff/command.d.ts +29 -0
- package/dist/tools/handoff/files.d.ts +33 -0
- package/dist/tools/handoff/index.d.ts +10 -0
- package/dist/tools/handoff/state.d.ts +7 -0
- package/dist/tools/handoff/tools.d.ts +23 -0
- package/dist/tools/handoff/vendor.d.ts +28 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/subtask/command.d.ts +30 -0
- package/dist/tools/subtask/files.d.ts +34 -0
- package/dist/tools/subtask/index.d.ts +11 -0
- package/dist/tools/subtask/state.d.ts +7 -0
- package/dist/tools/subtask/tools.d.ts +23 -0
- package/dist/tools/subtask/vendor.d.ts +27 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +679 -11
- package/dist/utils/session.d.ts +11 -4
- package/oh-my-opencode-slim.schema.json +59 -0
- package/package.json +3 -2
- package/src/skills/clonedeps/README.md +23 -0
- package/src/skills/clonedeps/SKILL.md +237 -0
- package/src/skills/clonedeps/codemap.md +41 -0
- package/src/skills/codemap.md +8 -5
package/dist/index.js
CHANGED
|
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
|
|
|
6246
6246
|
else
|
|
6247
6247
|
return basepath.substring(0, lastslash + 1) + refpath;
|
|
6248
6248
|
}
|
|
6249
|
-
function remove_dot_segments(
|
|
6250
|
-
if (!
|
|
6251
|
-
return
|
|
6249
|
+
function remove_dot_segments(path16) {
|
|
6250
|
+
if (!path16)
|
|
6251
|
+
return path16;
|
|
6252
6252
|
var output = "";
|
|
6253
|
-
while (
|
|
6254
|
-
if (
|
|
6255
|
-
|
|
6253
|
+
while (path16.length > 0) {
|
|
6254
|
+
if (path16 === "." || path16 === "..") {
|
|
6255
|
+
path16 = "";
|
|
6256
6256
|
break;
|
|
6257
6257
|
}
|
|
6258
|
-
var twochars =
|
|
6259
|
-
var threechars =
|
|
6260
|
-
var fourchars =
|
|
6258
|
+
var twochars = path16.substring(0, 2);
|
|
6259
|
+
var threechars = path16.substring(0, 3);
|
|
6260
|
+
var fourchars = path16.substring(0, 4);
|
|
6261
6261
|
if (threechars === "../") {
|
|
6262
|
-
|
|
6262
|
+
path16 = path16.substring(3);
|
|
6263
6263
|
} else if (twochars === "./") {
|
|
6264
|
-
|
|
6264
|
+
path16 = path16.substring(2);
|
|
6265
6265
|
} else if (threechars === "/./") {
|
|
6266
|
-
|
|
6267
|
-
} else if (twochars === "/." &&
|
|
6268
|
-
|
|
6269
|
-
} else if (fourchars === "/../" || threechars === "/.." &&
|
|
6270
|
-
|
|
6266
|
+
path16 = "/" + path16.substring(3);
|
|
6267
|
+
} else if (twochars === "/." && path16.length === 2) {
|
|
6268
|
+
path16 = "/";
|
|
6269
|
+
} else if (fourchars === "/../" || threechars === "/.." && path16.length === 3) {
|
|
6270
|
+
path16 = "/" + path16.substring(4);
|
|
6271
6271
|
output = output.replace(/\/?[^\/]*$/, "");
|
|
6272
6272
|
} else {
|
|
6273
|
-
var segment =
|
|
6273
|
+
var segment = path16.match(/(\/?([^\/]*))/)[0];
|
|
6274
6274
|
output += segment;
|
|
6275
|
-
|
|
6275
|
+
path16 = path16.substring(segment.length);
|
|
6276
6276
|
}
|
|
6277
6277
|
}
|
|
6278
6278
|
return output;
|
|
@@ -18220,6 +18220,12 @@ var CUSTOM_SKILLS = [
|
|
|
18220
18220
|
description: "Repository understanding and hierarchical codemap generation",
|
|
18221
18221
|
allowedAgents: ["orchestrator"],
|
|
18222
18222
|
sourcePath: "src/skills/codemap"
|
|
18223
|
+
},
|
|
18224
|
+
{
|
|
18225
|
+
name: "clonedeps",
|
|
18226
|
+
description: "Clone important dependency source for local inspection",
|
|
18227
|
+
allowedAgents: ["orchestrator"],
|
|
18228
|
+
sourcePath: "src/skills/clonedeps"
|
|
18223
18229
|
}
|
|
18224
18230
|
];
|
|
18225
18231
|
|
|
@@ -18393,6 +18399,56 @@ var CouncilConfigSchema = z.object({
|
|
|
18393
18399
|
import * as fs from "node:fs";
|
|
18394
18400
|
import * as path from "node:path";
|
|
18395
18401
|
|
|
18402
|
+
// src/utils/compat.ts
|
|
18403
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
18404
|
+
import { writeFile as fsWriteFile } from "node:fs/promises";
|
|
18405
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
18406
|
+
function collectStream(stream) {
|
|
18407
|
+
if (!stream)
|
|
18408
|
+
return () => Promise.resolve("");
|
|
18409
|
+
const chunks = [];
|
|
18410
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
18411
|
+
return () => new Promise((resolve, reject) => {
|
|
18412
|
+
if (!stream.readable) {
|
|
18413
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
18414
|
+
return;
|
|
18415
|
+
}
|
|
18416
|
+
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
18417
|
+
stream.on("error", reject);
|
|
18418
|
+
});
|
|
18419
|
+
}
|
|
18420
|
+
function crossSpawn(command, options) {
|
|
18421
|
+
const [cmd, ...args] = command;
|
|
18422
|
+
const proc = nodeSpawn(cmd, args, {
|
|
18423
|
+
stdio: [
|
|
18424
|
+
options?.stdin ?? "ignore",
|
|
18425
|
+
options?.stdout ?? "pipe",
|
|
18426
|
+
options?.stderr ?? "pipe"
|
|
18427
|
+
],
|
|
18428
|
+
cwd: options?.cwd,
|
|
18429
|
+
env: options?.env
|
|
18430
|
+
});
|
|
18431
|
+
const stdoutCollector = collectStream(proc.stdout);
|
|
18432
|
+
const stderrCollector = collectStream(proc.stderr);
|
|
18433
|
+
const exited = new Promise((resolve, reject) => {
|
|
18434
|
+
proc.on("error", reject);
|
|
18435
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
18436
|
+
});
|
|
18437
|
+
return {
|
|
18438
|
+
proc,
|
|
18439
|
+
stdout: stdoutCollector,
|
|
18440
|
+
stderr: stderrCollector,
|
|
18441
|
+
exited,
|
|
18442
|
+
kill: (signal) => proc.kill(signal),
|
|
18443
|
+
get exitCode() {
|
|
18444
|
+
return proc.exitCode;
|
|
18445
|
+
}
|
|
18446
|
+
};
|
|
18447
|
+
}
|
|
18448
|
+
async function crossWrite(path, data) {
|
|
18449
|
+
await fsWriteFile(path, Buffer.from(data));
|
|
18450
|
+
}
|
|
18451
|
+
|
|
18396
18452
|
// src/config/agent-mcps.ts
|
|
18397
18453
|
var DEFAULT_AGENT_MCPS = {
|
|
18398
18454
|
orchestrator: ["*", "!context7"],
|
|
@@ -18530,6 +18586,17 @@ var SessionManagerConfigSchema = z2.object({
|
|
|
18530
18586
|
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
18531
18587
|
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
18532
18588
|
});
|
|
18589
|
+
var DivoomConfigSchema = z2.object({
|
|
18590
|
+
enabled: z2.boolean().default(false),
|
|
18591
|
+
python: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python"),
|
|
18592
|
+
script: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py"),
|
|
18593
|
+
size: z2.number().int().min(1).max(1024).default(128),
|
|
18594
|
+
fps: z2.number().int().min(1).max(60).default(8),
|
|
18595
|
+
speed: z2.number().int().min(1).max(1e4).default(125),
|
|
18596
|
+
maxFrames: z2.number().int().min(1).max(500).default(24),
|
|
18597
|
+
posterizeBits: z2.number().int().min(1).max(8).default(3),
|
|
18598
|
+
gifs: z2.record(z2.string(), z2.string().min(1)).optional()
|
|
18599
|
+
});
|
|
18533
18600
|
var TodoContinuationConfigSchema = z2.object({
|
|
18534
18601
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
18535
18602
|
cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
|
|
@@ -18581,6 +18648,7 @@ var PluginConfigSchema = z2.object({
|
|
|
18581
18648
|
websearch: WebsearchConfigSchema.optional(),
|
|
18582
18649
|
interview: InterviewConfigSchema.optional(),
|
|
18583
18650
|
sessionManager: SessionManagerConfigSchema.optional(),
|
|
18651
|
+
divoom: DivoomConfigSchema.optional(),
|
|
18584
18652
|
todoContinuation: TodoContinuationConfigSchema.optional(),
|
|
18585
18653
|
fallback: FailoverConfigSchema.optional(),
|
|
18586
18654
|
council: CouncilConfigSchema.optional()
|
|
@@ -18597,20 +18665,49 @@ var PluginConfigSchema = z2.object({
|
|
|
18597
18665
|
|
|
18598
18666
|
// src/config/loader.ts
|
|
18599
18667
|
var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
|
|
18600
|
-
function loadConfigFromPath(configPath) {
|
|
18668
|
+
function loadConfigFromPath(configPath, options) {
|
|
18601
18669
|
try {
|
|
18602
18670
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
18603
|
-
|
|
18671
|
+
let rawConfig;
|
|
18672
|
+
try {
|
|
18673
|
+
rawConfig = JSON.parse(stripJsonComments(content));
|
|
18674
|
+
} catch (error) {
|
|
18675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18676
|
+
options?.onWarning?.({
|
|
18677
|
+
path: configPath,
|
|
18678
|
+
kind: "invalid-json",
|
|
18679
|
+
message
|
|
18680
|
+
});
|
|
18681
|
+
if (!options?.silent) {
|
|
18682
|
+
console.warn(`[oh-my-opencode-slim] Invalid JSON in ${configPath}:`, message);
|
|
18683
|
+
}
|
|
18684
|
+
return null;
|
|
18685
|
+
}
|
|
18604
18686
|
const result = PluginConfigSchema.safeParse(rawConfig);
|
|
18605
18687
|
if (!result.success) {
|
|
18606
|
-
|
|
18607
|
-
|
|
18688
|
+
options?.onWarning?.({
|
|
18689
|
+
path: configPath,
|
|
18690
|
+
kind: "invalid-schema",
|
|
18691
|
+
message: "Config does not match schema",
|
|
18692
|
+
formatted: result.error.format()
|
|
18693
|
+
});
|
|
18694
|
+
if (!options?.silent) {
|
|
18695
|
+
console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
|
|
18696
|
+
console.warn(result.error.format());
|
|
18697
|
+
}
|
|
18608
18698
|
return null;
|
|
18609
18699
|
}
|
|
18610
18700
|
return result.data;
|
|
18611
18701
|
} catch (error) {
|
|
18612
18702
|
if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
|
|
18613
|
-
|
|
18703
|
+
options?.onWarning?.({
|
|
18704
|
+
path: configPath,
|
|
18705
|
+
kind: "read-error",
|
|
18706
|
+
message: error.message
|
|
18707
|
+
});
|
|
18708
|
+
if (!options?.silent) {
|
|
18709
|
+
console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
|
|
18710
|
+
}
|
|
18614
18711
|
}
|
|
18615
18712
|
return null;
|
|
18616
18713
|
}
|
|
@@ -18635,6 +18732,26 @@ function findConfigPathInDirs(configDirs, baseName) {
|
|
|
18635
18732
|
}
|
|
18636
18733
|
return null;
|
|
18637
18734
|
}
|
|
18735
|
+
function findPluginConfigPaths(directory) {
|
|
18736
|
+
const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
|
|
18737
|
+
const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
|
|
18738
|
+
const projectConfigPath = findConfigPath(projectConfigBasePath);
|
|
18739
|
+
return { userConfigPath, projectConfigPath };
|
|
18740
|
+
}
|
|
18741
|
+
function mergePluginConfigs(base, override) {
|
|
18742
|
+
return {
|
|
18743
|
+
...base,
|
|
18744
|
+
...override,
|
|
18745
|
+
agents: deepMerge(base.agents, override.agents),
|
|
18746
|
+
tmux: deepMerge(base.tmux, override.tmux),
|
|
18747
|
+
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
18748
|
+
interview: deepMerge(base.interview, override.interview),
|
|
18749
|
+
sessionManager: deepMerge(base.sessionManager, override.sessionManager),
|
|
18750
|
+
divoom: deepMerge(base.divoom, override.divoom),
|
|
18751
|
+
fallback: deepMerge(base.fallback, override.fallback),
|
|
18752
|
+
council: deepMerge(base.council, override.council)
|
|
18753
|
+
};
|
|
18754
|
+
}
|
|
18638
18755
|
function deepMerge(base, override) {
|
|
18639
18756
|
if (!base)
|
|
18640
18757
|
return override;
|
|
@@ -18652,24 +18769,12 @@ function deepMerge(base, override) {
|
|
|
18652
18769
|
}
|
|
18653
18770
|
return result;
|
|
18654
18771
|
}
|
|
18655
|
-
function loadPluginConfig(directory) {
|
|
18656
|
-
const userConfigPath =
|
|
18657
|
-
|
|
18658
|
-
const
|
|
18659
|
-
let config = userConfigPath ? loadConfigFromPath(userConfigPath) ?? {} : {};
|
|
18660
|
-
const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath) : null;
|
|
18772
|
+
function loadPluginConfig(directory, options) {
|
|
18773
|
+
const { userConfigPath, projectConfigPath } = findPluginConfigPaths(directory);
|
|
18774
|
+
let config = userConfigPath ? loadConfigFromPath(userConfigPath, options) ?? {} : {};
|
|
18775
|
+
const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, options) : null;
|
|
18661
18776
|
if (projectConfig) {
|
|
18662
|
-
config =
|
|
18663
|
-
...config,
|
|
18664
|
-
...projectConfig,
|
|
18665
|
-
agents: deepMerge(config.agents, projectConfig.agents),
|
|
18666
|
-
tmux: deepMerge(config.tmux, projectConfig.tmux),
|
|
18667
|
-
multiplexer: deepMerge(config.multiplexer, projectConfig.multiplexer),
|
|
18668
|
-
interview: deepMerge(config.interview, projectConfig.interview),
|
|
18669
|
-
sessionManager: deepMerge(config.sessionManager, projectConfig.sessionManager),
|
|
18670
|
-
fallback: deepMerge(config.fallback, projectConfig.fallback),
|
|
18671
|
-
council: deepMerge(config.council, projectConfig.council)
|
|
18672
|
-
};
|
|
18777
|
+
config = mergePluginConfigs(config, projectConfig);
|
|
18673
18778
|
}
|
|
18674
18779
|
config = migrateTmuxToMultiplexer(config);
|
|
18675
18780
|
const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
|
|
@@ -18683,7 +18788,15 @@ function loadPluginConfig(directory) {
|
|
|
18683
18788
|
} else {
|
|
18684
18789
|
const presetSource = envPreset === config.preset ? "environment variable" : "config file";
|
|
18685
18790
|
const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
|
|
18686
|
-
|
|
18791
|
+
const message = `Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`;
|
|
18792
|
+
options?.onWarning?.({
|
|
18793
|
+
path: projectConfigPath ?? userConfigPath ?? "",
|
|
18794
|
+
kind: "missing-preset",
|
|
18795
|
+
message
|
|
18796
|
+
});
|
|
18797
|
+
if (!options?.silent) {
|
|
18798
|
+
console.warn(`[oh-my-opencode-slim] ${message}`);
|
|
18799
|
+
}
|
|
18687
18800
|
}
|
|
18688
18801
|
}
|
|
18689
18802
|
return config;
|
|
@@ -18744,6 +18857,34 @@ function getCustomAgentNames(config) {
|
|
|
18744
18857
|
});
|
|
18745
18858
|
}
|
|
18746
18859
|
// src/utils/session.ts
|
|
18860
|
+
var SESSION_ABORT_TIMEOUT_MS = 1000;
|
|
18861
|
+
|
|
18862
|
+
class OperationTimeoutError extends Error {
|
|
18863
|
+
constructor(message) {
|
|
18864
|
+
super(message);
|
|
18865
|
+
this.name = "OperationTimeoutError";
|
|
18866
|
+
}
|
|
18867
|
+
}
|
|
18868
|
+
async function withTimeout(operation, timeoutMs, message) {
|
|
18869
|
+
if (timeoutMs <= 0)
|
|
18870
|
+
return operation;
|
|
18871
|
+
let timer;
|
|
18872
|
+
try {
|
|
18873
|
+
return await Promise.race([
|
|
18874
|
+
operation,
|
|
18875
|
+
new Promise((_, reject) => {
|
|
18876
|
+
timer = setTimeout(() => {
|
|
18877
|
+
reject(new OperationTimeoutError(message));
|
|
18878
|
+
}, timeoutMs);
|
|
18879
|
+
})
|
|
18880
|
+
]);
|
|
18881
|
+
} finally {
|
|
18882
|
+
clearTimeout(timer);
|
|
18883
|
+
}
|
|
18884
|
+
}
|
|
18885
|
+
async function abortSessionWithTimeout(client, sessionId, timeoutMs = SESSION_ABORT_TIMEOUT_MS) {
|
|
18886
|
+
await withTimeout(client.session.abort({ path: { id: sessionId } }), timeoutMs, `Session abort timed out after ${timeoutMs}ms`);
|
|
18887
|
+
}
|
|
18747
18888
|
function shortModelLabel(model) {
|
|
18748
18889
|
return model.split("/").pop() ?? model;
|
|
18749
18890
|
}
|
|
@@ -18757,13 +18898,16 @@ function parseModelReference(model) {
|
|
|
18757
18898
|
modelID: model.slice(slashIndex + 1)
|
|
18758
18899
|
};
|
|
18759
18900
|
}
|
|
18760
|
-
async function promptWithTimeout(client, args, timeoutMs) {
|
|
18901
|
+
async function promptWithTimeout(client, args, timeoutMs, signal) {
|
|
18902
|
+
if (signal?.aborted)
|
|
18903
|
+
throw new Error("Prompt cancelled");
|
|
18761
18904
|
if (timeoutMs <= 0) {
|
|
18762
18905
|
await client.session.prompt(args);
|
|
18763
18906
|
return;
|
|
18764
18907
|
}
|
|
18765
18908
|
const sessionId = args.path.id;
|
|
18766
18909
|
let timer;
|
|
18910
|
+
let onAbort;
|
|
18767
18911
|
try {
|
|
18768
18912
|
const promptPromise = client.session.prompt(args);
|
|
18769
18913
|
promptPromise.catch(() => {});
|
|
@@ -18771,19 +18915,38 @@ async function promptWithTimeout(client, args, timeoutMs) {
|
|
|
18771
18915
|
promptPromise,
|
|
18772
18916
|
new Promise((_, reject) => {
|
|
18773
18917
|
timer = setTimeout(() => {
|
|
18774
|
-
|
|
18775
|
-
reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
|
|
18918
|
+
reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
|
|
18776
18919
|
}, timeoutMs);
|
|
18920
|
+
}),
|
|
18921
|
+
new Promise((_, reject) => {
|
|
18922
|
+
if (!signal)
|
|
18923
|
+
return;
|
|
18924
|
+
if (signal.aborted) {
|
|
18925
|
+
reject(new Error("Prompt cancelled"));
|
|
18926
|
+
return;
|
|
18927
|
+
}
|
|
18928
|
+
onAbort = () => reject(new Error("Prompt cancelled"));
|
|
18929
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
18777
18930
|
})
|
|
18778
18931
|
]);
|
|
18932
|
+
} catch (error) {
|
|
18933
|
+
if (error instanceof OperationTimeoutError) {
|
|
18934
|
+
try {
|
|
18935
|
+
await abortSessionWithTimeout(client, sessionId);
|
|
18936
|
+
} catch {}
|
|
18937
|
+
}
|
|
18938
|
+
throw error;
|
|
18779
18939
|
} finally {
|
|
18780
18940
|
clearTimeout(timer);
|
|
18941
|
+
if (onAbort)
|
|
18942
|
+
signal?.removeEventListener("abort", onAbort);
|
|
18781
18943
|
}
|
|
18782
18944
|
}
|
|
18783
18945
|
async function extractSessionResult(client, sessionId, options) {
|
|
18784
18946
|
const includeReasoning = options?.includeReasoning ?? true;
|
|
18785
18947
|
const messagesResult = await client.session.messages({
|
|
18786
|
-
path: { id: sessionId }
|
|
18948
|
+
path: { id: sessionId },
|
|
18949
|
+
...options?.directory ? { query: { directory: options.directory } } : {}
|
|
18787
18950
|
});
|
|
18788
18951
|
const messages = messagesResult.data ?? [];
|
|
18789
18952
|
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
@@ -18822,7 +18985,7 @@ var AGENT_DESCRIPTIONS = {
|
|
|
18822
18985
|
- **Don't delegate when:** Know the path and need actual content • Need full file anyway • Single specific lookup • About to edit the file`,
|
|
18823
18986
|
librarian: `@librarian
|
|
18824
18987
|
- Role: Authoritative source for current library docs and API references
|
|
18825
|
-
- Permissions:
|
|
18988
|
+
- Permissions: External docs/search MCPs; no file edits
|
|
18826
18989
|
- Stats: 10x better finding up-to-date library docs than orchestrator, 1/2 cost of orchestrator
|
|
18827
18990
|
- Capabilities: Fetches latest official docs, examples, API signatures, version-specific behavior via grep_app MCP
|
|
18828
18991
|
- **Delegate when:** Libraries with frequent API changes (React, Next.js, AI SDKs) • Complex APIs needing official examples (ORMs, auth) • Version-specific behavior matters • Unfamiliar library • Edge cases or advanced features • Nuanced best practices
|
|
@@ -18939,6 +19102,25 @@ ${enabledParallelExamples}
|
|
|
18939
19102
|
|
|
18940
19103
|
Balance: respect dependencies, avoid parallelizing what must be sequential.
|
|
18941
19104
|
|
|
19105
|
+
### Context Isolation
|
|
19106
|
+
If no specialist delegation is needed, consider \`subtask\` before doing
|
|
19107
|
+
context-heavy work directly.
|
|
19108
|
+
|
|
19109
|
+
Ask whether the parent context needs the details or only the result. Use
|
|
19110
|
+
\`subtask\` when the work is bounded, context-heavy, and the parent only needs a
|
|
19111
|
+
compact outcome.
|
|
19112
|
+
|
|
19113
|
+
Use \`subtask\` for focused investigation, bounded analysis, cleanup, or
|
|
19114
|
+
verification across files/logs/messages.
|
|
19115
|
+
|
|
19116
|
+
Do not use \`subtask\` for tiny tasks, open-ended work, interactive decisions,
|
|
19117
|
+
work better handled by a named specialist, or cases where the parent must reason
|
|
19118
|
+
over the details.
|
|
19119
|
+
|
|
19120
|
+
When calling \`subtask\`, give a self-contained prompt with objective,
|
|
19121
|
+
constraints, relevant context, deliverable, and validation. Pass only clearly
|
|
19122
|
+
relevant files. Wait for the summary, then integrate and verify it.
|
|
19123
|
+
|
|
18942
19124
|
### OpenCode subagent execution model
|
|
18943
19125
|
- A delegated specialist runs in a separate child session.
|
|
18944
19126
|
- Delegation is blocking for the parent at that point: send work out, then continue that line after results return.
|
|
@@ -19207,7 +19389,8 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
|
|
|
19207
19389
|
grep: "allow",
|
|
19208
19390
|
lsp: "allow",
|
|
19209
19391
|
list: "allow",
|
|
19210
|
-
codesearch: "allow"
|
|
19392
|
+
codesearch: "allow",
|
|
19393
|
+
ast_grep_search: "allow"
|
|
19211
19394
|
}
|
|
19212
19395
|
}
|
|
19213
19396
|
};
|
|
@@ -19529,10 +19712,14 @@ ${customAppendPrompt}`;
|
|
|
19529
19712
|
|
|
19530
19713
|
// src/agents/index.ts
|
|
19531
19714
|
var COUNCIL_TOOL_ALLOWED_AGENTS = new Set(["council"]);
|
|
19715
|
+
var SAFE_AGENT_ALIAS_RE = /^[a-z][a-z0-9_-]*$/i;
|
|
19532
19716
|
function normalizeDisplayName(displayName) {
|
|
19533
19717
|
const trimmed = displayName.trim();
|
|
19534
19718
|
return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
19535
19719
|
}
|
|
19720
|
+
function isSafeDisplayName(displayName) {
|
|
19721
|
+
return SAFE_AGENT_ALIAS_RE.test(displayName);
|
|
19722
|
+
}
|
|
19536
19723
|
function escapeRegExp(value) {
|
|
19537
19724
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19538
19725
|
}
|
|
@@ -19566,7 +19753,7 @@ function normalizeCustomAgentName(name) {
|
|
|
19566
19753
|
return name.trim();
|
|
19567
19754
|
}
|
|
19568
19755
|
function isSafeCustomAgentName(name) {
|
|
19569
|
-
return
|
|
19756
|
+
return SAFE_AGENT_ALIAS_RE.test(name) && !isKnownAgentName(name);
|
|
19570
19757
|
}
|
|
19571
19758
|
function hasCustomAgentModel(override) {
|
|
19572
19759
|
if (!override?.model) {
|
|
@@ -19626,6 +19813,9 @@ var SUBAGENT_FACTORIES = {
|
|
|
19626
19813
|
};
|
|
19627
19814
|
function createAgents(config) {
|
|
19628
19815
|
const disabled = getDisabledAgents(config);
|
|
19816
|
+
if (!config?.council) {
|
|
19817
|
+
disabled.add("council");
|
|
19818
|
+
}
|
|
19629
19819
|
const getModelForAgent = (name) => {
|
|
19630
19820
|
if (name === "fixer" && !getAgentOverride(config, "fixer")?.model) {
|
|
19631
19821
|
const librarianOverride = getAgentOverride(config, "librarian")?.model;
|
|
@@ -19712,6 +19902,9 @@ function createAgents(config) {
|
|
|
19712
19902
|
const usedDisplayNames = new Set;
|
|
19713
19903
|
for (const [, displayName] of displayNameMap) {
|
|
19714
19904
|
const normalizedDisplayName = normalizeDisplayName(displayName);
|
|
19905
|
+
if (!isSafeDisplayName(normalizedDisplayName)) {
|
|
19906
|
+
throw new Error(`displayName '${normalizedDisplayName}' must match /^[a-z][a-z0-9_-]*$/i`);
|
|
19907
|
+
}
|
|
19715
19908
|
if (usedDisplayNames.has(normalizedDisplayName)) {
|
|
19716
19909
|
throw new Error(`Duplicate displayName '${normalizedDisplayName}' assigned to multiple agents`);
|
|
19717
19910
|
}
|
|
@@ -20138,6 +20331,286 @@ class CouncilManager {
|
|
|
20138
20331
|
};
|
|
20139
20332
|
}
|
|
20140
20333
|
}
|
|
20334
|
+
// src/divoom/manager.ts
|
|
20335
|
+
import { spawn } from "node:child_process";
|
|
20336
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
20337
|
+
import * as os2 from "node:os";
|
|
20338
|
+
import path3 from "node:path";
|
|
20339
|
+
import { fileURLToPath } from "node:url";
|
|
20340
|
+
var AGENT_GIFS = {
|
|
20341
|
+
council: "council.gif",
|
|
20342
|
+
councillor: "council.gif",
|
|
20343
|
+
designer: "designer.gif",
|
|
20344
|
+
explorer: "explorer.gif",
|
|
20345
|
+
fixer: "fixer.gif",
|
|
20346
|
+
input: "input.gif",
|
|
20347
|
+
intro: "intro.gif",
|
|
20348
|
+
librarian: "librarian.gif",
|
|
20349
|
+
oracle: "oracle.gif",
|
|
20350
|
+
orchestrator: "orchestrator.gif"
|
|
20351
|
+
};
|
|
20352
|
+
var DEFAULT_DIVOOM_CONFIG = {
|
|
20353
|
+
enabled: false,
|
|
20354
|
+
python: "/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python",
|
|
20355
|
+
script: "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py",
|
|
20356
|
+
size: 128,
|
|
20357
|
+
fps: 8,
|
|
20358
|
+
speed: 125,
|
|
20359
|
+
maxFrames: 24,
|
|
20360
|
+
posterizeBits: 3
|
|
20361
|
+
};
|
|
20362
|
+
var DIVOOM_ENABLE_ENV = "OH_MY_OPENCODE_SLIM_DIVOOM";
|
|
20363
|
+
function resolveAssetDir() {
|
|
20364
|
+
const moduleDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
20365
|
+
const candidates = [
|
|
20366
|
+
moduleDir,
|
|
20367
|
+
path3.resolve(moduleDir, "divoom"),
|
|
20368
|
+
path3.resolve(moduleDir, "../../src/divoom"),
|
|
20369
|
+
path3.resolve(process.cwd(), "src/divoom")
|
|
20370
|
+
];
|
|
20371
|
+
for (const candidate of candidates) {
|
|
20372
|
+
if (existsSync2(path3.join(candidate, "intro.gif"))) {
|
|
20373
|
+
return candidate;
|
|
20374
|
+
}
|
|
20375
|
+
}
|
|
20376
|
+
return null;
|
|
20377
|
+
}
|
|
20378
|
+
function normalizeAgentName(value) {
|
|
20379
|
+
if (typeof value !== "string")
|
|
20380
|
+
return null;
|
|
20381
|
+
const normalized = value.trim().toLowerCase();
|
|
20382
|
+
return normalized.length > 0 ? normalized : null;
|
|
20383
|
+
}
|
|
20384
|
+
function isEnvEnabled(value) {
|
|
20385
|
+
if (!value)
|
|
20386
|
+
return false;
|
|
20387
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
20388
|
+
}
|
|
20389
|
+
function inputKey(sessionId, requestId) {
|
|
20390
|
+
return `${sessionId}:${requestId}`;
|
|
20391
|
+
}
|
|
20392
|
+
function getDivoomOutDir(homeDir = os2.homedir()) {
|
|
20393
|
+
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
20394
|
+
const baseDir = xdg && xdg.length > 0 && path3.isAbsolute(xdg) ? xdg : path3.join(homeDir, ".local", "share");
|
|
20395
|
+
return path3.join(baseDir, "opencode", "storage", "oh-my-opencode-slim", "divoom", "captures");
|
|
20396
|
+
}
|
|
20397
|
+
|
|
20398
|
+
class DivoomManager {
|
|
20399
|
+
sender;
|
|
20400
|
+
assetDir;
|
|
20401
|
+
config;
|
|
20402
|
+
parentStates = new Map;
|
|
20403
|
+
pendingUserInputs = new Set;
|
|
20404
|
+
orchestratorBusy = false;
|
|
20405
|
+
latestRequestedGifPath;
|
|
20406
|
+
lastGifPath;
|
|
20407
|
+
sendQueue = Promise.resolve();
|
|
20408
|
+
constructor(config, sender = defaultSender, options = {}) {
|
|
20409
|
+
this.sender = sender;
|
|
20410
|
+
this.config = {
|
|
20411
|
+
...DEFAULT_DIVOOM_CONFIG,
|
|
20412
|
+
...config,
|
|
20413
|
+
enabled: isEnvEnabled(process.env[DIVOOM_ENABLE_ENV]) ? true : config?.enabled ?? false,
|
|
20414
|
+
gifs: config?.gifs
|
|
20415
|
+
};
|
|
20416
|
+
this.assetDir = options.assetDir ?? resolveAssetDir();
|
|
20417
|
+
if (options.sender) {
|
|
20418
|
+
this.sender = options.sender;
|
|
20419
|
+
}
|
|
20420
|
+
}
|
|
20421
|
+
onPluginLoad() {
|
|
20422
|
+
this.show("intro");
|
|
20423
|
+
}
|
|
20424
|
+
onTaskStart(input) {
|
|
20425
|
+
if (!input.parentSessionId || !input.callId)
|
|
20426
|
+
return;
|
|
20427
|
+
if (!isTaskArgs(input.args))
|
|
20428
|
+
return;
|
|
20429
|
+
const agent = normalizeAgentName(input.args.subagent_type);
|
|
20430
|
+
if (!agent)
|
|
20431
|
+
return;
|
|
20432
|
+
const state = this.getParentState(input.parentSessionId);
|
|
20433
|
+
const wasIdle = state.activeCalls.size === 0;
|
|
20434
|
+
state.activeCalls.set(input.callId, agent);
|
|
20435
|
+
this.orchestratorBusy = true;
|
|
20436
|
+
if (wasIdle && !state.displayedAgent) {
|
|
20437
|
+
state.displayedAgent = agent;
|
|
20438
|
+
}
|
|
20439
|
+
this.render();
|
|
20440
|
+
}
|
|
20441
|
+
onTaskEnd(input) {
|
|
20442
|
+
if (!input.parentSessionId || !input.callId)
|
|
20443
|
+
return;
|
|
20444
|
+
const state = this.parentStates.get(input.parentSessionId);
|
|
20445
|
+
if (!state)
|
|
20446
|
+
return;
|
|
20447
|
+
state.activeCalls.delete(input.callId);
|
|
20448
|
+
if (state.activeCalls.size === 0) {
|
|
20449
|
+
this.parentStates.delete(input.parentSessionId);
|
|
20450
|
+
}
|
|
20451
|
+
this.render();
|
|
20452
|
+
}
|
|
20453
|
+
onUserInputRequired(input) {
|
|
20454
|
+
if (!input.sessionId || !input.requestId)
|
|
20455
|
+
return;
|
|
20456
|
+
this.pendingUserInputs.add(inputKey(input.sessionId, input.requestId));
|
|
20457
|
+
this.render();
|
|
20458
|
+
}
|
|
20459
|
+
onUserInputResolved(input) {
|
|
20460
|
+
if (!input.sessionId || !input.requestId)
|
|
20461
|
+
return;
|
|
20462
|
+
this.pendingUserInputs.delete(inputKey(input.sessionId, input.requestId));
|
|
20463
|
+
this.render();
|
|
20464
|
+
}
|
|
20465
|
+
onOrchestratorStatus(input) {
|
|
20466
|
+
if (!input.sessionId || !input.isOrchestrator)
|
|
20467
|
+
return;
|
|
20468
|
+
if (input.status === "busy") {
|
|
20469
|
+
this.orchestratorBusy = true;
|
|
20470
|
+
this.render();
|
|
20471
|
+
return;
|
|
20472
|
+
}
|
|
20473
|
+
if (input.status === "idle") {
|
|
20474
|
+
this.orchestratorBusy = false;
|
|
20475
|
+
this.parentStates.delete(input.sessionId);
|
|
20476
|
+
this.render();
|
|
20477
|
+
}
|
|
20478
|
+
}
|
|
20479
|
+
onSessionDeleted(input) {
|
|
20480
|
+
const sessionId = input.sessionId;
|
|
20481
|
+
if (!sessionId)
|
|
20482
|
+
return;
|
|
20483
|
+
if (input.isOrchestrator)
|
|
20484
|
+
this.orchestratorBusy = false;
|
|
20485
|
+
this.parentStates.delete(sessionId);
|
|
20486
|
+
this.pendingUserInputs = new Set(Array.from(this.pendingUserInputs).filter((key) => !key.startsWith(`${sessionId}:`)));
|
|
20487
|
+
this.render();
|
|
20488
|
+
}
|
|
20489
|
+
render() {
|
|
20490
|
+
if (this.pendingUserInputs.size > 0) {
|
|
20491
|
+
this.show("input");
|
|
20492
|
+
return;
|
|
20493
|
+
}
|
|
20494
|
+
const activeAgent = Array.from(this.parentStates.values()).find((state) => state.displayedAgent && state.activeCalls.size > 0)?.displayedAgent;
|
|
20495
|
+
if (activeAgent) {
|
|
20496
|
+
this.show(activeAgent);
|
|
20497
|
+
return;
|
|
20498
|
+
}
|
|
20499
|
+
this.show(this.orchestratorBusy ? "orchestrator" : "intro");
|
|
20500
|
+
}
|
|
20501
|
+
getParentState(parentSessionId) {
|
|
20502
|
+
const existing = this.parentStates.get(parentSessionId);
|
|
20503
|
+
if (existing)
|
|
20504
|
+
return existing;
|
|
20505
|
+
const created = {
|
|
20506
|
+
activeCalls: new Map
|
|
20507
|
+
};
|
|
20508
|
+
this.parentStates.set(parentSessionId, created);
|
|
20509
|
+
return created;
|
|
20510
|
+
}
|
|
20511
|
+
show(agent) {
|
|
20512
|
+
if (!this.config.enabled)
|
|
20513
|
+
return;
|
|
20514
|
+
if (!this.assetDir) {
|
|
20515
|
+
log("[divoom] asset directory not found");
|
|
20516
|
+
return;
|
|
20517
|
+
}
|
|
20518
|
+
const fileName = this.config.gifs?.[agent] ?? AGENT_GIFS[agent] ?? AGENT_GIFS.orchestrator;
|
|
20519
|
+
const requestedGifPath = path3.isAbsolute(fileName) ? fileName : path3.join(this.assetDir, fileName);
|
|
20520
|
+
const fallbackGifPath = path3.join(this.assetDir, AGENT_GIFS.intro);
|
|
20521
|
+
const gifPath = existsSync2(requestedGifPath) ? requestedGifPath : agent === "input" ? fallbackGifPath : requestedGifPath;
|
|
20522
|
+
if (!existsSync2(gifPath)) {
|
|
20523
|
+
log("[divoom] gif not found", { agent, gifPath: requestedGifPath });
|
|
20524
|
+
return;
|
|
20525
|
+
}
|
|
20526
|
+
if (gifPath === this.latestRequestedGifPath)
|
|
20527
|
+
return;
|
|
20528
|
+
this.latestRequestedGifPath = gifPath;
|
|
20529
|
+
const outDir = getDivoomOutDir();
|
|
20530
|
+
try {
|
|
20531
|
+
mkdirSync2(outDir, { recursive: true });
|
|
20532
|
+
} catch (error) {
|
|
20533
|
+
this.clearLatestIfCurrent(gifPath);
|
|
20534
|
+
log("[divoom] output directory not writable", {
|
|
20535
|
+
outDir,
|
|
20536
|
+
error: String(error)
|
|
20537
|
+
});
|
|
20538
|
+
return;
|
|
20539
|
+
}
|
|
20540
|
+
const call = {
|
|
20541
|
+
command: this.config.python,
|
|
20542
|
+
args: [
|
|
20543
|
+
this.config.script,
|
|
20544
|
+
gifPath,
|
|
20545
|
+
"--size",
|
|
20546
|
+
String(this.config.size),
|
|
20547
|
+
"--fps",
|
|
20548
|
+
String(this.config.fps),
|
|
20549
|
+
"--speed",
|
|
20550
|
+
String(this.config.speed),
|
|
20551
|
+
"--max-frames",
|
|
20552
|
+
String(this.config.maxFrames),
|
|
20553
|
+
"--posterize-bits",
|
|
20554
|
+
String(this.config.posterizeBits),
|
|
20555
|
+
"--out-dir",
|
|
20556
|
+
outDir
|
|
20557
|
+
]
|
|
20558
|
+
};
|
|
20559
|
+
this.sendQueue = this.sendQueue.catch(() => {}).then(async () => {
|
|
20560
|
+
if (gifPath !== this.latestRequestedGifPath)
|
|
20561
|
+
return;
|
|
20562
|
+
if (!existsSync2(this.config.python)) {
|
|
20563
|
+
this.clearLatestIfCurrent(gifPath);
|
|
20564
|
+
log("[divoom] python executable not found", this.config.python);
|
|
20565
|
+
return;
|
|
20566
|
+
}
|
|
20567
|
+
if (!existsSync2(this.config.script)) {
|
|
20568
|
+
this.clearLatestIfCurrent(gifPath);
|
|
20569
|
+
log("[divoom] sender script not found", this.config.script);
|
|
20570
|
+
return;
|
|
20571
|
+
}
|
|
20572
|
+
try {
|
|
20573
|
+
await this.sender(call);
|
|
20574
|
+
this.lastGifPath = gifPath;
|
|
20575
|
+
log("[divoom] showing gif", { agent, gifPath });
|
|
20576
|
+
} catch (error) {
|
|
20577
|
+
this.clearLatestIfCurrent(gifPath);
|
|
20578
|
+
log("[divoom] failed to send gif", String(error));
|
|
20579
|
+
}
|
|
20580
|
+
});
|
|
20581
|
+
}
|
|
20582
|
+
async flush() {
|
|
20583
|
+
await this.sendQueue.catch(() => {});
|
|
20584
|
+
}
|
|
20585
|
+
clearLatestIfCurrent(gifPath) {
|
|
20586
|
+
if (this.latestRequestedGifPath === gifPath) {
|
|
20587
|
+
this.latestRequestedGifPath = undefined;
|
|
20588
|
+
}
|
|
20589
|
+
if (this.lastGifPath === gifPath) {
|
|
20590
|
+
this.lastGifPath = undefined;
|
|
20591
|
+
}
|
|
20592
|
+
}
|
|
20593
|
+
}
|
|
20594
|
+
function isTaskArgs(value) {
|
|
20595
|
+
return typeof value === "object" && value !== null;
|
|
20596
|
+
}
|
|
20597
|
+
function defaultSender(call) {
|
|
20598
|
+
return new Promise((resolve, reject) => {
|
|
20599
|
+
const child = spawn(call.command, call.args, {
|
|
20600
|
+
detached: true,
|
|
20601
|
+
stdio: "ignore"
|
|
20602
|
+
});
|
|
20603
|
+
child.once("error", reject);
|
|
20604
|
+
child.once("spawn", () => {
|
|
20605
|
+
child.unref();
|
|
20606
|
+
resolve();
|
|
20607
|
+
});
|
|
20608
|
+
});
|
|
20609
|
+
}
|
|
20610
|
+
function createDivoomManager(config) {
|
|
20611
|
+
return new DivoomManager(config);
|
|
20612
|
+
}
|
|
20613
|
+
|
|
20141
20614
|
// src/hooks/apply-patch/errors.ts
|
|
20142
20615
|
var APPLY_PATCH_ERROR_PREFIX = {
|
|
20143
20616
|
blocked: "apply_patch blocked",
|
|
@@ -20206,7 +20679,7 @@ function ensureApplyPatchError(error, context) {
|
|
|
20206
20679
|
|
|
20207
20680
|
// src/hooks/apply-patch/execution-context.ts
|
|
20208
20681
|
import * as fs3 from "node:fs/promises";
|
|
20209
|
-
import
|
|
20682
|
+
import path4 from "node:path";
|
|
20210
20683
|
|
|
20211
20684
|
// src/hooks/apply-patch/codec.ts
|
|
20212
20685
|
function normalizeLineEndings(text) {
|
|
@@ -21075,7 +21548,7 @@ function isMissingPathError(error) {
|
|
|
21075
21548
|
}
|
|
21076
21549
|
async function real(target) {
|
|
21077
21550
|
const parts = [];
|
|
21078
|
-
let current =
|
|
21551
|
+
let current = path4.resolve(target);
|
|
21079
21552
|
while (true) {
|
|
21080
21553
|
const exact = await fs3.realpath(current).catch((error) => {
|
|
21081
21554
|
if (isMissingPathError(error)) {
|
|
@@ -21084,19 +21557,19 @@ async function real(target) {
|
|
|
21084
21557
|
throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
|
|
21085
21558
|
});
|
|
21086
21559
|
if (exact) {
|
|
21087
|
-
return parts.length === 0 ? exact :
|
|
21560
|
+
return parts.length === 0 ? exact : path4.join(exact, ...parts.reverse());
|
|
21088
21561
|
}
|
|
21089
|
-
const parent =
|
|
21562
|
+
const parent = path4.dirname(current);
|
|
21090
21563
|
if (parent === current) {
|
|
21091
|
-
return parts.length === 0 ? current :
|
|
21564
|
+
return parts.length === 0 ? current : path4.join(current, ...parts.reverse());
|
|
21092
21565
|
}
|
|
21093
|
-
parts.push(
|
|
21566
|
+
parts.push(path4.basename(current));
|
|
21094
21567
|
current = parent;
|
|
21095
21568
|
}
|
|
21096
21569
|
}
|
|
21097
21570
|
function inside(root, target) {
|
|
21098
|
-
const rel =
|
|
21099
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
21571
|
+
const rel = path4.relative(root, target);
|
|
21572
|
+
return rel === "" || !rel.startsWith("..") && !path4.isAbsolute(rel);
|
|
21100
21573
|
}
|
|
21101
21574
|
function createPathGuardContext(root, worktree) {
|
|
21102
21575
|
return {
|
|
@@ -21106,7 +21579,7 @@ function createPathGuardContext(root, worktree) {
|
|
|
21106
21579
|
};
|
|
21107
21580
|
}
|
|
21108
21581
|
async function realCached(ctx, target) {
|
|
21109
|
-
const resolvedTarget =
|
|
21582
|
+
const resolvedTarget = path4.resolve(target);
|
|
21110
21583
|
let pending = ctx.realCache.get(resolvedTarget);
|
|
21111
21584
|
if (!pending) {
|
|
21112
21585
|
pending = real(resolvedTarget);
|
|
@@ -21157,22 +21630,22 @@ async function assertRegularFile(ctx, filePath, verb) {
|
|
|
21157
21630
|
function collectPatchTargets(root, hunks) {
|
|
21158
21631
|
const targets = new Set;
|
|
21159
21632
|
for (const hunk of hunks) {
|
|
21160
|
-
targets.add(
|
|
21633
|
+
targets.add(path4.resolve(root, hunk.path));
|
|
21161
21634
|
if (hunk.type === "update" && hunk.move_path) {
|
|
21162
|
-
targets.add(
|
|
21635
|
+
targets.add(path4.resolve(root, hunk.move_path));
|
|
21163
21636
|
}
|
|
21164
21637
|
}
|
|
21165
21638
|
return [...targets];
|
|
21166
21639
|
}
|
|
21167
21640
|
function toRelativePatchPath(root, target) {
|
|
21168
|
-
const relative =
|
|
21641
|
+
const relative = path4.relative(root, target);
|
|
21169
21642
|
return (relative.length === 0 ? "." : relative).replaceAll("\\", "/");
|
|
21170
21643
|
}
|
|
21171
21644
|
function normalizePatchPath(root, value) {
|
|
21172
|
-
return
|
|
21645
|
+
return path4.isAbsolute(value) ? toRelativePatchPath(root, path4.resolve(value)) : value;
|
|
21173
21646
|
}
|
|
21174
21647
|
function normalizePatchPaths(root, hunks) {
|
|
21175
|
-
const resolvedRoot =
|
|
21648
|
+
const resolvedRoot = path4.resolve(root);
|
|
21176
21649
|
const normalized = [];
|
|
21177
21650
|
let changed = false;
|
|
21178
21651
|
for (const hunk of hunks) {
|
|
@@ -21296,7 +21769,7 @@ function stageAddedText(contents) {
|
|
|
21296
21769
|
`;
|
|
21297
21770
|
}
|
|
21298
21771
|
// src/hooks/apply-patch/rewrite.ts
|
|
21299
|
-
import
|
|
21772
|
+
import path5 from "node:path";
|
|
21300
21773
|
function normalizeTextLineEndings(text) {
|
|
21301
21774
|
return text.replace(/\r\n/g, `
|
|
21302
21775
|
`).replace(/\r/g, `
|
|
@@ -21453,7 +21926,7 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
21453
21926
|
const dependencyGroups = new Map;
|
|
21454
21927
|
for (const hunk of hunks) {
|
|
21455
21928
|
if (hunk.type === "add") {
|
|
21456
|
-
const filePath2 =
|
|
21929
|
+
const filePath2 = path5.resolve(root, hunk.path);
|
|
21457
21930
|
await assertPreparedPathMissing(filePath2, "add");
|
|
21458
21931
|
rewritten.push(hunk);
|
|
21459
21932
|
clearDependencyGroup(filePath2);
|
|
@@ -21475,20 +21948,20 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
21475
21948
|
continue;
|
|
21476
21949
|
}
|
|
21477
21950
|
if (hunk.type === "delete") {
|
|
21478
|
-
const filePath2 =
|
|
21951
|
+
const filePath2 = path5.resolve(root, hunk.path);
|
|
21479
21952
|
await getPreparedFileState(filePath2, "delete");
|
|
21480
21953
|
clearDependencyGroup(filePath2);
|
|
21481
21954
|
rewritten.push(hunk);
|
|
21482
21955
|
staged.set(filePath2, { exists: false, derived: true });
|
|
21483
21956
|
continue;
|
|
21484
21957
|
}
|
|
21485
|
-
const filePath =
|
|
21958
|
+
const filePath = path5.resolve(root, hunk.path);
|
|
21486
21959
|
const currentDependency = dependencyGroups.get(filePath);
|
|
21487
21960
|
const current = await getPreparedFileState(filePath, "update");
|
|
21488
21961
|
if (!current.exists) {
|
|
21489
21962
|
throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
|
|
21490
21963
|
}
|
|
21491
|
-
const movePath = hunk.move_path ?
|
|
21964
|
+
const movePath = hunk.move_path ? path5.resolve(root, hunk.move_path) : undefined;
|
|
21492
21965
|
if (movePath && movePath !== filePath) {
|
|
21493
21966
|
await assertPreparedPathMissing(movePath, "move");
|
|
21494
21967
|
}
|
|
@@ -21584,6 +22057,15 @@ var APPLY_PATCH_RESCUE_OPTIONS = {
|
|
|
21584
22057
|
prefixSuffix: true,
|
|
21585
22058
|
lcsRescue: true
|
|
21586
22059
|
};
|
|
22060
|
+
function replacePatchArgs(output, args, patchText) {
|
|
22061
|
+
const nextArgs = { ...args, patchText };
|
|
22062
|
+
try {
|
|
22063
|
+
output.args = nextArgs;
|
|
22064
|
+
} catch {
|
|
22065
|
+
return false;
|
|
22066
|
+
}
|
|
22067
|
+
return output.args?.patchText === patchText;
|
|
22068
|
+
}
|
|
21587
22069
|
function createApplyPatchHook(ctx) {
|
|
21588
22070
|
function logHookStatus(state, data) {
|
|
21589
22071
|
log(`apply-patch hook ${state}`, data);
|
|
@@ -21603,8 +22085,16 @@ function createApplyPatchHook(ctx) {
|
|
|
21603
22085
|
try {
|
|
21604
22086
|
const result = await rewritePatch(root, patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
|
|
21605
22087
|
if (result.changed) {
|
|
21606
|
-
args
|
|
21607
|
-
|
|
22088
|
+
if (replacePatchArgs(output, args, result.patchText)) {
|
|
22089
|
+
logHookStatus("rewrite");
|
|
22090
|
+
} else {
|
|
22091
|
+
logHookStatus("skipped", {
|
|
22092
|
+
reason: "readonly output args",
|
|
22093
|
+
failOpen: true,
|
|
22094
|
+
rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
|
|
22095
|
+
rewriteStage: "before-native"
|
|
22096
|
+
});
|
|
22097
|
+
}
|
|
21608
22098
|
return;
|
|
21609
22099
|
}
|
|
21610
22100
|
logHookStatus("unchanged");
|
|
@@ -21636,78 +22126,28 @@ function createApplyPatchHook(ctx) {
|
|
|
21636
22126
|
}
|
|
21637
22127
|
};
|
|
21638
22128
|
}
|
|
21639
|
-
// src/utils/compat.ts
|
|
21640
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
21641
|
-
import { writeFile as fsWriteFile } from "node:fs/promises";
|
|
21642
|
-
var isBun = typeof globalThis.Bun !== "undefined";
|
|
21643
|
-
function collectStream(stream) {
|
|
21644
|
-
if (!stream)
|
|
21645
|
-
return () => Promise.resolve("");
|
|
21646
|
-
const chunks = [];
|
|
21647
|
-
stream.on("data", (chunk) => chunks.push(chunk));
|
|
21648
|
-
return () => new Promise((resolve, reject) => {
|
|
21649
|
-
if (!stream.readable) {
|
|
21650
|
-
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
21651
|
-
return;
|
|
21652
|
-
}
|
|
21653
|
-
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
21654
|
-
stream.on("error", reject);
|
|
21655
|
-
});
|
|
21656
|
-
}
|
|
21657
|
-
function crossSpawn(command, options) {
|
|
21658
|
-
const [cmd, ...args] = command;
|
|
21659
|
-
const proc = nodeSpawn(cmd, args, {
|
|
21660
|
-
stdio: [
|
|
21661
|
-
options?.stdin ?? "ignore",
|
|
21662
|
-
options?.stdout ?? "pipe",
|
|
21663
|
-
options?.stderr ?? "pipe"
|
|
21664
|
-
],
|
|
21665
|
-
cwd: options?.cwd,
|
|
21666
|
-
env: options?.env
|
|
21667
|
-
});
|
|
21668
|
-
const stdoutCollector = collectStream(proc.stdout);
|
|
21669
|
-
const stderrCollector = collectStream(proc.stderr);
|
|
21670
|
-
const exited = new Promise((resolve, reject) => {
|
|
21671
|
-
proc.on("error", reject);
|
|
21672
|
-
proc.on("close", (code) => resolve(code ?? 1));
|
|
21673
|
-
});
|
|
21674
|
-
return {
|
|
21675
|
-
proc,
|
|
21676
|
-
stdout: stdoutCollector,
|
|
21677
|
-
stderr: stderrCollector,
|
|
21678
|
-
exited,
|
|
21679
|
-
kill: (signal) => proc.kill(signal),
|
|
21680
|
-
get exitCode() {
|
|
21681
|
-
return proc.exitCode;
|
|
21682
|
-
}
|
|
21683
|
-
};
|
|
21684
|
-
}
|
|
21685
|
-
async function crossWrite(path5, data) {
|
|
21686
|
-
await fsWriteFile(path5, Buffer.from(data));
|
|
21687
|
-
}
|
|
21688
|
-
|
|
21689
22129
|
// src/hooks/auto-update-checker/cache.ts
|
|
21690
22130
|
import * as fs5 from "node:fs";
|
|
21691
|
-
import * as
|
|
22131
|
+
import * as path8 from "node:path";
|
|
21692
22132
|
// src/hooks/auto-update-checker/checker.ts
|
|
21693
22133
|
import * as fs4 from "node:fs";
|
|
21694
|
-
import * as
|
|
21695
|
-
import { fileURLToPath } from "node:url";
|
|
22134
|
+
import * as path7 from "node:path";
|
|
22135
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
21696
22136
|
|
|
21697
22137
|
// src/hooks/auto-update-checker/constants.ts
|
|
21698
|
-
import * as
|
|
21699
|
-
import * as
|
|
22138
|
+
import * as os3 from "node:os";
|
|
22139
|
+
import * as path6 from "node:path";
|
|
21700
22140
|
var PACKAGE_NAME = "oh-my-opencode-slim";
|
|
21701
22141
|
var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
21702
22142
|
var NPM_FETCH_TIMEOUT = 5000;
|
|
21703
22143
|
function getCacheDir() {
|
|
21704
22144
|
if (process.platform === "win32") {
|
|
21705
|
-
return
|
|
22145
|
+
return path6.join(process.env.LOCALAPPDATA ?? os3.homedir(), "opencode");
|
|
21706
22146
|
}
|
|
21707
|
-
return
|
|
22147
|
+
return path6.join(os3.homedir(), ".cache", "opencode");
|
|
21708
22148
|
}
|
|
21709
22149
|
var CACHE_DIR = getCacheDir();
|
|
21710
|
-
var INSTALLED_PACKAGE_JSON =
|
|
22150
|
+
var INSTALLED_PACKAGE_JSON = path6.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
|
|
21711
22151
|
var configPaths = getOpenCodeConfigPaths();
|
|
21712
22152
|
var USER_OPENCODE_CONFIG = configPaths[0];
|
|
21713
22153
|
var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
|
|
@@ -21742,8 +22182,8 @@ function extractChannel(version) {
|
|
|
21742
22182
|
}
|
|
21743
22183
|
function getConfigPaths(directory) {
|
|
21744
22184
|
return [
|
|
21745
|
-
|
|
21746
|
-
|
|
22185
|
+
path7.join(directory, ".opencode", "opencode.json"),
|
|
22186
|
+
path7.join(directory, ".opencode", "opencode.jsonc"),
|
|
21747
22187
|
USER_OPENCODE_CONFIG,
|
|
21748
22188
|
USER_OPENCODE_CONFIG_JSONC
|
|
21749
22189
|
];
|
|
@@ -21759,7 +22199,7 @@ function getLocalDevPath(directory) {
|
|
|
21759
22199
|
for (const entry of plugins) {
|
|
21760
22200
|
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
|
|
21761
22201
|
try {
|
|
21762
|
-
return
|
|
22202
|
+
return fileURLToPath2(entry);
|
|
21763
22203
|
} catch {
|
|
21764
22204
|
return entry.replace("file://", "");
|
|
21765
22205
|
}
|
|
@@ -21772,9 +22212,9 @@ function getLocalDevPath(directory) {
|
|
|
21772
22212
|
function findPackageJsonUp(startPath) {
|
|
21773
22213
|
try {
|
|
21774
22214
|
const stat2 = fs4.statSync(startPath);
|
|
21775
|
-
let dir = stat2.isDirectory() ? startPath :
|
|
22215
|
+
let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
|
|
21776
22216
|
for (let i = 0;i < 10; i++) {
|
|
21777
|
-
const pkgPath =
|
|
22217
|
+
const pkgPath = path7.join(dir, "package.json");
|
|
21778
22218
|
if (fs4.existsSync(pkgPath)) {
|
|
21779
22219
|
try {
|
|
21780
22220
|
const content = fs4.readFileSync(pkgPath, "utf-8");
|
|
@@ -21783,7 +22223,7 @@ function findPackageJsonUp(startPath) {
|
|
|
21783
22223
|
return pkgPath;
|
|
21784
22224
|
} catch {}
|
|
21785
22225
|
}
|
|
21786
|
-
const parent =
|
|
22226
|
+
const parent = path7.dirname(dir);
|
|
21787
22227
|
if (parent === dir)
|
|
21788
22228
|
break;
|
|
21789
22229
|
dir = parent;
|
|
@@ -21808,7 +22248,7 @@ function getLocalDevVersion(directory) {
|
|
|
21808
22248
|
}
|
|
21809
22249
|
function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
|
|
21810
22250
|
try {
|
|
21811
|
-
const currentDir =
|
|
22251
|
+
const currentDir = path7.dirname(fileURLToPath2(currentModuleUrl));
|
|
21812
22252
|
return findPackageJsonUp(currentDir);
|
|
21813
22253
|
} catch (err) {
|
|
21814
22254
|
log("[auto-update-checker] Failed to resolve runtime package path:", err);
|
|
@@ -21892,7 +22332,7 @@ async function getLatestVersion(channel = "latest") {
|
|
|
21892
22332
|
|
|
21893
22333
|
// src/hooks/auto-update-checker/cache.ts
|
|
21894
22334
|
function removeFromBunLock(installDir, packageName) {
|
|
21895
|
-
const lockPath =
|
|
22335
|
+
const lockPath = path8.join(installDir, "bun.lock");
|
|
21896
22336
|
if (!fs5.existsSync(lockPath))
|
|
21897
22337
|
return false;
|
|
21898
22338
|
try {
|
|
@@ -21943,7 +22383,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
|
|
|
21943
22383
|
}
|
|
21944
22384
|
}
|
|
21945
22385
|
function removeInstalledPackage(installDir, packageName) {
|
|
21946
|
-
const pkgDir =
|
|
22386
|
+
const pkgDir = path8.join(installDir, "node_modules", packageName);
|
|
21947
22387
|
if (!fs5.existsSync(pkgDir))
|
|
21948
22388
|
return false;
|
|
21949
22389
|
fs5.rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -21952,18 +22392,18 @@ function removeInstalledPackage(installDir, packageName) {
|
|
|
21952
22392
|
}
|
|
21953
22393
|
function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
|
|
21954
22394
|
if (runtimePackageJsonPath) {
|
|
21955
|
-
const packageDir =
|
|
21956
|
-
const nodeModulesDir =
|
|
21957
|
-
if (
|
|
21958
|
-
const installDir =
|
|
21959
|
-
const packageJsonPath =
|
|
22395
|
+
const packageDir = path8.dirname(runtimePackageJsonPath);
|
|
22396
|
+
const nodeModulesDir = path8.dirname(packageDir);
|
|
22397
|
+
if (path8.basename(packageDir) === PACKAGE_NAME && path8.basename(nodeModulesDir) === "node_modules") {
|
|
22398
|
+
const installDir = path8.dirname(nodeModulesDir);
|
|
22399
|
+
const packageJsonPath = path8.join(installDir, "package.json");
|
|
21960
22400
|
if (fs5.existsSync(packageJsonPath)) {
|
|
21961
22401
|
return { installDir, packageJsonPath };
|
|
21962
22402
|
}
|
|
21963
22403
|
}
|
|
21964
22404
|
return null;
|
|
21965
22405
|
}
|
|
21966
|
-
const legacyPackageJsonPath =
|
|
22406
|
+
const legacyPackageJsonPath = path8.join(CACHE_DIR, "package.json");
|
|
21967
22407
|
if (fs5.existsSync(legacyPackageJsonPath)) {
|
|
21968
22408
|
return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
|
|
21969
22409
|
}
|
|
@@ -22097,7 +22537,7 @@ function showToast(ctx, title, message, variant = "info", duration = 3000) {
|
|
|
22097
22537
|
}).catch(() => {});
|
|
22098
22538
|
}
|
|
22099
22539
|
// src/utils/agent-variant.ts
|
|
22100
|
-
function
|
|
22540
|
+
function normalizeAgentName2(agentName) {
|
|
22101
22541
|
const trimmed = agentName.trim();
|
|
22102
22542
|
return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
22103
22543
|
}
|
|
@@ -22109,7 +22549,7 @@ function getRuntimeAgentNames(config) {
|
|
|
22109
22549
|
return [...unique];
|
|
22110
22550
|
}
|
|
22111
22551
|
function resolveRuntimeAgentName(config, agentName) {
|
|
22112
|
-
const normalized =
|
|
22552
|
+
const normalized = normalizeAgentName2(agentName);
|
|
22113
22553
|
if (!normalized) {
|
|
22114
22554
|
return normalized;
|
|
22115
22555
|
}
|
|
@@ -22121,7 +22561,7 @@ function resolveRuntimeAgentName(config, agentName) {
|
|
|
22121
22561
|
if (!displayName) {
|
|
22122
22562
|
continue;
|
|
22123
22563
|
}
|
|
22124
|
-
if (
|
|
22564
|
+
if (normalizeAgentName2(displayName) === normalized) {
|
|
22125
22565
|
return internalName;
|
|
22126
22566
|
}
|
|
22127
22567
|
}
|
|
@@ -22137,7 +22577,7 @@ function createDisplayNameMentionRewriter(config) {
|
|
|
22137
22577
|
if (!displayName) {
|
|
22138
22578
|
continue;
|
|
22139
22579
|
}
|
|
22140
|
-
const normalizedDisplayName =
|
|
22580
|
+
const normalizedDisplayName = normalizeAgentName2(displayName);
|
|
22141
22581
|
if (!normalizedDisplayName || normalizedDisplayName === internalName) {
|
|
22142
22582
|
continue;
|
|
22143
22583
|
}
|
|
@@ -22447,8 +22887,8 @@ function isPwshAvailable() {
|
|
|
22447
22887
|
});
|
|
22448
22888
|
return result.status === 0;
|
|
22449
22889
|
}
|
|
22450
|
-
function escapePowerShellPath(
|
|
22451
|
-
return
|
|
22890
|
+
function escapePowerShellPath(path9) {
|
|
22891
|
+
return path9.replace(/'/g, "''");
|
|
22452
22892
|
}
|
|
22453
22893
|
function getWindowsZipExtractor() {
|
|
22454
22894
|
const buildNumber = getWindowsBuildNumber();
|
|
@@ -22777,6 +23217,7 @@ function parseModel(model) {
|
|
|
22777
23217
|
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
22778
23218
|
}
|
|
22779
23219
|
var DEDUP_WINDOW_MS = 5000;
|
|
23220
|
+
var REPROMPT_DELAY_MS = 500;
|
|
22780
23221
|
|
|
22781
23222
|
class ForegroundFallbackManager {
|
|
22782
23223
|
client;
|
|
@@ -22909,11 +23350,20 @@ class ForegroundFallbackManager {
|
|
|
22909
23350
|
log("[foreground-fallback] no user message found", { sessionID });
|
|
22910
23351
|
return;
|
|
22911
23352
|
}
|
|
22912
|
-
try {
|
|
22913
|
-
await this.client.session.abort({ path: { id: sessionID } });
|
|
22914
|
-
} catch {}
|
|
22915
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
22916
23353
|
const sessionClient = this.client.session;
|
|
23354
|
+
if (typeof sessionClient.promptAsync !== "function") {
|
|
23355
|
+
log("[foreground-fallback] promptAsync unavailable", { sessionID });
|
|
23356
|
+
return;
|
|
23357
|
+
}
|
|
23358
|
+
try {
|
|
23359
|
+
await abortSessionWithTimeout(this.client, sessionID);
|
|
23360
|
+
} catch (error) {
|
|
23361
|
+
log("[foreground-fallback] abort did not complete cleanly", {
|
|
23362
|
+
sessionID,
|
|
23363
|
+
error: error instanceof Error ? error.message : String(error)
|
|
23364
|
+
});
|
|
23365
|
+
}
|
|
23366
|
+
await new Promise((r) => setTimeout(r, REPROMPT_DELAY_MS));
|
|
22917
23367
|
await sessionClient.promptAsync({
|
|
22918
23368
|
path: { id: sessionID },
|
|
22919
23369
|
body: { parts: lastUser.parts, model: ref }
|
|
@@ -22960,8 +23410,8 @@ class ForegroundFallbackManager {
|
|
|
22960
23410
|
// src/hooks/image-hook.ts
|
|
22961
23411
|
import { createHash } from "node:crypto";
|
|
22962
23412
|
import {
|
|
22963
|
-
existsSync as
|
|
22964
|
-
mkdirSync as
|
|
23413
|
+
existsSync as existsSync5,
|
|
23414
|
+
mkdirSync as mkdirSync3,
|
|
22965
23415
|
readdirSync as readdirSync2,
|
|
22966
23416
|
rmdirSync,
|
|
22967
23417
|
statSync as statSync3,
|
|
@@ -23056,7 +23506,7 @@ function writeUniqueFile(dir, name, data, log2) {
|
|
|
23056
23506
|
const ext = extname(name);
|
|
23057
23507
|
const base = basename2(name, ext) || name;
|
|
23058
23508
|
let candidate = join7(dir, name);
|
|
23059
|
-
if (
|
|
23509
|
+
if (existsSync5(candidate)) {
|
|
23060
23510
|
return candidate;
|
|
23061
23511
|
}
|
|
23062
23512
|
let counter = 0;
|
|
@@ -23094,14 +23544,14 @@ function processImageAttachments(args) {
|
|
|
23094
23544
|
}
|
|
23095
23545
|
const saveDir = join7(workDir, ".opencode", "images");
|
|
23096
23546
|
if (messagesWithImages.length === 0) {
|
|
23097
|
-
if (
|
|
23547
|
+
if (existsSync5(saveDir))
|
|
23098
23548
|
cleanupAllSessions(saveDir);
|
|
23099
23549
|
return;
|
|
23100
23550
|
}
|
|
23101
23551
|
const gitignorePath = join7(workDir, ".opencode", ".gitignore");
|
|
23102
23552
|
try {
|
|
23103
|
-
|
|
23104
|
-
if (!
|
|
23553
|
+
mkdirSync3(saveDir, { recursive: true });
|
|
23554
|
+
if (!existsSync5(gitignorePath))
|
|
23105
23555
|
writeFileSync3(gitignorePath, `*
|
|
23106
23556
|
`);
|
|
23107
23557
|
} catch (e) {
|
|
@@ -23112,7 +23562,7 @@ function processImageAttachments(args) {
|
|
|
23112
23562
|
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
23113
23563
|
const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
|
|
23114
23564
|
try {
|
|
23115
|
-
|
|
23565
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
23116
23566
|
} catch (e) {
|
|
23117
23567
|
log2(`[image-hook] failed to create target image directory: ${e}`);
|
|
23118
23568
|
}
|
|
@@ -23271,7 +23721,7 @@ function createPostFileToolNudgeHook(options = {}) {
|
|
|
23271
23721
|
};
|
|
23272
23722
|
}
|
|
23273
23723
|
// src/hooks/task-session-manager/index.ts
|
|
23274
|
-
import
|
|
23724
|
+
import path9 from "node:path";
|
|
23275
23725
|
var AGENT_NAME_SET = new Set([
|
|
23276
23726
|
"orchestrator",
|
|
23277
23727
|
"oracle",
|
|
@@ -23296,8 +23746,8 @@ function extractPath(output) {
|
|
|
23296
23746
|
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
23297
23747
|
}
|
|
23298
23748
|
function normalizePath(root, file) {
|
|
23299
|
-
const relative =
|
|
23300
|
-
if (!relative || relative.startsWith("..") ||
|
|
23749
|
+
const relative = path9.relative(root, file);
|
|
23750
|
+
if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
|
|
23301
23751
|
return file;
|
|
23302
23752
|
}
|
|
23303
23753
|
return relative;
|
|
@@ -23689,6 +24139,7 @@ function createTodoHygiene(options) {
|
|
|
23689
24139
|
// src/hooks/todo-continuation/index.ts
|
|
23690
24140
|
var HOOK_NAME = "todo-continuation";
|
|
23691
24141
|
var COMMAND_NAME = "auto-continue";
|
|
24142
|
+
var TODO_STATE_TIMEOUT_MS = 500;
|
|
23692
24143
|
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.]";
|
|
23693
24144
|
var TODO_HYGIENE_INSTRUCTION_OPEN = '<instruction name="todo_hygiene">';
|
|
23694
24145
|
var TODO_HYGIENE_INSTRUCTION_CLOSE = "</instruction>";
|
|
@@ -23776,12 +24227,15 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23776
24227
|
notifyingSessionIds: new Set,
|
|
23777
24228
|
notificationBusyUntilBySession: new Map
|
|
23778
24229
|
};
|
|
24230
|
+
async function fetchTodos(sessionID) {
|
|
24231
|
+
const result = await withTimeout(ctx.client.session.todo({
|
|
24232
|
+
path: { id: sessionID }
|
|
24233
|
+
}), TODO_STATE_TIMEOUT_MS, `Todo state lookup timed out after ${TODO_STATE_TIMEOUT_MS}ms`);
|
|
24234
|
+
return result.data;
|
|
24235
|
+
}
|
|
23779
24236
|
const hygiene = createTodoHygiene({
|
|
23780
24237
|
getTodoState: async (sessionID) => {
|
|
23781
|
-
const
|
|
23782
|
-
path: { id: sessionID }
|
|
23783
|
-
});
|
|
23784
|
-
const todos = result.data;
|
|
24238
|
+
const todos = await fetchTodos(sessionID);
|
|
23785
24239
|
const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
|
|
23786
24240
|
return {
|
|
23787
24241
|
hasOpenTodos: openTodos.length > 0,
|
|
@@ -23966,10 +24420,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23966
24420
|
}
|
|
23967
24421
|
if (autoEnable && !state.enabled) {
|
|
23968
24422
|
try {
|
|
23969
|
-
const
|
|
23970
|
-
path: { id: sessionID }
|
|
23971
|
-
});
|
|
23972
|
-
const todos = todosResult.data;
|
|
24423
|
+
const todos = await fetchTodos(sessionID);
|
|
23973
24424
|
const incompleteCount2 = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
|
|
23974
24425
|
if (incompleteCount2 >= autoEnableThreshold) {
|
|
23975
24426
|
state.enabled = true;
|
|
@@ -23995,10 +24446,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23995
24446
|
let hasIncompleteTodos = false;
|
|
23996
24447
|
let incompleteCount = 0;
|
|
23997
24448
|
try {
|
|
23998
|
-
const
|
|
23999
|
-
path: { id: sessionID }
|
|
24000
|
-
});
|
|
24001
|
-
const todos = todosResult.data;
|
|
24449
|
+
const todos = await fetchTodos(sessionID);
|
|
24002
24450
|
incompleteCount = todos.filter((t) => !TERMINAL_TODO_STATUSES.includes(t.status)).length;
|
|
24003
24451
|
hasIncompleteTodos = incompleteCount > 0;
|
|
24004
24452
|
log(`[${HOOK_NAME}] Fetched todos`, {
|
|
@@ -24209,10 +24657,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
24209
24657
|
});
|
|
24210
24658
|
let hasIncompleteTodos = false;
|
|
24211
24659
|
try {
|
|
24212
|
-
const
|
|
24213
|
-
path: { id: input.sessionID }
|
|
24214
|
-
});
|
|
24215
|
-
const todos = todosResult.data;
|
|
24660
|
+
const todos = await fetchTodos(input.sessionID);
|
|
24216
24661
|
hasIncompleteTodos = todos.some((t) => !TERMINAL_TODO_STATUSES.includes(t.status));
|
|
24217
24662
|
} catch (error) {
|
|
24218
24663
|
log(`[${HOOK_NAME}] Warning: failed to fetch todos in command hook`, {
|
|
@@ -24236,7 +24681,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
24236
24681
|
};
|
|
24237
24682
|
}
|
|
24238
24683
|
// src/interview/manager.ts
|
|
24239
|
-
import
|
|
24684
|
+
import path13 from "node:path";
|
|
24240
24685
|
|
|
24241
24686
|
// src/interview/dashboard.ts
|
|
24242
24687
|
import crypto from "node:crypto";
|
|
@@ -24245,28 +24690,28 @@ import fs7 from "node:fs/promises";
|
|
|
24245
24690
|
import {
|
|
24246
24691
|
createServer
|
|
24247
24692
|
} from "node:http";
|
|
24248
|
-
import
|
|
24249
|
-
import
|
|
24693
|
+
import os4 from "node:os";
|
|
24694
|
+
import path11 from "node:path";
|
|
24250
24695
|
import { URL as URL2 } from "node:url";
|
|
24251
24696
|
|
|
24252
24697
|
// src/interview/document.ts
|
|
24253
24698
|
import * as fsSync from "node:fs";
|
|
24254
24699
|
import * as fs6 from "node:fs/promises";
|
|
24255
|
-
import * as
|
|
24700
|
+
import * as path10 from "node:path";
|
|
24256
24701
|
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
24257
24702
|
function normalizeOutputFolder(outputFolder) {
|
|
24258
24703
|
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
24259
24704
|
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
24260
24705
|
}
|
|
24261
24706
|
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
24262
|
-
return
|
|
24707
|
+
return path10.join(directory, normalizeOutputFolder(outputFolder));
|
|
24263
24708
|
}
|
|
24264
24709
|
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
24265
24710
|
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
24266
|
-
return
|
|
24711
|
+
return path10.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
24267
24712
|
}
|
|
24268
24713
|
function relativeInterviewPath(directory, filePath) {
|
|
24269
|
-
return
|
|
24714
|
+
return path10.relative(directory, filePath) || path10.basename(filePath);
|
|
24270
24715
|
}
|
|
24271
24716
|
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
24272
24717
|
const trimmed = value.trim();
|
|
@@ -24275,22 +24720,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
|
24275
24720
|
}
|
|
24276
24721
|
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
24277
24722
|
const candidates = new Set;
|
|
24278
|
-
const resolvedRoot =
|
|
24279
|
-
if (
|
|
24723
|
+
const resolvedRoot = path10.resolve(directory);
|
|
24724
|
+
if (path10.isAbsolute(trimmed)) {
|
|
24280
24725
|
candidates.add(trimmed);
|
|
24281
24726
|
} else {
|
|
24282
|
-
candidates.add(
|
|
24283
|
-
candidates.add(
|
|
24727
|
+
candidates.add(path10.resolve(directory, trimmed));
|
|
24728
|
+
candidates.add(path10.join(outputDir, trimmed));
|
|
24284
24729
|
if (!trimmed.endsWith(".md")) {
|
|
24285
|
-
candidates.add(
|
|
24730
|
+
candidates.add(path10.join(outputDir, `${trimmed}.md`));
|
|
24286
24731
|
}
|
|
24287
24732
|
}
|
|
24288
24733
|
for (const candidate of candidates) {
|
|
24289
|
-
if (
|
|
24734
|
+
if (path10.extname(candidate) !== ".md") {
|
|
24290
24735
|
continue;
|
|
24291
24736
|
}
|
|
24292
|
-
const resolved =
|
|
24293
|
-
if (!resolved.startsWith(resolvedRoot +
|
|
24737
|
+
const resolved = path10.resolve(candidate);
|
|
24738
|
+
if (!resolved.startsWith(resolvedRoot + path10.sep) && resolved !== resolvedRoot) {
|
|
24294
24739
|
continue;
|
|
24295
24740
|
}
|
|
24296
24741
|
if (fsSync.existsSync(candidate)) {
|
|
@@ -24370,7 +24815,7 @@ function parseFrontmatter(content) {
|
|
|
24370
24815
|
return result;
|
|
24371
24816
|
}
|
|
24372
24817
|
async function ensureInterviewFile(record) {
|
|
24373
|
-
await fs6.mkdir(
|
|
24818
|
+
await fs6.mkdir(path10.dirname(record.markdownPath), { recursive: true });
|
|
24374
24819
|
try {
|
|
24375
24820
|
await fs6.access(record.markdownPath);
|
|
24376
24821
|
} catch {
|
|
@@ -26040,12 +26485,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26040
26485
|
|
|
26041
26486
|
// src/interview/dashboard.ts
|
|
26042
26487
|
function getAuthFilePath(port) {
|
|
26043
|
-
const dataHome = process.env.XDG_DATA_HOME ||
|
|
26044
|
-
return
|
|
26488
|
+
const dataHome = process.env.XDG_DATA_HOME || path11.join(os4.homedir(), ".local", "share");
|
|
26489
|
+
return path11.join(dataHome, "opencode", `.dashboard-${port}.json`);
|
|
26045
26490
|
}
|
|
26046
26491
|
function writeAuthFile(port, token) {
|
|
26047
26492
|
const filePath = getAuthFilePath(port);
|
|
26048
|
-
const dir =
|
|
26493
|
+
const dir = path11.dirname(filePath);
|
|
26049
26494
|
try {
|
|
26050
26495
|
fsSync2.mkdirSync(dir, { recursive: true });
|
|
26051
26496
|
} catch {}
|
|
@@ -26142,7 +26587,7 @@ function createDashboardServer(config) {
|
|
|
26142
26587
|
let scanDays = 30;
|
|
26143
26588
|
function getKnownDirectories() {
|
|
26144
26589
|
const dirs = new Set;
|
|
26145
|
-
dirs.add(
|
|
26590
|
+
dirs.add(os4.homedir());
|
|
26146
26591
|
for (const session2 of sessions.values()) {
|
|
26147
26592
|
if (session2.directory)
|
|
26148
26593
|
dirs.add(session2.directory);
|
|
@@ -26182,7 +26627,7 @@ function createDashboardServer(config) {
|
|
|
26182
26627
|
const directories = getKnownDirectories();
|
|
26183
26628
|
const items = [];
|
|
26184
26629
|
for (const dir of directories) {
|
|
26185
|
-
const interviewDir =
|
|
26630
|
+
const interviewDir = path11.join(dir, config.outputFolder);
|
|
26186
26631
|
let entries;
|
|
26187
26632
|
try {
|
|
26188
26633
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -26194,7 +26639,7 @@ function createDashboardServer(config) {
|
|
|
26194
26639
|
continue;
|
|
26195
26640
|
let content;
|
|
26196
26641
|
try {
|
|
26197
|
-
content = await fs7.readFile(
|
|
26642
|
+
content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
|
|
26198
26643
|
} catch {
|
|
26199
26644
|
continue;
|
|
26200
26645
|
}
|
|
@@ -26220,7 +26665,7 @@ function createDashboardServer(config) {
|
|
|
26220
26665
|
const directories = getKnownDirectories();
|
|
26221
26666
|
let rebuilt = 0;
|
|
26222
26667
|
for (const dir of directories) {
|
|
26223
|
-
const interviewDir =
|
|
26668
|
+
const interviewDir = path11.join(dir, config.outputFolder);
|
|
26224
26669
|
let entries;
|
|
26225
26670
|
try {
|
|
26226
26671
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -26232,7 +26677,7 @@ function createDashboardServer(config) {
|
|
|
26232
26677
|
continue;
|
|
26233
26678
|
let content;
|
|
26234
26679
|
try {
|
|
26235
|
-
content = await fs7.readFile(
|
|
26680
|
+
content = await fs7.readFile(path11.join(interviewDir, entry), "utf8");
|
|
26236
26681
|
} catch {
|
|
26237
26682
|
continue;
|
|
26238
26683
|
}
|
|
@@ -26258,7 +26703,7 @@ function createDashboardServer(config) {
|
|
|
26258
26703
|
questions: [],
|
|
26259
26704
|
pendingAnswers: null,
|
|
26260
26705
|
lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
|
|
26261
|
-
filePath:
|
|
26706
|
+
filePath: path11.join(interviewDir, entry),
|
|
26262
26707
|
nudgeAction: null
|
|
26263
26708
|
});
|
|
26264
26709
|
if (!sessions.has(fm.sessionID)) {
|
|
@@ -26522,7 +26967,7 @@ function createDashboardServer(config) {
|
|
|
26522
26967
|
const dirs = getKnownDirectories();
|
|
26523
26968
|
for (const dir of dirs) {
|
|
26524
26969
|
const slug = extractResumeSlug(interviewId);
|
|
26525
|
-
const candidate =
|
|
26970
|
+
const candidate = path11.join(dir, config.outputFolder, `${slug}.md`);
|
|
26526
26971
|
try {
|
|
26527
26972
|
document = await fs7.readFile(candidate, "utf8");
|
|
26528
26973
|
markdownPath = candidate;
|
|
@@ -27048,9 +27493,9 @@ function createInterviewServer(deps) {
|
|
|
27048
27493
|
}
|
|
27049
27494
|
|
|
27050
27495
|
// src/interview/service.ts
|
|
27051
|
-
import { spawn } from "node:child_process";
|
|
27496
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
27052
27497
|
import * as fs8 from "node:fs/promises";
|
|
27053
|
-
import * as
|
|
27498
|
+
import * as path12 from "node:path";
|
|
27054
27499
|
|
|
27055
27500
|
// src/interview/types.ts
|
|
27056
27501
|
import { z as z3 } from "zod";
|
|
@@ -27252,7 +27697,7 @@ function openBrowser(url) {
|
|
|
27252
27697
|
args = [url];
|
|
27253
27698
|
}
|
|
27254
27699
|
try {
|
|
27255
|
-
const child =
|
|
27700
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore" });
|
|
27256
27701
|
child.on("error", (error) => {
|
|
27257
27702
|
log("[interview] failed to open browser:", { error: error.message, url });
|
|
27258
27703
|
});
|
|
@@ -27317,12 +27762,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27317
27762
|
if (!newSlug) {
|
|
27318
27763
|
return;
|
|
27319
27764
|
}
|
|
27320
|
-
const currentFileName =
|
|
27765
|
+
const currentFileName = path12.basename(interview.markdownPath, ".md");
|
|
27321
27766
|
if (currentFileName === newSlug) {
|
|
27322
27767
|
return;
|
|
27323
27768
|
}
|
|
27324
|
-
const dir =
|
|
27325
|
-
const newPath =
|
|
27769
|
+
const dir = path12.dirname(interview.markdownPath);
|
|
27770
|
+
const newPath = path12.join(dir, `${newSlug}.md`);
|
|
27326
27771
|
try {
|
|
27327
27772
|
await fs8.access(newPath);
|
|
27328
27773
|
return;
|
|
@@ -27398,9 +27843,9 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27398
27843
|
const messages = await loadMessages(sessionID);
|
|
27399
27844
|
const title = extractTitle(document);
|
|
27400
27845
|
const record = {
|
|
27401
|
-
id: `${Date.now()}-${++idCounter}-${slugify(
|
|
27846
|
+
id: `${Date.now()}-${++idCounter}-${slugify(path12.basename(markdownPath, ".md")) || "interview"}`,
|
|
27402
27847
|
sessionID,
|
|
27403
|
-
idea: title ||
|
|
27848
|
+
idea: title || path12.basename(markdownPath, ".md"),
|
|
27404
27849
|
markdownPath,
|
|
27405
27850
|
createdAt: nowIso(),
|
|
27406
27851
|
status: "active",
|
|
@@ -27627,7 +28072,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27627
28072
|
return fileCache.items;
|
|
27628
28073
|
}
|
|
27629
28074
|
const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
|
|
27630
|
-
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) =>
|
|
28075
|
+
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path12.resolve(i.markdownPath)));
|
|
27631
28076
|
let entries;
|
|
27632
28077
|
try {
|
|
27633
28078
|
entries = await fs8.readdir(outputDir);
|
|
@@ -27638,8 +28083,8 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27638
28083
|
for (const entry of entries) {
|
|
27639
28084
|
if (!entry.endsWith(".md"))
|
|
27640
28085
|
continue;
|
|
27641
|
-
const fullPath =
|
|
27642
|
-
if (activePaths.has(
|
|
28086
|
+
const fullPath = path12.join(outputDir, entry);
|
|
28087
|
+
if (activePaths.has(path12.resolve(fullPath)))
|
|
27643
28088
|
continue;
|
|
27644
28089
|
let content;
|
|
27645
28090
|
try {
|
|
@@ -27738,7 +28183,7 @@ function createInterviewManager(ctx, config) {
|
|
|
27738
28183
|
const outputFolder = interviewConfig?.outputFolder ?? "interview";
|
|
27739
28184
|
if (!dashboardEnabled) {
|
|
27740
28185
|
const service2 = createInterviewService(ctx, interviewConfig);
|
|
27741
|
-
const resolvedOutputPath =
|
|
28186
|
+
const resolvedOutputPath = path13.join(ctx.directory, outputFolder);
|
|
27742
28187
|
const server = createInterviewServer({
|
|
27743
28188
|
getState: async (interviewId) => service2.getInterviewState(interviewId),
|
|
27744
28189
|
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
@@ -27843,7 +28288,7 @@ function createInterviewManager(ctx, config) {
|
|
|
27843
28288
|
listInterviews: () => service.listInterviews(),
|
|
27844
28289
|
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
27845
28290
|
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
27846
|
-
outputFolder:
|
|
28291
|
+
outputFolder: path13.join(ctx.directory, outputFolder),
|
|
27847
28292
|
port: 0
|
|
27848
28293
|
});
|
|
27849
28294
|
service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
|
|
@@ -28111,6 +28556,8 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
|
|
|
28111
28556
|
}
|
|
28112
28557
|
|
|
28113
28558
|
// src/multiplexer/tmux/index.ts
|
|
28559
|
+
var TMUX_LAYOUT_DEBOUNCE_MS = 150;
|
|
28560
|
+
|
|
28114
28561
|
class TmuxMultiplexer {
|
|
28115
28562
|
type = "tmux";
|
|
28116
28563
|
binaryPath = null;
|
|
@@ -28118,6 +28565,8 @@ class TmuxMultiplexer {
|
|
|
28118
28565
|
storedLayout;
|
|
28119
28566
|
storedMainPaneSize;
|
|
28120
28567
|
targetPane = process.env.TMUX_PANE;
|
|
28568
|
+
layoutTimer;
|
|
28569
|
+
layoutGeneration = 0;
|
|
28121
28570
|
constructor(layout = "main-vertical", mainPaneSize = 60) {
|
|
28122
28571
|
this.storedLayout = layout;
|
|
28123
28572
|
this.storedMainPaneSize = mainPaneSize;
|
|
@@ -28179,7 +28628,7 @@ class TmuxMultiplexer {
|
|
|
28179
28628
|
if (exitCode === 0 && paneId) {
|
|
28180
28629
|
const renameProc = crossSpawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
|
|
28181
28630
|
await renameProc.exited;
|
|
28182
|
-
|
|
28631
|
+
this.scheduleLayout();
|
|
28183
28632
|
log("[tmux] spawnPane: SUCCESS", { paneId });
|
|
28184
28633
|
return { success: true, paneId };
|
|
28185
28634
|
}
|
|
@@ -28216,7 +28665,7 @@ class TmuxMultiplexer {
|
|
|
28216
28665
|
const stderr = await proc.stderr();
|
|
28217
28666
|
log("[tmux] closePane: result", { exitCode, stderr: stderr.trim() });
|
|
28218
28667
|
if (exitCode === 0) {
|
|
28219
|
-
|
|
28668
|
+
this.scheduleLayout();
|
|
28220
28669
|
return true;
|
|
28221
28670
|
}
|
|
28222
28671
|
log("[tmux] closePane: failed (pane may already be closed)", { paneId });
|
|
@@ -28227,41 +28676,82 @@ class TmuxMultiplexer {
|
|
|
28227
28676
|
}
|
|
28228
28677
|
}
|
|
28229
28678
|
async applyLayout(layout, mainPaneSize) {
|
|
28679
|
+
if (this.layoutTimer) {
|
|
28680
|
+
clearTimeout(this.layoutTimer);
|
|
28681
|
+
this.layoutTimer = undefined;
|
|
28682
|
+
}
|
|
28683
|
+
this.layoutGeneration++;
|
|
28684
|
+
await this.applyLayoutNow(layout, mainPaneSize);
|
|
28685
|
+
}
|
|
28686
|
+
scheduleLayout() {
|
|
28687
|
+
if (this.layoutTimer)
|
|
28688
|
+
clearTimeout(this.layoutTimer);
|
|
28689
|
+
const gen = ++this.layoutGeneration;
|
|
28690
|
+
this.layoutTimer = setTimeout(() => {
|
|
28691
|
+
this.layoutTimer = undefined;
|
|
28692
|
+
if (this.layoutGeneration === gen) {
|
|
28693
|
+
this.applyLayoutNow(this.storedLayout, this.storedMainPaneSize);
|
|
28694
|
+
}
|
|
28695
|
+
}, TMUX_LAYOUT_DEBOUNCE_MS);
|
|
28696
|
+
this.layoutTimer.unref?.();
|
|
28697
|
+
}
|
|
28698
|
+
async applyLayoutNow(layout, mainPaneSize) {
|
|
28230
28699
|
const tmux = await this.getBinary();
|
|
28231
28700
|
if (!tmux)
|
|
28232
28701
|
return;
|
|
28233
28702
|
this.storedLayout = layout;
|
|
28234
28703
|
this.storedMainPaneSize = mainPaneSize;
|
|
28235
28704
|
try {
|
|
28236
|
-
const
|
|
28237
|
-
|
|
28238
|
-
|
|
28239
|
-
|
|
28240
|
-
|
|
28705
|
+
const layoutResult = await this.runTmux(tmux, [
|
|
28706
|
+
"select-layout",
|
|
28707
|
+
...this.targetArgs(),
|
|
28708
|
+
layout
|
|
28709
|
+
]);
|
|
28710
|
+
if (layoutResult !== 0)
|
|
28711
|
+
return;
|
|
28241
28712
|
if (layout === "main-horizontal" || layout === "main-vertical") {
|
|
28242
28713
|
const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
|
|
28243
|
-
const
|
|
28244
|
-
tmux,
|
|
28714
|
+
const sizeResult = await this.runTmux(tmux, [
|
|
28245
28715
|
"set-window-option",
|
|
28246
28716
|
...this.targetArgs(),
|
|
28247
28717
|
sizeOption,
|
|
28248
28718
|
`${mainPaneSize}%`
|
|
28249
|
-
]
|
|
28250
|
-
|
|
28251
|
-
|
|
28252
|
-
|
|
28253
|
-
|
|
28254
|
-
|
|
28255
|
-
|
|
28256
|
-
|
|
28257
|
-
|
|
28258
|
-
|
|
28719
|
+
]);
|
|
28720
|
+
if (sizeResult !== 0)
|
|
28721
|
+
return;
|
|
28722
|
+
const reapplyResult = await this.runTmux(tmux, [
|
|
28723
|
+
"select-layout",
|
|
28724
|
+
...this.targetArgs(),
|
|
28725
|
+
layout
|
|
28726
|
+
]);
|
|
28727
|
+
if (reapplyResult !== 0)
|
|
28728
|
+
return;
|
|
28259
28729
|
}
|
|
28260
28730
|
log("[tmux] applyLayout: applied", { layout, mainPaneSize });
|
|
28261
28731
|
} catch (err) {
|
|
28262
28732
|
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
28263
28733
|
}
|
|
28264
28734
|
}
|
|
28735
|
+
async runTmux(tmux, args) {
|
|
28736
|
+
const proc = crossSpawn([tmux, ...args], {
|
|
28737
|
+
stdout: "pipe",
|
|
28738
|
+
stderr: "pipe"
|
|
28739
|
+
});
|
|
28740
|
+
const [exitCode, , stderr] = await Promise.all([
|
|
28741
|
+
proc.exited,
|
|
28742
|
+
proc.stdout(),
|
|
28743
|
+
proc.stderr()
|
|
28744
|
+
]);
|
|
28745
|
+
if (exitCode !== 0) {
|
|
28746
|
+
log("[tmux] command failed", {
|
|
28747
|
+
command: args[0],
|
|
28748
|
+
args: [tmux, ...args],
|
|
28749
|
+
exitCode,
|
|
28750
|
+
stderr: stderr.trim()
|
|
28751
|
+
});
|
|
28752
|
+
}
|
|
28753
|
+
return exitCode;
|
|
28754
|
+
}
|
|
28265
28755
|
async getBinary() {
|
|
28266
28756
|
await this.isAvailable();
|
|
28267
28757
|
return this.binaryPath;
|
|
@@ -28283,23 +28773,23 @@ class TmuxMultiplexer {
|
|
|
28283
28773
|
return null;
|
|
28284
28774
|
}
|
|
28285
28775
|
const stdout = await proc.stdout();
|
|
28286
|
-
const
|
|
28776
|
+
const path14 = stdout.trim().split(`
|
|
28287
28777
|
`)[0];
|
|
28288
|
-
if (!
|
|
28778
|
+
if (!path14) {
|
|
28289
28779
|
log("[tmux] findBinary: no path in output");
|
|
28290
28780
|
return null;
|
|
28291
28781
|
}
|
|
28292
|
-
const verifyProc = crossSpawn([
|
|
28782
|
+
const verifyProc = crossSpawn([path14, "-V"], {
|
|
28293
28783
|
stdout: "pipe",
|
|
28294
28784
|
stderr: "pipe"
|
|
28295
28785
|
});
|
|
28296
28786
|
const verifyExit = await verifyProc.exited;
|
|
28297
28787
|
if (verifyExit !== 0) {
|
|
28298
|
-
log("[tmux] findBinary: tmux -V failed", { path:
|
|
28788
|
+
log("[tmux] findBinary: tmux -V failed", { path: path14, verifyExit });
|
|
28299
28789
|
return null;
|
|
28300
28790
|
}
|
|
28301
|
-
log("[tmux] findBinary: found", { path:
|
|
28302
|
-
return
|
|
28791
|
+
log("[tmux] findBinary: found", { path: path14 });
|
|
28792
|
+
return path14;
|
|
28303
28793
|
} catch (err) {
|
|
28304
28794
|
log("[tmux] findBinary: exception", { error: String(err) });
|
|
28305
28795
|
return null;
|
|
@@ -29010,17 +29500,17 @@ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
|
|
|
29010
29500
|
import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
29011
29501
|
|
|
29012
29502
|
// src/tools/ast-grep/cli.ts
|
|
29013
|
-
import { existsSync as
|
|
29503
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
29014
29504
|
|
|
29015
29505
|
// src/tools/ast-grep/constants.ts
|
|
29016
|
-
import { existsSync as
|
|
29506
|
+
import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
|
|
29017
29507
|
import { createRequire as createRequire3 } from "node:module";
|
|
29018
29508
|
import { dirname as dirname6, join as join11 } from "node:path";
|
|
29019
29509
|
|
|
29020
29510
|
// src/tools/ast-grep/downloader.ts
|
|
29021
|
-
import { chmodSync, existsSync as
|
|
29511
|
+
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
|
|
29022
29512
|
import { createRequire as createRequire2 } from "node:module";
|
|
29023
|
-
import { homedir as
|
|
29513
|
+
import { homedir as homedir5 } from "node:os";
|
|
29024
29514
|
import { join as join10 } from "node:path";
|
|
29025
29515
|
var REPO = "ast-grep/ast-grep";
|
|
29026
29516
|
var DEFAULT_VERSION = "0.40.0";
|
|
@@ -29045,11 +29535,11 @@ var PLATFORM_MAP = {
|
|
|
29045
29535
|
function getCacheDir2() {
|
|
29046
29536
|
if (process.platform === "win32") {
|
|
29047
29537
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
29048
|
-
const base2 = localAppData || join10(
|
|
29538
|
+
const base2 = localAppData || join10(homedir5(), "AppData", "Local");
|
|
29049
29539
|
return join10(base2, "oh-my-opencode-slim", "bin");
|
|
29050
29540
|
}
|
|
29051
29541
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
29052
|
-
const base = xdgCache || join10(
|
|
29542
|
+
const base = xdgCache || join10(homedir5(), ".cache");
|
|
29053
29543
|
return join10(base, "oh-my-opencode-slim", "bin");
|
|
29054
29544
|
}
|
|
29055
29545
|
function getBinaryName() {
|
|
@@ -29057,7 +29547,7 @@ function getBinaryName() {
|
|
|
29057
29547
|
}
|
|
29058
29548
|
function getCachedBinaryPath() {
|
|
29059
29549
|
const binaryPath = join10(getCacheDir2(), getBinaryName());
|
|
29060
|
-
return
|
|
29550
|
+
return existsSync7(binaryPath) ? binaryPath : null;
|
|
29061
29551
|
}
|
|
29062
29552
|
async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
29063
29553
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
@@ -29069,16 +29559,16 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
29069
29559
|
const cacheDir = getCacheDir2();
|
|
29070
29560
|
const binaryName = getBinaryName();
|
|
29071
29561
|
const binaryPath = join10(cacheDir, binaryName);
|
|
29072
|
-
if (
|
|
29562
|
+
if (existsSync7(binaryPath)) {
|
|
29073
29563
|
return binaryPath;
|
|
29074
29564
|
}
|
|
29075
|
-
const { arch, os:
|
|
29076
|
-
const assetName = `app-${arch}-${
|
|
29565
|
+
const { arch, os: os5 } = platformInfo;
|
|
29566
|
+
const assetName = `app-${arch}-${os5}.zip`;
|
|
29077
29567
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
29078
29568
|
console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
|
|
29079
29569
|
try {
|
|
29080
|
-
if (!
|
|
29081
|
-
|
|
29570
|
+
if (!existsSync7(cacheDir)) {
|
|
29571
|
+
mkdirSync5(cacheDir, { recursive: true });
|
|
29082
29572
|
}
|
|
29083
29573
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
29084
29574
|
if (!response.ok) {
|
|
@@ -29088,10 +29578,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
29088
29578
|
const arrayBuffer = await response.arrayBuffer();
|
|
29089
29579
|
await crossWrite(archivePath, arrayBuffer);
|
|
29090
29580
|
await extractZip(archivePath, cacheDir);
|
|
29091
|
-
if (
|
|
29581
|
+
if (existsSync7(archivePath)) {
|
|
29092
29582
|
unlinkSync4(archivePath);
|
|
29093
29583
|
}
|
|
29094
|
-
if (process.platform !== "win32" &&
|
|
29584
|
+
if (process.platform !== "win32" && existsSync7(binaryPath)) {
|
|
29095
29585
|
chmodSync(binaryPath, 493);
|
|
29096
29586
|
}
|
|
29097
29587
|
console.log(`[oh-my-opencode-slim] ast-grep binary ready.`);
|
|
@@ -29174,7 +29664,7 @@ function findSgCliPathSync() {
|
|
|
29174
29664
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
29175
29665
|
const cliDir = dirname6(cliPkgPath);
|
|
29176
29666
|
const sgPath = join11(cliDir, binaryName);
|
|
29177
|
-
if (
|
|
29667
|
+
if (existsSync8(sgPath) && isValidBinary(sgPath)) {
|
|
29178
29668
|
return sgPath;
|
|
29179
29669
|
}
|
|
29180
29670
|
} catch {}
|
|
@@ -29186,16 +29676,16 @@ function findSgCliPathSync() {
|
|
|
29186
29676
|
const pkgDir = dirname6(pkgPath);
|
|
29187
29677
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
29188
29678
|
const binaryPath = join11(pkgDir, astGrepName);
|
|
29189
|
-
if (
|
|
29679
|
+
if (existsSync8(binaryPath) && isValidBinary(binaryPath)) {
|
|
29190
29680
|
return binaryPath;
|
|
29191
29681
|
}
|
|
29192
29682
|
} catch {}
|
|
29193
29683
|
}
|
|
29194
29684
|
if (process.platform === "darwin") {
|
|
29195
29685
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
29196
|
-
for (const
|
|
29197
|
-
if (
|
|
29198
|
-
return
|
|
29686
|
+
for (const path14 of homebrewPaths) {
|
|
29687
|
+
if (existsSync8(path14) && isValidBinary(path14)) {
|
|
29688
|
+
return path14;
|
|
29199
29689
|
}
|
|
29200
29690
|
}
|
|
29201
29691
|
}
|
|
@@ -29212,8 +29702,8 @@ function getSgCliPath() {
|
|
|
29212
29702
|
}
|
|
29213
29703
|
return "sg";
|
|
29214
29704
|
}
|
|
29215
|
-
function setSgCliPath(
|
|
29216
|
-
resolvedCliPath =
|
|
29705
|
+
function setSgCliPath(path14) {
|
|
29706
|
+
resolvedCliPath = path14;
|
|
29217
29707
|
}
|
|
29218
29708
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
29219
29709
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -29223,7 +29713,7 @@ var DEFAULT_MAX_MATCHES = 500;
|
|
|
29223
29713
|
var initPromise = null;
|
|
29224
29714
|
async function getAstGrepPath() {
|
|
29225
29715
|
const currentPath = getSgCliPath();
|
|
29226
|
-
if (currentPath !== "sg" &&
|
|
29716
|
+
if (currentPath !== "sg" && existsSync9(currentPath)) {
|
|
29227
29717
|
return currentPath;
|
|
29228
29718
|
}
|
|
29229
29719
|
if (initPromise) {
|
|
@@ -29231,7 +29721,7 @@ async function getAstGrepPath() {
|
|
|
29231
29721
|
}
|
|
29232
29722
|
initPromise = (async () => {
|
|
29233
29723
|
const syncPath = findSgCliPathSync();
|
|
29234
|
-
if (syncPath &&
|
|
29724
|
+
if (syncPath && existsSync9(syncPath)) {
|
|
29235
29725
|
setSgCliPath(syncPath);
|
|
29236
29726
|
return syncPath;
|
|
29237
29727
|
}
|
|
@@ -29270,7 +29760,7 @@ async function runSg(options) {
|
|
|
29270
29760
|
const paths2 = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
29271
29761
|
args.push(...paths2);
|
|
29272
29762
|
let cliPath = getSgCliPath();
|
|
29273
|
-
if (!
|
|
29763
|
+
if (!existsSync9(cliPath) && cliPath !== "sg") {
|
|
29274
29764
|
const downloadedPath = await getAstGrepPath();
|
|
29275
29765
|
if (downloadedPath) {
|
|
29276
29766
|
cliPath = downloadedPath;
|
|
@@ -29620,15 +30110,15 @@ Returns the councillor responses with a summary footer.`,
|
|
|
29620
30110
|
}
|
|
29621
30111
|
// src/tui-state.ts
|
|
29622
30112
|
import * as fs9 from "node:fs";
|
|
29623
|
-
import * as
|
|
29624
|
-
import * as
|
|
30113
|
+
import * as os5 from "node:os";
|
|
30114
|
+
import * as path14 from "node:path";
|
|
29625
30115
|
var STATE_DIR = "oh-my-opencode-slim";
|
|
29626
30116
|
var STATE_FILE = "tui-state.json";
|
|
29627
30117
|
function dataDir() {
|
|
29628
|
-
return process.env.XDG_DATA_HOME ??
|
|
30118
|
+
return process.env.XDG_DATA_HOME ?? path14.join(os5.homedir(), ".local", "share");
|
|
29629
30119
|
}
|
|
29630
30120
|
function getTuiStatePath() {
|
|
29631
|
-
return
|
|
30121
|
+
return path14.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
|
|
29632
30122
|
}
|
|
29633
30123
|
function emptySnapshot() {
|
|
29634
30124
|
return {
|
|
@@ -29664,7 +30154,7 @@ async function readTuiSnapshotAsync() {
|
|
|
29664
30154
|
function writeTuiSnapshot(snapshot) {
|
|
29665
30155
|
try {
|
|
29666
30156
|
const filePath = getTuiStatePath();
|
|
29667
|
-
fs9.mkdirSync(
|
|
30157
|
+
fs9.mkdirSync(path14.dirname(filePath), { recursive: true });
|
|
29668
30158
|
fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
|
|
29669
30159
|
`);
|
|
29670
30160
|
} catch {}
|
|
@@ -29880,15 +30370,15 @@ var BINARY_PREFIXES = [
|
|
|
29880
30370
|
];
|
|
29881
30371
|
var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
|
|
29882
30372
|
// src/tools/smartfetch/tool.ts
|
|
29883
|
-
import
|
|
29884
|
-
import
|
|
30373
|
+
import os6 from "node:os";
|
|
30374
|
+
import path18 from "node:path";
|
|
29885
30375
|
import {
|
|
29886
30376
|
tool as tool4
|
|
29887
30377
|
} from "@opencode-ai/plugin";
|
|
29888
30378
|
|
|
29889
30379
|
// src/tools/smartfetch/binary.ts
|
|
29890
30380
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
29891
|
-
import
|
|
30381
|
+
import path15 from "node:path";
|
|
29892
30382
|
function extensionForMime(contentType) {
|
|
29893
30383
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
29894
30384
|
const map = {
|
|
@@ -29909,10 +30399,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
29909
30399
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
29910
30400
|
await mkdir2(binaryDir, { recursive: true });
|
|
29911
30401
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
29912
|
-
const parsed =
|
|
30402
|
+
const parsed = path15.parse(initialName);
|
|
29913
30403
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
29914
30404
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
29915
|
-
const file =
|
|
30405
|
+
const file = path15.join(binaryDir, candidateName);
|
|
29916
30406
|
try {
|
|
29917
30407
|
await writeFile2(file, data, { flag: "wx" });
|
|
29918
30408
|
return file;
|
|
@@ -30566,7 +31056,7 @@ var L = class u2 {
|
|
|
30566
31056
|
};
|
|
30567
31057
|
|
|
30568
31058
|
// src/tools/smartfetch/network.ts
|
|
30569
|
-
import
|
|
31059
|
+
import path16 from "node:path";
|
|
30570
31060
|
|
|
30571
31061
|
// src/tools/smartfetch/utils.ts
|
|
30572
31062
|
var import_readability = __toESM(require_readability(), 1);
|
|
@@ -31291,7 +31781,7 @@ function inferFilenameFromUrl(url) {
|
|
|
31291
31781
|
function truncateFilename(name, maxLength = 180) {
|
|
31292
31782
|
if (name.length <= maxLength)
|
|
31293
31783
|
return name;
|
|
31294
|
-
const parsed =
|
|
31784
|
+
const parsed = path16.parse(name);
|
|
31295
31785
|
const ext = parsed.ext || "";
|
|
31296
31786
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
31297
31787
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -31461,9 +31951,9 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
31461
31951
|
}
|
|
31462
31952
|
|
|
31463
31953
|
// src/tools/smartfetch/secondary-model.ts
|
|
31464
|
-
import { existsSync as
|
|
31954
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
31465
31955
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
31466
|
-
import
|
|
31956
|
+
import path17 from "node:path";
|
|
31467
31957
|
function parseModelRef(value) {
|
|
31468
31958
|
if (!value)
|
|
31469
31959
|
return;
|
|
@@ -31489,8 +31979,8 @@ function pickAgentModelRef(value) {
|
|
|
31489
31979
|
}
|
|
31490
31980
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
31491
31981
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
31492
|
-
const fullPath =
|
|
31493
|
-
if (
|
|
31982
|
+
const fullPath = path17.join(baseDir, file);
|
|
31983
|
+
if (existsSync10(fullPath))
|
|
31494
31984
|
return fullPath;
|
|
31495
31985
|
}
|
|
31496
31986
|
return;
|
|
@@ -31506,7 +31996,7 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
31506
31996
|
}
|
|
31507
31997
|
}
|
|
31508
31998
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
31509
|
-
const projectDir =
|
|
31999
|
+
const projectDir = path17.join(directory, ".opencode");
|
|
31510
32000
|
const userDirs = getConfigSearchDirs();
|
|
31511
32001
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
31512
32002
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -31667,7 +32157,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
31667
32157
|
// src/tools/smartfetch/tool.ts
|
|
31668
32158
|
var z5 = tool4.schema;
|
|
31669
32159
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
31670
|
-
const binaryDir = options.binaryDir ||
|
|
32160
|
+
const binaryDir = options.binaryDir || path18.join(os6.tmpdir(), "opencode-smartfetch");
|
|
31671
32161
|
return tool4({
|
|
31672
32162
|
description: WEBFETCH_DESCRIPTION,
|
|
31673
32163
|
args: {
|
|
@@ -32148,6 +32638,460 @@ function createWebfetchTool(pluginCtx, options = {}) {
|
|
|
32148
32638
|
}
|
|
32149
32639
|
});
|
|
32150
32640
|
}
|
|
32641
|
+
// src/tools/subtask/command.ts
|
|
32642
|
+
var COMMAND_NAME4 = "subtask";
|
|
32643
|
+
var SUBTASK_COMMAND_TEMPLATE = `Start a focused subtask worker.
|
|
32644
|
+
|
|
32645
|
+
The user's request below is the full scope for the worker. Do not broaden it.
|
|
32646
|
+
Create a self-contained worker prompt that includes:
|
|
32647
|
+
- the exact objective
|
|
32648
|
+
- relevant context from this conversation
|
|
32649
|
+
- specific files/paths that matter
|
|
32650
|
+
- expected deliverables
|
|
32651
|
+
- validation the worker should run, if applicable
|
|
32652
|
+
|
|
32653
|
+
USER REQUEST:
|
|
32654
|
+
$ARGUMENTS
|
|
32655
|
+
|
|
32656
|
+
Then call the subtask tool:
|
|
32657
|
+
\`subtask(prompt="...", files=["src/foo.ts", "docs/bar.md"])\`
|
|
32658
|
+
|
|
32659
|
+
Only include files that are clearly relevant. If no files are needed, omit files.`;
|
|
32660
|
+
function createSubtaskCommandManager(_ctx, state) {
|
|
32661
|
+
function registerCommand(opencodeConfig) {
|
|
32662
|
+
const configCommand = opencodeConfig.command;
|
|
32663
|
+
if (!configCommand?.[COMMAND_NAME4]) {
|
|
32664
|
+
if (!opencodeConfig.command) {
|
|
32665
|
+
opencodeConfig.command = {};
|
|
32666
|
+
}
|
|
32667
|
+
opencodeConfig.command[COMMAND_NAME4] = {
|
|
32668
|
+
description: "Create a focused subtask prompt for a new session",
|
|
32669
|
+
template: SUBTASK_COMMAND_TEMPLATE
|
|
32670
|
+
};
|
|
32671
|
+
}
|
|
32672
|
+
}
|
|
32673
|
+
return {
|
|
32674
|
+
registerCommand,
|
|
32675
|
+
handleEvent(input) {
|
|
32676
|
+
if (input.event.type === "session.created") {
|
|
32677
|
+
const info = input.event.properties?.info;
|
|
32678
|
+
if (!info?.id || !info.parentID)
|
|
32679
|
+
return;
|
|
32680
|
+
const source = state.sourceFor(info.parentID);
|
|
32681
|
+
if (source)
|
|
32682
|
+
state.markSession(info.id, source);
|
|
32683
|
+
return;
|
|
32684
|
+
}
|
|
32685
|
+
if (input.event.type !== "session.deleted")
|
|
32686
|
+
return;
|
|
32687
|
+
const sessionID = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
32688
|
+
if (sessionID)
|
|
32689
|
+
state.unmarkSession(sessionID);
|
|
32690
|
+
}
|
|
32691
|
+
};
|
|
32692
|
+
}
|
|
32693
|
+
// src/tools/subtask/files.ts
|
|
32694
|
+
import * as fs11 from "node:fs/promises";
|
|
32695
|
+
import * as path20 from "node:path";
|
|
32696
|
+
|
|
32697
|
+
// src/tools/subtask/vendor.ts
|
|
32698
|
+
import * as fs10 from "node:fs/promises";
|
|
32699
|
+
import * as path19 from "node:path";
|
|
32700
|
+
var DEFAULT_READ_LIMIT = 2000;
|
|
32701
|
+
var MAX_LINE_LENGTH = 2000;
|
|
32702
|
+
var MAX_BYTES = 50 * 1024;
|
|
32703
|
+
var MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
|
|
32704
|
+
var MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`;
|
|
32705
|
+
var SAMPLE_BYTES = 4096;
|
|
32706
|
+
var BINARY_EXTENSIONS = new Set([
|
|
32707
|
+
".zip",
|
|
32708
|
+
".tar",
|
|
32709
|
+
".gz",
|
|
32710
|
+
".exe",
|
|
32711
|
+
".dll",
|
|
32712
|
+
".so",
|
|
32713
|
+
".class",
|
|
32714
|
+
".jar",
|
|
32715
|
+
".war",
|
|
32716
|
+
".7z",
|
|
32717
|
+
".doc",
|
|
32718
|
+
".docx",
|
|
32719
|
+
".xls",
|
|
32720
|
+
".xlsx",
|
|
32721
|
+
".ppt",
|
|
32722
|
+
".pptx",
|
|
32723
|
+
".odt",
|
|
32724
|
+
".ods",
|
|
32725
|
+
".odp",
|
|
32726
|
+
".bin",
|
|
32727
|
+
".dat",
|
|
32728
|
+
".obj",
|
|
32729
|
+
".o",
|
|
32730
|
+
".a",
|
|
32731
|
+
".lib",
|
|
32732
|
+
".wasm",
|
|
32733
|
+
".pyc",
|
|
32734
|
+
".pyo"
|
|
32735
|
+
]);
|
|
32736
|
+
async function isBinaryFile(filepath) {
|
|
32737
|
+
const ext = path19.extname(filepath).toLowerCase();
|
|
32738
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
32739
|
+
return true;
|
|
32740
|
+
}
|
|
32741
|
+
try {
|
|
32742
|
+
const file = await fs10.open(filepath, "r");
|
|
32743
|
+
try {
|
|
32744
|
+
const buffer = Buffer.alloc(SAMPLE_BYTES);
|
|
32745
|
+
const result = await file.read(buffer, 0, SAMPLE_BYTES, 0);
|
|
32746
|
+
if (result.bytesRead === 0)
|
|
32747
|
+
return false;
|
|
32748
|
+
const bytes = buffer.subarray(0, result.bytesRead);
|
|
32749
|
+
let nonPrintableCount = 0;
|
|
32750
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
32751
|
+
const byte = bytes[i];
|
|
32752
|
+
if (byte === undefined)
|
|
32753
|
+
continue;
|
|
32754
|
+
if (byte === 0)
|
|
32755
|
+
return true;
|
|
32756
|
+
if (byte < 9 || byte > 13 && byte < 32) {
|
|
32757
|
+
nonPrintableCount++;
|
|
32758
|
+
}
|
|
32759
|
+
}
|
|
32760
|
+
return nonPrintableCount / bytes.length > 0.3;
|
|
32761
|
+
} finally {
|
|
32762
|
+
await file.close();
|
|
32763
|
+
}
|
|
32764
|
+
} catch {
|
|
32765
|
+
return false;
|
|
32766
|
+
}
|
|
32767
|
+
}
|
|
32768
|
+
function formatFileContent(_filepath, content) {
|
|
32769
|
+
const cappedContent = Buffer.byteLength(content, "utf8") > MAX_BYTES;
|
|
32770
|
+
const contentToFormat = cappedContent ? content.slice(0, MAX_BYTES) : content;
|
|
32771
|
+
const lines = contentToFormat.split(`
|
|
32772
|
+
`);
|
|
32773
|
+
const limit = DEFAULT_READ_LIMIT;
|
|
32774
|
+
const offset = 0;
|
|
32775
|
+
const raw = lines.slice(offset, offset + limit).map((line) => {
|
|
32776
|
+
return line.length > MAX_LINE_LENGTH ? `${line.substring(0, MAX_LINE_LENGTH)}${MAX_LINE_SUFFIX}` : line;
|
|
32777
|
+
});
|
|
32778
|
+
const formatted = raw.map((line, index) => {
|
|
32779
|
+
return `${index + offset + 1}: ${line}`;
|
|
32780
|
+
});
|
|
32781
|
+
let output = [
|
|
32782
|
+
`<path>${_filepath}</path>`,
|
|
32783
|
+
"<type>file</type>",
|
|
32784
|
+
`<content>
|
|
32785
|
+
`
|
|
32786
|
+
].join(`
|
|
32787
|
+
`);
|
|
32788
|
+
output += formatted.join(`
|
|
32789
|
+
`);
|
|
32790
|
+
const totalLines = lines.length;
|
|
32791
|
+
const lastReadLine = offset + formatted.length;
|
|
32792
|
+
const hasMoreLines = totalLines > lastReadLine;
|
|
32793
|
+
if (cappedContent) {
|
|
32794
|
+
output += `
|
|
32795
|
+
|
|
32796
|
+
(Output capped at ${MAX_BYTES_LABEL}. Showing lines 1-${lastReadLine}. Use offset=${lastReadLine + 1} to continue.)`;
|
|
32797
|
+
} else if (hasMoreLines) {
|
|
32798
|
+
output += `
|
|
32799
|
+
|
|
32800
|
+
(Showing lines 1-${lastReadLine} of ${totalLines}. Use offset=${lastReadLine + 1} to continue.)`;
|
|
32801
|
+
} else {
|
|
32802
|
+
output += `
|
|
32803
|
+
|
|
32804
|
+
(End of file - total ${totalLines} lines)`;
|
|
32805
|
+
}
|
|
32806
|
+
output += `
|
|
32807
|
+
</content>`;
|
|
32808
|
+
return output;
|
|
32809
|
+
}
|
|
32810
|
+
|
|
32811
|
+
// src/tools/subtask/files.ts
|
|
32812
|
+
var FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
|
|
32813
|
+
var TRAILING_PATH_PUNCTUATION = /[!?:;]+$/;
|
|
32814
|
+
function cleanFileReference(ref) {
|
|
32815
|
+
return ref.replace(/^@/, "").replace(TRAILING_PATH_PUNCTUATION, "");
|
|
32816
|
+
}
|
|
32817
|
+
function parseFileReferences(text) {
|
|
32818
|
+
const fileRefs = new Set;
|
|
32819
|
+
for (const match of text.matchAll(FILE_REGEX)) {
|
|
32820
|
+
if (match[1]) {
|
|
32821
|
+
fileRefs.add(cleanFileReference(match[1]));
|
|
32822
|
+
}
|
|
32823
|
+
}
|
|
32824
|
+
return fileRefs;
|
|
32825
|
+
}
|
|
32826
|
+
async function buildSyntheticFileParts(directory, refs) {
|
|
32827
|
+
const parts = [];
|
|
32828
|
+
const realDirectory = await fs11.realpath(directory);
|
|
32829
|
+
for (const ref of refs) {
|
|
32830
|
+
const filepath = path20.resolve(directory, ref);
|
|
32831
|
+
const relative3 = path20.relative(directory, filepath);
|
|
32832
|
+
if (relative3.startsWith("..") || path20.isAbsolute(relative3))
|
|
32833
|
+
continue;
|
|
32834
|
+
try {
|
|
32835
|
+
const realFilepath = await fs11.realpath(filepath);
|
|
32836
|
+
const realRelative = path20.relative(realDirectory, realFilepath);
|
|
32837
|
+
if (realRelative.startsWith("..") || path20.isAbsolute(realRelative)) {
|
|
32838
|
+
continue;
|
|
32839
|
+
}
|
|
32840
|
+
const stats = await fs11.stat(realFilepath);
|
|
32841
|
+
if (!stats.isFile())
|
|
32842
|
+
continue;
|
|
32843
|
+
if (await isBinaryFile(realFilepath))
|
|
32844
|
+
continue;
|
|
32845
|
+
const content = await fs11.readFile(realFilepath, "utf-8");
|
|
32846
|
+
parts.push({
|
|
32847
|
+
type: "text",
|
|
32848
|
+
synthetic: true,
|
|
32849
|
+
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: realFilepath })}`
|
|
32850
|
+
});
|
|
32851
|
+
parts.push({
|
|
32852
|
+
type: "text",
|
|
32853
|
+
synthetic: true,
|
|
32854
|
+
text: formatFileContent(realFilepath, content)
|
|
32855
|
+
});
|
|
32856
|
+
} catch {}
|
|
32857
|
+
}
|
|
32858
|
+
return parts;
|
|
32859
|
+
}
|
|
32860
|
+
// src/tools/subtask/state.ts
|
|
32861
|
+
function createSubtaskState() {
|
|
32862
|
+
const sourceBySession = new Map;
|
|
32863
|
+
return {
|
|
32864
|
+
markSession(sessionID, sourceSessionID) {
|
|
32865
|
+
sourceBySession.set(sessionID, sourceSessionID);
|
|
32866
|
+
},
|
|
32867
|
+
unmarkSession(sessionID) {
|
|
32868
|
+
sourceBySession.delete(sessionID);
|
|
32869
|
+
},
|
|
32870
|
+
isSubtaskSession(sessionID) {
|
|
32871
|
+
return sourceBySession.has(sessionID);
|
|
32872
|
+
},
|
|
32873
|
+
sourceFor(sessionID) {
|
|
32874
|
+
return sourceBySession.get(sessionID);
|
|
32875
|
+
}
|
|
32876
|
+
};
|
|
32877
|
+
}
|
|
32878
|
+
// src/tools/subtask/tools.ts
|
|
32879
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
32880
|
+
var SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
32881
|
+
var SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
|
|
32882
|
+
function normalizeSubtaskSummary(text) {
|
|
32883
|
+
return text.replace(SUBTASK_SUMMARY_TAG_REGEX, "").trim();
|
|
32884
|
+
}
|
|
32885
|
+
function getAbortSignal(context) {
|
|
32886
|
+
if (!context || typeof context !== "object" || !("abort" in context)) {
|
|
32887
|
+
return;
|
|
32888
|
+
}
|
|
32889
|
+
const signal = context.abort;
|
|
32890
|
+
return signal && typeof signal === "object" && "addEventListener" in signal && "removeEventListener" in signal && "aborted" in signal ? signal : undefined;
|
|
32891
|
+
}
|
|
32892
|
+
function createSubtaskTool(ctx, state, depthTracker) {
|
|
32893
|
+
const client = ctx.client;
|
|
32894
|
+
return tool5({
|
|
32895
|
+
description: "Run a child worker session and return its completion summary to the caller",
|
|
32896
|
+
args: {
|
|
32897
|
+
prompt: tool5.schema.string().describe("The generated subtask prompt"),
|
|
32898
|
+
files: tool5.schema.array(tool5.schema.string()).optional().describe("Array of file paths to load into the new session's context")
|
|
32899
|
+
},
|
|
32900
|
+
async execute(args, context) {
|
|
32901
|
+
const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : ctx.directory;
|
|
32902
|
+
const sessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : "unknown";
|
|
32903
|
+
const abortSignal = getAbortSignal(context);
|
|
32904
|
+
if (state.isSubtaskSession(sessionID)) {
|
|
32905
|
+
return "Nested subtask is disabled: this session is already a subtask worker. Finish this worker and return its summary to the parent session instead.";
|
|
32906
|
+
}
|
|
32907
|
+
if (sessionID !== "unknown" && depthTracker && depthTracker.getDepth(sessionID) + 1 > depthTracker.maxDepth) {
|
|
32908
|
+
return `Subtask worker blocked: max subagent depth ${depthTracker.maxDepth} would be exceeded.`;
|
|
32909
|
+
}
|
|
32910
|
+
const sessionReference = `You are a subtask worker spawned by parent session ${sessionID}.
|
|
32911
|
+
|
|
32912
|
+
Your job is bounded: complete only the task below. Do not expand scope.
|
|
32913
|
+
If needed context is missing, use read_session to inspect the parent session.
|
|
32914
|
+
Do not spawn another subtask.`;
|
|
32915
|
+
const files = new Set([
|
|
32916
|
+
...parseFileReferences(args.prompt),
|
|
32917
|
+
...(args.files ?? []).map(cleanFileReference)
|
|
32918
|
+
]);
|
|
32919
|
+
const fileRefs = files.size > 0 ? [...files].map((f) => `@${f}`).join(" ") : "";
|
|
32920
|
+
const fullPrompt = fileRefs ? `${sessionReference}
|
|
32921
|
+
|
|
32922
|
+
TASK:
|
|
32923
|
+
${args.prompt}
|
|
32924
|
+
|
|
32925
|
+
FILES PROVIDED:
|
|
32926
|
+
${fileRefs}` : `${sessionReference}
|
|
32927
|
+
|
|
32928
|
+
TASK:
|
|
32929
|
+
${args.prompt}`;
|
|
32930
|
+
let childSessionID;
|
|
32931
|
+
try {
|
|
32932
|
+
const session2 = await client.session.create({
|
|
32933
|
+
responseStyle: "data",
|
|
32934
|
+
throwOnError: true,
|
|
32935
|
+
query: { directory },
|
|
32936
|
+
body: {
|
|
32937
|
+
parentID: sessionID === "unknown" ? undefined : sessionID,
|
|
32938
|
+
title: `Subtask worker from ${sessionID}`
|
|
32939
|
+
}
|
|
32940
|
+
});
|
|
32941
|
+
childSessionID = session2?.data?.id ?? session2?.id;
|
|
32942
|
+
if (!childSessionID) {
|
|
32943
|
+
throw new Error("Subtask worker session did not return an id");
|
|
32944
|
+
}
|
|
32945
|
+
if (sessionID !== "unknown" && depthTracker) {
|
|
32946
|
+
const registered = depthTracker.registerChild(sessionID, childSessionID);
|
|
32947
|
+
if (!registered) {
|
|
32948
|
+
throw new Error("Subtask worker blocked: max subagent depth exceeded");
|
|
32949
|
+
}
|
|
32950
|
+
}
|
|
32951
|
+
state.markSession(childSessionID, sessionID);
|
|
32952
|
+
await promptWithTimeout(client, {
|
|
32953
|
+
responseStyle: "data",
|
|
32954
|
+
throwOnError: true,
|
|
32955
|
+
query: { directory },
|
|
32956
|
+
path: { id: childSessionID },
|
|
32957
|
+
body: {
|
|
32958
|
+
agent: "orchestrator",
|
|
32959
|
+
parts: [
|
|
32960
|
+
{
|
|
32961
|
+
type: "text",
|
|
32962
|
+
text: `${fullPrompt}
|
|
32963
|
+
|
|
32964
|
+
Instructions:
|
|
32965
|
+
1. Understand the task and relevant file context.
|
|
32966
|
+
2. Make only necessary changes.
|
|
32967
|
+
3. Run the most relevant validation checks when practical.
|
|
32968
|
+
4. Stop when the requested task is done.
|
|
32969
|
+
|
|
32970
|
+
Return your final response in this format:
|
|
32971
|
+
|
|
32972
|
+
<subtask_summary>
|
|
32973
|
+
Status: completed | blocked | partial
|
|
32974
|
+
|
|
32975
|
+
What changed:
|
|
32976
|
+
- ...
|
|
32977
|
+
|
|
32978
|
+
Files touched:
|
|
32979
|
+
- ...
|
|
32980
|
+
|
|
32981
|
+
Validation:
|
|
32982
|
+
- ...
|
|
32983
|
+
|
|
32984
|
+
Risks / follow-up:
|
|
32985
|
+
- ...
|
|
32986
|
+
</subtask_summary>`
|
|
32987
|
+
},
|
|
32988
|
+
...await buildSyntheticFileParts(directory, files)
|
|
32989
|
+
]
|
|
32990
|
+
}
|
|
32991
|
+
}, SUBTASK_TIMEOUT_MS, abortSignal);
|
|
32992
|
+
const extraction = await extractSessionResult(client, childSessionID, {
|
|
32993
|
+
directory,
|
|
32994
|
+
includeReasoning: false
|
|
32995
|
+
});
|
|
32996
|
+
if (extraction.empty) {
|
|
32997
|
+
throw new Error("Subtask worker returned no summary");
|
|
32998
|
+
}
|
|
32999
|
+
const summary = normalizeSubtaskSummary(extraction.text);
|
|
33000
|
+
return [
|
|
33001
|
+
`task_id: ${childSessionID}`,
|
|
33002
|
+
"",
|
|
33003
|
+
"<subtask_summary>",
|
|
33004
|
+
summary,
|
|
33005
|
+
"</subtask_summary>"
|
|
33006
|
+
].join(`
|
|
33007
|
+
`);
|
|
33008
|
+
} finally {
|
|
33009
|
+
if (childSessionID) {
|
|
33010
|
+
try {
|
|
33011
|
+
await client.session.abort({
|
|
33012
|
+
path: { id: childSessionID },
|
|
33013
|
+
query: { directory }
|
|
33014
|
+
});
|
|
33015
|
+
state.unmarkSession(childSessionID);
|
|
33016
|
+
} catch {}
|
|
33017
|
+
}
|
|
33018
|
+
}
|
|
33019
|
+
}
|
|
33020
|
+
});
|
|
33021
|
+
}
|
|
33022
|
+
function formatTranscript(messages, limit) {
|
|
33023
|
+
const lines = [];
|
|
33024
|
+
for (const msg of messages) {
|
|
33025
|
+
const role = msg.info?.role;
|
|
33026
|
+
const parts = msg.parts;
|
|
33027
|
+
if (role === "user") {
|
|
33028
|
+
lines.push("## User");
|
|
33029
|
+
for (const part of parts) {
|
|
33030
|
+
if (part.type === "text" && !part.ignored && typeof part.text === "string") {
|
|
33031
|
+
lines.push(part.text);
|
|
33032
|
+
}
|
|
33033
|
+
if (part.type === "file") {
|
|
33034
|
+
lines.push(`[Attached: ${part.filename || "file"}]`);
|
|
33035
|
+
}
|
|
33036
|
+
}
|
|
33037
|
+
lines.push("");
|
|
33038
|
+
}
|
|
33039
|
+
if (role === "assistant") {
|
|
33040
|
+
lines.push("## Assistant");
|
|
33041
|
+
for (const part of parts) {
|
|
33042
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
33043
|
+
lines.push(part.text);
|
|
33044
|
+
}
|
|
33045
|
+
if (part.type === "tool" && part.state?.status === "completed" && part.tool) {
|
|
33046
|
+
lines.push(`[Tool: ${part.tool}] ${part.state.title ?? ""}`);
|
|
33047
|
+
}
|
|
33048
|
+
}
|
|
33049
|
+
lines.push("");
|
|
33050
|
+
}
|
|
33051
|
+
}
|
|
33052
|
+
const output = lines.join(`
|
|
33053
|
+
`).trim();
|
|
33054
|
+
if (messages.length >= (limit ?? 100)) {
|
|
33055
|
+
return output + `
|
|
33056
|
+
|
|
33057
|
+
(Showing ${messages.length} most recent messages. Use a higher 'limit' to see more.)`;
|
|
33058
|
+
}
|
|
33059
|
+
return `${output}
|
|
33060
|
+
|
|
33061
|
+
(End of session - ${messages.length} messages)`;
|
|
33062
|
+
}
|
|
33063
|
+
function createReadSessionTool(client, state) {
|
|
33064
|
+
return tool5({
|
|
33065
|
+
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.",
|
|
33066
|
+
args: {
|
|
33067
|
+
sessionID: tool5.schema.string().describe("The full session ID (e.g., sess_01jxyz...)"),
|
|
33068
|
+
limit: tool5.schema.number().optional().describe("Maximum number of messages to read (defaults to 100, max 500)")
|
|
33069
|
+
},
|
|
33070
|
+
async execute(args, context) {
|
|
33071
|
+
const limit = Math.min(args.limit ?? 100, 500);
|
|
33072
|
+
const directory = context && typeof context === "object" && "directory" in context && typeof context.directory === "string" ? context.directory : undefined;
|
|
33073
|
+
const callerSessionID = context && typeof context === "object" && "sessionID" in context ? context.sessionID : undefined;
|
|
33074
|
+
if (!callerSessionID || !state.isSubtaskSession(callerSessionID)) {
|
|
33075
|
+
return "read_session is only available from subtask worker sessions.";
|
|
33076
|
+
}
|
|
33077
|
+
if (state.sourceFor(callerSessionID) !== args.sessionID) {
|
|
33078
|
+
return "read_session can only read the source session for this subtask worker.";
|
|
33079
|
+
}
|
|
33080
|
+
try {
|
|
33081
|
+
const response = await client.session.messages({
|
|
33082
|
+
path: { id: args.sessionID },
|
|
33083
|
+
query: { limit, ...directory ? { directory } : {} }
|
|
33084
|
+
});
|
|
33085
|
+
if (!response.data || response.data.length === 0) {
|
|
33086
|
+
return "Session has no messages or does not exist.";
|
|
33087
|
+
}
|
|
33088
|
+
return formatTranscript(response.data, limit);
|
|
33089
|
+
} catch (error) {
|
|
33090
|
+
return `Could not read session ${args.sessionID}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
33091
|
+
}
|
|
33092
|
+
}
|
|
33093
|
+
});
|
|
33094
|
+
}
|
|
32151
33095
|
// src/utils/subagent-depth.ts
|
|
32152
33096
|
class SubagentDepthTracker {
|
|
32153
33097
|
depthBySession = new Map;
|
|
@@ -32263,9 +33207,12 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32263
33207
|
let taskSessionManagerHook;
|
|
32264
33208
|
let interviewManager;
|
|
32265
33209
|
let presetManager;
|
|
33210
|
+
let divoomManager;
|
|
32266
33211
|
let councilTools;
|
|
32267
33212
|
let webfetch;
|
|
32268
33213
|
let rewriteDisplayNameMentions;
|
|
33214
|
+
let subtaskCommandManager;
|
|
33215
|
+
let subtaskState;
|
|
32269
33216
|
let toolCount = 0;
|
|
32270
33217
|
try {
|
|
32271
33218
|
config = loadPluginConfig(ctx.directory);
|
|
@@ -32357,7 +33304,10 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32357
33304
|
});
|
|
32358
33305
|
interviewManager = createInterviewManager(ctx, config);
|
|
32359
33306
|
presetManager = createPresetManager(ctx, config);
|
|
32360
|
-
|
|
33307
|
+
divoomManager = createDivoomManager(config.divoom);
|
|
33308
|
+
subtaskState = createSubtaskState();
|
|
33309
|
+
subtaskCommandManager = createSubtaskCommandManager(ctx, subtaskState);
|
|
33310
|
+
toolCount = Object.keys(councilTools).length + Object.keys(todoContinuationHook.tool).length + 1 + 2 + 2;
|
|
32361
33311
|
} catch (err) {
|
|
32362
33312
|
log("[plugin] FATAL: init failed", String(err));
|
|
32363
33313
|
await appLog(ctx, "error", `INIT FAILED: ${String(err)}. Report at github.com/alvinunreal/oh-my-opencode-slim/issues/310`);
|
|
@@ -32393,6 +33343,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32393
33343
|
appLog(ctx, "warn", msg).catch(() => {});
|
|
32394
33344
|
}
|
|
32395
33345
|
});
|
|
33346
|
+
divoomManager.onPluginLoad();
|
|
32396
33347
|
return {
|
|
32397
33348
|
name: "oh-my-opencode-slim",
|
|
32398
33349
|
agent: agents,
|
|
@@ -32401,7 +33352,9 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32401
33352
|
webfetch,
|
|
32402
33353
|
...todoContinuationHook.tool,
|
|
32403
33354
|
ast_grep_search,
|
|
32404
|
-
ast_grep_replace
|
|
33355
|
+
ast_grep_replace,
|
|
33356
|
+
subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
|
|
33357
|
+
read_session: createReadSessionTool(ctx.client, subtaskState)
|
|
32405
33358
|
},
|
|
32406
33359
|
mcp: mcps,
|
|
32407
33360
|
config: async (opencodeConfig) => {
|
|
@@ -32598,6 +33551,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32598
33551
|
}
|
|
32599
33552
|
interviewManager.registerCommand(opencodeConfig);
|
|
32600
33553
|
presetManager.registerCommand(opencodeConfig);
|
|
33554
|
+
subtaskCommandManager.registerCommand(opencodeConfig);
|
|
32601
33555
|
},
|
|
32602
33556
|
event: async (input) => {
|
|
32603
33557
|
const event = input.event;
|
|
@@ -32625,6 +33579,38 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32625
33579
|
await multiplexerSessionManager.onSessionDeleted(event);
|
|
32626
33580
|
await interviewManager.handleEvent(input);
|
|
32627
33581
|
await taskSessionManagerHook.event(input);
|
|
33582
|
+
subtaskCommandManager.handleEvent(input);
|
|
33583
|
+
if (event.type === "permission.asked" || event.type === "question.asked") {
|
|
33584
|
+
const props = event.properties;
|
|
33585
|
+
divoomManager.onUserInputRequired({
|
|
33586
|
+
sessionId: props?.sessionID,
|
|
33587
|
+
requestId: props?.id ?? props?.requestID
|
|
33588
|
+
});
|
|
33589
|
+
}
|
|
33590
|
+
if (event.type === "permission.replied" || event.type === "question.replied" || event.type === "question.rejected") {
|
|
33591
|
+
const props = event.properties;
|
|
33592
|
+
divoomManager.onUserInputResolved({
|
|
33593
|
+
sessionId: props?.sessionID,
|
|
33594
|
+
requestId: props?.requestID ?? props?.id
|
|
33595
|
+
});
|
|
33596
|
+
}
|
|
33597
|
+
if (input.event.type === "session.status") {
|
|
33598
|
+
const props = input.event.properties;
|
|
33599
|
+
const sessionID = props?.sessionID;
|
|
33600
|
+
divoomManager.onOrchestratorStatus({
|
|
33601
|
+
sessionId: sessionID,
|
|
33602
|
+
status: props?.status?.type,
|
|
33603
|
+
isOrchestrator: sessionID ? sessionAgentMap.get(sessionID) === "orchestrator" : false
|
|
33604
|
+
});
|
|
33605
|
+
}
|
|
33606
|
+
if (input.event.type === "session.deleted") {
|
|
33607
|
+
const props = input.event.properties;
|
|
33608
|
+
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
33609
|
+
divoomManager.onSessionDeleted({
|
|
33610
|
+
sessionId: sessionID,
|
|
33611
|
+
isOrchestrator: sessionID ? sessionAgentMap.get(sessionID) === "orchestrator" : false
|
|
33612
|
+
});
|
|
33613
|
+
}
|
|
32628
33614
|
if (input.event.type === "session.deleted") {
|
|
32629
33615
|
const props = input.event.properties;
|
|
32630
33616
|
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
@@ -32639,6 +33625,13 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32639
33625
|
"tool.execute.before": async (input, output) => {
|
|
32640
33626
|
await applyPatchHook["tool.execute.before"](input, output);
|
|
32641
33627
|
await taskSessionManagerHook["tool.execute.before"](input, output);
|
|
33628
|
+
if (input.tool.toLowerCase() === "task") {
|
|
33629
|
+
divoomManager.onTaskStart({
|
|
33630
|
+
parentSessionId: input.sessionID,
|
|
33631
|
+
callId: input.callID,
|
|
33632
|
+
args: output.args
|
|
33633
|
+
});
|
|
33634
|
+
}
|
|
32642
33635
|
},
|
|
32643
33636
|
"command.execute.before": async (input, output) => {
|
|
32644
33637
|
await todoContinuationHook.handleCommandExecuteBefore(input, output);
|
|
@@ -32701,11 +33694,31 @@ ${output.system[0]}` : "");
|
|
|
32701
33694
|
await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
|
|
32702
33695
|
},
|
|
32703
33696
|
"tool.execute.after": async (input, output) => {
|
|
32704
|
-
|
|
32705
|
-
|
|
32706
|
-
|
|
32707
|
-
|
|
32708
|
-
|
|
33697
|
+
const meta = input;
|
|
33698
|
+
const runPostToolHook = async (name, fn) => {
|
|
33699
|
+
try {
|
|
33700
|
+
await fn();
|
|
33701
|
+
} catch (error) {
|
|
33702
|
+
log("[plugin] post-tool hook failed open", {
|
|
33703
|
+
hook: name,
|
|
33704
|
+
tool: meta.tool,
|
|
33705
|
+
sessionID: meta.sessionID,
|
|
33706
|
+
callID: meta.callID,
|
|
33707
|
+
error: error instanceof Error ? error.message : String(error)
|
|
33708
|
+
});
|
|
33709
|
+
}
|
|
33710
|
+
};
|
|
33711
|
+
await runPostToolHook("delegate-task-retry", () => delegateTaskRetryHook["tool.execute.after"](input, output));
|
|
33712
|
+
await runPostToolHook("json-error-recovery", () => jsonErrorRecoveryHook["tool.execute.after"](input, output));
|
|
33713
|
+
await runPostToolHook("todo-continuation", () => todoContinuationHook.handleToolExecuteAfter(input, output));
|
|
33714
|
+
await runPostToolHook("post-file-tool-nudge", () => postFileToolNudgeHook["tool.execute.after"](input, output));
|
|
33715
|
+
await runPostToolHook("task-session-manager", () => taskSessionManagerHook["tool.execute.after"](input, output));
|
|
33716
|
+
if (input.tool.toLowerCase() === "task") {
|
|
33717
|
+
divoomManager.onTaskEnd({
|
|
33718
|
+
parentSessionId: input.sessionID,
|
|
33719
|
+
callId: input.callID
|
|
33720
|
+
});
|
|
33721
|
+
}
|
|
32709
33722
|
}
|
|
32710
33723
|
};
|
|
32711
33724
|
};
|