oh-my-opencode-slim 2.0.5 → 2.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.ja-JP.md +47 -21
- package/README.ko-KR.md +45 -19
- package/README.md +56 -32
- package/README.zh-CN.md +48 -24
- package/dist/agents/council.d.ts +1 -1
- package/dist/agents/index.d.ts +7 -2
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/cli/custom-skills-registry.d.ts +18 -0
- package/dist/cli/custom-skills.d.ts +3 -19
- package/dist/cli/index.js +1089 -185
- package/dist/cli/providers.d.ts +5 -9
- package/dist/cli/skills.d.ts +2 -2
- package/dist/companion/manager.d.ts +7 -0
- package/dist/config/loader.d.ts +5 -2
- package/dist/config/schema.d.ts +4 -0
- package/dist/council/council-manager.d.ts +1 -1
- package/dist/hooks/auto-update-checker/skill-sync.d.ts +59 -1
- package/dist/hooks/filter-available-skills/index.d.ts +1 -2
- package/dist/hooks/foreground-fallback/index.d.ts +5 -1
- package/dist/hooks/image-hook.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/loop-command/index.d.ts +13 -0
- package/dist/hooks/phase-reminder/index.d.ts +1 -2
- package/dist/hooks/task-session-manager/index.d.ts +1 -2
- package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
- package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
- package/dist/hooks/types.d.ts +3 -1
- package/dist/index.js +2116 -670
- package/dist/interview/dashboard-manager.d.ts +21 -0
- package/dist/interview/manager.d.ts +0 -14
- package/dist/interview/service.d.ts +7 -0
- package/dist/interview/session-server.d.ts +21 -0
- package/dist/interview/types.d.ts +3 -1
- package/dist/loop/loop-session.d.ts +64 -0
- package/dist/multiplexer/factory.d.ts +5 -5
- package/dist/multiplexer/herdr/index.d.ts +31 -0
- package/dist/multiplexer/index.d.ts +1 -0
- package/dist/multiplexer/session-manager.d.ts +1 -0
- package/dist/multiplexer/types.d.ts +4 -4
- package/dist/tui.js +82 -39
- package/dist/utils/background-job-board.d.ts +17 -1
- package/dist/utils/session.d.ts +1 -1
- package/oh-my-opencode-slim.schema.json +6 -1
- package/package.json +1 -1
- package/src/skills/clonedeps/SKILL.md +2 -2
- package/src/skills/clonedeps/codemap.md +23 -32
- package/src/skills/codemap/SKILL.md +2 -2
- package/src/skills/codemap.md +63 -36
- package/src/skills/loop-engineering/SKILL.md +30 -0
- package/src/skills/reflect/SKILL.md +133 -0
- package/src/skills/release-smoke-test/SKILL.md +159 -0
- package/src/skills/simplify/SKILL.md +6 -6
package/dist/index.js
CHANGED
|
@@ -6344,9 +6344,9 @@ var require_URL = __commonJS((exports, module) => {
|
|
|
6344
6344
|
s += "#" + this.fragment;
|
|
6345
6345
|
return s;
|
|
6346
6346
|
},
|
|
6347
|
-
resolve: function(
|
|
6347
|
+
resolve: function(relative3) {
|
|
6348
6348
|
var base = this;
|
|
6349
|
-
var r = new URL4(
|
|
6349
|
+
var r = new URL4(relative3);
|
|
6350
6350
|
var t = new URL4;
|
|
6351
6351
|
if (r.scheme !== undefined) {
|
|
6352
6352
|
t.scheme = r.scheme;
|
|
@@ -6398,33 +6398,33 @@ var require_URL = __commonJS((exports, module) => {
|
|
|
6398
6398
|
else
|
|
6399
6399
|
return basepath.substring(0, lastslash + 1) + refpath;
|
|
6400
6400
|
}
|
|
6401
|
-
function remove_dot_segments(
|
|
6402
|
-
if (!
|
|
6403
|
-
return
|
|
6401
|
+
function remove_dot_segments(path20) {
|
|
6402
|
+
if (!path20)
|
|
6403
|
+
return path20;
|
|
6404
6404
|
var output = "";
|
|
6405
|
-
while (
|
|
6406
|
-
if (
|
|
6407
|
-
|
|
6405
|
+
while (path20.length > 0) {
|
|
6406
|
+
if (path20 === "." || path20 === "..") {
|
|
6407
|
+
path20 = "";
|
|
6408
6408
|
break;
|
|
6409
6409
|
}
|
|
6410
|
-
var twochars =
|
|
6411
|
-
var threechars =
|
|
6412
|
-
var fourchars =
|
|
6410
|
+
var twochars = path20.substring(0, 2);
|
|
6411
|
+
var threechars = path20.substring(0, 3);
|
|
6412
|
+
var fourchars = path20.substring(0, 4);
|
|
6413
6413
|
if (threechars === "../") {
|
|
6414
|
-
|
|
6414
|
+
path20 = path20.substring(3);
|
|
6415
6415
|
} else if (twochars === "./") {
|
|
6416
|
-
|
|
6416
|
+
path20 = path20.substring(2);
|
|
6417
6417
|
} else if (threechars === "/./") {
|
|
6418
|
-
|
|
6419
|
-
} else if (twochars === "/." &&
|
|
6420
|
-
|
|
6421
|
-
} else if (fourchars === "/../" || threechars === "/.." &&
|
|
6422
|
-
|
|
6418
|
+
path20 = "/" + path20.substring(3);
|
|
6419
|
+
} else if (twochars === "/." && path20.length === 2) {
|
|
6420
|
+
path20 = "/";
|
|
6421
|
+
} else if (fourchars === "/../" || threechars === "/.." && path20.length === 3) {
|
|
6422
|
+
path20 = "/" + path20.substring(4);
|
|
6423
6423
|
output = output.replace(/\/?[^\/]*$/, "");
|
|
6424
6424
|
} else {
|
|
6425
|
-
var segment =
|
|
6425
|
+
var segment = path20.match(/(\/?([^\/]*))/)[0];
|
|
6426
6426
|
output += segment;
|
|
6427
|
-
|
|
6427
|
+
path20 = path20.substring(segment.length);
|
|
6428
6428
|
}
|
|
6429
6429
|
}
|
|
6430
6430
|
return output;
|
|
@@ -18337,36 +18337,7 @@ var require_turndown_cjs = __commonJS((exports, module) => {
|
|
|
18337
18337
|
module.exports = TurndownService;
|
|
18338
18338
|
});
|
|
18339
18339
|
|
|
18340
|
-
// src/cli/
|
|
18341
|
-
import { homedir } from "node:os";
|
|
18342
|
-
import { dirname, join } from "node:path";
|
|
18343
|
-
function getDefaultOpenCodeConfigDir() {
|
|
18344
|
-
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
|
|
18345
|
-
return join(userConfigDir, "opencode");
|
|
18346
|
-
}
|
|
18347
|
-
function getCustomOpenCodeConfigDir() {
|
|
18348
|
-
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
18349
|
-
return configDir || undefined;
|
|
18350
|
-
}
|
|
18351
|
-
function getConfigDir() {
|
|
18352
|
-
const customConfigDir = getCustomOpenCodeConfigDir();
|
|
18353
|
-
if (customConfigDir) {
|
|
18354
|
-
return customConfigDir;
|
|
18355
|
-
}
|
|
18356
|
-
return getDefaultOpenCodeConfigDir();
|
|
18357
|
-
}
|
|
18358
|
-
function getConfigSearchDirs() {
|
|
18359
|
-
const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
|
|
18360
|
-
return dirs.filter((dir, index) => {
|
|
18361
|
-
return Boolean(dir) && dirs.indexOf(dir) === index;
|
|
18362
|
-
});
|
|
18363
|
-
}
|
|
18364
|
-
function getOpenCodeConfigPaths() {
|
|
18365
|
-
const configDir = getConfigDir();
|
|
18366
|
-
return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
|
|
18367
|
-
}
|
|
18368
|
-
|
|
18369
|
-
// src/cli/custom-skills.ts
|
|
18340
|
+
// src/cli/custom-skills-registry.ts
|
|
18370
18341
|
var CUSTOM_SKILLS = [
|
|
18371
18342
|
{
|
|
18372
18343
|
name: "simplify",
|
|
@@ -18404,6 +18375,12 @@ var CUSTOM_SKILLS = [
|
|
|
18404
18375
|
allowedAgents: ["orchestrator"],
|
|
18405
18376
|
sourcePath: "src/skills/oh-my-opencode-slim"
|
|
18406
18377
|
},
|
|
18378
|
+
{
|
|
18379
|
+
name: "release-smoke-test",
|
|
18380
|
+
description: "Validate packed release candidates and bugfixes before public publish",
|
|
18381
|
+
allowedAgents: ["orchestrator"],
|
|
18382
|
+
sourcePath: "src/skills/release-smoke-test"
|
|
18383
|
+
},
|
|
18407
18384
|
{
|
|
18408
18385
|
name: "worktrees",
|
|
18409
18386
|
description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
|
|
@@ -18412,6 +18389,35 @@ var CUSTOM_SKILLS = [
|
|
|
18412
18389
|
}
|
|
18413
18390
|
];
|
|
18414
18391
|
|
|
18392
|
+
// src/cli/paths.ts
|
|
18393
|
+
import { homedir } from "node:os";
|
|
18394
|
+
import { dirname, join } from "node:path";
|
|
18395
|
+
function getDefaultOpenCodeConfigDir() {
|
|
18396
|
+
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
|
|
18397
|
+
return join(userConfigDir, "opencode");
|
|
18398
|
+
}
|
|
18399
|
+
function getCustomOpenCodeConfigDir() {
|
|
18400
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
18401
|
+
return configDir || undefined;
|
|
18402
|
+
}
|
|
18403
|
+
function getConfigDir() {
|
|
18404
|
+
const customConfigDir = getCustomOpenCodeConfigDir();
|
|
18405
|
+
if (customConfigDir) {
|
|
18406
|
+
return customConfigDir;
|
|
18407
|
+
}
|
|
18408
|
+
return getDefaultOpenCodeConfigDir();
|
|
18409
|
+
}
|
|
18410
|
+
function getConfigSearchDirs() {
|
|
18411
|
+
const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
|
|
18412
|
+
return dirs.filter((dir, index) => {
|
|
18413
|
+
return Boolean(dir) && dirs.indexOf(dir) === index;
|
|
18414
|
+
});
|
|
18415
|
+
}
|
|
18416
|
+
function getOpenCodeConfigPaths() {
|
|
18417
|
+
const configDir = getConfigDir();
|
|
18418
|
+
return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
|
|
18419
|
+
}
|
|
18420
|
+
|
|
18415
18421
|
// src/cli/skills.ts
|
|
18416
18422
|
var PERMISSION_ONLY_SKILLS = [
|
|
18417
18423
|
{
|
|
@@ -18556,7 +18562,7 @@ var CouncilConfigSchema = z.object({
|
|
|
18556
18562
|
default_preset: z.string().default("default"),
|
|
18557
18563
|
councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
|
|
18558
18564
|
councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
|
|
18559
|
-
master: z.unknown().optional().describe("DEPRECATED
|
|
18565
|
+
master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
|
|
18560
18566
|
}).transform((data) => {
|
|
18561
18567
|
const deprecated = [];
|
|
18562
18568
|
if (data.master !== undefined)
|
|
@@ -18671,7 +18677,13 @@ var AgentOverrideConfigSchema = z2.object({
|
|
|
18671
18677
|
options: z2.record(z2.string(), z2.unknown()).optional(),
|
|
18672
18678
|
displayName: z2.string().min(1).optional()
|
|
18673
18679
|
}).strict();
|
|
18674
|
-
var MultiplexerTypeSchema = z2.enum([
|
|
18680
|
+
var MultiplexerTypeSchema = z2.enum([
|
|
18681
|
+
"auto",
|
|
18682
|
+
"tmux",
|
|
18683
|
+
"zellij",
|
|
18684
|
+
"herdr",
|
|
18685
|
+
"none"
|
|
18686
|
+
]);
|
|
18675
18687
|
var MultiplexerLayoutSchema = z2.enum([
|
|
18676
18688
|
"main-horizontal",
|
|
18677
18689
|
"main-vertical",
|
|
@@ -18740,24 +18752,13 @@ var AcpAgentConfigSchema = z2.object({
|
|
|
18740
18752
|
permissionMode: AcpAgentPermissionModeSchema.default("ask")
|
|
18741
18753
|
}).strict();
|
|
18742
18754
|
var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
|
|
18743
|
-
function
|
|
18755
|
+
function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
|
|
18744
18756
|
for (const [name, override] of Object.entries(overrides)) {
|
|
18745
|
-
|
|
18746
|
-
if (!isBuiltInOrAlias) {
|
|
18747
|
-
continue;
|
|
18748
|
-
}
|
|
18749
|
-
if (override.prompt !== undefined) {
|
|
18750
|
-
ctx.addIssue({
|
|
18751
|
-
code: z2.ZodIssueCode.custom,
|
|
18752
|
-
path: [...pathPrefix, name, "prompt"],
|
|
18753
|
-
message: "prompt is only supported for custom agents"
|
|
18754
|
-
});
|
|
18755
|
-
}
|
|
18756
|
-
if (override.orchestratorPrompt !== undefined) {
|
|
18757
|
+
if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
|
|
18757
18758
|
ctx.addIssue({
|
|
18758
18759
|
code: z2.ZodIssueCode.custom,
|
|
18759
18760
|
path: [...pathPrefix, name, "orchestratorPrompt"],
|
|
18760
|
-
message: "orchestratorPrompt is
|
|
18761
|
+
message: "orchestratorPrompt is not supported for the orchestrator agent"
|
|
18761
18762
|
});
|
|
18762
18763
|
}
|
|
18763
18764
|
}
|
|
@@ -18765,6 +18766,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
|
|
|
18765
18766
|
var PluginConfigSchema = z2.object({
|
|
18766
18767
|
preset: z2.string().optional(),
|
|
18767
18768
|
setDefaultAgent: z2.boolean().optional(),
|
|
18769
|
+
compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout when enabled."),
|
|
18768
18770
|
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
18769
18771
|
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
18770
18772
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
@@ -18783,11 +18785,14 @@ var PluginConfigSchema = z2.object({
|
|
|
18783
18785
|
acpAgents: AcpAgentsConfigSchema.optional()
|
|
18784
18786
|
}).superRefine((value, ctx) => {
|
|
18785
18787
|
if (value.agents) {
|
|
18786
|
-
|
|
18788
|
+
rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
|
|
18787
18789
|
}
|
|
18788
18790
|
if (value.presets) {
|
|
18789
18791
|
for (const [presetName, preset] of Object.entries(value.presets)) {
|
|
18790
|
-
|
|
18792
|
+
rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
|
|
18793
|
+
"presets",
|
|
18794
|
+
presetName
|
|
18795
|
+
]);
|
|
18791
18796
|
}
|
|
18792
18797
|
}
|
|
18793
18798
|
});
|
|
@@ -18874,6 +18879,7 @@ function mergePluginConfigs(base, override) {
|
|
|
18874
18879
|
...base,
|
|
18875
18880
|
...override,
|
|
18876
18881
|
agents: deepMerge(base.agents, override.agents),
|
|
18882
|
+
presets: deepMerge(base.presets, override.presets),
|
|
18877
18883
|
tmux: deepMerge(base.tmux, override.tmux),
|
|
18878
18884
|
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
18879
18885
|
interview: deepMerge(base.interview, override.interview),
|
|
@@ -18945,15 +18951,33 @@ function loadPluginConfig(directory, options) {
|
|
|
18945
18951
|
}
|
|
18946
18952
|
return config;
|
|
18947
18953
|
}
|
|
18948
|
-
function loadAgentPrompt(agentName,
|
|
18954
|
+
function loadAgentPrompt(agentName, optionsOrPreset) {
|
|
18955
|
+
let preset;
|
|
18956
|
+
let projectDirectory;
|
|
18957
|
+
if (typeof optionsOrPreset === "string") {
|
|
18958
|
+
preset = optionsOrPreset;
|
|
18959
|
+
} else if (optionsOrPreset && typeof optionsOrPreset === "object") {
|
|
18960
|
+
preset = optionsOrPreset.preset;
|
|
18961
|
+
projectDirectory = optionsOrPreset.projectDirectory;
|
|
18962
|
+
}
|
|
18949
18963
|
const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
|
|
18950
|
-
const
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
}
|
|
18954
|
-
|
|
18964
|
+
const searchDirs = [];
|
|
18965
|
+
if (projectDirectory && presetDirName) {
|
|
18966
|
+
searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME, presetDirName));
|
|
18967
|
+
}
|
|
18968
|
+
if (projectDirectory) {
|
|
18969
|
+
searchDirs.push(path.join(projectDirectory, ".opencode", PROMPTS_DIR_NAME));
|
|
18970
|
+
}
|
|
18971
|
+
if (presetDirName) {
|
|
18972
|
+
for (const userDir of getConfigSearchDirs()) {
|
|
18973
|
+
searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME, presetDirName));
|
|
18974
|
+
}
|
|
18975
|
+
}
|
|
18976
|
+
for (const userDir of getConfigSearchDirs()) {
|
|
18977
|
+
searchDirs.push(path.join(userDir, PROMPTS_DIR_NAME));
|
|
18978
|
+
}
|
|
18955
18979
|
const readFirstPrompt = (fileName, errorPrefix) => {
|
|
18956
|
-
for (const dir of
|
|
18980
|
+
for (const dir of searchDirs) {
|
|
18957
18981
|
const promptPath = path.join(dir, fileName);
|
|
18958
18982
|
if (!fs.existsSync(promptPath)) {
|
|
18959
18983
|
continue;
|
|
@@ -18966,6 +18990,7 @@ function loadAgentPrompt(agentName, preset) {
|
|
|
18966
18990
|
}
|
|
18967
18991
|
return;
|
|
18968
18992
|
};
|
|
18993
|
+
const result = {};
|
|
18969
18994
|
result.prompt = readFirstPrompt(`${agentName}.md`, "Error reading prompt file");
|
|
18970
18995
|
result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
|
|
18971
18996
|
return result;
|
|
@@ -19113,13 +19138,10 @@ async function extractSessionResult(client, sessionId, options) {
|
|
|
19113
19138
|
|
|
19114
19139
|
// src/agents/orchestrator.ts
|
|
19115
19140
|
function resolvePrompt(base, customPrompt, customAppendPrompt) {
|
|
19116
|
-
|
|
19117
|
-
|
|
19118
|
-
if (customAppendPrompt)
|
|
19119
|
-
return `${base}
|
|
19141
|
+
const effectiveBase = customPrompt !== undefined ? customPrompt : base;
|
|
19142
|
+
return customAppendPrompt !== undefined ? `${effectiveBase}
|
|
19120
19143
|
|
|
19121
|
-
${customAppendPrompt}
|
|
19122
|
-
return base;
|
|
19144
|
+
${customAppendPrompt}` : effectiveBase;
|
|
19123
19145
|
}
|
|
19124
19146
|
var AGENT_DESCRIPTIONS = {
|
|
19125
19147
|
explorer: `@explorer
|
|
@@ -19162,7 +19184,7 @@ var AGENT_DESCRIPTIONS = {
|
|
|
19162
19184
|
- Permissions: read_files, write_files
|
|
19163
19185
|
- Stats: 2x faster code edits, 1/2 cost of orchestrator
|
|
19164
19186
|
- Weakness: design, taste
|
|
19165
|
-
- Tools/Constraints: Execution-focused
|
|
19187
|
+
- Tools/Constraints: Execution-focused-no research, no architectural decisions
|
|
19166
19188
|
- **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer • Parallelization benefits: Task involves multiple folders and multiple files modification, scoping work per folder and spawning parallel @fixers for each folder.
|
|
19167
19189
|
- **Don't delegate when:** Needs discovery/research/decisions • Single small change (<20 lines, one file) • Unclear requirements needing iteration • Explaining to fixer > doing • Tight integration with your current work • Requires design taste, visual hierarchy, interaction polish, responsive layout decisions, animation/motion, component feel, or UI copy/design trade-offs
|
|
19168
19190
|
- **Rule of thumb:** Headless/mechanical implementation → @fixer. User-visible design or polish → @designer. If @designer already set direction, @fixer may only do bounded mechanical follow-up that preserves that design exactly.`,
|
|
@@ -19181,12 +19203,12 @@ var AGENT_DESCRIPTIONS = {
|
|
|
19181
19203
|
- Lane: Visual/media analysis isolated from orchestrator context
|
|
19182
19204
|
- Role: Visual analysis specialist for images, PDFs, and diagrams
|
|
19183
19205
|
- Permissions: Read files
|
|
19184
|
-
- Stats: Saves main context tokens
|
|
19206
|
+
- Stats: Saves main context tokens - Observer processes raw files, returns structured observations
|
|
19185
19207
|
- Capabilities: Interprets images, screenshots, PDFs, and diagrams via native read tool; extracts UI elements, layouts, text, relationships
|
|
19186
19208
|
- **Delegate when:** Need to analyze a multimedia file• Extract information
|
|
19187
19209
|
- **Don't delegate when:** Plain text files that Read can handle directly • Files that need editing afterward (need literal content from Read)
|
|
19188
|
-
- **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer
|
|
19189
|
-
- **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png
|
|
19210
|
+
- **Rule of thumb:** Even if your model supports vision, delegate visual analysis to @observer - it isolates large image/PDF bytes from your context window, returning only concise structured text. Need exact file contents for routing? → Read only the minimal context yourself.
|
|
19211
|
+
- **IMPORTANT:** When delegating to @observer, always include the **full file path** in the prompt so it can read the file. Example: "Analyze the screenshot at /path/to/file.png - describe the UI elements and error messages."`
|
|
19190
19212
|
};
|
|
19191
19213
|
var VALIDATION_ROUTING = [
|
|
19192
19214
|
"- Route UI/UX validation and review to @designer",
|
|
@@ -19384,7 +19406,7 @@ function createReadOnlyAgentPermission() {
|
|
|
19384
19406
|
}
|
|
19385
19407
|
|
|
19386
19408
|
// src/agents/council.ts
|
|
19387
|
-
var COUNCIL_AGENT_PROMPT = `You are the Council agent
|
|
19409
|
+
var COUNCIL_AGENT_PROMPT = `You are the Council agent - a multi-LLM orchestration system that runs consensus across multiple models.
|
|
19388
19410
|
|
|
19389
19411
|
**Tool**: You have access to the \`council_session\` tool. You also have read-only codebase inspection tools. You do not have write, edit, shell, or subagent-delegation tools.
|
|
19390
19412
|
|
|
@@ -19400,9 +19422,9 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
|
|
|
19400
19422
|
4. Follow the Synthesis Process below
|
|
19401
19423
|
5. Present the result to the user
|
|
19402
19424
|
|
|
19403
|
-
**Synthesis Process** (MANDATORY
|
|
19425
|
+
**Synthesis Process** (MANDATORY - follow in order):
|
|
19404
19426
|
1. Read the original user prompt
|
|
19405
|
-
2. Review each councillor's response individually
|
|
19427
|
+
2. Review each councillor's response individually - note each councillor's key insight and unique contribution by name
|
|
19406
19428
|
3. Identify agreements and contradictions between councillors
|
|
19407
19429
|
4. Resolve contradictions with explicit reasoning
|
|
19408
19430
|
5. Synthesize the optimal final answer
|
|
@@ -19416,7 +19438,7 @@ var COUNCIL_AGENT_PROMPT = `You are the Council agent — a multi-LLM orchestrat
|
|
|
19416
19438
|
- Do not omit per-councillor details from the final response
|
|
19417
19439
|
- Do not collapse the output into only a final summary
|
|
19418
19440
|
- Be transparent about trade-offs when different approaches have valid pros/cons
|
|
19419
|
-
- Don't just average responses
|
|
19441
|
+
- Don't just average responses - choose the best approach and improve upon it
|
|
19420
19442
|
|
|
19421
19443
|
${READONLY_FILE_OPERATIONS_RULES}
|
|
19422
19444
|
|
|
@@ -19477,10 +19499,10 @@ ${cr.result}`;
|
|
|
19477
19499
|
}).join(`
|
|
19478
19500
|
|
|
19479
19501
|
`);
|
|
19480
|
-
const failedSection = councillorResults.filter((cr) => cr.status !== "completed").map((cr) => `**${cr.name}**: ${cr.status}
|
|
19502
|
+
const failedSection = councillorResults.filter((cr) => cr.status !== "completed").map((cr) => `**${cr.name}**: ${cr.status} - ${cr.error ?? "Unknown"}`).join(`
|
|
19481
19503
|
`);
|
|
19482
19504
|
if (completedWithResults.length === 0) {
|
|
19483
|
-
const errorDetails = councillorResults.map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status}
|
|
19505
|
+
const errorDetails = councillorResults.map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} - ${cr.error ?? "Unknown"}`).join(`
|
|
19484
19506
|
`);
|
|
19485
19507
|
return `---
|
|
19486
19508
|
|
|
@@ -19538,12 +19560,12 @@ You CANNOT edit files, write files, run shell commands, or delegate to other age
|
|
|
19538
19560
|
${NO_SHELL_READONLY_FILE_OPERATIONS_RULES}
|
|
19539
19561
|
|
|
19540
19562
|
**Behavior**:
|
|
19541
|
-
- **Examine the codebase** before answering
|
|
19563
|
+
- **Examine the codebase** before answering - your read access is what makes council valuable. Don't guess at code you can see.
|
|
19542
19564
|
- Analyze the problem thoroughly
|
|
19543
19565
|
- Provide a complete, well-reasoned response
|
|
19544
19566
|
- Focus on the quality and correctness of your solution
|
|
19545
19567
|
- Be direct and concise
|
|
19546
|
-
- Don't be influenced by what other councillors might say
|
|
19568
|
+
- Don't be influenced by what other councillors might say - you won't see their responses
|
|
19547
19569
|
|
|
19548
19570
|
**Output**:
|
|
19549
19571
|
- Give your honest assessment
|
|
@@ -19574,7 +19596,7 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
|
|
|
19574
19596
|
|
|
19575
19597
|
**Typography**
|
|
19576
19598
|
- Choose distinctive, characterful fonts that elevate aesthetics
|
|
19577
|
-
- Avoid generic defaults (Arial, Inter)
|
|
19599
|
+
- Avoid generic defaults (Arial, Inter)-opt for unexpected, beautiful choices
|
|
19578
19600
|
- Pair display fonts with refined body fonts for hierarchy
|
|
19579
19601
|
|
|
19580
19602
|
**Color & Theme**
|
|
@@ -19591,7 +19613,7 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
|
|
|
19591
19613
|
|
|
19592
19614
|
**Spatial Composition**
|
|
19593
19615
|
- Break conventions: asymmetry, overlap, diagonal flow, grid-breaking
|
|
19594
|
-
- Generous negative space OR controlled density
|
|
19616
|
+
- Generous negative space OR controlled density-commit to the choice
|
|
19595
19617
|
- Unexpected layouts that guide the eye
|
|
19596
19618
|
|
|
19597
19619
|
**Visual Depth**
|
|
@@ -19600,7 +19622,7 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
|
|
|
19600
19622
|
- Contextual effects that match the aesthetic (grain overlays, custom cursors)
|
|
19601
19623
|
|
|
19602
19624
|
**Styling Approach**
|
|
19603
|
-
- Default to Tailwind CSS utility classes when available
|
|
19625
|
+
- Default to Tailwind CSS utility classes when available-fast, maintainable, consistent
|
|
19604
19626
|
- Use custom CSS when the vision requires it: complex animations, unique effects, advanced compositions
|
|
19605
19627
|
- Balance utility-first speed with creative freedom where it matters
|
|
19606
19628
|
|
|
@@ -19612,7 +19634,7 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
|
|
|
19612
19634
|
## Constraints
|
|
19613
19635
|
- Respect existing design systems when present
|
|
19614
19636
|
- Leverage component libraries where available
|
|
19615
|
-
- Prioritize visual excellence
|
|
19637
|
+
- Prioritize visual excellence-code perfection comes second
|
|
19616
19638
|
- Use grounded, normal, regular english - don't use jargon or overly technical language
|
|
19617
19639
|
|
|
19618
19640
|
${WRITABLE_FILE_OPERATIONS_RULES}
|
|
@@ -19716,7 +19738,7 @@ ${WRITABLE_FILE_OPERATIONS_RULES}
|
|
|
19716
19738
|
- NO external research (no websearch, context7, gh_grep)
|
|
19717
19739
|
- NO delegation or spawning subagents
|
|
19718
19740
|
- No multi-step research/planning; minimal execution sequence ok
|
|
19719
|
-
- If context is insufficient: use grep/glob/read directly
|
|
19741
|
+
- If context is insufficient: use grep/glob/read directly - do not delegate
|
|
19720
19742
|
- Only ask for missing inputs you truly cannot retrieve yourself
|
|
19721
19743
|
- Do not act as the primary reviewer; implement requested changes and surface obvious issues briefly
|
|
19722
19744
|
|
|
@@ -19806,21 +19828,21 @@ ${customAppendPrompt}`;
|
|
|
19806
19828
|
}
|
|
19807
19829
|
|
|
19808
19830
|
// src/agents/observer.ts
|
|
19809
|
-
var OBSERVER_PROMPT = `You are Observer
|
|
19831
|
+
var OBSERVER_PROMPT = `You are Observer - a visual analysis specialist.
|
|
19810
19832
|
|
|
19811
19833
|
**Role**: Interpret images, screenshots, PDFs, and diagrams. Extract structured observations for the Orchestrator to act on.
|
|
19812
19834
|
|
|
19813
19835
|
**Behavior**:
|
|
19814
19836
|
- Read the file(s) specified in the prompt
|
|
19815
|
-
- Analyze visual content
|
|
19816
|
-
- For screenshots with text/code/errors: extract the **exact text** via OCR
|
|
19837
|
+
- Analyze visual content - layouts, UI elements, text, relationships, flows
|
|
19838
|
+
- For screenshots with text/code/errors: extract the **exact text** via OCR - never paraphrase error messages or code
|
|
19817
19839
|
- For multiple files: analyze each, then compare or relate as requested
|
|
19818
19840
|
- Return ONLY the extracted information relevant to the goal
|
|
19819
|
-
- If the image is unclear, blurry, or partially visible: state what you CAN see and explicitly note what is uncertain
|
|
19841
|
+
- If the image is unclear, blurry, or partially visible: state what you CAN see and explicitly note what is uncertain - never guess or fabricate details
|
|
19820
19842
|
|
|
19821
19843
|
**Constraints**:
|
|
19822
19844
|
- READ-ONLY: Analyze and report, don't modify files
|
|
19823
|
-
- Save context tokens
|
|
19845
|
+
- Save context tokens - the Orchestrator never processes the raw file
|
|
19824
19846
|
- Match the language of the request
|
|
19825
19847
|
- If info not found, state clearly what's missing
|
|
19826
19848
|
|
|
@@ -19837,7 +19859,7 @@ ${customAppendPrompt}`;
|
|
|
19837
19859
|
}
|
|
19838
19860
|
return {
|
|
19839
19861
|
name: "observer",
|
|
19840
|
-
description: "Visual analysis. Use for interpreting images, screenshots, PDFs, and diagrams
|
|
19862
|
+
description: "Visual analysis. Use for interpreting images, screenshots, PDFs, and diagrams - extracts structured observations without loading raw files into main context. Requires a vision-capable model.",
|
|
19841
19863
|
config: {
|
|
19842
19864
|
model,
|
|
19843
19865
|
temperature: 0.1,
|
|
@@ -19974,7 +19996,7 @@ function applyOverrides(agent, override) {
|
|
|
19974
19996
|
if (override.model) {
|
|
19975
19997
|
if (Array.isArray(override.model)) {
|
|
19976
19998
|
agent._modelArray = override.model.map((m) => typeof m === "string" ? { id: m } : m);
|
|
19977
|
-
agent.config.model =
|
|
19999
|
+
agent.config.model = agent._modelArray[0].id;
|
|
19978
20000
|
} else {
|
|
19979
20001
|
agent.config.model = override.model;
|
|
19980
20002
|
}
|
|
@@ -20061,7 +20083,7 @@ var SUBAGENT_FACTORIES = {
|
|
|
20061
20083
|
council: createCouncilAgent,
|
|
20062
20084
|
councillor: createCouncillorAgent
|
|
20063
20085
|
};
|
|
20064
|
-
function createAgents(config) {
|
|
20086
|
+
function createAgents(config, options) {
|
|
20065
20087
|
const disabled = getDisabledAgents(config);
|
|
20066
20088
|
if (!config?.council) {
|
|
20067
20089
|
disabled.add("council");
|
|
@@ -20082,8 +20104,17 @@ function createAgents(config) {
|
|
|
20082
20104
|
return primaryModel ?? DEFAULT_MODELS[name];
|
|
20083
20105
|
};
|
|
20084
20106
|
const protoSubAgents = Object.entries(SUBAGENT_FACTORIES).filter(([name]) => !disabled.has(name)).map(([name, factory]) => {
|
|
20085
|
-
const
|
|
20086
|
-
|
|
20107
|
+
const agent = factory(getModelForAgent(name), undefined, undefined);
|
|
20108
|
+
const customPrompts = loadAgentPrompt(name, {
|
|
20109
|
+
preset: config?.preset,
|
|
20110
|
+
projectDirectory: options?.projectDirectory
|
|
20111
|
+
});
|
|
20112
|
+
const override = getAgentOverride(config, name);
|
|
20113
|
+
const inlinePrompt = override?.prompt;
|
|
20114
|
+
const defaultPrompt = agent.config.prompt ?? "";
|
|
20115
|
+
const basePrompt = inlinePrompt !== undefined ? inlinePrompt : defaultPrompt;
|
|
20116
|
+
agent.config.prompt = resolvePrompt(basePrompt, customPrompts.prompt, customPrompts.appendPrompt);
|
|
20117
|
+
return agent;
|
|
20087
20118
|
});
|
|
20088
20119
|
const customAgentNames = getCustomAgentNames(config).map(normalizeCustomAgentName).filter((name) => name.length > 0).filter((name) => {
|
|
20089
20120
|
if (!isSafeCustomAgentName(name)) {
|
|
@@ -20100,7 +20131,10 @@ function createAgents(config) {
|
|
|
20100
20131
|
console.warn(`[oh-my-opencode] Custom agent '${name}' skipped: 'model' is required`);
|
|
20101
20132
|
return [];
|
|
20102
20133
|
}
|
|
20103
|
-
const customPrompts = loadAgentPrompt(name,
|
|
20134
|
+
const customPrompts = loadAgentPrompt(name, {
|
|
20135
|
+
preset: config?.preset,
|
|
20136
|
+
projectDirectory: options?.projectDirectory
|
|
20137
|
+
});
|
|
20104
20138
|
return [
|
|
20105
20139
|
buildCustomAgentDefinition(name, override, customPrompts.prompt, customPrompts.appendPrompt)
|
|
20106
20140
|
];
|
|
@@ -20157,8 +20191,15 @@ function createAgents(config) {
|
|
|
20157
20191
|
];
|
|
20158
20192
|
const orchestratorOverride = getAgentOverride(config, "orchestrator");
|
|
20159
20193
|
const orchestratorModel = orchestratorOverride?.model ?? DEFAULT_MODELS.orchestrator;
|
|
20160
|
-
const orchestratorPrompts = loadAgentPrompt("orchestrator",
|
|
20161
|
-
|
|
20194
|
+
const orchestratorPrompts = loadAgentPrompt("orchestrator", {
|
|
20195
|
+
preset: config?.preset,
|
|
20196
|
+
projectDirectory: options?.projectDirectory
|
|
20197
|
+
});
|
|
20198
|
+
const orchestrator = createOrchestratorAgent(orchestratorModel, undefined, undefined, disabled);
|
|
20199
|
+
const inlineOrchestratorPrompt = orchestratorOverride?.prompt;
|
|
20200
|
+
const defaultOrchestratorPrompt = orchestrator.config.prompt ?? "";
|
|
20201
|
+
const baseOrchestratorPrompt = inlineOrchestratorPrompt !== undefined ? inlineOrchestratorPrompt : defaultOrchestratorPrompt;
|
|
20202
|
+
orchestrator.config.prompt = resolvePrompt(baseOrchestratorPrompt, orchestratorPrompts.prompt, orchestratorPrompts.appendPrompt);
|
|
20162
20203
|
applyDefaultPermissions(orchestrator, orchestratorOverride?.skills, config?.disabled_skills);
|
|
20163
20204
|
if (orchestratorOverride) {
|
|
20164
20205
|
applyOverrides(orchestrator, orchestratorOverride);
|
|
@@ -20172,7 +20213,7 @@ function createAgents(config) {
|
|
|
20172
20213
|
displayNameMap.set(agent.name, agent.displayName);
|
|
20173
20214
|
}
|
|
20174
20215
|
}
|
|
20175
|
-
const
|
|
20216
|
+
const extraOrchestratorPromptsList = [...builtInSubAgents, ...customSubAgents].map((agent) => {
|
|
20176
20217
|
const override = getAgentOverride(config, agent.name);
|
|
20177
20218
|
return override?.orchestratorPrompt;
|
|
20178
20219
|
}).filter((prompt) => Boolean(prompt));
|
|
@@ -20207,28 +20248,37 @@ function createAgents(config) {
|
|
|
20207
20248
|
}
|
|
20208
20249
|
}
|
|
20209
20250
|
injectDisplayNames(orchestrator, displayNameMap);
|
|
20210
|
-
const
|
|
20211
|
-
|
|
20212
|
-
|
|
20213
|
-
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20221
|
-
}
|
|
20222
|
-
|
|
20251
|
+
const rewritePrompt = (promptText) => {
|
|
20252
|
+
let text = promptText;
|
|
20253
|
+
for (const [internalName, displayName] of displayNameMap) {
|
|
20254
|
+
text = text.replace(new RegExp(`@${escapeRegExp(internalName)}\\b`, "g"), `@${normalizeDisplayName(displayName)}`);
|
|
20255
|
+
}
|
|
20256
|
+
return text;
|
|
20257
|
+
};
|
|
20258
|
+
const rewrittenOverrides = extraOrchestratorPromptsList.map(rewritePrompt);
|
|
20259
|
+
const rewrittenAcps = acpOrchestratorPrompts.map(rewritePrompt);
|
|
20260
|
+
let updatedPrompt = orchestrator.config.prompt ?? "";
|
|
20261
|
+
if (rewrittenOverrides.length > 0) {
|
|
20262
|
+
updatedPrompt = `${updatedPrompt}
|
|
20263
|
+
|
|
20264
|
+
# Project-specific routing guidance
|
|
20265
|
+
|
|
20266
|
+
${rewrittenOverrides.join(`
|
|
20223
20267
|
|
|
20224
|
-
|
|
20268
|
+
`)}`;
|
|
20269
|
+
}
|
|
20270
|
+
if (rewrittenAcps.length > 0) {
|
|
20271
|
+
updatedPrompt = `${updatedPrompt}
|
|
20272
|
+
|
|
20273
|
+
${rewrittenAcps.join(`
|
|
20225
20274
|
|
|
20226
20275
|
`)}`;
|
|
20227
20276
|
}
|
|
20277
|
+
orchestrator.config.prompt = updatedPrompt;
|
|
20228
20278
|
return [orchestrator, ...allSubAgents];
|
|
20229
20279
|
}
|
|
20230
|
-
function getAgentConfigs(config) {
|
|
20231
|
-
const agents = createAgents(config);
|
|
20280
|
+
function getAgentConfigs(config, options) {
|
|
20281
|
+
const agents = createAgents(config, options);
|
|
20232
20282
|
const applyClassification = (name, sdkConfig) => {
|
|
20233
20283
|
if (name === "council") {
|
|
20234
20284
|
sdkConfig.mode = "all";
|
|
@@ -20368,6 +20418,8 @@ function log(message, data) {
|
|
|
20368
20418
|
}
|
|
20369
20419
|
|
|
20370
20420
|
// src/companion/manager.ts
|
|
20421
|
+
var activeExitListener = null;
|
|
20422
|
+
var activeManagers = new Set;
|
|
20371
20423
|
function stateFilePath() {
|
|
20372
20424
|
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
20373
20425
|
const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(os2.homedir(), ".local", "share");
|
|
@@ -20446,6 +20498,7 @@ class CompanionManager {
|
|
|
20446
20498
|
}
|
|
20447
20499
|
onLoad() {
|
|
20448
20500
|
if (this.config?.enabled !== true) {
|
|
20501
|
+
CompanionManager.disposeActiveManagers(this.id);
|
|
20449
20502
|
try {
|
|
20450
20503
|
if (!existsSync2(stateFilePath()))
|
|
20451
20504
|
return;
|
|
@@ -20455,10 +20508,29 @@ class CompanionManager {
|
|
|
20455
20508
|
} catch {}
|
|
20456
20509
|
return;
|
|
20457
20510
|
}
|
|
20458
|
-
|
|
20511
|
+
this.registerActiveManager();
|
|
20459
20512
|
this.flush();
|
|
20460
20513
|
this.spawnIfAvailable();
|
|
20461
20514
|
}
|
|
20515
|
+
registerActiveManager() {
|
|
20516
|
+
for (const manager of [...activeManagers]) {
|
|
20517
|
+
if (manager !== this && manager.id === this.id) {
|
|
20518
|
+
manager.onExit();
|
|
20519
|
+
}
|
|
20520
|
+
}
|
|
20521
|
+
activeManagers.add(this);
|
|
20522
|
+
if (!activeExitListener) {
|
|
20523
|
+
activeExitListener = () => CompanionManager.disposeActiveManagers();
|
|
20524
|
+
process.on("exit", activeExitListener);
|
|
20525
|
+
}
|
|
20526
|
+
}
|
|
20527
|
+
static disposeActiveManagers(sessionId) {
|
|
20528
|
+
for (const manager of [...activeManagers]) {
|
|
20529
|
+
if (sessionId && manager.id !== sessionId)
|
|
20530
|
+
continue;
|
|
20531
|
+
manager.onExit();
|
|
20532
|
+
}
|
|
20533
|
+
}
|
|
20462
20534
|
onSessionStatus(input) {
|
|
20463
20535
|
if (this.config?.enabled !== true)
|
|
20464
20536
|
return;
|
|
@@ -20501,14 +20573,21 @@ class CompanionManager {
|
|
|
20501
20573
|
this.flush();
|
|
20502
20574
|
}
|
|
20503
20575
|
onExit() {
|
|
20504
|
-
|
|
20505
|
-
return;
|
|
20576
|
+
activeManagers.delete(this);
|
|
20506
20577
|
if (this.companionProcess) {
|
|
20507
20578
|
try {
|
|
20508
20579
|
this.companionProcess.kill();
|
|
20509
20580
|
} catch {}
|
|
20510
20581
|
this.companionProcess = null;
|
|
20511
20582
|
}
|
|
20583
|
+
if (activeManagers.size === 0 && activeExitListener) {
|
|
20584
|
+
try {
|
|
20585
|
+
process.removeListener("exit", activeExitListener);
|
|
20586
|
+
} catch {}
|
|
20587
|
+
activeExitListener = null;
|
|
20588
|
+
}
|
|
20589
|
+
if (this.config?.enabled !== true)
|
|
20590
|
+
return;
|
|
20512
20591
|
writeState((state) => {
|
|
20513
20592
|
state.sessions = state.sessions.filter((s) => s.session_id !== this.id);
|
|
20514
20593
|
});
|
|
@@ -21003,7 +21082,7 @@ class CouncilManager {
|
|
|
21003
21082
|
log(`[council-manager] Preset "${resolvedPreset}" has no councillors`);
|
|
21004
21083
|
return {
|
|
21005
21084
|
success: false,
|
|
21006
|
-
error: `Preset "${resolvedPreset}" has no councillors configured. Note: the reserved key "master" is ignored
|
|
21085
|
+
error: `Preset "${resolvedPreset}" has no councillors configured. Note: the reserved key "master" is ignored - use councillor names as keys`,
|
|
21007
21086
|
councillorResults: []
|
|
21008
21087
|
};
|
|
21009
21088
|
}
|
|
@@ -21039,7 +21118,7 @@ class CouncilManager {
|
|
|
21039
21118
|
}
|
|
21040
21119
|
async sendStartNotification(parentSessionId, councillorCount) {
|
|
21041
21120
|
const message = [
|
|
21042
|
-
`⎔ Council starting
|
|
21121
|
+
`⎔ Council starting - ${councillorCount} councillors launching - ctrl+x ↓ to watch`,
|
|
21043
21122
|
"",
|
|
21044
21123
|
"[system status: continue without acknowledging this notification]"
|
|
21045
21124
|
].join(`
|
|
@@ -23179,17 +23258,65 @@ function preparePackageUpdate(version, packageName = PACKAGE_NAME, runtimePackag
|
|
|
23179
23258
|
}
|
|
23180
23259
|
|
|
23181
23260
|
// src/hooks/auto-update-checker/skill-sync.ts
|
|
23261
|
+
import * as crypto from "node:crypto";
|
|
23182
23262
|
import {
|
|
23183
23263
|
copyFileSync as copyFileSync2,
|
|
23184
23264
|
existsSync as existsSync6,
|
|
23185
23265
|
lstatSync,
|
|
23186
23266
|
mkdirSync as mkdirSync4,
|
|
23187
|
-
mkdtempSync as mkdtempSync2,
|
|
23188
23267
|
readdirSync as readdirSync2,
|
|
23268
|
+
readFileSync as readFileSync6,
|
|
23189
23269
|
renameSync as renameSync3,
|
|
23190
|
-
rmSync as rmSync4
|
|
23270
|
+
rmSync as rmSync4,
|
|
23271
|
+
unlinkSync as unlinkSync2,
|
|
23272
|
+
writeFileSync as writeFileSync5
|
|
23191
23273
|
} from "node:fs";
|
|
23274
|
+
import * as os4 from "node:os";
|
|
23192
23275
|
import * as path10 from "node:path";
|
|
23276
|
+
var localProcessToken = globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN;
|
|
23277
|
+
if (!localProcessToken) {
|
|
23278
|
+
localProcessToken = crypto.randomUUID();
|
|
23279
|
+
globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN = localProcessToken;
|
|
23280
|
+
}
|
|
23281
|
+
var PROCESS_TOKEN = localProcessToken;
|
|
23282
|
+
var ACQUIRED_LOCKS = new Set;
|
|
23283
|
+
var LEGACY_MANAGED_SKILL_HASHES = {};
|
|
23284
|
+
function validateManifest(data) {
|
|
23285
|
+
if (typeof data !== "object" || data === null)
|
|
23286
|
+
return false;
|
|
23287
|
+
const d = data;
|
|
23288
|
+
if (d.schemaVersion !== 1)
|
|
23289
|
+
return false;
|
|
23290
|
+
if (typeof d.skills !== "object" || d.skills === null)
|
|
23291
|
+
return false;
|
|
23292
|
+
const allowedStatuses = new Set([
|
|
23293
|
+
"managed",
|
|
23294
|
+
"customized",
|
|
23295
|
+
"deleted",
|
|
23296
|
+
"conflict"
|
|
23297
|
+
]);
|
|
23298
|
+
const skillsObj = d.skills;
|
|
23299
|
+
for (const key of Object.keys(skillsObj)) {
|
|
23300
|
+
const entry = skillsObj[key];
|
|
23301
|
+
if (typeof entry !== "object" || entry === null)
|
|
23302
|
+
return false;
|
|
23303
|
+
if (typeof entry.status !== "string" || !allowedStatuses.has(entry.status))
|
|
23304
|
+
return false;
|
|
23305
|
+
if (typeof entry.packageVersion !== "string")
|
|
23306
|
+
return false;
|
|
23307
|
+
if (typeof entry.sourceHash !== "string")
|
|
23308
|
+
return false;
|
|
23309
|
+
if (typeof entry.lastManagedHash !== "string")
|
|
23310
|
+
return false;
|
|
23311
|
+
if (typeof entry.lastSeenHash !== "string")
|
|
23312
|
+
return false;
|
|
23313
|
+
if (entry.stagedPath !== undefined && typeof entry.stagedPath !== "string")
|
|
23314
|
+
return false;
|
|
23315
|
+
if (typeof entry.updatedAt !== "string")
|
|
23316
|
+
return false;
|
|
23317
|
+
}
|
|
23318
|
+
return true;
|
|
23319
|
+
}
|
|
23193
23320
|
function copyDirRecursive(src, dest) {
|
|
23194
23321
|
const stat2 = lstatSync(src);
|
|
23195
23322
|
if (stat2.isSymbolicLink()) {
|
|
@@ -23209,143 +23336,908 @@ function copyDirRecursive(src, dest) {
|
|
|
23209
23336
|
copyFileSync2(src, dest);
|
|
23210
23337
|
}
|
|
23211
23338
|
}
|
|
23212
|
-
function
|
|
23213
|
-
const
|
|
23214
|
-
const
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
|
|
23220
|
-
|
|
23221
|
-
|
|
23339
|
+
function computeDirectoryHash(dirPath) {
|
|
23340
|
+
const hash = crypto.createHash("sha256");
|
|
23341
|
+
const entriesToHash = [];
|
|
23342
|
+
function traverse(currentDir) {
|
|
23343
|
+
const entries = readdirSync2(currentDir);
|
|
23344
|
+
for (const entry of entries) {
|
|
23345
|
+
const absolutePath = path10.join(currentDir, entry);
|
|
23346
|
+
const stat2 = lstatSync(absolutePath);
|
|
23347
|
+
const relativePath = path10.relative(dirPath, absolutePath);
|
|
23348
|
+
if (stat2.isSymbolicLink()) {
|
|
23349
|
+
continue;
|
|
23350
|
+
}
|
|
23351
|
+
if (stat2.isDirectory()) {
|
|
23352
|
+
entriesToHash.push({
|
|
23353
|
+
relativePath,
|
|
23354
|
+
absolutePath,
|
|
23355
|
+
kind: "directory",
|
|
23356
|
+
mode: stat2.mode
|
|
23357
|
+
});
|
|
23358
|
+
traverse(absolutePath);
|
|
23359
|
+
} else if (stat2.isFile()) {
|
|
23360
|
+
entriesToHash.push({
|
|
23361
|
+
relativePath,
|
|
23362
|
+
absolutePath,
|
|
23363
|
+
kind: "file",
|
|
23364
|
+
mode: stat2.mode
|
|
23365
|
+
});
|
|
23366
|
+
}
|
|
23222
23367
|
}
|
|
23223
|
-
} catch {
|
|
23224
|
-
log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
|
|
23225
|
-
return { installed, skippedExisting, failed };
|
|
23226
23368
|
}
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
if (
|
|
23230
|
-
|
|
23369
|
+
traverse(dirPath);
|
|
23370
|
+
entriesToHash.sort((a, b) => {
|
|
23371
|
+
if (a.relativePath < b.relativePath)
|
|
23372
|
+
return -1;
|
|
23373
|
+
if (a.relativePath > b.relativePath)
|
|
23374
|
+
return 1;
|
|
23375
|
+
return 0;
|
|
23376
|
+
});
|
|
23377
|
+
for (const entry of entriesToHash) {
|
|
23378
|
+
hash.update(entry.kind);
|
|
23379
|
+
hash.update("\x00");
|
|
23380
|
+
hash.update(entry.relativePath);
|
|
23381
|
+
hash.update("\x00");
|
|
23382
|
+
hash.update(String(entry.mode & 4095));
|
|
23383
|
+
hash.update("\x00");
|
|
23384
|
+
if (entry.kind === "file") {
|
|
23385
|
+
const content = readFileSync6(entry.absolutePath);
|
|
23386
|
+
hash.update(content);
|
|
23231
23387
|
}
|
|
23232
|
-
} catch (err) {
|
|
23233
|
-
log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
|
|
23234
23388
|
}
|
|
23235
|
-
|
|
23389
|
+
return hash.digest("hex");
|
|
23390
|
+
}
|
|
23391
|
+
function isPidRunning(pid) {
|
|
23236
23392
|
try {
|
|
23237
|
-
|
|
23393
|
+
process.kill(pid, 0);
|
|
23394
|
+
return true;
|
|
23238
23395
|
} catch (err) {
|
|
23239
|
-
|
|
23240
|
-
return { installed, skippedExisting, failed };
|
|
23396
|
+
return err.code === "EPERM";
|
|
23241
23397
|
}
|
|
23242
|
-
|
|
23243
|
-
|
|
23398
|
+
}
|
|
23399
|
+
var CROSS_HOST_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
23400
|
+
function acquireLock(lockDir) {
|
|
23401
|
+
const metadataPath2 = path10.join(lockDir, "owner.json");
|
|
23402
|
+
const currentHost = os4.hostname();
|
|
23403
|
+
const currentPid = process.pid;
|
|
23404
|
+
const writeMetadata = () => {
|
|
23244
23405
|
try {
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23406
|
+
const metadata = {
|
|
23407
|
+
pid: currentPid,
|
|
23408
|
+
host: currentHost,
|
|
23409
|
+
time: Date.now(),
|
|
23410
|
+
token: PROCESS_TOKEN
|
|
23411
|
+
};
|
|
23412
|
+
writeFileSync5(metadataPath2, JSON.stringify(metadata), "utf-8");
|
|
23413
|
+
} catch {}
|
|
23414
|
+
};
|
|
23415
|
+
try {
|
|
23416
|
+
mkdirSync4(lockDir);
|
|
23417
|
+
writeMetadata();
|
|
23418
|
+
ACQUIRED_LOCKS.add(path10.resolve(lockDir));
|
|
23419
|
+
return true;
|
|
23420
|
+
} catch (err) {
|
|
23421
|
+
if (err.code !== "EEXIST") {
|
|
23422
|
+
throw err;
|
|
23423
|
+
}
|
|
23424
|
+
}
|
|
23425
|
+
try {
|
|
23426
|
+
let shouldSteal = false;
|
|
23427
|
+
let ageMs = 0;
|
|
23428
|
+
if (existsSync6(metadataPath2)) {
|
|
23253
23429
|
try {
|
|
23254
|
-
const
|
|
23255
|
-
|
|
23256
|
-
|
|
23430
|
+
const content = readFileSync6(metadataPath2, "utf-8");
|
|
23431
|
+
const metadata = JSON.parse(content);
|
|
23432
|
+
ageMs = Date.now() - metadata.time;
|
|
23433
|
+
if (metadata.host === currentHost) {
|
|
23434
|
+
if (!isPidRunning(metadata.pid)) {
|
|
23435
|
+
log(`[skill-sync] Lock owner process ${metadata.pid} is not running on this host. Recovery path.`);
|
|
23436
|
+
shouldSteal = true;
|
|
23437
|
+
}
|
|
23438
|
+
} else {
|
|
23439
|
+
if (ageMs > CROSS_HOST_LOCK_EXPIRY_MS) {
|
|
23440
|
+
log(`[skill-sync] Lock owned by different host ${metadata.host} has expired (${Math.round(ageMs / 1000)}s old). Reclaiming lock.`);
|
|
23441
|
+
shouldSteal = true;
|
|
23442
|
+
} else {
|
|
23443
|
+
log(`[skill-sync] Lock is owned by different host ${metadata.host}; failing closed.`);
|
|
23444
|
+
}
|
|
23257
23445
|
}
|
|
23258
23446
|
} catch {
|
|
23259
|
-
|
|
23447
|
+
shouldSteal = true;
|
|
23260
23448
|
}
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
} catch {}
|
|
23267
|
-
if (destExists) {
|
|
23268
|
-
log(`[skill-sync] Skill already exists in destination: ${entry}`);
|
|
23269
|
-
skippedExisting.push(entry);
|
|
23270
|
-
continue;
|
|
23449
|
+
} else {
|
|
23450
|
+
const stat2 = lstatSync(lockDir);
|
|
23451
|
+
ageMs = Date.now() - stat2.mtimeMs;
|
|
23452
|
+
if (ageMs > 30000) {
|
|
23453
|
+
shouldSteal = true;
|
|
23271
23454
|
}
|
|
23272
|
-
|
|
23455
|
+
}
|
|
23456
|
+
if (!shouldSteal)
|
|
23457
|
+
return false;
|
|
23458
|
+
log(`[skill-sync] Stealing/recovering lock directory.`);
|
|
23459
|
+
rmSync4(lockDir, { recursive: true, force: true });
|
|
23460
|
+
mkdirSync4(lockDir);
|
|
23461
|
+
writeMetadata();
|
|
23462
|
+
ACQUIRED_LOCKS.add(path10.resolve(lockDir));
|
|
23463
|
+
return true;
|
|
23464
|
+
} catch (err) {
|
|
23465
|
+
log(`[skill-sync] Failed to check/recover lock at ${lockDir}:`, err);
|
|
23466
|
+
return false;
|
|
23467
|
+
}
|
|
23468
|
+
}
|
|
23469
|
+
function releaseLock(lockDir) {
|
|
23470
|
+
const resolvedPath = path10.resolve(lockDir);
|
|
23471
|
+
try {
|
|
23472
|
+
let isOurLock = false;
|
|
23473
|
+
const metadataPath2 = path10.join(lockDir, "owner.json");
|
|
23474
|
+
if (existsSync6(metadataPath2)) {
|
|
23273
23475
|
try {
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
destExistsLate = true;
|
|
23279
|
-
} catch {}
|
|
23280
|
-
if (destExistsLate) {
|
|
23281
|
-
log(`[skill-sync] Destination path was created during staging for ${entry}, skipping promotion.`);
|
|
23282
|
-
skippedExisting.push(entry);
|
|
23476
|
+
const content = readFileSync6(metadataPath2, "utf-8");
|
|
23477
|
+
const metadata = JSON.parse(content);
|
|
23478
|
+
if (metadata.host === os4.hostname() && metadata.pid === process.pid && metadata.token === PROCESS_TOKEN) {
|
|
23479
|
+
isOurLock = true;
|
|
23283
23480
|
} else {
|
|
23284
|
-
|
|
23285
|
-
installed.push(entry);
|
|
23286
|
-
log(`[skill-sync] Successfully synced skill: ${entry}`);
|
|
23481
|
+
isOurLock = false;
|
|
23287
23482
|
}
|
|
23288
23483
|
} catch (err) {
|
|
23289
|
-
log(`[skill-sync]
|
|
23290
|
-
|
|
23291
|
-
} finally {
|
|
23292
|
-
try {
|
|
23293
|
-
if (existsSync6(stagingDir)) {
|
|
23294
|
-
rmSync4(stagingDir, { recursive: true, force: true });
|
|
23295
|
-
}
|
|
23296
|
-
} catch (err) {
|
|
23297
|
-
log(`[skill-sync] Failed to clean up staging directory ${stagingDir}:`, err);
|
|
23298
|
-
}
|
|
23484
|
+
log(`[skill-sync] Lock owner.json is unreadable/corrupt:`, err);
|
|
23485
|
+
isOurLock = false;
|
|
23299
23486
|
}
|
|
23300
|
-
}
|
|
23301
|
-
|
|
23302
|
-
failed.push(entry);
|
|
23487
|
+
} else if (ACQUIRED_LOCKS.has(resolvedPath)) {
|
|
23488
|
+
isOurLock = true;
|
|
23303
23489
|
}
|
|
23490
|
+
if (isOurLock) {
|
|
23491
|
+
if (existsSync6(lockDir)) {
|
|
23492
|
+
rmSync4(lockDir, { recursive: true, force: true });
|
|
23493
|
+
}
|
|
23494
|
+
} else if (existsSync6(lockDir)) {
|
|
23495
|
+
log(`[skill-sync] Skipping lock directory removal: lock is not owned by this process/token or owner.json check failed.`);
|
|
23496
|
+
}
|
|
23497
|
+
} catch (err) {
|
|
23498
|
+
log(`[skill-sync] Failed to release lock at ${lockDir}:`, err);
|
|
23499
|
+
} finally {
|
|
23500
|
+
ACQUIRED_LOCKS.delete(resolvedPath);
|
|
23304
23501
|
}
|
|
23305
|
-
return { installed, skippedExisting, failed };
|
|
23306
23502
|
}
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23319
|
-
|
|
23320
|
-
|
|
23321
|
-
|
|
23322
|
-
|
|
23323
|
-
|
|
23324
|
-
|
|
23325
|
-
|
|
23326
|
-
|
|
23503
|
+
function atomicReplaceDir(sourceDir, destDir) {
|
|
23504
|
+
const parentDir = path10.dirname(destDir);
|
|
23505
|
+
if (!existsSync6(parentDir)) {
|
|
23506
|
+
mkdirSync4(parentDir, { recursive: true });
|
|
23507
|
+
}
|
|
23508
|
+
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
23509
|
+
const stagingDir = path10.join(parentDir, `.staging-${path10.basename(destDir)}-${uniqueSuffix}`);
|
|
23510
|
+
const backupDir = path10.join(parentDir, `.backup-${path10.basename(destDir)}-${uniqueSuffix}`);
|
|
23511
|
+
let backupCreated = false;
|
|
23512
|
+
try {
|
|
23513
|
+
copyDirRecursive(sourceDir, stagingDir);
|
|
23514
|
+
if (existsSync6(destDir)) {
|
|
23515
|
+
renameSync3(destDir, backupDir);
|
|
23516
|
+
backupCreated = true;
|
|
23517
|
+
}
|
|
23518
|
+
renameSync3(stagingDir, destDir);
|
|
23519
|
+
if (backupCreated) {
|
|
23520
|
+
rmSync4(backupDir, { recursive: true, force: true });
|
|
23521
|
+
}
|
|
23522
|
+
} catch (err) {
|
|
23523
|
+
log(`[skill-sync] Error during atomic replace for ${destDir}. Rolling back:`, err);
|
|
23524
|
+
if (backupCreated) {
|
|
23525
|
+
try {
|
|
23526
|
+
if (existsSync6(destDir)) {
|
|
23527
|
+
rmSync4(destDir, { recursive: true, force: true });
|
|
23327
23528
|
}
|
|
23328
|
-
|
|
23329
|
-
|
|
23330
|
-
});
|
|
23331
|
-
}
|
|
23529
|
+
renameSync3(backupDir, destDir);
|
|
23530
|
+
} catch (rollbackErr) {
|
|
23531
|
+
log(`[skill-sync] Critical error during rollback for ${destDir}:`, rollbackErr);
|
|
23532
|
+
}
|
|
23332
23533
|
}
|
|
23333
|
-
|
|
23334
|
-
|
|
23335
|
-
|
|
23336
|
-
|
|
23337
|
-
|
|
23338
|
-
|
|
23339
|
-
return;
|
|
23534
|
+
try {
|
|
23535
|
+
if (existsSync6(stagingDir)) {
|
|
23536
|
+
rmSync4(stagingDir, { recursive: true, force: true });
|
|
23537
|
+
}
|
|
23538
|
+
} catch {}
|
|
23539
|
+
throw err;
|
|
23340
23540
|
}
|
|
23341
|
-
|
|
23342
|
-
|
|
23343
|
-
if (!
|
|
23344
|
-
|
|
23345
|
-
|
|
23541
|
+
}
|
|
23542
|
+
function matchesArtifactPattern(entry, prefix2, skillName) {
|
|
23543
|
+
if (!entry.startsWith(prefix2))
|
|
23544
|
+
return false;
|
|
23545
|
+
const rest = entry.slice(prefix2.length);
|
|
23546
|
+
if (!rest.startsWith(`${skillName}-`))
|
|
23547
|
+
return false;
|
|
23548
|
+
const suffix2 = rest.slice(skillName.length + 1);
|
|
23549
|
+
const firstPart = suffix2.split("-")[0];
|
|
23550
|
+
const timestamp = Number(firstPart);
|
|
23551
|
+
if (Number.isNaN(timestamp) || timestamp <= 0)
|
|
23552
|
+
return false;
|
|
23553
|
+
return true;
|
|
23554
|
+
}
|
|
23555
|
+
function recoverOrphanArtifacts(destSkillsDir, skillName) {
|
|
23556
|
+
if (!existsSync6(destSkillsDir))
|
|
23557
|
+
return false;
|
|
23558
|
+
let hadArtifacts = false;
|
|
23559
|
+
let entries = [];
|
|
23560
|
+
try {
|
|
23561
|
+
entries = readdirSync2(destSkillsDir);
|
|
23562
|
+
} catch {
|
|
23563
|
+
return false;
|
|
23346
23564
|
}
|
|
23347
|
-
const
|
|
23348
|
-
const
|
|
23565
|
+
const backups = [];
|
|
23566
|
+
const stagings = [];
|
|
23567
|
+
for (const entry of entries) {
|
|
23568
|
+
if (matchesArtifactPattern(entry, ".backup-", skillName)) {
|
|
23569
|
+
backups.push(path10.join(destSkillsDir, entry));
|
|
23570
|
+
hadArtifacts = true;
|
|
23571
|
+
} else if (matchesArtifactPattern(entry, ".staging-", skillName)) {
|
|
23572
|
+
stagings.push(path10.join(destSkillsDir, entry));
|
|
23573
|
+
hadArtifacts = true;
|
|
23574
|
+
}
|
|
23575
|
+
}
|
|
23576
|
+
const destPath = path10.join(destSkillsDir, skillName);
|
|
23577
|
+
if (backups.length > 0) {
|
|
23578
|
+
backups.sort();
|
|
23579
|
+
const mostRecentBackup = backups[backups.length - 1];
|
|
23580
|
+
if (!existsSync6(destPath)) {
|
|
23581
|
+
backups.pop();
|
|
23582
|
+
try {
|
|
23583
|
+
renameSync3(mostRecentBackup, destPath);
|
|
23584
|
+
log(`[skill-sync] Recovered backup for ${skillName} back to destination.`);
|
|
23585
|
+
} catch (err) {
|
|
23586
|
+
log(`[skill-sync] Failed to restore backup for ${skillName}:`, err);
|
|
23587
|
+
}
|
|
23588
|
+
}
|
|
23589
|
+
for (const backup of backups) {
|
|
23590
|
+
try {
|
|
23591
|
+
rmSync4(backup, { recursive: true, force: true });
|
|
23592
|
+
} catch (err) {
|
|
23593
|
+
log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
|
|
23594
|
+
}
|
|
23595
|
+
}
|
|
23596
|
+
}
|
|
23597
|
+
for (const staging of stagings) {
|
|
23598
|
+
try {
|
|
23599
|
+
rmSync4(staging, { recursive: true, force: true });
|
|
23600
|
+
} catch (err) {
|
|
23601
|
+
log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
|
|
23602
|
+
}
|
|
23603
|
+
}
|
|
23604
|
+
return hadArtifacts;
|
|
23605
|
+
}
|
|
23606
|
+
function removeManagedStagedPath(stagedPath, manifestDir, skillName) {
|
|
23607
|
+
try {
|
|
23608
|
+
const absoluteStagedPath = path10.resolve(stagedPath);
|
|
23609
|
+
const absoluteAllowedRoot = path10.resolve(path10.join(manifestDir, "skill-updates"));
|
|
23610
|
+
const relative2 = path10.relative(absoluteAllowedRoot, absoluteStagedPath);
|
|
23611
|
+
const isUnderRoot = relative2 && !relative2.startsWith("..") && !path10.isAbsolute(relative2);
|
|
23612
|
+
if (isUnderRoot) {
|
|
23613
|
+
if (existsSync6(absoluteStagedPath)) {
|
|
23614
|
+
rmSync4(absoluteStagedPath, { recursive: true, force: true });
|
|
23615
|
+
log(`[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`);
|
|
23616
|
+
}
|
|
23617
|
+
} else {
|
|
23618
|
+
log(`[skill-sync] Refusing to delete staged path for ${skillName}: path ${absoluteStagedPath} is not under managed root ${absoluteAllowedRoot}`);
|
|
23619
|
+
}
|
|
23620
|
+
} catch (err) {
|
|
23621
|
+
log(`[skill-sync] Error while trying to verify and remove staged path for ${skillName} (${stagedPath}):`, err);
|
|
23622
|
+
}
|
|
23623
|
+
}
|
|
23624
|
+
function syncBundledSkillsFromPackage(packageRoot, options = {}) {
|
|
23625
|
+
const installed = [];
|
|
23626
|
+
const skippedExisting = [];
|
|
23627
|
+
const failed = [];
|
|
23628
|
+
const staged = [];
|
|
23629
|
+
const adopted = [];
|
|
23630
|
+
const customized = [];
|
|
23631
|
+
const sourceSkillsDir = path10.join(packageRoot, "src", "skills");
|
|
23632
|
+
try {
|
|
23633
|
+
const stat2 = lstatSync(sourceSkillsDir);
|
|
23634
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
23635
|
+
log(`[skill-sync] Source skills directory is not a valid directory: ${sourceSkillsDir}`);
|
|
23636
|
+
return {
|
|
23637
|
+
installed,
|
|
23638
|
+
skippedExisting,
|
|
23639
|
+
failed,
|
|
23640
|
+
staged,
|
|
23641
|
+
adopted,
|
|
23642
|
+
customized
|
|
23643
|
+
};
|
|
23644
|
+
}
|
|
23645
|
+
} catch {
|
|
23646
|
+
log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
|
|
23647
|
+
return {
|
|
23648
|
+
installed,
|
|
23649
|
+
skippedExisting,
|
|
23650
|
+
failed,
|
|
23651
|
+
staged,
|
|
23652
|
+
adopted,
|
|
23653
|
+
customized
|
|
23654
|
+
};
|
|
23655
|
+
}
|
|
23656
|
+
let packageVersion = "unknown";
|
|
23657
|
+
try {
|
|
23658
|
+
const pkgJsonPath = path10.join(packageRoot, "package.json");
|
|
23659
|
+
if (existsSync6(pkgJsonPath)) {
|
|
23660
|
+
const content = readFileSync6(pkgJsonPath, "utf-8");
|
|
23661
|
+
const pkg = JSON.parse(content);
|
|
23662
|
+
if (pkg.version) {
|
|
23663
|
+
packageVersion = pkg.version;
|
|
23664
|
+
}
|
|
23665
|
+
}
|
|
23666
|
+
} catch (err) {
|
|
23667
|
+
log(`[skill-sync] Failed to read package version from ${packageRoot}:`, err);
|
|
23668
|
+
}
|
|
23669
|
+
const manifestDir = path10.join(getConfigDir(), ".oh-my-opencode-slim");
|
|
23670
|
+
const lockDir = path10.join(manifestDir, "skills.lock");
|
|
23671
|
+
try {
|
|
23672
|
+
mkdirSync4(manifestDir, { recursive: true });
|
|
23673
|
+
} catch (err) {
|
|
23674
|
+
log(`[skill-sync] Failed to create manifest directory: ${manifestDir}`, err);
|
|
23675
|
+
}
|
|
23676
|
+
if (!acquireLock(lockDir)) {
|
|
23677
|
+
log("[skill-sync] Failed to acquire lock for skill synchronization. Skipping.");
|
|
23678
|
+
return {
|
|
23679
|
+
installed,
|
|
23680
|
+
skippedExisting,
|
|
23681
|
+
failed: ["__lock__"],
|
|
23682
|
+
staged,
|
|
23683
|
+
adopted,
|
|
23684
|
+
customized
|
|
23685
|
+
};
|
|
23686
|
+
}
|
|
23687
|
+
try {
|
|
23688
|
+
const manifestPath = path10.join(manifestDir, "skills-manifest.json");
|
|
23689
|
+
let manifest = {
|
|
23690
|
+
schemaVersion: 1,
|
|
23691
|
+
updatedAt: new Date().toISOString(),
|
|
23692
|
+
skills: {}
|
|
23693
|
+
};
|
|
23694
|
+
let isManifestCorrupt = false;
|
|
23695
|
+
if (existsSync6(manifestPath)) {
|
|
23696
|
+
try {
|
|
23697
|
+
const content = readFileSync6(manifestPath, "utf-8");
|
|
23698
|
+
const parsed = JSON.parse(content);
|
|
23699
|
+
if (validateManifest(parsed)) {
|
|
23700
|
+
manifest = parsed;
|
|
23701
|
+
} else {
|
|
23702
|
+
throw new Error("Manifest validation failed");
|
|
23703
|
+
}
|
|
23704
|
+
} catch (err) {
|
|
23705
|
+
log("[skill-sync] Manifest is corrupt/unreadable. Failing closed.", err);
|
|
23706
|
+
isManifestCorrupt = true;
|
|
23707
|
+
}
|
|
23708
|
+
}
|
|
23709
|
+
const destSkillsDir = path10.join(getConfigDir(), "skills");
|
|
23710
|
+
try {
|
|
23711
|
+
if (!existsSync6(destSkillsDir)) {
|
|
23712
|
+
mkdirSync4(destSkillsDir, { recursive: true });
|
|
23713
|
+
}
|
|
23714
|
+
} catch (err) {
|
|
23715
|
+
log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
|
|
23716
|
+
}
|
|
23717
|
+
const skillsToProcess = (options.skills ?? CUSTOM_SKILLS).map((s) => ({
|
|
23718
|
+
name: s.name,
|
|
23719
|
+
sourcePath: s.sourcePath
|
|
23720
|
+
}));
|
|
23721
|
+
for (const skill of skillsToProcess) {
|
|
23722
|
+
try {
|
|
23723
|
+
const sourcePath = path10.join(packageRoot, skill.sourcePath);
|
|
23724
|
+
try {
|
|
23725
|
+
const stat2 = lstatSync(sourcePath);
|
|
23726
|
+
if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
|
|
23727
|
+
continue;
|
|
23728
|
+
}
|
|
23729
|
+
const skillMdPath = path10.join(sourcePath, "SKILL.md");
|
|
23730
|
+
const skillMdStat = lstatSync(skillMdPath);
|
|
23731
|
+
if (skillMdStat.isSymbolicLink() || !skillMdStat.isFile()) {
|
|
23732
|
+
continue;
|
|
23733
|
+
}
|
|
23734
|
+
} catch {
|
|
23735
|
+
continue;
|
|
23736
|
+
}
|
|
23737
|
+
const destPath = path10.join(destSkillsDir, skill.name);
|
|
23738
|
+
const hadArtifacts = recoverOrphanArtifacts(destSkillsDir, skill.name);
|
|
23739
|
+
let destExists = false;
|
|
23740
|
+
let destIsDir = false;
|
|
23741
|
+
try {
|
|
23742
|
+
const destStat = lstatSync(destPath);
|
|
23743
|
+
destExists = true;
|
|
23744
|
+
destIsDir = destStat.isDirectory() && !destStat.isSymbolicLink();
|
|
23745
|
+
} catch {}
|
|
23746
|
+
if (destExists && !destIsDir) {
|
|
23747
|
+
log(`[skill-sync] Skill ${skill.name} destination is a file or symlink (conflict). Skipping.`);
|
|
23748
|
+
skippedExisting.push(skill.name);
|
|
23749
|
+
const sourceHash2 = computeDirectoryHash(sourcePath);
|
|
23750
|
+
const entry2 = manifest.skills[skill.name];
|
|
23751
|
+
if (entry2?.stagedPath) {
|
|
23752
|
+
removeManagedStagedPath(entry2.stagedPath, manifestDir, skill.name);
|
|
23753
|
+
}
|
|
23754
|
+
manifest.skills[skill.name] = {
|
|
23755
|
+
status: "conflict",
|
|
23756
|
+
packageVersion,
|
|
23757
|
+
sourceHash: sourceHash2,
|
|
23758
|
+
lastManagedHash: "",
|
|
23759
|
+
lastSeenHash: "",
|
|
23760
|
+
updatedAt: new Date().toISOString()
|
|
23761
|
+
};
|
|
23762
|
+
continue;
|
|
23763
|
+
}
|
|
23764
|
+
const sourceHash = computeDirectoryHash(sourcePath);
|
|
23765
|
+
if (isManifestCorrupt) {
|
|
23766
|
+
if (!destExists) {
|
|
23767
|
+
try {
|
|
23768
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
23769
|
+
installed.push(skill.name);
|
|
23770
|
+
manifest.skills[skill.name] = {
|
|
23771
|
+
status: "managed",
|
|
23772
|
+
packageVersion,
|
|
23773
|
+
sourceHash,
|
|
23774
|
+
lastManagedHash: sourceHash,
|
|
23775
|
+
lastSeenHash: sourceHash,
|
|
23776
|
+
updatedAt: new Date().toISOString()
|
|
23777
|
+
};
|
|
23778
|
+
} catch (err) {
|
|
23779
|
+
log(`[skill-sync] Failed to install missing skill ${skill.name} (corrupt manifest mode):`, err);
|
|
23780
|
+
failed.push(skill.name);
|
|
23781
|
+
}
|
|
23782
|
+
} else {
|
|
23783
|
+
log(`[skill-sync] Skipping existing skill ${skill.name} because manifest is corrupt.`);
|
|
23784
|
+
skippedExisting.push(skill.name);
|
|
23785
|
+
const destHash2 = computeDirectoryHash(destPath);
|
|
23786
|
+
if (destHash2 === sourceHash) {
|
|
23787
|
+
manifest.skills[skill.name] = {
|
|
23788
|
+
status: "managed",
|
|
23789
|
+
packageVersion,
|
|
23790
|
+
sourceHash,
|
|
23791
|
+
lastManagedHash: sourceHash,
|
|
23792
|
+
lastSeenHash: sourceHash,
|
|
23793
|
+
updatedAt: new Date().toISOString()
|
|
23794
|
+
};
|
|
23795
|
+
} else {
|
|
23796
|
+
try {
|
|
23797
|
+
const stagedSkillDir = path10.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
23798
|
+
if (existsSync6(stagedSkillDir)) {
|
|
23799
|
+
rmSync4(stagedSkillDir, { recursive: true, force: true });
|
|
23800
|
+
}
|
|
23801
|
+
mkdirSync4(stagedSkillDir, { recursive: true });
|
|
23802
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
23803
|
+
manifest.skills[skill.name] = {
|
|
23804
|
+
status: "customized",
|
|
23805
|
+
packageVersion,
|
|
23806
|
+
sourceHash,
|
|
23807
|
+
lastManagedHash: "",
|
|
23808
|
+
lastSeenHash: destHash2,
|
|
23809
|
+
stagedPath: stagedSkillDir,
|
|
23810
|
+
updatedAt: new Date().toISOString()
|
|
23811
|
+
};
|
|
23812
|
+
staged.push(skill.name);
|
|
23813
|
+
customized.push(skill.name);
|
|
23814
|
+
} catch (err) {
|
|
23815
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name} during recovery:`, err);
|
|
23816
|
+
manifest.skills[skill.name] = {
|
|
23817
|
+
status: "customized",
|
|
23818
|
+
packageVersion: "unknown",
|
|
23819
|
+
sourceHash: "",
|
|
23820
|
+
lastManagedHash: "",
|
|
23821
|
+
lastSeenHash: destHash2,
|
|
23822
|
+
updatedAt: new Date().toISOString()
|
|
23823
|
+
};
|
|
23824
|
+
}
|
|
23825
|
+
}
|
|
23826
|
+
}
|
|
23827
|
+
continue;
|
|
23828
|
+
}
|
|
23829
|
+
const entry = manifest.skills[skill.name];
|
|
23830
|
+
if (!destExists) {
|
|
23831
|
+
if (entry && entry.status === "deleted") {
|
|
23832
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user. Skipping.`);
|
|
23833
|
+
skippedExisting.push(skill.name);
|
|
23834
|
+
continue;
|
|
23835
|
+
}
|
|
23836
|
+
if (entry && entry.status !== "deleted") {
|
|
23837
|
+
if (hadArtifacts) {
|
|
23838
|
+
log(`[skill-sync] Managed skill ${skill.name} has backup/staging artifacts. Skipping delete, re-installing.`);
|
|
23839
|
+
try {
|
|
23840
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
23841
|
+
installed.push(skill.name);
|
|
23842
|
+
manifest.skills[skill.name] = {
|
|
23843
|
+
status: "managed",
|
|
23844
|
+
packageVersion,
|
|
23845
|
+
sourceHash,
|
|
23846
|
+
lastManagedHash: sourceHash,
|
|
23847
|
+
lastSeenHash: sourceHash,
|
|
23848
|
+
updatedAt: new Date().toISOString()
|
|
23849
|
+
};
|
|
23850
|
+
} catch (err) {
|
|
23851
|
+
log(`[skill-sync] Failed to re-install skill ${skill.name}:`, err);
|
|
23852
|
+
failed.push(skill.name);
|
|
23853
|
+
}
|
|
23854
|
+
continue;
|
|
23855
|
+
} else {
|
|
23856
|
+
if (entry.stagedPath) {
|
|
23857
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
23858
|
+
delete entry.stagedPath;
|
|
23859
|
+
}
|
|
23860
|
+
const rawEntry = entry;
|
|
23861
|
+
delete rawEntry.stagedVersion;
|
|
23862
|
+
delete rawEntry.stagedHash;
|
|
23863
|
+
entry.status = "deleted";
|
|
23864
|
+
entry.updatedAt = new Date().toISOString();
|
|
23865
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user (detected now). Skipping.`);
|
|
23866
|
+
skippedExisting.push(skill.name);
|
|
23867
|
+
continue;
|
|
23868
|
+
}
|
|
23869
|
+
}
|
|
23870
|
+
try {
|
|
23871
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
23872
|
+
installed.push(skill.name);
|
|
23873
|
+
manifest.skills[skill.name] = {
|
|
23874
|
+
status: "managed",
|
|
23875
|
+
packageVersion,
|
|
23876
|
+
sourceHash,
|
|
23877
|
+
lastManagedHash: sourceHash,
|
|
23878
|
+
lastSeenHash: sourceHash,
|
|
23879
|
+
updatedAt: new Date().toISOString()
|
|
23880
|
+
};
|
|
23881
|
+
log(`[skill-sync] Successfully installed missing skill: ${skill.name}`);
|
|
23882
|
+
} catch (err) {
|
|
23883
|
+
log(`[skill-sync] Failed to install skill ${skill.name}:`, err);
|
|
23884
|
+
failed.push(skill.name);
|
|
23885
|
+
}
|
|
23886
|
+
continue;
|
|
23887
|
+
}
|
|
23888
|
+
const destHash = computeDirectoryHash(destPath);
|
|
23889
|
+
if (entry) {
|
|
23890
|
+
if (entry.status === "managed") {
|
|
23891
|
+
if (destHash === entry.lastManagedHash) {
|
|
23892
|
+
if (destHash === sourceHash) {
|
|
23893
|
+
entry.packageVersion = packageVersion;
|
|
23894
|
+
entry.sourceHash = sourceHash;
|
|
23895
|
+
entry.lastManagedHash = sourceHash;
|
|
23896
|
+
entry.lastSeenHash = sourceHash;
|
|
23897
|
+
entry.updatedAt = new Date().toISOString();
|
|
23898
|
+
skippedExisting.push(skill.name);
|
|
23899
|
+
} else {
|
|
23900
|
+
try {
|
|
23901
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
23902
|
+
installed.push(skill.name);
|
|
23903
|
+
manifest.skills[skill.name] = {
|
|
23904
|
+
status: "managed",
|
|
23905
|
+
packageVersion,
|
|
23906
|
+
sourceHash,
|
|
23907
|
+
lastManagedHash: sourceHash,
|
|
23908
|
+
lastSeenHash: sourceHash,
|
|
23909
|
+
updatedAt: new Date().toISOString()
|
|
23910
|
+
};
|
|
23911
|
+
log(`[skill-sync] Updated managed skill: ${skill.name}`);
|
|
23912
|
+
} catch (err) {
|
|
23913
|
+
log(`[skill-sync] Failed to update managed skill ${skill.name}:`, err);
|
|
23914
|
+
failed.push(skill.name);
|
|
23915
|
+
}
|
|
23916
|
+
}
|
|
23917
|
+
} else {
|
|
23918
|
+
if (destHash === sourceHash) {
|
|
23919
|
+
manifest.skills[skill.name] = {
|
|
23920
|
+
status: "managed",
|
|
23921
|
+
packageVersion,
|
|
23922
|
+
sourceHash,
|
|
23923
|
+
lastManagedHash: sourceHash,
|
|
23924
|
+
lastSeenHash: sourceHash,
|
|
23925
|
+
updatedAt: new Date().toISOString()
|
|
23926
|
+
};
|
|
23927
|
+
skippedExisting.push(skill.name);
|
|
23928
|
+
} else {
|
|
23929
|
+
try {
|
|
23930
|
+
const stagedSkillDir = path10.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
23931
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
23932
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
23933
|
+
}
|
|
23934
|
+
if (existsSync6(stagedSkillDir)) {
|
|
23935
|
+
rmSync4(stagedSkillDir, { recursive: true, force: true });
|
|
23936
|
+
}
|
|
23937
|
+
mkdirSync4(stagedSkillDir, { recursive: true });
|
|
23938
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
23939
|
+
entry.status = "customized";
|
|
23940
|
+
entry.lastSeenHash = destHash;
|
|
23941
|
+
entry.stagedPath = stagedSkillDir;
|
|
23942
|
+
entry.sourceHash = sourceHash;
|
|
23943
|
+
entry.packageVersion = packageVersion;
|
|
23944
|
+
entry.updatedAt = new Date().toISOString();
|
|
23945
|
+
staged.push(skill.name);
|
|
23946
|
+
customized.push(skill.name);
|
|
23947
|
+
skippedExisting.push(skill.name);
|
|
23948
|
+
log(`[skill-sync] Skill ${skill.name} is customized. Staged update at ${stagedSkillDir}`);
|
|
23949
|
+
} catch (err) {
|
|
23950
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
23951
|
+
failed.push(skill.name);
|
|
23952
|
+
}
|
|
23953
|
+
}
|
|
23954
|
+
}
|
|
23955
|
+
} else if (entry.status === "customized") {
|
|
23956
|
+
if (destHash === sourceHash) {
|
|
23957
|
+
if (entry.stagedPath) {
|
|
23958
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
23959
|
+
}
|
|
23960
|
+
entry.status = "managed";
|
|
23961
|
+
entry.lastManagedHash = sourceHash;
|
|
23962
|
+
entry.lastSeenHash = sourceHash;
|
|
23963
|
+
entry.sourceHash = sourceHash;
|
|
23964
|
+
entry.packageVersion = packageVersion;
|
|
23965
|
+
delete entry.stagedPath;
|
|
23966
|
+
entry.updatedAt = new Date().toISOString();
|
|
23967
|
+
adopted.push(skill.name);
|
|
23968
|
+
skippedExisting.push(skill.name);
|
|
23969
|
+
log(`[skill-sync] Customized skill ${skill.name} converged with current version. Adopted back to managed.`);
|
|
23970
|
+
} else {
|
|
23971
|
+
entry.lastSeenHash = destHash;
|
|
23972
|
+
entry.updatedAt = new Date().toISOString();
|
|
23973
|
+
if (destHash !== sourceHash && entry.sourceHash !== sourceHash) {
|
|
23974
|
+
try {
|
|
23975
|
+
const stagedSkillDir = path10.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
23976
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
23977
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
23978
|
+
}
|
|
23979
|
+
if (existsSync6(stagedSkillDir)) {
|
|
23980
|
+
rmSync4(stagedSkillDir, { recursive: true, force: true });
|
|
23981
|
+
}
|
|
23982
|
+
mkdirSync4(stagedSkillDir, { recursive: true });
|
|
23983
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
23984
|
+
entry.stagedPath = stagedSkillDir;
|
|
23985
|
+
entry.sourceHash = sourceHash;
|
|
23986
|
+
entry.packageVersion = packageVersion;
|
|
23987
|
+
staged.push(skill.name);
|
|
23988
|
+
customized.push(skill.name);
|
|
23989
|
+
skippedExisting.push(skill.name);
|
|
23990
|
+
log(`[skill-sync] Staged new update for customized skill ${skill.name} at ${stagedSkillDir}`);
|
|
23991
|
+
} catch (err) {
|
|
23992
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
23993
|
+
failed.push(skill.name);
|
|
23994
|
+
}
|
|
23995
|
+
} else {
|
|
23996
|
+
customized.push(skill.name);
|
|
23997
|
+
skippedExisting.push(skill.name);
|
|
23998
|
+
}
|
|
23999
|
+
}
|
|
24000
|
+
} else if (entry.status === "deleted") {
|
|
24001
|
+
if (destHash === sourceHash) {
|
|
24002
|
+
entry.status = "managed";
|
|
24003
|
+
entry.packageVersion = packageVersion;
|
|
24004
|
+
entry.sourceHash = sourceHash;
|
|
24005
|
+
entry.lastManagedHash = sourceHash;
|
|
24006
|
+
entry.lastSeenHash = sourceHash;
|
|
24007
|
+
entry.updatedAt = new Date().toISOString();
|
|
24008
|
+
skippedExisting.push(skill.name);
|
|
24009
|
+
adopted.push(skill.name);
|
|
24010
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (matching current). Adopted as managed.`);
|
|
24011
|
+
} else {
|
|
24012
|
+
try {
|
|
24013
|
+
const stagedSkillDir = path10.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
24014
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
24015
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
24016
|
+
}
|
|
24017
|
+
if (existsSync6(stagedSkillDir)) {
|
|
24018
|
+
rmSync4(stagedSkillDir, { recursive: true, force: true });
|
|
24019
|
+
}
|
|
24020
|
+
mkdirSync4(stagedSkillDir, { recursive: true });
|
|
24021
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
24022
|
+
entry.status = "customized";
|
|
24023
|
+
entry.packageVersion = packageVersion;
|
|
24024
|
+
entry.sourceHash = sourceHash;
|
|
24025
|
+
entry.lastManagedHash = sourceHash;
|
|
24026
|
+
entry.lastSeenHash = destHash;
|
|
24027
|
+
entry.stagedPath = stagedSkillDir;
|
|
24028
|
+
entry.updatedAt = new Date().toISOString();
|
|
24029
|
+
staged.push(skill.name);
|
|
24030
|
+
customized.push(skill.name);
|
|
24031
|
+
skippedExisting.push(skill.name);
|
|
24032
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (custom). Marked customized and staged.`);
|
|
24033
|
+
} catch (err) {
|
|
24034
|
+
log(`[skill-sync] Failed to stage update for deleted/recreated skill ${skill.name}:`, err);
|
|
24035
|
+
failed.push(skill.name);
|
|
24036
|
+
}
|
|
24037
|
+
}
|
|
24038
|
+
} else if (entry.status === "conflict") {
|
|
24039
|
+
if (destHash === sourceHash) {
|
|
24040
|
+
if (entry.stagedPath) {
|
|
24041
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
24042
|
+
}
|
|
24043
|
+
entry.status = "managed";
|
|
24044
|
+
entry.packageVersion = packageVersion;
|
|
24045
|
+
entry.sourceHash = sourceHash;
|
|
24046
|
+
entry.lastManagedHash = sourceHash;
|
|
24047
|
+
entry.lastSeenHash = sourceHash;
|
|
24048
|
+
delete entry.stagedPath;
|
|
24049
|
+
entry.updatedAt = new Date().toISOString();
|
|
24050
|
+
adopted.push(skill.name);
|
|
24051
|
+
} else {
|
|
24052
|
+
try {
|
|
24053
|
+
const stagedSkillDir = path10.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
24054
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
24055
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
24056
|
+
}
|
|
24057
|
+
if (existsSync6(stagedSkillDir)) {
|
|
24058
|
+
rmSync4(stagedSkillDir, { recursive: true, force: true });
|
|
24059
|
+
}
|
|
24060
|
+
mkdirSync4(stagedSkillDir, { recursive: true });
|
|
24061
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
24062
|
+
entry.status = "customized";
|
|
24063
|
+
entry.packageVersion = packageVersion;
|
|
24064
|
+
entry.sourceHash = sourceHash;
|
|
24065
|
+
entry.lastManagedHash = sourceHash;
|
|
24066
|
+
entry.lastSeenHash = destHash;
|
|
24067
|
+
entry.stagedPath = stagedSkillDir;
|
|
24068
|
+
entry.updatedAt = new Date().toISOString();
|
|
24069
|
+
staged.push(skill.name);
|
|
24070
|
+
customized.push(skill.name);
|
|
24071
|
+
skippedExisting.push(skill.name);
|
|
24072
|
+
log(`[skill-sync] Conflicted skill ${skill.name} recovered as customized and staged at ${stagedSkillDir}`);
|
|
24073
|
+
} catch (err) {
|
|
24074
|
+
log(`[skill-sync] Failed to stage update for conflicted skill ${skill.name}:`, err);
|
|
24075
|
+
failed.push(skill.name);
|
|
24076
|
+
}
|
|
24077
|
+
}
|
|
24078
|
+
}
|
|
24079
|
+
} else {
|
|
24080
|
+
if (destHash === sourceHash) {
|
|
24081
|
+
manifest.skills[skill.name] = {
|
|
24082
|
+
status: "managed",
|
|
24083
|
+
packageVersion,
|
|
24084
|
+
sourceHash,
|
|
24085
|
+
lastManagedHash: sourceHash,
|
|
24086
|
+
lastSeenHash: sourceHash,
|
|
24087
|
+
updatedAt: new Date().toISOString()
|
|
24088
|
+
};
|
|
24089
|
+
skippedExisting.push(skill.name);
|
|
24090
|
+
adopted.push(skill.name);
|
|
24091
|
+
log(`[skill-sync] Adopted existing matching skill: ${skill.name}`);
|
|
24092
|
+
} else if (LEGACY_MANAGED_SKILL_HASHES[skill.name]?.includes(destHash)) {
|
|
24093
|
+
try {
|
|
24094
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
24095
|
+
installed.push(skill.name);
|
|
24096
|
+
manifest.skills[skill.name] = {
|
|
24097
|
+
status: "managed",
|
|
24098
|
+
packageVersion,
|
|
24099
|
+
sourceHash,
|
|
24100
|
+
lastManagedHash: sourceHash,
|
|
24101
|
+
lastSeenHash: sourceHash,
|
|
24102
|
+
updatedAt: new Date().toISOString()
|
|
24103
|
+
};
|
|
24104
|
+
log(`[skill-sync] Adopted and updated legacy skill: ${skill.name}`);
|
|
24105
|
+
} catch (err) {
|
|
24106
|
+
log(`[skill-sync] Failed to update legacy skill ${skill.name}:`, err);
|
|
24107
|
+
failed.push(skill.name);
|
|
24108
|
+
}
|
|
24109
|
+
} else {
|
|
24110
|
+
try {
|
|
24111
|
+
const stagedSkillDir = path10.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
24112
|
+
if (existsSync6(stagedSkillDir)) {
|
|
24113
|
+
rmSync4(stagedSkillDir, { recursive: true, force: true });
|
|
24114
|
+
}
|
|
24115
|
+
mkdirSync4(stagedSkillDir, { recursive: true });
|
|
24116
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
24117
|
+
manifest.skills[skill.name] = {
|
|
24118
|
+
status: "customized",
|
|
24119
|
+
packageVersion,
|
|
24120
|
+
sourceHash,
|
|
24121
|
+
lastManagedHash: "",
|
|
24122
|
+
lastSeenHash: destHash,
|
|
24123
|
+
stagedPath: stagedSkillDir,
|
|
24124
|
+
updatedAt: new Date().toISOString()
|
|
24125
|
+
};
|
|
24126
|
+
staged.push(skill.name);
|
|
24127
|
+
customized.push(skill.name);
|
|
24128
|
+
skippedExisting.push(skill.name);
|
|
24129
|
+
log(`[skill-sync] Skill ${skill.name} is customized (no manifest entry). Staged update at ${stagedSkillDir}`);
|
|
24130
|
+
} catch (err) {
|
|
24131
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
24132
|
+
failed.push(skill.name);
|
|
24133
|
+
}
|
|
24134
|
+
}
|
|
24135
|
+
}
|
|
24136
|
+
} catch (err) {
|
|
24137
|
+
log(`[skill-sync] Failed processing skill ${skill.name}:`, err);
|
|
24138
|
+
failed.push(skill.name);
|
|
24139
|
+
}
|
|
24140
|
+
}
|
|
24141
|
+
let manifestWriteFailed = false;
|
|
24142
|
+
manifest.updatedAt = new Date().toISOString();
|
|
24143
|
+
const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
24144
|
+
try {
|
|
24145
|
+
writeFileSync5(tempManifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
24146
|
+
renameSync3(tempManifestPath, manifestPath);
|
|
24147
|
+
} catch (err) {
|
|
24148
|
+
log("[skill-sync] Failed to write skills manifest atomically:", err);
|
|
24149
|
+
manifestWriteFailed = true;
|
|
24150
|
+
try {
|
|
24151
|
+
if (existsSync6(tempManifestPath)) {
|
|
24152
|
+
unlinkSync2(tempManifestPath);
|
|
24153
|
+
}
|
|
24154
|
+
} catch {}
|
|
24155
|
+
}
|
|
24156
|
+
if (manifestWriteFailed) {
|
|
24157
|
+
failed.push("__manifest__");
|
|
24158
|
+
}
|
|
24159
|
+
} finally {
|
|
24160
|
+
releaseLock(lockDir);
|
|
24161
|
+
}
|
|
24162
|
+
return {
|
|
24163
|
+
installed,
|
|
24164
|
+
skippedExisting,
|
|
24165
|
+
failed,
|
|
24166
|
+
staged,
|
|
24167
|
+
adopted,
|
|
24168
|
+
customized
|
|
24169
|
+
};
|
|
24170
|
+
}
|
|
24171
|
+
|
|
24172
|
+
// src/hooks/auto-update-checker/index.ts
|
|
24173
|
+
function createAutoUpdateCheckerHook(ctx, options = {}) {
|
|
24174
|
+
const { autoUpdate = true, companion } = options;
|
|
24175
|
+
let hasChecked = false;
|
|
24176
|
+
return {
|
|
24177
|
+
event: ({ event }) => {
|
|
24178
|
+
if (event.type !== "session.created")
|
|
24179
|
+
return;
|
|
24180
|
+
if (hasChecked)
|
|
24181
|
+
return;
|
|
24182
|
+
const props = event.properties;
|
|
24183
|
+
if (props?.info?.parentID)
|
|
24184
|
+
return;
|
|
24185
|
+
hasChecked = true;
|
|
24186
|
+
setTimeout(async () => {
|
|
24187
|
+
const localDevVersion = getLocalDevVersion(ctx.directory);
|
|
24188
|
+
if (localDevVersion) {
|
|
24189
|
+
log("[auto-update-checker] Local development mode");
|
|
24190
|
+
return;
|
|
24191
|
+
}
|
|
24192
|
+
runBackgroundUpdateCheck(ctx, autoUpdate, companion).catch((err) => {
|
|
24193
|
+
log("[auto-update-checker] Background update check failed:", err);
|
|
24194
|
+
});
|
|
24195
|
+
}, 0);
|
|
24196
|
+
}
|
|
24197
|
+
};
|
|
24198
|
+
}
|
|
24199
|
+
var hasReconciledAtStartup = false;
|
|
24200
|
+
async function runBackgroundUpdateCheck(ctx, autoUpdate, companion) {
|
|
24201
|
+
if (!hasReconciledAtStartup) {
|
|
24202
|
+
try {
|
|
24203
|
+
const runtimePackageJsonPath = getCurrentRuntimePackageJsonPath();
|
|
24204
|
+
if (runtimePackageJsonPath) {
|
|
24205
|
+
hasReconciledAtStartup = true;
|
|
24206
|
+
const packageRoot = path11.dirname(runtimePackageJsonPath);
|
|
24207
|
+
log("[auto-update-checker] Running startup skill reconciliation");
|
|
24208
|
+
const syncResult = syncBundledSkillsFromPackage(packageRoot);
|
|
24209
|
+
if (syncResult.installed.length > 0) {
|
|
24210
|
+
log(`[auto-update-checker] Startup skill sync installed: ${syncResult.installed.join(", ")}`);
|
|
24211
|
+
}
|
|
24212
|
+
if (syncResult.failed.length > 0) {
|
|
24213
|
+
log(`[auto-update-checker] Startup skill sync failures: ${syncResult.failed.join(", ")}`);
|
|
24214
|
+
}
|
|
24215
|
+
if (syncResult.staged.length > 0) {
|
|
24216
|
+
log(`[auto-update-checker] Startup skill sync staged: ${syncResult.staged.join(", ")}`);
|
|
24217
|
+
}
|
|
24218
|
+
if (syncResult.customized.length > 0) {
|
|
24219
|
+
log(`[auto-update-checker] Startup skill sync customized: ${syncResult.customized.join(", ")}`);
|
|
24220
|
+
}
|
|
24221
|
+
} else {
|
|
24222
|
+
log("[auto-update-checker] Could not resolve runtime package path for startup skill reconciliation");
|
|
24223
|
+
}
|
|
24224
|
+
} catch (err) {
|
|
24225
|
+
log("[auto-update-checker] Startup skill reconciliation failed:", err);
|
|
24226
|
+
}
|
|
24227
|
+
}
|
|
24228
|
+
const pluginInfo = findPluginEntry(ctx.directory);
|
|
24229
|
+
if (!pluginInfo) {
|
|
24230
|
+
log("[auto-update-checker] Plugin not found in config");
|
|
24231
|
+
return;
|
|
24232
|
+
}
|
|
24233
|
+
const cachedVersion = getCachedVersion();
|
|
24234
|
+
const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion;
|
|
24235
|
+
if (!currentVersion) {
|
|
24236
|
+
log("[auto-update-checker] No version found (cached or pinned)");
|
|
24237
|
+
return;
|
|
24238
|
+
}
|
|
24239
|
+
const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
|
|
24240
|
+
const latestInfo = await getLatestCompatibleVersion(currentVersion, channel);
|
|
23349
24241
|
if (latestInfo.unsafeReason === "unparseable-current-version") {
|
|
23350
24242
|
log(`[auto-update-checker] Current version is not semver; skipping auto-update: ${currentVersion}`);
|
|
23351
24243
|
if (latestInfo.latestMajorVersion) {
|
|
@@ -23388,12 +24280,16 @@ Version is pinned. Update your plugin config to apply.`, "info", 8000);
|
|
|
23388
24280
|
const installSuccess = await runBunInstallSafe(installDir);
|
|
23389
24281
|
if (installSuccess) {
|
|
23390
24282
|
let installedSkills = [];
|
|
24283
|
+
let stagedSkills = [];
|
|
24284
|
+
let customizedSkills = [];
|
|
23391
24285
|
let companionUpdated = false;
|
|
23392
24286
|
let companionWillRetry = false;
|
|
23393
24287
|
const packageRoot = path11.join(installDir, "node_modules", PACKAGE_NAME);
|
|
23394
24288
|
try {
|
|
23395
24289
|
const syncResult = syncBundledSkillsFromPackage(packageRoot);
|
|
23396
24290
|
installedSkills = syncResult.installed;
|
|
24291
|
+
stagedSkills = syncResult.staged;
|
|
24292
|
+
customizedSkills = syncResult.customized;
|
|
23397
24293
|
if (syncResult.failed.length > 0) {
|
|
23398
24294
|
log(`[auto-update-checker] Skill sync warnings/failures: ${syncResult.failed.join(", ")}`);
|
|
23399
24295
|
}
|
|
@@ -23427,6 +24323,12 @@ Version is pinned. Update your plugin config to apply.`, "info", 8000);
|
|
|
23427
24323
|
if (installedSkills.length > 0) {
|
|
23428
24324
|
messageLines.push(`Added bundled skills: ${installedSkills.join(", ")}`);
|
|
23429
24325
|
}
|
|
24326
|
+
if (stagedSkills.length > 0) {
|
|
24327
|
+
messageLines.push(`Staged skill updates: ${stagedSkills.join(", ")}`);
|
|
24328
|
+
}
|
|
24329
|
+
if (customizedSkills.length > 0) {
|
|
24330
|
+
messageLines.push(`Customized skills: ${customizedSkills.join(", ")}`);
|
|
24331
|
+
}
|
|
23430
24332
|
if (companionUpdated) {
|
|
23431
24333
|
messageLines.push("Companion updated.");
|
|
23432
24334
|
} else if (companionWillRetry) {
|
|
@@ -23452,7 +24354,7 @@ async function runBunInstallSafe(installDir) {
|
|
|
23452
24354
|
stdout: "pipe",
|
|
23453
24355
|
stderr: "pipe"
|
|
23454
24356
|
});
|
|
23455
|
-
const timeoutPromise = new Promise((
|
|
24357
|
+
const timeoutPromise = new Promise((resolve2) => setTimeout(() => resolve2("timeout"), 60000));
|
|
23456
24358
|
const exitPromise = proc.exited.then(() => "completed");
|
|
23457
24359
|
const result = await Promise.race([exitPromise, timeoutPromise]);
|
|
23458
24360
|
if (result === "timeout") {
|
|
@@ -23617,7 +24519,7 @@ var AGENT_PREFIX = {
|
|
|
23617
24519
|
class BackgroundJobBoard {
|
|
23618
24520
|
jobs = new Map;
|
|
23619
24521
|
counters = new Map;
|
|
23620
|
-
|
|
24522
|
+
terminalStateListeners = [];
|
|
23621
24523
|
maxReusablePerAgent;
|
|
23622
24524
|
readContextMinLines;
|
|
23623
24525
|
readContextMaxFiles;
|
|
@@ -23626,8 +24528,19 @@ class BackgroundJobBoard {
|
|
|
23626
24528
|
this.readContextMinLines = options.readContextMinLines ?? 10;
|
|
23627
24529
|
this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
|
|
23628
24530
|
}
|
|
24531
|
+
addTerminalStateListener(listener) {
|
|
24532
|
+
this.terminalStateListeners.push(listener);
|
|
24533
|
+
}
|
|
24534
|
+
removeTerminalStateListener(listener) {
|
|
24535
|
+
this.terminalStateListeners = this.terminalStateListeners.filter((entry) => entry !== listener);
|
|
24536
|
+
}
|
|
23629
24537
|
setTerminalStateListener(listener) {
|
|
23630
|
-
this.
|
|
24538
|
+
this.terminalStateListeners = listener ? [listener] : [];
|
|
24539
|
+
}
|
|
24540
|
+
notifyTerminalStateListeners(taskID) {
|
|
24541
|
+
for (const listener of this.terminalStateListeners) {
|
|
24542
|
+
listener(taskID);
|
|
24543
|
+
}
|
|
23631
24544
|
}
|
|
23632
24545
|
registerLaunch(input) {
|
|
23633
24546
|
const now = input.now ?? Date.now();
|
|
@@ -23640,6 +24553,7 @@ class BackgroundJobBoard {
|
|
|
23640
24553
|
objective: input.objective ?? existing.objective,
|
|
23641
24554
|
state: "running",
|
|
23642
24555
|
timedOut: false,
|
|
24556
|
+
recoverableAfterLiveBusy: false,
|
|
23643
24557
|
statusUncertain: false,
|
|
23644
24558
|
cancellationRequested: false,
|
|
23645
24559
|
terminalUnreconciled: false,
|
|
@@ -23650,7 +24564,9 @@ class BackgroundJobBoard {
|
|
|
23650
24564
|
lastLaunchedAt: now,
|
|
23651
24565
|
lastLiveBusyAt: now,
|
|
23652
24566
|
lastUsedAt: now,
|
|
23653
|
-
updatedAt: now
|
|
24567
|
+
updatedAt: now,
|
|
24568
|
+
totalErrors: existing.totalErrors ?? 0,
|
|
24569
|
+
timeoutCount: existing.timeoutCount ?? 0
|
|
23654
24570
|
};
|
|
23655
24571
|
this.jobs.set(input.taskID, updated);
|
|
23656
24572
|
return updated;
|
|
@@ -23663,6 +24579,7 @@ class BackgroundJobBoard {
|
|
|
23663
24579
|
objective: input.objective,
|
|
23664
24580
|
state: "running",
|
|
23665
24581
|
timedOut: false,
|
|
24582
|
+
recoverableAfterLiveBusy: false,
|
|
23666
24583
|
statusUncertain: false,
|
|
23667
24584
|
cancellationRequested: false,
|
|
23668
24585
|
terminalUnreconciled: false,
|
|
@@ -23672,7 +24589,9 @@ class BackgroundJobBoard {
|
|
|
23672
24589
|
lastUsedAt: now,
|
|
23673
24590
|
updatedAt: now,
|
|
23674
24591
|
alias: this.nextAlias(input.parentSessionID, input.agent),
|
|
23675
|
-
contextFiles: []
|
|
24592
|
+
contextFiles: [],
|
|
24593
|
+
totalErrors: 0,
|
|
24594
|
+
timeoutCount: 0
|
|
23676
24595
|
};
|
|
23677
24596
|
this.jobs.set(input.taskID, record);
|
|
23678
24597
|
return record;
|
|
@@ -23691,6 +24610,7 @@ class BackgroundJobBoard {
|
|
|
23691
24610
|
...existing,
|
|
23692
24611
|
state: input.state,
|
|
23693
24612
|
timedOut: input.timedOut ?? false,
|
|
24613
|
+
recoverableAfterLiveBusy: input.state !== "running" ? false : input.timedOut === true ? false : existing.recoverableAfterLiveBusy,
|
|
23694
24614
|
statusUncertain: input.statusUncertain ?? false,
|
|
23695
24615
|
terminalUnreconciled: terminal ? true : existing.terminalUnreconciled,
|
|
23696
24616
|
updatedAt: now,
|
|
@@ -23699,10 +24619,20 @@ class BackgroundJobBoard {
|
|
|
23699
24619
|
resultSummary: input.resultSummary ?? existing.resultSummary,
|
|
23700
24620
|
lastStatusError: input.lastStatusError
|
|
23701
24621
|
};
|
|
24622
|
+
if (input.state === "completed") {
|
|
24623
|
+
updated.timeoutCount = 0;
|
|
24624
|
+
}
|
|
24625
|
+
if (input.state === "error") {
|
|
24626
|
+
updated.totalErrors = (existing.totalErrors ?? 0) + 1;
|
|
24627
|
+
updated.lastErrorAt = updated.updatedAt;
|
|
24628
|
+
}
|
|
24629
|
+
if (input.timedOut && input.state !== "completed") {
|
|
24630
|
+
updated.timeoutCount = (existing.timeoutCount ?? 0) + 1;
|
|
24631
|
+
}
|
|
23702
24632
|
this.jobs.set(input.taskID, updated);
|
|
23703
24633
|
this.trimReusable(input.taskID);
|
|
23704
24634
|
if (notifyTerminal)
|
|
23705
|
-
this.
|
|
24635
|
+
this.notifyTerminalStateListeners(input.taskID);
|
|
23706
24636
|
return updated;
|
|
23707
24637
|
}
|
|
23708
24638
|
updateFromStatusOutput(output) {
|
|
@@ -23732,7 +24662,10 @@ class BackgroundJobBoard {
|
|
|
23732
24662
|
const updated = {
|
|
23733
24663
|
...existing,
|
|
23734
24664
|
updatedAt: now,
|
|
23735
|
-
lastLiveBusyAt: now
|
|
24665
|
+
lastLiveBusyAt: now,
|
|
24666
|
+
timedOut: false,
|
|
24667
|
+
recoverableAfterLiveBusy: existing.recoverableAfterLiveBusy || existing.timedOut,
|
|
24668
|
+
statusUncertain: false
|
|
23736
24669
|
};
|
|
23737
24670
|
this.jobs.set(taskID, updated);
|
|
23738
24671
|
return updated;
|
|
@@ -23773,6 +24706,7 @@ class BackgroundJobBoard {
|
|
|
23773
24706
|
...existing,
|
|
23774
24707
|
state: "cancelled",
|
|
23775
24708
|
timedOut: false,
|
|
24709
|
+
recoverableAfterLiveBusy: false,
|
|
23776
24710
|
statusUncertain: false,
|
|
23777
24711
|
cancellationRequested: true,
|
|
23778
24712
|
terminalUnreconciled: true,
|
|
@@ -23784,12 +24718,35 @@ class BackgroundJobBoard {
|
|
|
23784
24718
|
};
|
|
23785
24719
|
this.jobs.set(taskID, updated);
|
|
23786
24720
|
if (notifyTerminal)
|
|
23787
|
-
this.
|
|
24721
|
+
this.notifyTerminalStateListeners(taskID);
|
|
23788
24722
|
return updated;
|
|
23789
24723
|
}
|
|
23790
24724
|
get(taskID) {
|
|
23791
24725
|
return this.jobs.get(taskID);
|
|
23792
24726
|
}
|
|
24727
|
+
field(taskID, key) {
|
|
24728
|
+
return this.get(taskID)?.[key];
|
|
24729
|
+
}
|
|
24730
|
+
isRunning(taskID) {
|
|
24731
|
+
const job = this.get(taskID);
|
|
24732
|
+
return job?.state === "running";
|
|
24733
|
+
}
|
|
24734
|
+
isTerminalUnreconciled(taskID) {
|
|
24735
|
+
const job = this.get(taskID);
|
|
24736
|
+
return !!job?.terminalUnreconciled;
|
|
24737
|
+
}
|
|
24738
|
+
getResultSummary(taskID) {
|
|
24739
|
+
return this.field(taskID, "resultSummary");
|
|
24740
|
+
}
|
|
24741
|
+
getLastLiveBusyAt(taskID) {
|
|
24742
|
+
return this.field(taskID, "lastLiveBusyAt");
|
|
24743
|
+
}
|
|
24744
|
+
getParentSessionID(taskID) {
|
|
24745
|
+
return this.field(taskID, "parentSessionID");
|
|
24746
|
+
}
|
|
24747
|
+
getState(taskID) {
|
|
24748
|
+
return this.field(taskID, "state");
|
|
24749
|
+
}
|
|
23793
24750
|
resolve(parentSessionID, taskIDOrAlias) {
|
|
23794
24751
|
const value = taskIDOrAlias.trim();
|
|
23795
24752
|
return this.list(parentSessionID).find((job) => job.taskID === value || job.alias === value);
|
|
@@ -23802,6 +24759,17 @@ class BackgroundJobBoard {
|
|
|
23802
24759
|
return;
|
|
23803
24760
|
return job;
|
|
23804
24761
|
}
|
|
24762
|
+
resolveRecoverable(parentSessionID, taskIDOrAlias, agent) {
|
|
24763
|
+
const job = this.resolve(parentSessionID, taskIDOrAlias);
|
|
24764
|
+
if (!job)
|
|
24765
|
+
return;
|
|
24766
|
+
if (agent && job.agent !== agent)
|
|
24767
|
+
return;
|
|
24768
|
+
if (job.state !== "running" || !job.recoverableAfterLiveBusy) {
|
|
24769
|
+
return;
|
|
24770
|
+
}
|
|
24771
|
+
return job;
|
|
24772
|
+
}
|
|
23805
24773
|
markUsed(parentSessionID, key, now = Date.now()) {
|
|
23806
24774
|
const job = this.resolve(parentSessionID, key);
|
|
23807
24775
|
if (!job)
|
|
@@ -23821,8 +24789,11 @@ class BackgroundJobBoard {
|
|
|
23821
24789
|
for (const file of files) {
|
|
23822
24790
|
const previous = existing.get(file.path);
|
|
23823
24791
|
if (previous) {
|
|
23824
|
-
|
|
23825
|
-
|
|
24792
|
+
existing.set(file.path, {
|
|
24793
|
+
...previous,
|
|
24794
|
+
lineCount: Math.max(previous.lineCount, file.lineCount),
|
|
24795
|
+
lastReadAt: Math.max(previous.lastReadAt, file.lastReadAt)
|
|
24796
|
+
});
|
|
23826
24797
|
} else {
|
|
23827
24798
|
existing.set(file.path, { ...file });
|
|
23828
24799
|
}
|
|
@@ -23841,6 +24812,14 @@ class BackgroundJobBoard {
|
|
|
23841
24812
|
hasTerminalUnreconciled(parentSessionID) {
|
|
23842
24813
|
return this.list(parentSessionID).some((job) => job.terminalUnreconciled);
|
|
23843
24814
|
}
|
|
24815
|
+
hasConvergenceSignals(taskID, threshold = 3) {
|
|
24816
|
+
const job = this.jobs.get(taskID);
|
|
24817
|
+
if (!job)
|
|
24818
|
+
return false;
|
|
24819
|
+
const errors = job.totalErrors ?? 0;
|
|
24820
|
+
const timeouts = job.timeoutCount ?? 0;
|
|
24821
|
+
return errors >= threshold || timeouts >= threshold;
|
|
24822
|
+
}
|
|
23844
24823
|
formatForPrompt(parentSessionID, now = Date.now()) {
|
|
23845
24824
|
const active = this.list(parentSessionID).filter((job) => job.state === "running" || job.terminalUnreconciled);
|
|
23846
24825
|
const reusable = this.list(parentSessionID).filter(isReusable);
|
|
@@ -23849,7 +24828,10 @@ class BackgroundJobBoard {
|
|
|
23849
24828
|
return [
|
|
23850
24829
|
"### Background Job Board",
|
|
23851
24830
|
"SENTINEL: background-job-board-v2",
|
|
23852
|
-
"Do not poll running jobs. Wait for hook-driven completion, or use cancel_task only for explicit cancellation. Reconcile terminal jobs before final response.
|
|
24831
|
+
"Do not poll running jobs. Wait for hook-driven completion, or use cancel_task only for explicit cancellation. Reconcile terminal jobs before final response.",
|
|
24832
|
+
"Completed or reconciled sessions are reusable by alias for the same specialist/context.",
|
|
24833
|
+
"Timed-out running sessions are recoverable by alias for safe resume after a live busy signal.",
|
|
24834
|
+
"Cancelled or errored sessions are not reusable.",
|
|
23853
24835
|
"",
|
|
23854
24836
|
"#### Active / Unreconciled",
|
|
23855
24837
|
...active.length > 0 ? active.map((job) => formatJob(job, now)) : ["- none"],
|
|
@@ -24180,13 +25162,25 @@ ${buildRetryGuidance(detected)}`;
|
|
|
24180
25162
|
}
|
|
24181
25163
|
};
|
|
24182
25164
|
}
|
|
25165
|
+
// src/hooks/types.ts
|
|
25166
|
+
function isMessageWithParts(message) {
|
|
25167
|
+
if (!message || typeof message !== "object") {
|
|
25168
|
+
return false;
|
|
25169
|
+
}
|
|
25170
|
+
const candidate = message;
|
|
25171
|
+
return !!candidate.info && typeof candidate.info === "object" && typeof candidate.info.role === "string" && Array.isArray(candidate.parts);
|
|
25172
|
+
}
|
|
25173
|
+
function isUserMessageWithParts(message) {
|
|
25174
|
+
return isMessageWithParts(message) && message.info.role === "user";
|
|
25175
|
+
}
|
|
25176
|
+
|
|
24183
25177
|
// src/hooks/filter-available-skills/index.ts
|
|
24184
25178
|
var AVAILABLE_SKILLS_BLOCK_REGEX = /<available_skills>\s*([\s\S]*?)\s*<\/available_skills>/g;
|
|
24185
25179
|
var SKILL_NAME_REGEX = /<name>([^<]+)<\/name>/;
|
|
24186
25180
|
function getCurrentAgent(messages) {
|
|
24187
25181
|
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
24188
25182
|
const message = messages[index];
|
|
24189
|
-
if (message
|
|
25183
|
+
if (isUserMessageWithParts(message)) {
|
|
24190
25184
|
return message.info.agent ?? "orchestrator";
|
|
24191
25185
|
}
|
|
24192
25186
|
}
|
|
@@ -24243,7 +25237,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
|
|
|
24243
25237
|
};
|
|
24244
25238
|
return {
|
|
24245
25239
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
24246
|
-
const
|
|
25240
|
+
const messages = (Array.isArray(output.messages) ? output.messages : []).filter(isMessageWithParts);
|
|
24247
25241
|
if (messages.length === 0) {
|
|
24248
25242
|
return;
|
|
24249
25243
|
}
|
|
@@ -24274,7 +25268,11 @@ var RATE_LIMIT_PATTERNS = [
|
|
|
24274
25268
|
/resource.?exhausted/i,
|
|
24275
25269
|
/insufficient.?(quota|balance)/i,
|
|
24276
25270
|
/high concurrency/i,
|
|
24277
|
-
/reduce concurrency/i
|
|
25271
|
+
/reduce concurrency/i,
|
|
25272
|
+
/service unavailable/i,
|
|
25273
|
+
/monthly usage limit/i,
|
|
25274
|
+
/5-hour usage limit/i,
|
|
25275
|
+
/weekly usage limit/i
|
|
24278
25276
|
];
|
|
24279
25277
|
function isRateLimitError(error) {
|
|
24280
25278
|
if (!error || typeof error !== "object")
|
|
@@ -24300,6 +25298,7 @@ class ForegroundFallbackManager {
|
|
|
24300
25298
|
sessionTried = new Map;
|
|
24301
25299
|
inProgress = new Set;
|
|
24302
25300
|
lastTrigger = new Map;
|
|
25301
|
+
lastTriggerModel = new Map;
|
|
24303
25302
|
constructor(client, chains, enabled) {
|
|
24304
25303
|
this.client = client;
|
|
24305
25304
|
this.chains = chains;
|
|
@@ -24339,9 +25338,9 @@ class ForegroundFallbackManager {
|
|
|
24339
25338
|
}
|
|
24340
25339
|
case "session.status": {
|
|
24341
25340
|
const props = event.properties;
|
|
24342
|
-
if (!props?.sessionID || props.status?.
|
|
25341
|
+
if (!props?.sessionID || !props.status?.message)
|
|
24343
25342
|
break;
|
|
24344
|
-
const msg = props.status.message
|
|
25343
|
+
const msg = props.status.message.toLowerCase();
|
|
24345
25344
|
if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("exceededbudget") || msg.includes("over budget") || msg.includes("insufficient") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
|
|
24346
25345
|
await this.tryFallback(props.sessionID);
|
|
24347
25346
|
}
|
|
@@ -24363,6 +25362,7 @@ class ForegroundFallbackManager {
|
|
|
24363
25362
|
this.sessionTried.delete(id);
|
|
24364
25363
|
this.inProgress.delete(id);
|
|
24365
25364
|
this.lastTrigger.delete(id);
|
|
25365
|
+
this.lastTriggerModel.delete(id);
|
|
24366
25366
|
}
|
|
24367
25367
|
break;
|
|
24368
25368
|
}
|
|
@@ -24374,12 +25374,17 @@ class ForegroundFallbackManager {
|
|
|
24374
25374
|
if (this.inProgress.has(sessionID))
|
|
24375
25375
|
return;
|
|
24376
25376
|
const now = Date.now();
|
|
24377
|
-
|
|
25377
|
+
const curModel = this.sessionModel.get(sessionID);
|
|
25378
|
+
const modelChanged = this.lastTriggerModel.has(sessionID) && this.lastTriggerModel.get(sessionID) !== curModel;
|
|
25379
|
+
if (!modelChanged && now - (this.lastTrigger.get(sessionID) ?? 0) < DEDUP_WINDOW_MS)
|
|
24378
25380
|
return;
|
|
24379
25381
|
this.lastTrigger.set(sessionID, now);
|
|
25382
|
+
if (curModel !== undefined) {
|
|
25383
|
+
this.lastTriggerModel.set(sessionID, curModel);
|
|
25384
|
+
}
|
|
24380
25385
|
this.inProgress.add(sessionID);
|
|
24381
25386
|
try {
|
|
24382
|
-
|
|
25387
|
+
let currentModel = this.sessionModel.get(sessionID);
|
|
24383
25388
|
const agentName = this.sessionAgent.get(sessionID);
|
|
24384
25389
|
const chain = this.resolveChain(agentName, currentModel);
|
|
24385
25390
|
if (!chain.length) {
|
|
@@ -24389,20 +25394,42 @@ class ForegroundFallbackManager {
|
|
|
24389
25394
|
});
|
|
24390
25395
|
return;
|
|
24391
25396
|
}
|
|
25397
|
+
if (!currentModel && agentName && chain.length > 0) {
|
|
25398
|
+
currentModel = chain[0];
|
|
25399
|
+
}
|
|
24392
25400
|
if (!this.sessionTried.has(sessionID)) {
|
|
24393
25401
|
this.sessionTried.set(sessionID, new Set);
|
|
24394
25402
|
}
|
|
24395
|
-
|
|
25403
|
+
let tried = this.sessionTried.get(sessionID);
|
|
24396
25404
|
if (currentModel)
|
|
24397
25405
|
tried.add(currentModel);
|
|
24398
|
-
|
|
25406
|
+
let nextModel = chain.find((m) => !tried.has(m));
|
|
24399
25407
|
if (!nextModel) {
|
|
24400
|
-
|
|
24401
|
-
|
|
24402
|
-
|
|
24403
|
-
tried
|
|
24404
|
-
|
|
24405
|
-
|
|
25408
|
+
if (chain.length > 1) {
|
|
25409
|
+
const primary = chain[0];
|
|
25410
|
+
const stickyFallback = chain[chain.length - 1];
|
|
25411
|
+
log("[foreground-fallback] resetting tried set for re-fallback", {
|
|
25412
|
+
sessionID,
|
|
25413
|
+
agentName,
|
|
25414
|
+
currentModel,
|
|
25415
|
+
prevTried: [...tried],
|
|
25416
|
+
nextModel: stickyFallback
|
|
25417
|
+
});
|
|
25418
|
+
tried = new Set;
|
|
25419
|
+
if (primary)
|
|
25420
|
+
tried.add(primary);
|
|
25421
|
+
if (currentModel && currentModel !== primary)
|
|
25422
|
+
tried.add(currentModel);
|
|
25423
|
+
this.sessionTried.set(sessionID, tried);
|
|
25424
|
+
nextModel = stickyFallback;
|
|
25425
|
+
} else {
|
|
25426
|
+
log("[foreground-fallback] fallback chain exhausted", {
|
|
25427
|
+
sessionID,
|
|
25428
|
+
agentName,
|
|
25429
|
+
tried: [...tried]
|
|
25430
|
+
});
|
|
25431
|
+
return;
|
|
25432
|
+
}
|
|
24406
25433
|
}
|
|
24407
25434
|
tried.add(nextModel);
|
|
24408
25435
|
const ref = parseModelReference(nextModel);
|
|
@@ -24417,7 +25444,7 @@ class ForegroundFallbackManager {
|
|
|
24417
25444
|
path: { id: sessionID }
|
|
24418
25445
|
});
|
|
24419
25446
|
const messages = result.data ?? [];
|
|
24420
|
-
const lastUser = [...messages].reverse().find(
|
|
25447
|
+
const lastUser = [...messages].reverse().find(isUserMessageWithParts);
|
|
24421
25448
|
if (!lastUser) {
|
|
24422
25449
|
log("[foreground-fallback] no user message found", { sessionID });
|
|
24423
25450
|
return;
|
|
@@ -24458,7 +25485,11 @@ class ForegroundFallbackManager {
|
|
|
24458
25485
|
}
|
|
24459
25486
|
resolveChain(agentName, currentModel) {
|
|
24460
25487
|
if (agentName) {
|
|
24461
|
-
|
|
25488
|
+
const chain = this.chains[agentName];
|
|
25489
|
+
if (chain)
|
|
25490
|
+
return chain;
|
|
25491
|
+
if (ALL_AGENT_NAMES.includes(agentName))
|
|
25492
|
+
return [];
|
|
24462
25493
|
}
|
|
24463
25494
|
if (currentModel) {
|
|
24464
25495
|
for (const chain of Object.values(this.chains)) {
|
|
@@ -24480,17 +25511,17 @@ class ForegroundFallbackManager {
|
|
|
24480
25511
|
}
|
|
24481
25512
|
}
|
|
24482
25513
|
// src/hooks/image-hook.ts
|
|
24483
|
-
import { createHash as
|
|
25514
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
24484
25515
|
import {
|
|
24485
25516
|
existsSync as existsSync7,
|
|
24486
25517
|
mkdirSync as mkdirSync5,
|
|
24487
25518
|
readdirSync as readdirSync3,
|
|
24488
25519
|
rmdirSync,
|
|
24489
25520
|
statSync as statSync4,
|
|
24490
|
-
unlinkSync as
|
|
24491
|
-
writeFileSync as
|
|
25521
|
+
unlinkSync as unlinkSync3,
|
|
25522
|
+
writeFileSync as writeFileSync6
|
|
24492
25523
|
} from "node:fs";
|
|
24493
|
-
import { basename as
|
|
25524
|
+
import { basename as basename3, extname, join as join11 } from "node:path";
|
|
24494
25525
|
var lastCleanupByDir = new Map;
|
|
24495
25526
|
var CLEANUP_INTERVAL = 10 * 60 * 1000;
|
|
24496
25527
|
function isImagePart(p) {
|
|
@@ -24544,7 +25575,7 @@ function cleanupAllSessions(saveDir) {
|
|
|
24544
25575
|
} else {
|
|
24545
25576
|
try {
|
|
24546
25577
|
if (now - statSync4(fp).mtimeMs > maxAge)
|
|
24547
|
-
|
|
25578
|
+
unlinkSync3(fp);
|
|
24548
25579
|
} catch {}
|
|
24549
25580
|
}
|
|
24550
25581
|
}
|
|
@@ -24558,7 +25589,7 @@ function cleanupAllSessions(saveDir) {
|
|
|
24558
25589
|
const fp = join11(dir, f);
|
|
24559
25590
|
try {
|
|
24560
25591
|
if (now - statSync4(fp).mtimeMs > maxAge) {
|
|
24561
|
-
|
|
25592
|
+
unlinkSync3(fp);
|
|
24562
25593
|
} else {
|
|
24563
25594
|
allRemoved = false;
|
|
24564
25595
|
}
|
|
@@ -24576,7 +25607,7 @@ function cleanupAllSessions(saveDir) {
|
|
|
24576
25607
|
}
|
|
24577
25608
|
function writeUniqueFile(dir, name, data, log2) {
|
|
24578
25609
|
const ext = extname(name);
|
|
24579
|
-
const base =
|
|
25610
|
+
const base = basename3(name, ext) || name;
|
|
24580
25611
|
let candidate = join11(dir, name);
|
|
24581
25612
|
if (existsSync7(candidate)) {
|
|
24582
25613
|
return candidate;
|
|
@@ -24585,7 +25616,7 @@ function writeUniqueFile(dir, name, data, log2) {
|
|
|
24585
25616
|
const MAX_ATTEMPTS = 1000;
|
|
24586
25617
|
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
24587
25618
|
try {
|
|
24588
|
-
|
|
25619
|
+
writeFileSync6(candidate, data, { flag: "wx" });
|
|
24589
25620
|
return candidate;
|
|
24590
25621
|
} catch (e) {
|
|
24591
25622
|
if (e instanceof Error && e.code === "EEXIST") {
|
|
@@ -24607,7 +25638,7 @@ function processImageAttachments(args) {
|
|
|
24607
25638
|
return;
|
|
24608
25639
|
const messagesWithImages = [];
|
|
24609
25640
|
for (const msg of messages) {
|
|
24610
|
-
if (msg
|
|
25641
|
+
if (!isUserMessageWithParts(msg))
|
|
24611
25642
|
continue;
|
|
24612
25643
|
const imageParts = msg.parts.filter(isImagePart);
|
|
24613
25644
|
if (imageParts.length > 0) {
|
|
@@ -24624,7 +25655,7 @@ function processImageAttachments(args) {
|
|
|
24624
25655
|
try {
|
|
24625
25656
|
mkdirSync5(saveDir, { recursive: true });
|
|
24626
25657
|
if (!existsSync7(gitignorePath))
|
|
24627
|
-
|
|
25658
|
+
writeFileSync6(gitignorePath, `*
|
|
24628
25659
|
`);
|
|
24629
25660
|
} catch (e) {
|
|
24630
25661
|
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
@@ -24645,7 +25676,7 @@ function processImageAttachments(args) {
|
|
|
24645
25676
|
if (url) {
|
|
24646
25677
|
const decoded = decodeDataUrl(url);
|
|
24647
25678
|
if (decoded) {
|
|
24648
|
-
const hash =
|
|
25679
|
+
const hash = createHash3("sha1").update(decoded.data).digest("hex").slice(0, 8);
|
|
24649
25680
|
const sanitizedFilename = filename ? sanitizeFilename(filename) : undefined;
|
|
24650
25681
|
const baseName = sanitizedFilename ? sanitizedFilename.replace(/\.[^.]+$/, "") || "image" : "image";
|
|
24651
25682
|
const ext = sanitizedFilename ? extname(sanitizedFilename) || extFromMime(decoded.mime) : extFromMime(decoded.mime);
|
|
@@ -24717,17 +25748,84 @@ ${JSON_ERROR_REMINDER}`;
|
|
|
24717
25748
|
}
|
|
24718
25749
|
};
|
|
24719
25750
|
}
|
|
25751
|
+
// src/hooks/loop-command/index.ts
|
|
25752
|
+
var COMMAND_NAME2 = "loop";
|
|
25753
|
+
function historyDir() {
|
|
25754
|
+
const shortID = Math.random().toString(36).slice(2, 8);
|
|
25755
|
+
const timestamp = Date.now().toString(36);
|
|
25756
|
+
return `.opencode/loop-history/loop-${timestamp}-${shortID}`;
|
|
25757
|
+
}
|
|
25758
|
+
function activationPrompt2(text) {
|
|
25759
|
+
const dir = historyDir();
|
|
25760
|
+
return [
|
|
25761
|
+
"The user ran `/loop`. From the text below, extract: goal, successCriteria, maxAttempts.",
|
|
25762
|
+
"",
|
|
25763
|
+
"If ANY are missing or unclear - push back and ask the user to clarify.",
|
|
25764
|
+
"Do not assume or guess. All three must be explicit.",
|
|
25765
|
+
"",
|
|
25766
|
+
"Once all three are clear, run the loop:",
|
|
25767
|
+
"",
|
|
25768
|
+
text,
|
|
25769
|
+
"",
|
|
25770
|
+
"For each attempt:",
|
|
25771
|
+
`1. Read \`${dir}/\` for prior results`,
|
|
25772
|
+
"2. Dispatch @fixer with the goal",
|
|
25773
|
+
"3. Verify per the successCriteria",
|
|
25774
|
+
`4. Write result to \`${dir}/history-{NNN}.md\` (PASS/FAIL + reason)`,
|
|
25775
|
+
"5. PASS -> stop. FAIL under maxAttempts -> retry. FAIL at max -> escalate."
|
|
25776
|
+
].join(`
|
|
25777
|
+
`);
|
|
25778
|
+
}
|
|
25779
|
+
function helpPrompt() {
|
|
25780
|
+
return [
|
|
25781
|
+
"Usage: `/loop <description>`",
|
|
25782
|
+
"",
|
|
25783
|
+
"Describe what to accomplish, what success looks like, and how many tries.",
|
|
25784
|
+
"",
|
|
25785
|
+
"Examples:",
|
|
25786
|
+
" `/loop fix typescript errors until typecheck passes, max 3 tries`",
|
|
25787
|
+
" `/loop improve api performance until response under 500ms, try 5 times`",
|
|
25788
|
+
" `/loop refactor auth module, tests must pass, 4 attempts max`"
|
|
25789
|
+
].join(`
|
|
25790
|
+
`);
|
|
25791
|
+
}
|
|
25792
|
+
function createLoopCommandHook() {
|
|
25793
|
+
return {
|
|
25794
|
+
registerCommand: (opencodeConfig) => {
|
|
25795
|
+
const cfg = opencodeConfig.command;
|
|
25796
|
+
if (cfg?.[COMMAND_NAME2])
|
|
25797
|
+
return;
|
|
25798
|
+
if (!opencodeConfig.command)
|
|
25799
|
+
opencodeConfig.command = {};
|
|
25800
|
+
opencodeConfig.command[COMMAND_NAME2] = {
|
|
25801
|
+
template: "Run an automated execute-verify loop",
|
|
25802
|
+
description: "Dispatch fixer, verify, iterate with file-based history on disk."
|
|
25803
|
+
};
|
|
25804
|
+
},
|
|
25805
|
+
handleCommandExecuteBefore: async (input, output) => {
|
|
25806
|
+
if (input.command !== COMMAND_NAME2)
|
|
25807
|
+
return;
|
|
25808
|
+
output.parts.length = 0;
|
|
25809
|
+
const args = input.arguments.trim();
|
|
25810
|
+
if (!args) {
|
|
25811
|
+
output.parts.push(createInternalAgentTextPart(helpPrompt()));
|
|
25812
|
+
return;
|
|
25813
|
+
}
|
|
25814
|
+
output.parts.push({ type: "text", text: activationPrompt2(args) });
|
|
25815
|
+
}
|
|
25816
|
+
};
|
|
25817
|
+
}
|
|
24720
25818
|
// src/hooks/phase-reminder/index.ts
|
|
24721
25819
|
function createPhaseReminderHook() {
|
|
24722
25820
|
return {
|
|
24723
25821
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
24724
|
-
const
|
|
25822
|
+
const messages = Array.isArray(output.messages) ? output.messages : [];
|
|
24725
25823
|
if (messages.length === 0) {
|
|
24726
25824
|
return;
|
|
24727
25825
|
}
|
|
24728
25826
|
let lastUserMessageIndex = -1;
|
|
24729
25827
|
for (let i = messages.length - 1;i >= 0; i--) {
|
|
24730
|
-
if (messages[i]
|
|
25828
|
+
if (isUserMessageWithParts(messages[i])) {
|
|
24731
25829
|
lastUserMessageIndex = i;
|
|
24732
25830
|
break;
|
|
24733
25831
|
}
|
|
@@ -24736,6 +25834,9 @@ function createPhaseReminderHook() {
|
|
|
24736
25834
|
return;
|
|
24737
25835
|
}
|
|
24738
25836
|
const lastUserMessage = messages[lastUserMessageIndex];
|
|
25837
|
+
if (!isUserMessageWithParts(lastUserMessage)) {
|
|
25838
|
+
return;
|
|
25839
|
+
}
|
|
24739
25840
|
const agent = lastUserMessage.info.agent;
|
|
24740
25841
|
if (agent && agent !== "orchestrator") {
|
|
24741
25842
|
return;
|
|
@@ -24785,12 +25886,22 @@ ${PHASE_REMINDER}`;
|
|
|
24785
25886
|
};
|
|
24786
25887
|
}
|
|
24787
25888
|
// src/hooks/reflect/index.ts
|
|
24788
|
-
var
|
|
24789
|
-
function
|
|
25889
|
+
var COMMAND_NAME3 = "reflect";
|
|
25890
|
+
function activationPrompt3(focus, isSessionMode = false, lastN = 50) {
|
|
24790
25891
|
const focusBlock = focus ? ["Focus:", focus] : [
|
|
24791
25892
|
"Focus:",
|
|
24792
|
-
"Review recent work broadly and identify repeated workflow friction worth improving."
|
|
25893
|
+
isSessionMode ? "Analyze recent sessions to find repeated patterns, friction, and improvement opportunities." : "Review recent work broadly and identify repeated workflow friction worth improving."
|
|
24793
25894
|
];
|
|
25895
|
+
const modeBlock = isSessionMode ? [
|
|
25896
|
+
"",
|
|
25897
|
+
"Session Reflection Mode:",
|
|
25898
|
+
`- Analyze the last ${lastN} sessions (use --last N to adjust)`,
|
|
25899
|
+
"- Extract session IDs from OpenCode logs",
|
|
25900
|
+
"- Load session content from SQLite database",
|
|
25901
|
+
"- Analyze each session for patterns and friction",
|
|
25902
|
+
"- Aggregate findings across all sessions",
|
|
25903
|
+
"- Report with scope (global/cross-repo/project-specific), confidence, and impact"
|
|
25904
|
+
] : [];
|
|
24794
25905
|
return [
|
|
24795
25906
|
"Use the reflect skill for this request.",
|
|
24796
25907
|
"",
|
|
@@ -24802,6 +25913,7 @@ function activationPrompt2(focus) {
|
|
|
24802
25913
|
"- treat creating nothing as a valid result when evidence is weak;",
|
|
24803
25914
|
"- ask before changing prompts, skills, commands, agents, MCP access, or config unless the user explicitly requested the exact edit;",
|
|
24804
25915
|
"- return a compact report with findings, recommended changes, skipped candidates, and items needing more evidence.",
|
|
25916
|
+
...modeBlock,
|
|
24805
25917
|
"",
|
|
24806
25918
|
...focusBlock
|
|
24807
25919
|
].join(`
|
|
@@ -24812,32 +25924,155 @@ function createReflectCommandHook() {
|
|
|
24812
25924
|
return {
|
|
24813
25925
|
registerCommand: (opencodeConfig) => {
|
|
24814
25926
|
const commandConfig = opencodeConfig.command;
|
|
24815
|
-
if (commandConfig?.[
|
|
25927
|
+
if (commandConfig?.[COMMAND_NAME3]) {
|
|
24816
25928
|
shouldHandleCommand = false;
|
|
24817
25929
|
return;
|
|
24818
25930
|
}
|
|
24819
25931
|
if (!opencodeConfig.command)
|
|
24820
25932
|
opencodeConfig.command = {};
|
|
24821
|
-
opencodeConfig.command[
|
|
25933
|
+
opencodeConfig.command[COMMAND_NAME3] = {
|
|
24822
25934
|
template: "Review repeated work and suggest workflow improvements",
|
|
24823
25935
|
description: "Use reflect to learn from repeated workflows and suggest reusable improvements"
|
|
24824
25936
|
};
|
|
24825
25937
|
shouldHandleCommand = true;
|
|
24826
25938
|
},
|
|
24827
25939
|
handleCommandExecuteBefore: async (input, output) => {
|
|
24828
|
-
if (input.command !==
|
|
25940
|
+
if (input.command !== COMMAND_NAME3 || !shouldHandleCommand)
|
|
24829
25941
|
return;
|
|
25942
|
+
const args = input.arguments.trim();
|
|
25943
|
+
const isSessionMode = args.includes("--sessions");
|
|
25944
|
+
const lastMatch = args.match(/--last\s+(\d+)/);
|
|
25945
|
+
const last = lastMatch ? Math.min(parseInt(lastMatch[1], 10), 100) : 50;
|
|
25946
|
+
const focus = args.replace(/--sessions/g, "").replace(/--last\s+\d+/g, "").trim();
|
|
24830
25947
|
output.parts.length = 0;
|
|
24831
25948
|
output.parts.push({
|
|
24832
25949
|
type: "text",
|
|
24833
|
-
text:
|
|
25950
|
+
text: activationPrompt3(focus, isSessionMode, last)
|
|
24834
25951
|
});
|
|
24835
25952
|
}
|
|
24836
25953
|
};
|
|
24837
25954
|
}
|
|
24838
|
-
// src/hooks/task-session-manager/
|
|
24839
|
-
import path12 from "node:path";
|
|
25955
|
+
// src/hooks/task-session-manager/pending-call-tracker.ts
|
|
24840
25956
|
var MAX_PENDING_TASK_CALLS = 100;
|
|
25957
|
+
function createPendingCallTracker() {
|
|
25958
|
+
const pendingCalls = new Map;
|
|
25959
|
+
let anonymousPendingCallId = 0;
|
|
25960
|
+
return {
|
|
25961
|
+
add(call) {
|
|
25962
|
+
pendingCalls.delete(call.callId);
|
|
25963
|
+
pendingCalls.set(call.callId, call);
|
|
25964
|
+
while (pendingCalls.size > MAX_PENDING_TASK_CALLS) {
|
|
25965
|
+
const firstKey = pendingCalls.keys().next().value;
|
|
25966
|
+
if (firstKey === undefined)
|
|
25967
|
+
break;
|
|
25968
|
+
pendingCalls.delete(firstKey);
|
|
25969
|
+
}
|
|
25970
|
+
},
|
|
25971
|
+
take(callId, parentSessionId) {
|
|
25972
|
+
if (!callId && parentSessionId) {
|
|
25973
|
+
for (const id of pendingCalls.keys()) {
|
|
25974
|
+
const call = pendingCalls.get(id);
|
|
25975
|
+
if (call && call.parentSessionId === parentSessionId) {
|
|
25976
|
+
callId = id;
|
|
25977
|
+
break;
|
|
25978
|
+
}
|
|
25979
|
+
}
|
|
25980
|
+
}
|
|
25981
|
+
if (!callId)
|
|
25982
|
+
return;
|
|
25983
|
+
const pending = pendingCalls.get(callId);
|
|
25984
|
+
pendingCalls.delete(callId);
|
|
25985
|
+
return pending;
|
|
25986
|
+
},
|
|
25987
|
+
clearSession(sessionId) {
|
|
25988
|
+
for (const [callId, pending] of pendingCalls.entries()) {
|
|
25989
|
+
if (pending.parentSessionId === sessionId) {
|
|
25990
|
+
pendingCalls.delete(callId);
|
|
25991
|
+
}
|
|
25992
|
+
}
|
|
25993
|
+
},
|
|
25994
|
+
pendingCallId(sessionID, callID) {
|
|
25995
|
+
return callID ?? `${sessionID ?? "unknown"}:anonymous-${++anonymousPendingCallId}`;
|
|
25996
|
+
}
|
|
25997
|
+
};
|
|
25998
|
+
}
|
|
25999
|
+
|
|
26000
|
+
// src/hooks/task-session-manager/task-context-tracker.ts
|
|
26001
|
+
import path12 from "node:path";
|
|
26002
|
+
function createTaskContextTracker() {
|
|
26003
|
+
const contextByTask = new Map;
|
|
26004
|
+
const pendingManagedTaskIds = new Set;
|
|
26005
|
+
return {
|
|
26006
|
+
pendingManagedTaskIds,
|
|
26007
|
+
addContext(taskId, files) {
|
|
26008
|
+
if (files.length === 0)
|
|
26009
|
+
return;
|
|
26010
|
+
let context = contextByTask.get(taskId);
|
|
26011
|
+
if (!context) {
|
|
26012
|
+
context = new Map;
|
|
26013
|
+
contextByTask.set(taskId, context);
|
|
26014
|
+
}
|
|
26015
|
+
for (const file of files) {
|
|
26016
|
+
const pending = context.get(file.path) ?? {
|
|
26017
|
+
path: file.path,
|
|
26018
|
+
lines: new Set,
|
|
26019
|
+
lastReadAt: file.lastReadAt
|
|
26020
|
+
};
|
|
26021
|
+
for (const line of file.lineNumbers ?? []) {
|
|
26022
|
+
pending.lines.add(line);
|
|
26023
|
+
}
|
|
26024
|
+
pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
|
|
26025
|
+
context.set(file.path, pending);
|
|
26026
|
+
}
|
|
26027
|
+
},
|
|
26028
|
+
prune(backgroundJobBoard) {
|
|
26029
|
+
const remembered = backgroundJobBoard.taskIDs();
|
|
26030
|
+
for (const taskId of contextByTask.keys()) {
|
|
26031
|
+
if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
|
|
26032
|
+
contextByTask.delete(taskId);
|
|
26033
|
+
}
|
|
26034
|
+
}
|
|
26035
|
+
},
|
|
26036
|
+
clearSession(sessionId) {
|
|
26037
|
+
contextByTask.delete(sessionId);
|
|
26038
|
+
pendingManagedTaskIds.delete(sessionId);
|
|
26039
|
+
},
|
|
26040
|
+
contextFilesForPrompt(taskId) {
|
|
26041
|
+
const context = contextByTask.get(taskId);
|
|
26042
|
+
if (!context)
|
|
26043
|
+
return [];
|
|
26044
|
+
return [...context.values()].map((file) => ({
|
|
26045
|
+
path: file.path,
|
|
26046
|
+
lineCount: file.lines.size,
|
|
26047
|
+
lastReadAt: file.lastReadAt
|
|
26048
|
+
}));
|
|
26049
|
+
}
|
|
26050
|
+
};
|
|
26051
|
+
}
|
|
26052
|
+
function extractReadFiles(root, output) {
|
|
26053
|
+
if (typeof output.output !== "string")
|
|
26054
|
+
return [];
|
|
26055
|
+
const extractPath = /<path>([^<]+)<\/path>/.exec(output.output)?.[1];
|
|
26056
|
+
if (!extractPath)
|
|
26057
|
+
return [];
|
|
26058
|
+
const relative2 = path12.relative(root, extractPath);
|
|
26059
|
+
const normalized = !relative2 || relative2.startsWith("..") || path12.isAbsolute(relative2) ? extractPath : relative2;
|
|
26060
|
+
const matchedLines = new Set;
|
|
26061
|
+
for (const match of output.output.matchAll(/^([0-9]+):/gm)) {
|
|
26062
|
+
matchedLines.add(Number(match[1]));
|
|
26063
|
+
}
|
|
26064
|
+
const lineNumbers = [...matchedLines];
|
|
26065
|
+
return [
|
|
26066
|
+
{
|
|
26067
|
+
path: normalized,
|
|
26068
|
+
lineCount: lineNumbers.length,
|
|
26069
|
+
lineNumbers,
|
|
26070
|
+
lastReadAt: Date.now()
|
|
26071
|
+
}
|
|
26072
|
+
];
|
|
26073
|
+
}
|
|
26074
|
+
|
|
26075
|
+
// src/hooks/task-session-manager/index.ts
|
|
24841
26076
|
var BACKGROUND_JOB_BOARD_SENTINEL = "SENTINEL: background-job-board-v2";
|
|
24842
26077
|
var BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
|
|
24843
26078
|
var BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
|
|
@@ -24868,98 +26103,21 @@ function createOccurrenceId(part, message, partIndex) {
|
|
|
24868
26103
|
const hash = djb2Hash(`${sessionID}:${content}`);
|
|
24869
26104
|
return `anon:${hash}`;
|
|
24870
26105
|
}
|
|
24871
|
-
function extractPath(output) {
|
|
24872
|
-
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
24873
|
-
}
|
|
24874
26106
|
function extractTaskSummary(output) {
|
|
24875
26107
|
const summary = /<summary>\s*([\s\S]*?)\s*<\/summary>/i.exec(output)?.[1];
|
|
24876
26108
|
return summary?.trim() || undefined;
|
|
24877
26109
|
}
|
|
24878
|
-
function normalizePath(root, file) {
|
|
24879
|
-
const relative = path12.relative(root, file);
|
|
24880
|
-
if (!relative || relative.startsWith("..") || path12.isAbsolute(relative)) {
|
|
24881
|
-
return file;
|
|
24882
|
-
}
|
|
24883
|
-
return relative;
|
|
24884
|
-
}
|
|
24885
|
-
function extractReadFiles(root, output) {
|
|
24886
|
-
if (typeof output.output !== "string")
|
|
24887
|
-
return [];
|
|
24888
|
-
const file = extractPath(output.output);
|
|
24889
|
-
if (!file)
|
|
24890
|
-
return [];
|
|
24891
|
-
return [
|
|
24892
|
-
{
|
|
24893
|
-
path: normalizePath(root, file),
|
|
24894
|
-
lineCount: countReadLines(output.output).length,
|
|
24895
|
-
lineNumbers: countReadLines(output.output),
|
|
24896
|
-
lastReadAt: Date.now()
|
|
24897
|
-
}
|
|
24898
|
-
];
|
|
24899
|
-
}
|
|
24900
|
-
function countReadLines(output) {
|
|
24901
|
-
const lines = new Set;
|
|
24902
|
-
for (const match of output.matchAll(/^([0-9]+):/gm)) {
|
|
24903
|
-
lines.add(Number(match[1]));
|
|
24904
|
-
}
|
|
24905
|
-
return [...lines];
|
|
24906
|
-
}
|
|
24907
26110
|
function createTaskSessionManagerHook(_ctx, options) {
|
|
24908
26111
|
const backgroundJobBoard = options.backgroundJobBoard ?? new BackgroundJobBoard({
|
|
24909
26112
|
maxReusablePerAgent: options.maxSessionsPerAgent,
|
|
24910
26113
|
readContextMinLines: options.readContextMinLines,
|
|
24911
26114
|
readContextMaxFiles: options.readContextMaxFiles
|
|
24912
26115
|
});
|
|
24913
|
-
const
|
|
24914
|
-
const
|
|
24915
|
-
const contextByTask = new Map;
|
|
24916
|
-
const pendingManagedTaskIds = new Set;
|
|
24917
|
-
const terminalJobsInjectedByParent = new Map;
|
|
26116
|
+
const pendingCallTracker = createPendingCallTracker();
|
|
26117
|
+
const taskContextTracker = createTaskContextTracker();
|
|
24918
26118
|
const processedInjectedCompletions = new Set;
|
|
24919
26119
|
const processedInjectedCompletionOrder = [];
|
|
24920
|
-
|
|
24921
|
-
function addTaskContext(taskId, files) {
|
|
24922
|
-
if (files.length === 0)
|
|
24923
|
-
return;
|
|
24924
|
-
let context = contextByTask.get(taskId);
|
|
24925
|
-
if (!context) {
|
|
24926
|
-
context = new Map;
|
|
24927
|
-
contextByTask.set(taskId, context);
|
|
24928
|
-
}
|
|
24929
|
-
for (const file of files) {
|
|
24930
|
-
const pending = context.get(file.path) ?? {
|
|
24931
|
-
path: file.path,
|
|
24932
|
-
lines: new Set,
|
|
24933
|
-
lastReadAt: file.lastReadAt
|
|
24934
|
-
};
|
|
24935
|
-
for (const line of file.lineNumbers ?? []) {
|
|
24936
|
-
pending.lines.add(line);
|
|
24937
|
-
}
|
|
24938
|
-
pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
|
|
24939
|
-
context.set(file.path, pending);
|
|
24940
|
-
}
|
|
24941
|
-
backgroundJobBoard.addContext(taskId, contextFilesForPrompt(context));
|
|
24942
|
-
}
|
|
24943
|
-
function contextFilesForPrompt(context) {
|
|
24944
|
-
if (!context)
|
|
24945
|
-
return [];
|
|
24946
|
-
return [...context.values()].map((file) => ({
|
|
24947
|
-
path: file.path,
|
|
24948
|
-
lineCount: file.lines.size,
|
|
24949
|
-
lastReadAt: file.lastReadAt
|
|
24950
|
-
}));
|
|
24951
|
-
}
|
|
24952
|
-
function canTrackTaskContext(taskId) {
|
|
24953
|
-
return pendingManagedTaskIds.has(taskId) || backgroundJobBoard.taskIDs().has(taskId);
|
|
24954
|
-
}
|
|
24955
|
-
function pruneContext() {
|
|
24956
|
-
const remembered = backgroundJobBoard.taskIDs();
|
|
24957
|
-
for (const taskId of contextByTask.keys()) {
|
|
24958
|
-
if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
|
|
24959
|
-
contextByTask.delete(taskId);
|
|
24960
|
-
}
|
|
24961
|
-
}
|
|
24962
|
-
}
|
|
26120
|
+
const terminalJobsInjectedByParent = new Map;
|
|
24963
26121
|
function updateBackgroundJobFromOutput(output) {
|
|
24964
26122
|
if (typeof output !== "string")
|
|
24965
26123
|
return;
|
|
@@ -24977,7 +26135,8 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
24977
26135
|
log("[task-session-manager] suppressed late cancelled task error", {
|
|
24978
26136
|
taskID: status.taskID,
|
|
24979
26137
|
alias: existing?.alias,
|
|
24980
|
-
|
|
26138
|
+
parsedState: status.state,
|
|
26139
|
+
boardState: existing?.state,
|
|
24981
26140
|
terminalState: existing?.terminalState,
|
|
24982
26141
|
result: status.result
|
|
24983
26142
|
});
|
|
@@ -25004,10 +26163,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25004
26163
|
terminalUnreconciled: updated.terminalUnreconciled,
|
|
25005
26164
|
timedOut: updated.timedOut
|
|
25006
26165
|
});
|
|
25007
|
-
if (updated.
|
|
25008
|
-
pendingManagedTaskIds.delete(updated.taskID);
|
|
25009
|
-
backgroundJobBoard.addContext(updated.taskID, contextFilesForPrompt(
|
|
25010
|
-
|
|
26166
|
+
if (backgroundJobBoard.isTerminalUnreconciled(updated.taskID)) {
|
|
26167
|
+
taskContextTracker.pendingManagedTaskIds.delete(updated.taskID);
|
|
26168
|
+
backgroundJobBoard.addContext(updated.taskID, taskContextTracker.contextFilesForPrompt(updated.taskID));
|
|
26169
|
+
taskContextTracker.prune(backgroundJobBoard);
|
|
25011
26170
|
}
|
|
25012
26171
|
return updated;
|
|
25013
26172
|
}
|
|
@@ -25031,11 +26190,12 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25031
26190
|
const occurrenceId = createOccurrenceId(part, message, partIndex);
|
|
25032
26191
|
const existing = backgroundJobBoard.get(status.taskID);
|
|
25033
26192
|
if (isFailed && isLateCancelledTaskError(existing, status.state)) {
|
|
25034
|
-
part.text = formatCancelledTaskStatusOutput(status.taskID,
|
|
26193
|
+
part.text = formatCancelledTaskStatusOutput(status.taskID, backgroundJobBoard.getResultSummary(status.taskID));
|
|
25035
26194
|
log("[task-session-manager] normalized late cancelled injected failure", {
|
|
25036
26195
|
taskID: status.taskID,
|
|
25037
26196
|
alias: existing?.alias,
|
|
25038
|
-
|
|
26197
|
+
parsedState: status.state,
|
|
26198
|
+
boardState: existing?.state,
|
|
25039
26199
|
terminalState: existing?.terminalState,
|
|
25040
26200
|
result: status.result
|
|
25041
26201
|
});
|
|
@@ -25075,41 +26235,6 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25075
26235
|
const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
|
|
25076
26236
|
return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
|
|
25077
26237
|
}
|
|
25078
|
-
function pendingCallId(input) {
|
|
25079
|
-
return input.callID ?? `${input.sessionID ?? "unknown"}:anonymous-${++anonymousPendingCallId}`;
|
|
25080
|
-
}
|
|
25081
|
-
function rememberPendingCall(call) {
|
|
25082
|
-
const existingIndex = pendingCallOrder.indexOf(call.callId);
|
|
25083
|
-
if (existingIndex >= 0) {
|
|
25084
|
-
pendingCallOrder.splice(existingIndex, 1);
|
|
25085
|
-
}
|
|
25086
|
-
pendingCalls.set(call.callId, call);
|
|
25087
|
-
pendingCallOrder.push(call.callId);
|
|
25088
|
-
while (pendingCallOrder.length > MAX_PENDING_TASK_CALLS) {
|
|
25089
|
-
const evictedCallId = pendingCallOrder.shift();
|
|
25090
|
-
if (!evictedCallId) {
|
|
25091
|
-
break;
|
|
25092
|
-
}
|
|
25093
|
-
pendingCalls.delete(evictedCallId);
|
|
25094
|
-
}
|
|
25095
|
-
}
|
|
25096
|
-
function takePendingCall(callId, parentSessionId) {
|
|
25097
|
-
const resolvedCallId = callId ?? firstPendingCallForParent(parentSessionId);
|
|
25098
|
-
if (!resolvedCallId)
|
|
25099
|
-
return;
|
|
25100
|
-
const pending = pendingCalls.get(resolvedCallId);
|
|
25101
|
-
pendingCalls.delete(resolvedCallId);
|
|
25102
|
-
const orderIndex = pendingCallOrder.indexOf(resolvedCallId);
|
|
25103
|
-
if (orderIndex >= 0) {
|
|
25104
|
-
pendingCallOrder.splice(orderIndex, 1);
|
|
25105
|
-
}
|
|
25106
|
-
return pending;
|
|
25107
|
-
}
|
|
25108
|
-
function firstPendingCallForParent(parentSessionId) {
|
|
25109
|
-
if (!parentSessionId)
|
|
25110
|
-
return;
|
|
25111
|
-
return pendingCallOrder.find((callId) => pendingCalls.get(callId)?.parentSessionId === parentSessionId);
|
|
25112
|
-
}
|
|
25113
26238
|
function rememberInjectedTerminalJobs(parentSessionID) {
|
|
25114
26239
|
const taskIDs = backgroundJobBoard.list(parentSessionID).filter((job) => job.terminalUnreconciled).map((job) => job.taskID);
|
|
25115
26240
|
if (taskIDs.length === 0)
|
|
@@ -25161,45 +26286,50 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25161
26286
|
agentType
|
|
25162
26287
|
});
|
|
25163
26288
|
const pendingCall = {
|
|
25164
|
-
callId: pendingCallId(
|
|
25165
|
-
callID: input.callID,
|
|
25166
|
-
sessionID: input.sessionID
|
|
25167
|
-
}),
|
|
26289
|
+
callId: pendingCallTracker.pendingCallId(input.sessionID, input.callID),
|
|
25168
26290
|
parentSessionId: input.sessionID,
|
|
25169
26291
|
agentType,
|
|
25170
26292
|
label
|
|
25171
26293
|
};
|
|
25172
|
-
|
|
26294
|
+
pendingCallTracker.add(pendingCall);
|
|
25173
26295
|
if (typeof args.task_id !== "string" || args.task_id.trim() === "") {
|
|
25174
26296
|
return;
|
|
25175
26297
|
}
|
|
25176
26298
|
const requested = args.task_id.trim();
|
|
25177
|
-
const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, agentType);
|
|
26299
|
+
const remembered = backgroundJobBoard.resolveReusable(input.sessionID, requested, agentType) ?? backgroundJobBoard.resolveRecoverable(input.sessionID, requested, agentType);
|
|
25178
26300
|
if (!remembered) {
|
|
26301
|
+
const knownManagedTask = backgroundJobBoard.resolve(input.sessionID, requested);
|
|
26302
|
+
if (knownManagedTask) {
|
|
26303
|
+
delete args.task_id;
|
|
26304
|
+
return;
|
|
26305
|
+
}
|
|
25179
26306
|
if (RAW_SESSION_ID_PATTERN.test(requested)) {
|
|
25180
26307
|
pendingCall.resumedTaskId = requested;
|
|
25181
|
-
|
|
26308
|
+
pendingCallTracker.add(pendingCall);
|
|
25182
26309
|
return;
|
|
25183
26310
|
}
|
|
25184
26311
|
delete args.task_id;
|
|
25185
26312
|
return;
|
|
25186
26313
|
}
|
|
25187
26314
|
args.task_id = remembered.taskID;
|
|
25188
|
-
pendingManagedTaskIds.add(remembered.taskID);
|
|
26315
|
+
taskContextTracker.pendingManagedTaskIds.add(remembered.taskID);
|
|
25189
26316
|
backgroundJobBoard.markUsed(input.sessionID, remembered.taskID);
|
|
25190
26317
|
pendingCall.resumedTaskId = remembered.taskID;
|
|
25191
|
-
|
|
26318
|
+
pendingCallTracker.add(pendingCall);
|
|
25192
26319
|
},
|
|
25193
26320
|
"tool.execute.after": async (input, output) => {
|
|
25194
26321
|
if (input.tool.toLowerCase() === "read") {
|
|
25195
|
-
if (input.sessionID
|
|
25196
|
-
|
|
26322
|
+
if (input.sessionID) {
|
|
26323
|
+
const canTrack = taskContextTracker.pendingManagedTaskIds.has(input.sessionID) || backgroundJobBoard.taskIDs().has(input.sessionID);
|
|
26324
|
+
if (canTrack) {
|
|
26325
|
+
taskContextTracker.addContext(input.sessionID, extractReadFiles(_ctx.directory, output));
|
|
26326
|
+
}
|
|
25197
26327
|
}
|
|
25198
26328
|
return;
|
|
25199
26329
|
}
|
|
25200
26330
|
if (input.tool.toLowerCase() !== "task")
|
|
25201
26331
|
return;
|
|
25202
|
-
const pending =
|
|
26332
|
+
const pending = pendingCallTracker.take(input.callID, input.sessionID);
|
|
25203
26333
|
if (!pending || typeof output.output !== "string")
|
|
25204
26334
|
return;
|
|
25205
26335
|
const launch = parseTaskLaunchOutput(output.output);
|
|
@@ -25219,8 +26349,8 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25219
26349
|
description: record.description,
|
|
25220
26350
|
state: record.state
|
|
25221
26351
|
});
|
|
25222
|
-
|
|
25223
|
-
|
|
26352
|
+
taskContextTracker.pendingManagedTaskIds.add(launch.taskID);
|
|
26353
|
+
backgroundJobBoard.addContext(launch.taskID, taskContextTracker.contextFilesForPrompt(launch.taskID));
|
|
25224
26354
|
return;
|
|
25225
26355
|
}
|
|
25226
26356
|
normalizeLateCancelledTaskOutput(output);
|
|
@@ -25250,10 +26380,9 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25250
26380
|
if (pending.resumedTaskId && pending.resumedTaskId !== status.taskID) {
|
|
25251
26381
|
backgroundJobBoard.drop(pending.resumedTaskId);
|
|
25252
26382
|
}
|
|
25253
|
-
pendingManagedTaskIds.delete(status.taskID);
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
pruneContext();
|
|
26383
|
+
taskContextTracker.pendingManagedTaskIds.delete(status.taskID);
|
|
26384
|
+
backgroundJobBoard.addContext(status.taskID, taskContextTracker.contextFilesForPrompt(status.taskID));
|
|
26385
|
+
taskContextTracker.prune(backgroundJobBoard);
|
|
25257
26386
|
return;
|
|
25258
26387
|
}
|
|
25259
26388
|
const taskId = parseTaskIdFromTaskOutput(output.output);
|
|
@@ -25266,14 +26395,14 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25266
26395
|
if (pending.resumedTaskId && pending.resumedTaskId !== taskId) {
|
|
25267
26396
|
backgroundJobBoard.drop(pending.resumedTaskId);
|
|
25268
26397
|
}
|
|
25269
|
-
pendingManagedTaskIds.delete(taskId);
|
|
25270
|
-
|
|
25271
|
-
|
|
25272
|
-
pruneContext();
|
|
26398
|
+
taskContextTracker.pendingManagedTaskIds.delete(taskId);
|
|
26399
|
+
backgroundJobBoard.addContext(taskId, taskContextTracker.contextFilesForPrompt(taskId));
|
|
26400
|
+
taskContextTracker.prune(backgroundJobBoard);
|
|
25273
26401
|
},
|
|
25274
26402
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
25275
|
-
|
|
25276
|
-
|
|
26403
|
+
const messages = Array.isArray(output.messages) ? output.messages : [];
|
|
26404
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
26405
|
+
if (!isUserMessageWithParts(message))
|
|
25277
26406
|
continue;
|
|
25278
26407
|
if (message.info.agent && message.info.agent !== "orchestrator") {
|
|
25279
26408
|
continue;
|
|
@@ -25285,9 +26414,9 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25285
26414
|
updateFromInjectedCompletion(part, message, messageIndex, partIndex);
|
|
25286
26415
|
}
|
|
25287
26416
|
}
|
|
25288
|
-
for (let i =
|
|
25289
|
-
const message =
|
|
25290
|
-
if (message
|
|
26417
|
+
for (let i = messages.length - 1;i >= 0; i -= 1) {
|
|
26418
|
+
const message = messages[i];
|
|
26419
|
+
if (!isUserMessageWithParts(message))
|
|
25291
26420
|
continue;
|
|
25292
26421
|
if (message.info.agent && message.info.agent !== "orchestrator")
|
|
25293
26422
|
return;
|
|
@@ -25323,7 +26452,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25323
26452
|
managesParent: info?.parentID ? options.shouldManageSession(info.parentID) : false
|
|
25324
26453
|
});
|
|
25325
26454
|
if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
|
|
25326
|
-
pendingManagedTaskIds.add(info.id);
|
|
26455
|
+
taskContextTracker.pendingManagedTaskIds.add(info.id);
|
|
25327
26456
|
}
|
|
25328
26457
|
return;
|
|
25329
26458
|
}
|
|
@@ -25342,7 +26471,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25342
26471
|
if (input.event.type === "session.error") {
|
|
25343
26472
|
const sessionId2 = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
25344
26473
|
if (sessionId2 && options.shouldManageSession(sessionId2)) {
|
|
25345
|
-
|
|
26474
|
+
const props = input.event.properties;
|
|
26475
|
+
if (!props?.error || !isRateLimitError(props.error)) {
|
|
26476
|
+
terminalJobsInjectedByParent.delete(sessionId2);
|
|
26477
|
+
}
|
|
25346
26478
|
}
|
|
25347
26479
|
return;
|
|
25348
26480
|
}
|
|
@@ -25356,9 +26488,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25356
26488
|
previousState: before.state,
|
|
25357
26489
|
previousTerminalState: before.terminalState,
|
|
25358
26490
|
terminalUnreconciled: before.terminalUnreconciled,
|
|
25359
|
-
resultSummary: before.resultSummary
|
|
25360
|
-
updatedState: updated?.state,
|
|
25361
|
-
updatedCancellationRequested: updated?.cancellationRequested
|
|
26491
|
+
resultSummary: before.resultSummary
|
|
25362
26492
|
});
|
|
25363
26493
|
}
|
|
25364
26494
|
log("[task-session-manager] busy/status busy observed", {
|
|
@@ -25366,10 +26496,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25366
26496
|
managesSession: sessionId2 ? options.shouldManageSession(sessionId2) : false,
|
|
25367
26497
|
previousState: before?.state,
|
|
25368
26498
|
previousTerminalState: before?.terminalState,
|
|
25369
|
-
previousCancellationRequested: before?.cancellationRequested,
|
|
26499
|
+
previousCancellationRequested: before?.cancellationRequested ?? false,
|
|
25370
26500
|
previousLastLiveBusyAt: before?.lastLiveBusyAt,
|
|
25371
26501
|
updatedState: updated?.state,
|
|
25372
|
-
updatedCancellationRequested: updated?.cancellationRequested,
|
|
26502
|
+
updatedCancellationRequested: updated?.cancellationRequested ?? false,
|
|
25373
26503
|
updatedLastLiveBusyAt: updated?.lastLiveBusyAt
|
|
25374
26504
|
});
|
|
25375
26505
|
return;
|
|
@@ -25381,26 +26511,23 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25381
26511
|
return;
|
|
25382
26512
|
log("[task-session-manager] session.deleted observed; clearing job state", {
|
|
25383
26513
|
sessionID: sessionId,
|
|
25384
|
-
deletedJob:
|
|
25385
|
-
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
|
|
26514
|
+
deletedJob: (() => {
|
|
26515
|
+
const record = backgroundJobBoard.get(sessionId);
|
|
26516
|
+
return record ? {
|
|
26517
|
+
state: record.state,
|
|
26518
|
+
parentSessionID: record.parentSessionID,
|
|
26519
|
+
alias: record.alias
|
|
26520
|
+
} : undefined;
|
|
26521
|
+
})(),
|
|
25389
26522
|
childJobCount: backgroundJobBoard.list(sessionId).length,
|
|
25390
26523
|
managesSession: options.shouldManageSession(sessionId)
|
|
25391
26524
|
});
|
|
25392
26525
|
backgroundJobBoard.drop(sessionId);
|
|
25393
26526
|
backgroundJobBoard.clearParent(sessionId);
|
|
25394
26527
|
terminalJobsInjectedByParent.delete(sessionId);
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
for (const [callId, pending] of pendingCalls.entries()) {
|
|
25399
|
-
if (pending.parentSessionId !== sessionId) {
|
|
25400
|
-
continue;
|
|
25401
|
-
}
|
|
25402
|
-
takePendingCall(callId);
|
|
25403
|
-
}
|
|
26528
|
+
taskContextTracker.clearSession(sessionId);
|
|
26529
|
+
taskContextTracker.prune(backgroundJobBoard);
|
|
26530
|
+
pendingCallTracker.clearSession(sessionId);
|
|
25404
26531
|
}
|
|
25405
26532
|
};
|
|
25406
26533
|
function normalizeLateCancelledTaskOutput(output) {
|
|
@@ -25419,7 +26546,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
25419
26546
|
terminalState: existing?.terminalState,
|
|
25420
26547
|
result: status.result
|
|
25421
26548
|
});
|
|
25422
|
-
output.output = formatCancelledTaskStatusOutput(status.taskID,
|
|
26549
|
+
output.output = formatCancelledTaskStatusOutput(status.taskID, backgroundJobBoard.getResultSummary(status.taskID));
|
|
25423
26550
|
if (isRecord(output) && isRecord(output.metadata)) {
|
|
25424
26551
|
output.metadata.state = "cancelled";
|
|
25425
26552
|
}
|
|
@@ -25443,17 +26570,14 @@ function formatCancelledTaskStatusOutput(taskID, summary = "cancelled") {
|
|
|
25443
26570
|
].join(`
|
|
25444
26571
|
`);
|
|
25445
26572
|
}
|
|
25446
|
-
// src/interview/manager.ts
|
|
25447
|
-
import path16 from "node:path";
|
|
25448
|
-
|
|
25449
26573
|
// src/interview/dashboard.ts
|
|
25450
|
-
import
|
|
26574
|
+
import crypto2 from "node:crypto";
|
|
25451
26575
|
import * as fsSync2 from "node:fs";
|
|
25452
26576
|
import fs7 from "node:fs/promises";
|
|
25453
26577
|
import {
|
|
25454
26578
|
createServer
|
|
25455
26579
|
} from "node:http";
|
|
25456
|
-
import
|
|
26580
|
+
import os5 from "node:os";
|
|
25457
26581
|
import path14 from "node:path";
|
|
25458
26582
|
import { URL as URL2 } from "node:url";
|
|
25459
26583
|
|
|
@@ -26035,8 +27159,8 @@ function renderDashboardPage(interviews, files, outputFolder) {
|
|
|
26035
27159
|
<p class="muted">${totalCount} item${totalCount === 1 ? "" : "s"}</p>
|
|
26036
27160
|
</div>
|
|
26037
27161
|
<div class="info-box">
|
|
26038
|
-
<strong>Interviews</strong>
|
|
26039
|
-
<strong>Files without session</strong>
|
|
27162
|
+
<strong>Interviews</strong> - live sessions and recovered files. State is pushed from OpenCode sessions to this dashboard.<br>
|
|
27163
|
+
<strong>Files without session</strong> - <code>.md</code> files in <code>${escapeHtml(outputFolder)}</code> with no frontmatter. Resume with <code>/interview <name></code>.
|
|
26040
27164
|
</div>
|
|
26041
27165
|
<details style="margin-bottom:24px">
|
|
26042
27166
|
<summary style="cursor:pointer;font-size:13px;color:rgba(255,255,255,0.4);user-select:none">Settings</summary>
|
|
@@ -26064,7 +27188,7 @@ function renderDashboardPage(interviews, files, outputFolder) {
|
|
|
26064
27188
|
${fileSection}
|
|
26065
27189
|
<div class="footer">OH MY OPENCODE SLIM</div>
|
|
26066
27190
|
</div>
|
|
26067
|
-
<div class="update-banner" id="updateBanner">Dashboard updated
|
|
27191
|
+
<div class="update-banner" id="updateBanner">Dashboard updated - tap to refresh</div>
|
|
26068
27192
|
<script>
|
|
26069
27193
|
${clipboardHelperJs()}
|
|
26070
27194
|
document.querySelectorAll('.copy-btn').forEach(btn => {
|
|
@@ -26731,7 +27855,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
26731
27855
|
</div>
|
|
26732
27856
|
|
|
26733
27857
|
<div class="chat-panel" id="chatPanel">
|
|
26734
|
-
<input type="text" id="chatInput" class="chat-input" placeholder="Send a message to the agent
|
|
27858
|
+
<input type="text" id="chatInput" class="chat-input" placeholder="Send a message to the agent - add a section, revise content, ask questions..." autocomplete="off" />
|
|
26735
27859
|
<button class="chat-send" id="chatSendBtn" type="button" disabled>Send</button>
|
|
26736
27860
|
</div>
|
|
26737
27861
|
|
|
@@ -27090,7 +28214,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27090
28214
|
|
|
27091
28215
|
// simpleMarkdown: safe because escaping happens BEFORE markdown
|
|
27092
28216
|
// processing. Adding raw HTML output from user content would break
|
|
27093
|
-
// this safety
|
|
28217
|
+
// this safety - always use textContent for user data.
|
|
27094
28218
|
function simpleMarkdown(text) {
|
|
27095
28219
|
if (!text) return '';
|
|
27096
28220
|
const escaped = text
|
|
@@ -27236,7 +28360,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27236
28360
|
aBadge.textContent = 'A';
|
|
27237
28361
|
const aText = document.createElement('span');
|
|
27238
28362
|
aText.className = 'qa-text qa-text-a';
|
|
27239
|
-
aText.textContent = pair.a || '
|
|
28363
|
+
aText.textContent = pair.a || '-';
|
|
27240
28364
|
aRow.appendChild(aBadge);
|
|
27241
28365
|
aRow.appendChild(aText);
|
|
27242
28366
|
card.appendChild(aRow);
|
|
@@ -27304,7 +28428,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27304
28428
|
if (busy) {
|
|
27305
28429
|
input.placeholder = 'Agent is processing...';
|
|
27306
28430
|
} else {
|
|
27307
|
-
input.placeholder = 'Send a message to the agent
|
|
28431
|
+
input.placeholder = 'Send a message to the agent - add a section, revise content, ask questions...';
|
|
27308
28432
|
}
|
|
27309
28433
|
}
|
|
27310
28434
|
|
|
@@ -27462,7 +28586,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27462
28586
|
statusEl.textContent = data.mode.toUpperCase();
|
|
27463
28587
|
statusEl.className = isDone ? 'status-completed' : '';
|
|
27464
28588
|
|
|
27465
|
-
// Render Markdown Path
|
|
28589
|
+
// Render Markdown Path - always visible in completed mode
|
|
27466
28590
|
const pathContainer = document.getElementById('filePathContainer');
|
|
27467
28591
|
const pathElement = document.getElementById('markdownPath');
|
|
27468
28592
|
const mdPath = data.markdownPath || (data.interview && data.interview.markdownPath);
|
|
@@ -27480,7 +28604,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27480
28604
|
pathContainer.style.display = 'none';
|
|
27481
28605
|
}
|
|
27482
28606
|
|
|
27483
|
-
// Resume command
|
|
28607
|
+
// Resume command - only in session-disconnected mode
|
|
27484
28608
|
var resumeRow = document.getElementById('resumeRow');
|
|
27485
28609
|
if (resumeRow) {
|
|
27486
28610
|
var showResume = data.mode === 'session-disconnected';
|
|
@@ -27490,7 +28614,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27490
28614
|
}
|
|
27491
28615
|
}
|
|
27492
28616
|
|
|
27493
|
-
// Nudge actions
|
|
28617
|
+
// Nudge actions - only for completed (live session)
|
|
27494
28618
|
// session-disconnected can't nudge (no session to receive it)
|
|
27495
28619
|
const nudgeActions = document.getElementById('nudgeActions');
|
|
27496
28620
|
const moreBtn = document.getElementById('moreQuestionsBtn');
|
|
@@ -27613,7 +28737,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27613
28737
|
if (confirmBtn) confirmBtn.disabled = false;
|
|
27614
28738
|
} else {
|
|
27615
28739
|
try { await refresh(); } catch (_) {}
|
|
27616
|
-
schedulePoll(); // Restart polling
|
|
28740
|
+
schedulePoll(); // Restart polling - nudge may reactivate interview
|
|
27617
28741
|
}
|
|
27618
28742
|
} catch (_err) {
|
|
27619
28743
|
document.getElementById('submitStatus').textContent = 'Network error.';
|
|
@@ -27691,7 +28815,7 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
27691
28815
|
|
|
27692
28816
|
// src/interview/dashboard.ts
|
|
27693
28817
|
function getAuthFilePath(port) {
|
|
27694
|
-
const dataHome = process.env.XDG_DATA_HOME || path14.join(
|
|
28818
|
+
const dataHome = process.env.XDG_DATA_HOME || path14.join(os5.homedir(), ".local", "share");
|
|
27695
28819
|
return path14.join(dataHome, "opencode", `.dashboard-${port}.json`);
|
|
27696
28820
|
}
|
|
27697
28821
|
function writeAuthFile(port, token) {
|
|
@@ -27747,7 +28871,7 @@ function hasLiveForSlug(slug, cache) {
|
|
|
27747
28871
|
}
|
|
27748
28872
|
var DEFAULT_DASHBOARD_PORT = 43211;
|
|
27749
28873
|
function createDashboardServer(config) {
|
|
27750
|
-
const authToken =
|
|
28874
|
+
const authToken = crypto2.randomBytes(32).toString("hex");
|
|
27751
28875
|
let activeServer = null;
|
|
27752
28876
|
let baseUrl = null;
|
|
27753
28877
|
const sessions = new Map;
|
|
@@ -27804,15 +28928,19 @@ data: ${JSON.stringify(formatSseState(entry))}
|
|
|
27804
28928
|
]);
|
|
27805
28929
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
27806
28930
|
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
|
27807
|
-
|
|
27808
|
-
const
|
|
27809
|
-
|
|
27810
|
-
|
|
27811
|
-
|
|
28931
|
+
function createCleanupTimer() {
|
|
28932
|
+
const timer = setInterval(() => {
|
|
28933
|
+
const cutoff = Date.now() - CACHE_TTL_MS;
|
|
28934
|
+
for (const [id, entry] of stateCache) {
|
|
28935
|
+
if (TERMINAL_MODES.has(entry.mode) && entry.lastUpdatedAt < cutoff) {
|
|
28936
|
+
stateCache.delete(id);
|
|
28937
|
+
}
|
|
27812
28938
|
}
|
|
27813
|
-
}
|
|
27814
|
-
|
|
27815
|
-
|
|
28939
|
+
}, CLEANUP_INTERVAL_MS);
|
|
28940
|
+
timer.unref();
|
|
28941
|
+
return timer;
|
|
28942
|
+
}
|
|
28943
|
+
let cleanupTimer = null;
|
|
27816
28944
|
let fileCache = null;
|
|
27817
28945
|
const FILE_CACHE_TTL = 1e4;
|
|
27818
28946
|
function isAuthenticated(request) {
|
|
@@ -27838,7 +28966,7 @@ data: ${JSON.stringify(formatSseState(entry))}
|
|
|
27838
28966
|
let scanDays = 30;
|
|
27839
28967
|
function getKnownDirectories() {
|
|
27840
28968
|
const dirs = new Set;
|
|
27841
|
-
dirs.add(
|
|
28969
|
+
dirs.add(os5.homedir());
|
|
27842
28970
|
for (const session2 of sessions.values()) {
|
|
27843
28971
|
if (session2.directory)
|
|
27844
28972
|
dirs.add(session2.directory);
|
|
@@ -28555,7 +29683,10 @@ data: ${JSON.stringify(formatSseState(entry))}
|
|
|
28555
29683
|
function start() {
|
|
28556
29684
|
if (baseUrl)
|
|
28557
29685
|
return Promise.resolve(baseUrl);
|
|
28558
|
-
|
|
29686
|
+
if (!cleanupTimer) {
|
|
29687
|
+
cleanupTimer = createCleanupTimer();
|
|
29688
|
+
}
|
|
29689
|
+
return new Promise((resolve3, reject) => {
|
|
28559
29690
|
const server = createServer((request, response) => {
|
|
28560
29691
|
handleRequest(request, response).catch((error) => {
|
|
28561
29692
|
sendJson(response, 500, {
|
|
@@ -28582,13 +29713,31 @@ data: ${JSON.stringify(formatSseState(entry))}
|
|
|
28582
29713
|
activeServer = server;
|
|
28583
29714
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
28584
29715
|
writeAuthFile(config.port, authToken);
|
|
28585
|
-
|
|
29716
|
+
resolve3(baseUrl);
|
|
28586
29717
|
});
|
|
28587
29718
|
});
|
|
28588
29719
|
}
|
|
28589
29720
|
function close() {
|
|
29721
|
+
if (cleanupTimer) {
|
|
29722
|
+
clearInterval(cleanupTimer);
|
|
29723
|
+
cleanupTimer = null;
|
|
29724
|
+
}
|
|
29725
|
+
for (const clients of sseClients.values()) {
|
|
29726
|
+
for (const response of clients) {
|
|
29727
|
+
try {
|
|
29728
|
+
if (!response.writableEnded && !response.destroyed) {
|
|
29729
|
+
response.end();
|
|
29730
|
+
}
|
|
29731
|
+
} catch {
|
|
29732
|
+
try {
|
|
29733
|
+
response.destroy();
|
|
29734
|
+
} catch {}
|
|
29735
|
+
}
|
|
29736
|
+
}
|
|
29737
|
+
}
|
|
29738
|
+
sseClients.clear();
|
|
29739
|
+
removeAuthFile(config.port);
|
|
28590
29740
|
if (activeServer) {
|
|
28591
|
-
removeAuthFile(config.port);
|
|
28592
29741
|
activeServer.closeAllConnections();
|
|
28593
29742
|
activeServer.close();
|
|
28594
29743
|
activeServer = null;
|
|
@@ -28730,7 +29879,7 @@ async function tryBecomeDashboard(config, maxAttempts = 3) {
|
|
|
28730
29879
|
const message = error instanceof Error ? error.message : String(error);
|
|
28731
29880
|
if (message.includes("already in use")) {
|
|
28732
29881
|
if (attempt < maxAttempts - 1) {
|
|
28733
|
-
await new Promise((
|
|
29882
|
+
await new Promise((resolve3) => setTimeout(resolve3, jitterMs()));
|
|
28734
29883
|
continue;
|
|
28735
29884
|
}
|
|
28736
29885
|
return null;
|
|
@@ -28741,6 +29890,9 @@ async function tryBecomeDashboard(config, maxAttempts = 3) {
|
|
|
28741
29890
|
return null;
|
|
28742
29891
|
}
|
|
28743
29892
|
|
|
29893
|
+
// src/interview/dashboard-manager.ts
|
|
29894
|
+
import path16 from "node:path";
|
|
29895
|
+
|
|
28744
29896
|
// src/interview/server.ts
|
|
28745
29897
|
import {
|
|
28746
29898
|
createServer as createServer2
|
|
@@ -28938,7 +30090,7 @@ function createInterviewServer(deps) {
|
|
|
28938
30090
|
if (startPromise) {
|
|
28939
30091
|
return startPromise;
|
|
28940
30092
|
}
|
|
28941
|
-
startPromise = new Promise((
|
|
30093
|
+
startPromise = new Promise((resolve3, reject) => {
|
|
28942
30094
|
const server = createServer2((request, response) => {
|
|
28943
30095
|
handle(request, response).catch((error) => {
|
|
28944
30096
|
sendJson(response, 500, {
|
|
@@ -28967,7 +30119,7 @@ function createInterviewServer(deps) {
|
|
|
28967
30119
|
return;
|
|
28968
30120
|
}
|
|
28969
30121
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
28970
|
-
|
|
30122
|
+
resolve3(baseUrl);
|
|
28971
30123
|
});
|
|
28972
30124
|
});
|
|
28973
30125
|
return startPromise;
|
|
@@ -29252,8 +30404,9 @@ function buildAnswerPrompt(answers, questions, maxQuestions) {
|
|
|
29252
30404
|
}
|
|
29253
30405
|
|
|
29254
30406
|
// src/interview/service.ts
|
|
29255
|
-
var
|
|
30407
|
+
var COMMAND_NAME4 = "interview";
|
|
29256
30408
|
var DEFAULT_MAX_QUESTIONS = 2;
|
|
30409
|
+
var MAX_RETAINED_ABANDONED = 50;
|
|
29257
30410
|
function isTruthyEnvFlag(value) {
|
|
29258
30411
|
if (!value) {
|
|
29259
30412
|
return false;
|
|
@@ -29312,6 +30465,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29312
30465
|
let onStateChange = null;
|
|
29313
30466
|
let onInterviewCreated = null;
|
|
29314
30467
|
let idCounter = 0;
|
|
30468
|
+
let abandonedOrderCounter = 0;
|
|
29315
30469
|
function setBaseUrlResolver(resolver) {
|
|
29316
30470
|
resolveBaseUrl = resolver;
|
|
29317
30471
|
}
|
|
@@ -29386,7 +30540,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29386
30540
|
return messages;
|
|
29387
30541
|
}
|
|
29388
30542
|
}
|
|
29389
|
-
await new Promise((
|
|
30543
|
+
await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
29390
30544
|
}
|
|
29391
30545
|
return loadMessages(sessionID);
|
|
29392
30546
|
}
|
|
@@ -29396,6 +30550,29 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29396
30550
|
function getInterviewById(interviewId) {
|
|
29397
30551
|
return interviewsById.get(interviewId) ?? null;
|
|
29398
30552
|
}
|
|
30553
|
+
function abandonInterview(interview) {
|
|
30554
|
+
if (interview.status !== "abandoned") {
|
|
30555
|
+
interview.abandonedAt = nowIso();
|
|
30556
|
+
interview.abandonedOrder = ++abandonedOrderCounter;
|
|
30557
|
+
}
|
|
30558
|
+
interview.status = "abandoned";
|
|
30559
|
+
pruneAbandonedInterviews();
|
|
30560
|
+
}
|
|
30561
|
+
function pruneAbandonedInterviews() {
|
|
30562
|
+
const abandoned = [...interviewsById.values()].filter((record) => record.status === "abandoned");
|
|
30563
|
+
const overflow = abandoned.length - MAX_RETAINED_ABANDONED;
|
|
30564
|
+
if (overflow <= 0)
|
|
30565
|
+
return;
|
|
30566
|
+
abandoned.sort((a, b) => {
|
|
30567
|
+
const timeDelta = new Date(a.abandonedAt ?? a.createdAt).getTime() - new Date(b.abandonedAt ?? b.createdAt).getTime();
|
|
30568
|
+
if (timeDelta !== 0)
|
|
30569
|
+
return timeDelta;
|
|
30570
|
+
return (a.abandonedOrder ?? 0) - (b.abandonedOrder ?? 0);
|
|
30571
|
+
}).slice(0, overflow).forEach((record) => {
|
|
30572
|
+
interviewsById.delete(record.id);
|
|
30573
|
+
browserOpened.delete(record.id);
|
|
30574
|
+
});
|
|
30575
|
+
}
|
|
29399
30576
|
async function createInterview(sessionID, idea) {
|
|
29400
30577
|
const normalizedIdea = idea.trim();
|
|
29401
30578
|
const activeId = activeInterviewIds.get(sessionID);
|
|
@@ -29405,7 +30582,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29405
30582
|
if (active.idea === normalizedIdea) {
|
|
29406
30583
|
return active;
|
|
29407
30584
|
}
|
|
29408
|
-
active
|
|
30585
|
+
abandonInterview(active);
|
|
29409
30586
|
}
|
|
29410
30587
|
}
|
|
29411
30588
|
const messages = await loadMessages(sessionID);
|
|
@@ -29435,7 +30612,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29435
30612
|
if (active.markdownPath === markdownPath) {
|
|
29436
30613
|
return active;
|
|
29437
30614
|
}
|
|
29438
|
-
active
|
|
30615
|
+
abandonInterview(active);
|
|
29439
30616
|
}
|
|
29440
30617
|
}
|
|
29441
30618
|
const document = await fs8.readFile(markdownPath, "utf8");
|
|
@@ -29531,11 +30708,11 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29531
30708
|
}
|
|
29532
30709
|
function registerCommand(opencodeConfig) {
|
|
29533
30710
|
const configCommand = opencodeConfig.command;
|
|
29534
|
-
if (!configCommand?.[
|
|
30711
|
+
if (!configCommand?.[COMMAND_NAME4]) {
|
|
29535
30712
|
if (!opencodeConfig.command) {
|
|
29536
30713
|
opencodeConfig.command = {};
|
|
29537
30714
|
}
|
|
29538
|
-
opencodeConfig.command[
|
|
30715
|
+
opencodeConfig.command[COMMAND_NAME4] = {
|
|
29539
30716
|
template: "Start an interview and write a live markdown spec",
|
|
29540
30717
|
description: "Open a localhost interview UI linked to the current OpenCode session"
|
|
29541
30718
|
};
|
|
@@ -29609,7 +30786,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29609
30786
|
}
|
|
29610
30787
|
}
|
|
29611
30788
|
async function handleCommandExecuteBefore(input, output) {
|
|
29612
|
-
if (input.command !==
|
|
30789
|
+
if (input.command !== COMMAND_NAME4) {
|
|
29613
30790
|
return;
|
|
29614
30791
|
}
|
|
29615
30792
|
const idea = input.arguments.trim();
|
|
@@ -29673,7 +30850,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29673
30850
|
if (!interview) {
|
|
29674
30851
|
return;
|
|
29675
30852
|
}
|
|
29676
|
-
interview
|
|
30853
|
+
abandonInterview(interview);
|
|
29677
30854
|
fileCache = null;
|
|
29678
30855
|
activeInterviewIds.delete(deletedSessionId);
|
|
29679
30856
|
log("[interview] session deleted, interview marked abandoned", {
|
|
@@ -29802,7 +30979,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29802
30979
|
`The user sent a freeform message via the dashboard chat panel:`,
|
|
29803
30980
|
`${message}`,
|
|
29804
30981
|
``,
|
|
29805
|
-
`Process this request
|
|
30982
|
+
`Process this request - it may be a request to add a new section, revise existing content, ask clarifying questions, or make structural changes.`,
|
|
29806
30983
|
`Update the specification document accordingly and include the updated 11-section specification.`,
|
|
29807
30984
|
`Ask up to ${maxQuestions} clarifying questions if needed using the same <interview_state> JSON block format as before.`
|
|
29808
30985
|
].join(`
|
|
@@ -29864,7 +31041,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29864
31041
|
`The user confirmed the interview spec is complete.`,
|
|
29865
31042
|
``,
|
|
29866
31043
|
`Produce a final, polished version of the full spec document.`,
|
|
29867
|
-
`Do NOT include any <interview_state> block
|
|
31044
|
+
`Do NOT include any <interview_state> block - just output the final spec as clean markdown.`,
|
|
29868
31045
|
`The spec should be comprehensive, well-structured, and ready for implementation.`
|
|
29869
31046
|
].join(`
|
|
29870
31047
|
`);
|
|
@@ -29902,34 +31079,9 @@ function createInterviewService(ctx, config, deps) {
|
|
|
29902
31079
|
};
|
|
29903
31080
|
}
|
|
29904
31081
|
|
|
29905
|
-
// src/interview/manager.ts
|
|
29906
|
-
function
|
|
31082
|
+
// src/interview/dashboard-manager.ts
|
|
31083
|
+
function createDashboardManager(ctx, config, dashboardPort, outputFolder) {
|
|
29907
31084
|
const interviewConfig = config.interview;
|
|
29908
|
-
const effectivePort = interviewConfig?.port ?? 0;
|
|
29909
|
-
const dashboardEnabled = interviewConfig?.dashboard === true || effectivePort > 0;
|
|
29910
|
-
const outputFolder = interviewConfig?.outputFolder ?? "interview";
|
|
29911
|
-
if (!dashboardEnabled) {
|
|
29912
|
-
const service2 = createInterviewService(ctx, interviewConfig);
|
|
29913
|
-
const resolvedOutputPath = path16.join(ctx.directory, outputFolder);
|
|
29914
|
-
const server = createInterviewServer({
|
|
29915
|
-
getState: async (interviewId) => service2.getInterviewState(interviewId),
|
|
29916
|
-
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
29917
|
-
listInterviews: () => service2.listInterviews(),
|
|
29918
|
-
submitAnswers: async (interviewId, answers) => service2.submitAnswers(interviewId, answers),
|
|
29919
|
-
submitBlockComment: async (interviewId, section, comment) => service2.submitBlockComment(interviewId, section, comment),
|
|
29920
|
-
submitChat: async (interviewId, message) => service2.submitChat(interviewId, message),
|
|
29921
|
-
handleNudgeAction: async (interviewId, action) => service2.handleNudgeAction(interviewId, action),
|
|
29922
|
-
outputFolder: resolvedOutputPath,
|
|
29923
|
-
port: 0
|
|
29924
|
-
});
|
|
29925
|
-
service2.setBaseUrlResolver(() => server.ensureStarted());
|
|
29926
|
-
return {
|
|
29927
|
-
registerCommand: (c) => service2.registerCommand(c),
|
|
29928
|
-
handleCommandExecuteBefore: async (input, output) => service2.handleCommandExecuteBefore(input, output),
|
|
29929
|
-
handleEvent: async (input) => service2.handleEvent(input)
|
|
29930
|
-
};
|
|
29931
|
-
}
|
|
29932
|
-
const dashboardPort = effectivePort > 0 ? effectivePort : DEFAULT_DASHBOARD_PORT;
|
|
29933
31085
|
const service = createInterviewService(ctx, interviewConfig);
|
|
29934
31086
|
let initDone = false;
|
|
29935
31087
|
let isDashboard = false;
|
|
@@ -29939,6 +31091,12 @@ function createInterviewManager(ctx, config) {
|
|
|
29939
31091
|
const registeredSessions = new Set;
|
|
29940
31092
|
const FALLBACK_POLL_INTERVAL = 1e4;
|
|
29941
31093
|
let fallbackTimer = null;
|
|
31094
|
+
const stopFallbackTimer = () => {
|
|
31095
|
+
if (!fallbackTimer)
|
|
31096
|
+
return;
|
|
31097
|
+
clearInterval(fallbackTimer);
|
|
31098
|
+
fallbackTimer = null;
|
|
31099
|
+
};
|
|
29942
31100
|
const startFallbackTimer = () => {
|
|
29943
31101
|
if (fallbackTimer)
|
|
29944
31102
|
return;
|
|
@@ -30049,7 +31207,7 @@ function createInterviewManager(ctx, config) {
|
|
|
30049
31207
|
log("[interview] dashboard election failed or unreachable. Falling back to per-session server.", { error: err instanceof Error ? err.message : String(err) });
|
|
30050
31208
|
isDashboard = false;
|
|
30051
31209
|
const resolvedOutputPath = path16.join(ctx.directory, outputFolder);
|
|
30052
|
-
const
|
|
31210
|
+
const fallbackServer = createInterviewServer({
|
|
30053
31211
|
getState: async (interviewId) => service.getInterviewState(interviewId),
|
|
30054
31212
|
listInterviewFiles: async () => service.listInterviewFiles(),
|
|
30055
31213
|
listInterviews: () => service.listInterviews(),
|
|
@@ -30060,7 +31218,7 @@ function createInterviewManager(ctx, config) {
|
|
|
30060
31218
|
outputFolder: resolvedOutputPath,
|
|
30061
31219
|
port: 0
|
|
30062
31220
|
});
|
|
30063
|
-
service.setBaseUrlResolver(() =>
|
|
31221
|
+
service.setBaseUrlResolver(() => fallbackServer.ensureStarted());
|
|
30064
31222
|
service.setStatePushCallback(() => {});
|
|
30065
31223
|
} finally {
|
|
30066
31224
|
initDone = true;
|
|
@@ -30076,7 +31234,10 @@ function createInterviewManager(ctx, config) {
|
|
|
30076
31234
|
if (!interviewId)
|
|
30077
31235
|
return;
|
|
30078
31236
|
try {
|
|
30079
|
-
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/pending
|
|
31237
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/pending`, {
|
|
31238
|
+
headers: authHeaders(authToken),
|
|
31239
|
+
signal: AbortSignal.timeout(3000)
|
|
31240
|
+
});
|
|
30080
31241
|
const body = await res.json();
|
|
30081
31242
|
if (res.ok && body.answers && body.answers.length > 0) {
|
|
30082
31243
|
log("[interview] delivering pending answers (HTTP poll)", {
|
|
@@ -30096,7 +31257,10 @@ function createInterviewManager(ctx, config) {
|
|
|
30096
31257
|
if (!interviewId)
|
|
30097
31258
|
return;
|
|
30098
31259
|
try {
|
|
30099
|
-
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/nudge
|
|
31260
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/nudge`, {
|
|
31261
|
+
headers: authHeaders(authToken),
|
|
31262
|
+
signal: AbortSignal.timeout(3000)
|
|
31263
|
+
});
|
|
30100
31264
|
const body = await res.json();
|
|
30101
31265
|
if (res.ok && body.action) {
|
|
30102
31266
|
log("[interview] delivering nudge action (HTTP poll)", {
|
|
@@ -30116,7 +31280,10 @@ function createInterviewManager(ctx, config) {
|
|
|
30116
31280
|
if (!interviewId)
|
|
30117
31281
|
return;
|
|
30118
31282
|
try {
|
|
30119
|
-
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/block-comment
|
|
31283
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/block-comment`, {
|
|
31284
|
+
headers: authHeaders(authToken),
|
|
31285
|
+
signal: AbortSignal.timeout(3000)
|
|
31286
|
+
});
|
|
30120
31287
|
const body = await res.json();
|
|
30121
31288
|
if (res.ok && body.section && body.comment) {
|
|
30122
31289
|
log("[interview] delivering block comment (HTTP poll)", {
|
|
@@ -30136,7 +31303,10 @@ function createInterviewManager(ctx, config) {
|
|
|
30136
31303
|
if (!interviewId)
|
|
30137
31304
|
return;
|
|
30138
31305
|
try {
|
|
30139
|
-
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/chat
|
|
31306
|
+
const res = await fetch(`${dashboardBaseUrl}/api/interviews/${interviewId}/chat`, {
|
|
31307
|
+
headers: authHeaders(authToken),
|
|
31308
|
+
signal: AbortSignal.timeout(3000)
|
|
31309
|
+
});
|
|
30140
31310
|
const body = await res.json();
|
|
30141
31311
|
if (res.ok && body.message) {
|
|
30142
31312
|
log("[interview] delivering chat message (HTTP poll)", {
|
|
@@ -30157,9 +31327,12 @@ function createInterviewManager(ctx, config) {
|
|
|
30157
31327
|
const sessionID = input.sessionID;
|
|
30158
31328
|
registeredSessions.add(sessionID);
|
|
30159
31329
|
if (!isDashboard && dashboardBaseUrl) {
|
|
30160
|
-
fetch(`${dashboardBaseUrl}/api/sessions
|
|
31330
|
+
fetch(`${dashboardBaseUrl}/api/sessions`, {
|
|
30161
31331
|
method: "POST",
|
|
30162
|
-
headers: {
|
|
31332
|
+
headers: {
|
|
31333
|
+
...authHeaders(authToken),
|
|
31334
|
+
"content-type": "application/json"
|
|
31335
|
+
},
|
|
30163
31336
|
body: JSON.stringify({
|
|
30164
31337
|
sessionID,
|
|
30165
31338
|
directory: ctx.directory,
|
|
@@ -30177,9 +31350,10 @@ function createInterviewManager(ctx, config) {
|
|
|
30177
31350
|
const properties = event.properties ?? {};
|
|
30178
31351
|
const sessionID = properties.sessionID ?? null;
|
|
30179
31352
|
await service.handleEvent(input);
|
|
30180
|
-
if (event.type === "session.status") {
|
|
31353
|
+
if (event.type === "session.status" || event.type === "session.idle") {
|
|
30181
31354
|
const status = properties.status;
|
|
30182
|
-
|
|
31355
|
+
const isIdleEvent = event.type === "session.idle" || status?.type === "idle";
|
|
31356
|
+
if (sessionID && isIdleEvent) {
|
|
30183
31357
|
const interviewId = service.getActiveInterviewId(sessionID);
|
|
30184
31358
|
if (!isDashboard && dashboardBaseUrl) {
|
|
30185
31359
|
await pollPendingAnswers(sessionID);
|
|
@@ -30230,11 +31404,17 @@ function createInterviewManager(ctx, config) {
|
|
|
30230
31404
|
}
|
|
30231
31405
|
if (event.type === "session.deleted" && sessionID) {
|
|
30232
31406
|
registeredSessions.delete(sessionID);
|
|
31407
|
+
if (!isDashboard && registeredSessions.size === 0) {
|
|
31408
|
+
stopFallbackTimer();
|
|
31409
|
+
}
|
|
30233
31410
|
dashboard?.removeSession(sessionID);
|
|
30234
31411
|
}
|
|
30235
31412
|
}
|
|
30236
31413
|
};
|
|
30237
31414
|
}
|
|
31415
|
+
function authHeaders(token) {
|
|
31416
|
+
return { authorization: `Bearer ${token}` };
|
|
31417
|
+
}
|
|
30238
31418
|
function stateToEntry(interviewId, state) {
|
|
30239
31419
|
return {
|
|
30240
31420
|
interviewId,
|
|
@@ -30261,17 +31441,23 @@ function stateToEntry(interviewId, state) {
|
|
|
30261
31441
|
}
|
|
30262
31442
|
async function pushStateViaHttp(dashboardUrl, token, interviewId, state) {
|
|
30263
31443
|
const entry = stateToEntry(interviewId, state);
|
|
30264
|
-
await fetch(`${dashboardUrl}/api/interviews/${interviewId}/state
|
|
31444
|
+
await fetch(`${dashboardUrl}/api/interviews/${interviewId}/state`, {
|
|
30265
31445
|
method: "POST",
|
|
30266
|
-
headers: {
|
|
31446
|
+
headers: {
|
|
31447
|
+
...authHeaders(token),
|
|
31448
|
+
"content-type": "application/json"
|
|
31449
|
+
},
|
|
30267
31450
|
body: JSON.stringify(entry),
|
|
30268
31451
|
signal: AbortSignal.timeout(5000)
|
|
30269
31452
|
});
|
|
30270
31453
|
}
|
|
30271
31454
|
async function registerInterviewViaHttp(dashboardUrl, token, interview) {
|
|
30272
|
-
await fetch(`${dashboardUrl}/api/interviews
|
|
31455
|
+
await fetch(`${dashboardUrl}/api/interviews`, {
|
|
30273
31456
|
method: "POST",
|
|
30274
|
-
headers: {
|
|
31457
|
+
headers: {
|
|
31458
|
+
...authHeaders(token),
|
|
31459
|
+
"content-type": "application/json"
|
|
31460
|
+
},
|
|
30275
31461
|
body: JSON.stringify({
|
|
30276
31462
|
interviewId: interview.id,
|
|
30277
31463
|
sessionID: interview.sessionID,
|
|
@@ -30294,6 +31480,43 @@ async function registerInterviewViaHttp(dashboardUrl, token, interview) {
|
|
|
30294
31480
|
});
|
|
30295
31481
|
});
|
|
30296
31482
|
}
|
|
31483
|
+
|
|
31484
|
+
// src/interview/session-server.ts
|
|
31485
|
+
import path17 from "node:path";
|
|
31486
|
+
function createPerSessionInterviewServer(ctx, interviewConfig, outputFolder) {
|
|
31487
|
+
const service = createInterviewService(ctx, interviewConfig);
|
|
31488
|
+
const resolvedOutputPath = path17.join(ctx.directory, outputFolder);
|
|
31489
|
+
const server = createInterviewServer({
|
|
31490
|
+
getState: async (interviewId) => service.getInterviewState(interviewId),
|
|
31491
|
+
listInterviewFiles: async () => service.listInterviewFiles(),
|
|
31492
|
+
listInterviews: () => service.listInterviews(),
|
|
31493
|
+
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
31494
|
+
submitBlockComment: async (interviewId, section, comment) => service.submitBlockComment(interviewId, section, comment),
|
|
31495
|
+
submitChat: async (interviewId, message) => service.submitChat(interviewId, message),
|
|
31496
|
+
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
31497
|
+
outputFolder: resolvedOutputPath,
|
|
31498
|
+
port: 0
|
|
31499
|
+
});
|
|
31500
|
+
service.setBaseUrlResolver(() => server.ensureStarted());
|
|
31501
|
+
return {
|
|
31502
|
+
registerCommand: (c) => service.registerCommand(c),
|
|
31503
|
+
handleCommandExecuteBefore: async (input, output) => service.handleCommandExecuteBefore(input, output),
|
|
31504
|
+
handleEvent: async (input) => service.handleEvent(input)
|
|
31505
|
+
};
|
|
31506
|
+
}
|
|
31507
|
+
|
|
31508
|
+
// src/interview/manager.ts
|
|
31509
|
+
function createInterviewManager(ctx, config) {
|
|
31510
|
+
const interviewConfig = config.interview;
|
|
31511
|
+
const effectivePort = interviewConfig?.port ?? 0;
|
|
31512
|
+
const dashboardEnabled = interviewConfig?.dashboard === true || effectivePort > 0;
|
|
31513
|
+
const outputFolder = interviewConfig?.outputFolder ?? "interview";
|
|
31514
|
+
if (!dashboardEnabled) {
|
|
31515
|
+
return createPerSessionInterviewServer(ctx, interviewConfig, outputFolder);
|
|
31516
|
+
}
|
|
31517
|
+
const dashboardPort = effectivePort > 0 ? effectivePort : DEFAULT_DASHBOARD_PORT;
|
|
31518
|
+
return createDashboardManager(ctx, config, dashboardPort, outputFolder);
|
|
31519
|
+
}
|
|
30297
31520
|
// src/mcp/context7.ts
|
|
30298
31521
|
var context7 = {
|
|
30299
31522
|
type: "remote",
|
|
@@ -30350,6 +31573,209 @@ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
|
|
|
30350
31573
|
return mcps;
|
|
30351
31574
|
}
|
|
30352
31575
|
|
|
31576
|
+
// src/multiplexer/herdr/index.ts
|
|
31577
|
+
init_compat();
|
|
31578
|
+
class HerdrMultiplexer {
|
|
31579
|
+
type = "herdr";
|
|
31580
|
+
binaryPath = null;
|
|
31581
|
+
hasChecked = false;
|
|
31582
|
+
parentPaneId = process.env.HERDR_PANE_ID;
|
|
31583
|
+
paneDirection;
|
|
31584
|
+
constructor(layout = "main-vertical", mainPaneSize = 60) {
|
|
31585
|
+
this.paneDirection = getPaneDirection(layout);
|
|
31586
|
+
}
|
|
31587
|
+
async isAvailable() {
|
|
31588
|
+
if (this.hasChecked) {
|
|
31589
|
+
return this.binaryPath !== null;
|
|
31590
|
+
}
|
|
31591
|
+
this.binaryPath = await this.findBinary();
|
|
31592
|
+
this.hasChecked = true;
|
|
31593
|
+
return this.binaryPath !== null;
|
|
31594
|
+
}
|
|
31595
|
+
isInsideSession() {
|
|
31596
|
+
return !!(process.env.HERDR_ENV || process.env.HERDR_PANE_ID);
|
|
31597
|
+
}
|
|
31598
|
+
async spawnPane(sessionId, description, serverUrl, directory) {
|
|
31599
|
+
const herdr = await this.getBinary();
|
|
31600
|
+
if (!herdr) {
|
|
31601
|
+
log("[herdr] spawnPane: herdr binary not found");
|
|
31602
|
+
return { success: false };
|
|
31603
|
+
}
|
|
31604
|
+
try {
|
|
31605
|
+
const splitArgs = [
|
|
31606
|
+
herdr,
|
|
31607
|
+
"pane",
|
|
31608
|
+
"split",
|
|
31609
|
+
...this.targetPaneArg(),
|
|
31610
|
+
"--direction",
|
|
31611
|
+
this.paneDirection,
|
|
31612
|
+
"--cwd",
|
|
31613
|
+
directory,
|
|
31614
|
+
"--no-focus"
|
|
31615
|
+
];
|
|
31616
|
+
log("[herdr] spawnPane: splitting pane", { args: splitArgs });
|
|
31617
|
+
const splitProc = crossSpawn(splitArgs, {
|
|
31618
|
+
stdout: "pipe",
|
|
31619
|
+
stderr: "pipe"
|
|
31620
|
+
});
|
|
31621
|
+
const splitExitCode = await splitProc.exited;
|
|
31622
|
+
const splitStdout = await splitProc.stdout();
|
|
31623
|
+
const splitStderr = await splitProc.stderr();
|
|
31624
|
+
if (splitExitCode !== 0) {
|
|
31625
|
+
log("[herdr] spawnPane: split failed", {
|
|
31626
|
+
exitCode: splitExitCode,
|
|
31627
|
+
stderr: splitStderr.trim()
|
|
31628
|
+
});
|
|
31629
|
+
return { success: false };
|
|
31630
|
+
}
|
|
31631
|
+
const paneId = parsePaneId(splitStdout);
|
|
31632
|
+
if (!paneId) {
|
|
31633
|
+
log("[herdr] spawnPane: could not parse pane_id from output", {
|
|
31634
|
+
stdout: splitStdout.trim()
|
|
31635
|
+
});
|
|
31636
|
+
return { success: false };
|
|
31637
|
+
}
|
|
31638
|
+
await crossSpawn([herdr, "pane", "rename", paneId, description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" }).exited;
|
|
31639
|
+
const opencodeCmd = buildOpencodeAttachCommand(sessionId, serverUrl, directory);
|
|
31640
|
+
log("[herdr] spawnPane: running attach command", {
|
|
31641
|
+
paneId,
|
|
31642
|
+
command: opencodeCmd
|
|
31643
|
+
});
|
|
31644
|
+
const runProc = crossSpawn([herdr, "pane", "run", paneId, opencodeCmd], {
|
|
31645
|
+
stdout: "pipe",
|
|
31646
|
+
stderr: "pipe"
|
|
31647
|
+
});
|
|
31648
|
+
const runExitCode = await runProc.exited;
|
|
31649
|
+
if (runExitCode !== 0) {
|
|
31650
|
+
const runStderr = await runProc.stderr();
|
|
31651
|
+
log("[herdr] spawnPane: run failed", {
|
|
31652
|
+
exitCode: runExitCode,
|
|
31653
|
+
stderr: runStderr.trim()
|
|
31654
|
+
});
|
|
31655
|
+
return { success: false };
|
|
31656
|
+
}
|
|
31657
|
+
log("[herdr] spawnPane: SUCCESS", { paneId });
|
|
31658
|
+
return { success: true, paneId };
|
|
31659
|
+
} catch (err) {
|
|
31660
|
+
log("[herdr] spawnPane: exception", { error: String(err) });
|
|
31661
|
+
return { success: false };
|
|
31662
|
+
}
|
|
31663
|
+
}
|
|
31664
|
+
async closePane(paneId) {
|
|
31665
|
+
if (!paneId || paneId === "unknown")
|
|
31666
|
+
return true;
|
|
31667
|
+
const herdr = await this.getBinary();
|
|
31668
|
+
if (!herdr) {
|
|
31669
|
+
log("[herdr] closePane: herdr binary not found");
|
|
31670
|
+
return false;
|
|
31671
|
+
}
|
|
31672
|
+
try {
|
|
31673
|
+
log("[herdr] closePane: sending Ctrl+C", { paneId });
|
|
31674
|
+
await crossSpawn([herdr, "pane", "send-keys", paneId, "ctrl+c"], {
|
|
31675
|
+
stdout: "ignore",
|
|
31676
|
+
stderr: "ignore"
|
|
31677
|
+
}).exited;
|
|
31678
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
31679
|
+
log("[herdr] closePane: closing pane", { paneId });
|
|
31680
|
+
const proc = crossSpawn([herdr, "pane", "close", paneId], {
|
|
31681
|
+
stdout: "pipe",
|
|
31682
|
+
stderr: "pipe"
|
|
31683
|
+
});
|
|
31684
|
+
const exitCode = await proc.exited;
|
|
31685
|
+
const stderr = await proc.stderr();
|
|
31686
|
+
log("[herdr] closePane: result", { exitCode, stderr: stderr.trim() });
|
|
31687
|
+
if (exitCode === 0 || exitCode === 1) {
|
|
31688
|
+
return true;
|
|
31689
|
+
}
|
|
31690
|
+
log("[herdr] closePane: failed (pane may already be closed)", {
|
|
31691
|
+
paneId
|
|
31692
|
+
});
|
|
31693
|
+
return false;
|
|
31694
|
+
} catch (err) {
|
|
31695
|
+
log("[herdr] closePane: exception", { error: String(err) });
|
|
31696
|
+
return false;
|
|
31697
|
+
}
|
|
31698
|
+
}
|
|
31699
|
+
async applyLayout(_layout, _mainPaneSize) {}
|
|
31700
|
+
targetPaneArg() {
|
|
31701
|
+
return this.parentPaneId ? [this.parentPaneId] : ["--current"];
|
|
31702
|
+
}
|
|
31703
|
+
async getBinary() {
|
|
31704
|
+
await this.isAvailable();
|
|
31705
|
+
return this.binaryPath;
|
|
31706
|
+
}
|
|
31707
|
+
async findBinary() {
|
|
31708
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
31709
|
+
try {
|
|
31710
|
+
const proc = crossSpawn([cmd, "herdr"], {
|
|
31711
|
+
stdout: "pipe",
|
|
31712
|
+
stderr: "pipe"
|
|
31713
|
+
});
|
|
31714
|
+
const exitCode = await proc.exited;
|
|
31715
|
+
if (exitCode !== 0) {
|
|
31716
|
+
log("[herdr] findBinary: 'which herdr' failed", { exitCode });
|
|
31717
|
+
return null;
|
|
31718
|
+
}
|
|
31719
|
+
const stdout = await proc.stdout();
|
|
31720
|
+
const path18 = stdout.trim().split(`
|
|
31721
|
+
`)[0];
|
|
31722
|
+
if (!path18) {
|
|
31723
|
+
log("[herdr] findBinary: no path in output");
|
|
31724
|
+
return null;
|
|
31725
|
+
}
|
|
31726
|
+
log("[herdr] findBinary: found", { path: path18 });
|
|
31727
|
+
return path18;
|
|
31728
|
+
} catch (err) {
|
|
31729
|
+
log("[herdr] findBinary: exception", { error: String(err) });
|
|
31730
|
+
return null;
|
|
31731
|
+
}
|
|
31732
|
+
}
|
|
31733
|
+
}
|
|
31734
|
+
function parsePaneId(stdout) {
|
|
31735
|
+
const trimmed = stdout.trim();
|
|
31736
|
+
if (!trimmed)
|
|
31737
|
+
return null;
|
|
31738
|
+
for (const line of trimmed.split(`
|
|
31739
|
+
`)) {
|
|
31740
|
+
const candidate = line.trim();
|
|
31741
|
+
if (!candidate)
|
|
31742
|
+
continue;
|
|
31743
|
+
try {
|
|
31744
|
+
const response = JSON.parse(candidate);
|
|
31745
|
+
const paneId = response.result?.pane?.pane_id;
|
|
31746
|
+
if (paneId)
|
|
31747
|
+
return paneId;
|
|
31748
|
+
} catch {}
|
|
31749
|
+
}
|
|
31750
|
+
log("[herdr] parsePaneId: no pane_id found in output", { stdout: trimmed });
|
|
31751
|
+
return null;
|
|
31752
|
+
}
|
|
31753
|
+
function getPaneDirection(layout) {
|
|
31754
|
+
switch (layout) {
|
|
31755
|
+
case "main-horizontal":
|
|
31756
|
+
case "even-vertical":
|
|
31757
|
+
return "down";
|
|
31758
|
+
case "main-vertical":
|
|
31759
|
+
case "even-horizontal":
|
|
31760
|
+
case "tiled":
|
|
31761
|
+
return "right";
|
|
31762
|
+
}
|
|
31763
|
+
}
|
|
31764
|
+
function buildOpencodeAttachCommand(sessionId, serverUrl, directory) {
|
|
31765
|
+
return [
|
|
31766
|
+
"opencode",
|
|
31767
|
+
"attach",
|
|
31768
|
+
quoteShellArg(serverUrl),
|
|
31769
|
+
"--session",
|
|
31770
|
+
quoteShellArg(sessionId),
|
|
31771
|
+
"--dir",
|
|
31772
|
+
quoteShellArg(directory)
|
|
31773
|
+
].join(" ");
|
|
31774
|
+
}
|
|
31775
|
+
function quoteShellArg(value) {
|
|
31776
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
31777
|
+
}
|
|
31778
|
+
|
|
30353
31779
|
// src/multiplexer/tmux/index.ts
|
|
30354
31780
|
init_compat();
|
|
30355
31781
|
var TMUX_LAYOUT_DEBOUNCE_MS = 150;
|
|
@@ -30385,9 +31811,9 @@ class TmuxMultiplexer {
|
|
|
30385
31811
|
return { success: false };
|
|
30386
31812
|
}
|
|
30387
31813
|
try {
|
|
30388
|
-
const quotedDirectory =
|
|
30389
|
-
const quotedUrl =
|
|
30390
|
-
const quotedSessionId =
|
|
31814
|
+
const quotedDirectory = quoteShellArg2(directory);
|
|
31815
|
+
const quotedUrl = quoteShellArg2(serverUrl);
|
|
31816
|
+
const quotedSessionId = quoteShellArg2(sessionId);
|
|
30391
31817
|
const opencodeCmd = [
|
|
30392
31818
|
"opencode",
|
|
30393
31819
|
"attach",
|
|
@@ -30569,30 +31995,30 @@ class TmuxMultiplexer {
|
|
|
30569
31995
|
return null;
|
|
30570
31996
|
}
|
|
30571
31997
|
const stdout = await proc.stdout();
|
|
30572
|
-
const
|
|
31998
|
+
const path18 = stdout.trim().split(`
|
|
30573
31999
|
`)[0];
|
|
30574
|
-
if (!
|
|
32000
|
+
if (!path18) {
|
|
30575
32001
|
log("[tmux] findBinary: no path in output");
|
|
30576
32002
|
return null;
|
|
30577
32003
|
}
|
|
30578
|
-
const verifyProc = crossSpawn([
|
|
32004
|
+
const verifyProc = crossSpawn([path18, "-V"], {
|
|
30579
32005
|
stdout: "pipe",
|
|
30580
32006
|
stderr: "pipe"
|
|
30581
32007
|
});
|
|
30582
32008
|
const verifyExit = await verifyProc.exited;
|
|
30583
32009
|
if (verifyExit !== 0) {
|
|
30584
|
-
log("[tmux] findBinary: tmux -V failed", { path:
|
|
32010
|
+
log("[tmux] findBinary: tmux -V failed", { path: path18, verifyExit });
|
|
30585
32011
|
return null;
|
|
30586
32012
|
}
|
|
30587
|
-
log("[tmux] findBinary: found", { path:
|
|
30588
|
-
return
|
|
32013
|
+
log("[tmux] findBinary: found", { path: path18 });
|
|
32014
|
+
return path18;
|
|
30589
32015
|
} catch (err) {
|
|
30590
32016
|
log("[tmux] findBinary: exception", { error: String(err) });
|
|
30591
32017
|
return null;
|
|
30592
32018
|
}
|
|
30593
32019
|
}
|
|
30594
32020
|
}
|
|
30595
|
-
function
|
|
32021
|
+
function quoteShellArg2(value) {
|
|
30596
32022
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
30597
32023
|
}
|
|
30598
32024
|
|
|
@@ -30612,7 +32038,7 @@ class ZellijMultiplexer {
|
|
|
30612
32038
|
paneDirection;
|
|
30613
32039
|
constructor(layout = "main-vertical", mainPaneSize = 60, paneMode = "agent-tab") {
|
|
30614
32040
|
this.paneMode = paneMode;
|
|
30615
|
-
this.paneDirection =
|
|
32041
|
+
this.paneDirection = getPaneDirection2(layout);
|
|
30616
32042
|
}
|
|
30617
32043
|
async isAvailable() {
|
|
30618
32044
|
if (this.hasChecked) {
|
|
@@ -30653,7 +32079,7 @@ class ZellijMultiplexer {
|
|
|
30653
32079
|
}
|
|
30654
32080
|
}
|
|
30655
32081
|
async createPaneInCurrentTab(zellij, sessionId, serverUrl, directory, description) {
|
|
30656
|
-
const opencodeCmd =
|
|
32082
|
+
const opencodeCmd = buildOpencodeAttachCommand2(sessionId, serverUrl, directory);
|
|
30657
32083
|
const paneName = description.slice(0, 30).replace(/"/g, "\\\"");
|
|
30658
32084
|
const targetTabId = await this.getParentTabId(zellij);
|
|
30659
32085
|
const args = [
|
|
@@ -30682,7 +32108,7 @@ class ZellijMultiplexer {
|
|
|
30682
32108
|
return { success: false };
|
|
30683
32109
|
}
|
|
30684
32110
|
async createPaneInAgentTab(zellij, sessionId, serverUrl, directory, description) {
|
|
30685
|
-
const opencodeCmd =
|
|
32111
|
+
const opencodeCmd = buildOpencodeAttachCommand2(sessionId, serverUrl, directory);
|
|
30686
32112
|
const paneName = description.slice(0, 30).replace(/"/g, "\\\"");
|
|
30687
32113
|
const currentTabId = await this.getCurrentTabId(zellij);
|
|
30688
32114
|
const inAgentTab = currentTabId === this.agentTabId;
|
|
@@ -30751,7 +32177,7 @@ class ZellijMultiplexer {
|
|
|
30751
32177
|
}
|
|
30752
32178
|
async runInPane(zellij, paneId, sessionId, serverUrl, directory, description) {
|
|
30753
32179
|
try {
|
|
30754
|
-
const opencodeCmd =
|
|
32180
|
+
const opencodeCmd = buildOpencodeAttachCommand2(sessionId, serverUrl, directory);
|
|
30755
32181
|
await crossSpawn([zellij, "action", "focus-pane", "--pane-id", paneId], {
|
|
30756
32182
|
stdout: "ignore",
|
|
30757
32183
|
stderr: "ignore"
|
|
@@ -30975,7 +32401,7 @@ class ZellijMultiplexer {
|
|
|
30975
32401
|
function normalizePaneId(paneId) {
|
|
30976
32402
|
return paneId.replace(/^terminal_/, "");
|
|
30977
32403
|
}
|
|
30978
|
-
function
|
|
32404
|
+
function getPaneDirection2(layout) {
|
|
30979
32405
|
switch (layout) {
|
|
30980
32406
|
case "main-vertical":
|
|
30981
32407
|
return "right";
|
|
@@ -30987,21 +32413,21 @@ function getPaneDirection(layout) {
|
|
|
30987
32413
|
return null;
|
|
30988
32414
|
}
|
|
30989
32415
|
}
|
|
30990
|
-
function
|
|
32416
|
+
function buildOpencodeAttachCommand2(sessionId, serverUrl, directory) {
|
|
30991
32417
|
return [
|
|
30992
32418
|
"opencode",
|
|
30993
32419
|
"attach",
|
|
30994
|
-
|
|
32420
|
+
quoteShellArg3(serverUrl),
|
|
30995
32421
|
"--session",
|
|
30996
|
-
|
|
32422
|
+
quoteShellArg3(sessionId),
|
|
30997
32423
|
"--dir",
|
|
30998
|
-
|
|
32424
|
+
quoteShellArg3(directory)
|
|
30999
32425
|
].join(" ");
|
|
31000
32426
|
}
|
|
31001
32427
|
function buildShellLaunchCommand(command) {
|
|
31002
|
-
return ["sh", "-lc",
|
|
32428
|
+
return ["sh", "-lc", quoteShellArg3(command)].join(" ");
|
|
31003
32429
|
}
|
|
31004
|
-
function
|
|
32430
|
+
function quoteShellArg3(value) {
|
|
31005
32431
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
31006
32432
|
}
|
|
31007
32433
|
|
|
@@ -31022,6 +32448,10 @@ function getMultiplexer(config) {
|
|
|
31022
32448
|
multiplexer = new ZellijMultiplexer(config.layout, config.main_pane_size, config.zellij_pane_mode);
|
|
31023
32449
|
actualType = "zellij";
|
|
31024
32450
|
break;
|
|
32451
|
+
case "herdr":
|
|
32452
|
+
multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
|
|
32453
|
+
actualType = "herdr";
|
|
32454
|
+
break;
|
|
31025
32455
|
case "auto": {
|
|
31026
32456
|
if (process.env.TMUX) {
|
|
31027
32457
|
multiplexer = new TmuxMultiplexer(config.layout, config.main_pane_size);
|
|
@@ -31029,6 +32459,9 @@ function getMultiplexer(config) {
|
|
|
31029
32459
|
} else if (process.env.ZELLIJ) {
|
|
31030
32460
|
multiplexer = new ZellijMultiplexer(config.layout, config.main_pane_size, config.zellij_pane_mode);
|
|
31031
32461
|
actualType = "zellij";
|
|
32462
|
+
} else if (process.env.HERDR_ENV || process.env.HERDR_PANE_ID) {
|
|
32463
|
+
multiplexer = new HerdrMultiplexer(config.layout, config.main_pane_size);
|
|
32464
|
+
actualType = "herdr";
|
|
31032
32465
|
} else {
|
|
31033
32466
|
log("[multiplexer] auto: not inside any session, disabling");
|
|
31034
32467
|
return null;
|
|
@@ -31194,7 +32627,7 @@ class MultiplexerSessionManager {
|
|
|
31194
32627
|
tracked: this.sessions.has(sessionId2),
|
|
31195
32628
|
known: this.knownSessions.has(sessionId2),
|
|
31196
32629
|
ownerInstanceId: this.sessions.get(sessionId2)?.ownerInstanceId,
|
|
31197
|
-
backgroundJobState: this.
|
|
32630
|
+
backgroundJobState: this.backgroundJobState(sessionId2)
|
|
31198
32631
|
});
|
|
31199
32632
|
await this.closeSession(sessionId2, "idle");
|
|
31200
32633
|
return;
|
|
@@ -31212,22 +32645,23 @@ class MultiplexerSessionManager {
|
|
|
31212
32645
|
tracked: this.sessions.has(sessionId),
|
|
31213
32646
|
known: this.knownSessions.has(sessionId),
|
|
31214
32647
|
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
31215
|
-
backgroundJobState: this.
|
|
32648
|
+
backgroundJobState: this.backgroundJobState(sessionId)
|
|
31216
32649
|
});
|
|
31217
32650
|
await this.closeSession(sessionId, "idle");
|
|
31218
32651
|
return;
|
|
31219
32652
|
}
|
|
31220
32653
|
if (statusType) {
|
|
31221
|
-
|
|
31222
|
-
|
|
32654
|
+
if (statusType !== "busy") {
|
|
32655
|
+
this.deferredIdleCloses.delete(sessionId);
|
|
31223
32656
|
return;
|
|
32657
|
+
}
|
|
31224
32658
|
log("[multiplexer-session-manager] session busy event received", {
|
|
31225
32659
|
instanceId: this.instanceId,
|
|
31226
32660
|
sessionId,
|
|
31227
32661
|
tracked: this.sessions.has(sessionId),
|
|
31228
32662
|
known: this.knownSessions.has(sessionId),
|
|
31229
32663
|
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
31230
|
-
backgroundJobState: this.
|
|
32664
|
+
backgroundJobState: this.backgroundJobState(sessionId)
|
|
31231
32665
|
});
|
|
31232
32666
|
await this.respawnIfKnown(sessionId);
|
|
31233
32667
|
}
|
|
@@ -31246,7 +32680,7 @@ class MultiplexerSessionManager {
|
|
|
31246
32680
|
tracked: this.sessions.has(sessionId),
|
|
31247
32681
|
known: this.knownSessions.has(sessionId),
|
|
31248
32682
|
ownerInstanceId: this.sessions.get(sessionId)?.ownerInstanceId,
|
|
31249
|
-
backgroundJobState: this.
|
|
32683
|
+
backgroundJobState: this.backgroundJobState(sessionId)
|
|
31250
32684
|
});
|
|
31251
32685
|
this.deferredIdleCloses.delete(sessionId);
|
|
31252
32686
|
await this.closeSession(sessionId, "deleted");
|
|
@@ -31363,7 +32797,7 @@ class MultiplexerSessionManager {
|
|
|
31363
32797
|
sessionId,
|
|
31364
32798
|
paneId: tracked.paneId,
|
|
31365
32799
|
reason,
|
|
31366
|
-
backgroundJobState: this.
|
|
32800
|
+
backgroundJobState: this.backgroundJobState(sessionId)
|
|
31367
32801
|
});
|
|
31368
32802
|
return;
|
|
31369
32803
|
}
|
|
@@ -31374,7 +32808,7 @@ class MultiplexerSessionManager {
|
|
|
31374
32808
|
sessionId,
|
|
31375
32809
|
paneId: tracked.paneId,
|
|
31376
32810
|
reason,
|
|
31377
|
-
backgroundJobState: this.
|
|
32811
|
+
backgroundJobState: this.backgroundJobState(sessionId),
|
|
31378
32812
|
parentId: tracked.parentId,
|
|
31379
32813
|
title: tracked.title
|
|
31380
32814
|
});
|
|
@@ -31475,8 +32909,11 @@ class MultiplexerSessionManager {
|
|
|
31475
32909
|
getSessionId(event) {
|
|
31476
32910
|
return event.properties?.info?.id ?? event.properties?.sessionID;
|
|
31477
32911
|
}
|
|
32912
|
+
backgroundJobState(sessionId) {
|
|
32913
|
+
return this.backgroundJobBoard?.getState(sessionId);
|
|
32914
|
+
}
|
|
31478
32915
|
isRunningBackgroundJob(sessionId) {
|
|
31479
|
-
return this.backgroundJobBoard?.
|
|
32916
|
+
return this.backgroundJobBoard?.isRunning(sessionId) ?? false;
|
|
31480
32917
|
}
|
|
31481
32918
|
async retryDeferredIdleClose(sessionId) {
|
|
31482
32919
|
if (!this.enabled)
|
|
@@ -31633,8 +33070,8 @@ class AcpClient {
|
|
|
31633
33070
|
request(method, params) {
|
|
31634
33071
|
const id = this.next++;
|
|
31635
33072
|
const payload = { jsonrpc: "2.0", id, method, params };
|
|
31636
|
-
return new Promise((
|
|
31637
|
-
this.pending.set(id, { resolve:
|
|
33073
|
+
return new Promise((resolve4, reject) => {
|
|
33074
|
+
this.pending.set(id, { resolve: resolve4, reject });
|
|
31638
33075
|
this.child.stdin.write(`${JSON.stringify(payload)}
|
|
31639
33076
|
`, (error) => {
|
|
31640
33077
|
if (!error)
|
|
@@ -31651,7 +33088,7 @@ class AcpClient {
|
|
|
31651
33088
|
async drain() {
|
|
31652
33089
|
this.lastUpdate = Date.now();
|
|
31653
33090
|
while (this.activeRequests > 0 || Date.now() - this.lastUpdate < 100) {
|
|
31654
|
-
await new Promise((
|
|
33091
|
+
await new Promise((resolve4) => setTimeout(resolve4, 25));
|
|
31655
33092
|
}
|
|
31656
33093
|
}
|
|
31657
33094
|
async receive(line) {
|
|
@@ -31876,10 +33313,10 @@ import { existsSync as existsSync11 } from "node:fs";
|
|
|
31876
33313
|
// src/tools/ast-grep/constants.ts
|
|
31877
33314
|
import { existsSync as existsSync10, statSync as statSync5 } from "node:fs";
|
|
31878
33315
|
import { createRequire as createRequire3 } from "node:module";
|
|
31879
|
-
import { dirname as
|
|
33316
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
31880
33317
|
|
|
31881
33318
|
// src/tools/ast-grep/downloader.ts
|
|
31882
|
-
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as
|
|
33319
|
+
import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "node:fs";
|
|
31883
33320
|
import { createRequire as createRequire2 } from "node:module";
|
|
31884
33321
|
import { homedir as homedir6 } from "node:os";
|
|
31885
33322
|
import { join as join14 } from "node:path";
|
|
@@ -31934,8 +33371,8 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
31934
33371
|
if (existsSync9(binaryPath)) {
|
|
31935
33372
|
return binaryPath;
|
|
31936
33373
|
}
|
|
31937
|
-
const { arch, os:
|
|
31938
|
-
const assetName = `app-${arch}-${
|
|
33374
|
+
const { arch, os: os6 } = platformInfo;
|
|
33375
|
+
const assetName = `app-${arch}-${os6}.zip`;
|
|
31939
33376
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
31940
33377
|
console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
|
|
31941
33378
|
try {
|
|
@@ -31951,7 +33388,7 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
31951
33388
|
await crossWrite(archivePath, arrayBuffer);
|
|
31952
33389
|
await extractZip(archivePath, cacheDir);
|
|
31953
33390
|
if (existsSync9(archivePath)) {
|
|
31954
|
-
|
|
33391
|
+
unlinkSync5(archivePath);
|
|
31955
33392
|
}
|
|
31956
33393
|
if (process.platform !== "win32" && existsSync9(binaryPath)) {
|
|
31957
33394
|
chmodSync2(binaryPath, 493);
|
|
@@ -32034,7 +33471,7 @@ function findSgCliPathSync() {
|
|
|
32034
33471
|
try {
|
|
32035
33472
|
const require2 = createRequire3(import.meta.url);
|
|
32036
33473
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
32037
|
-
const cliDir =
|
|
33474
|
+
const cliDir = dirname10(cliPkgPath);
|
|
32038
33475
|
const sgPath = join15(cliDir, binaryName);
|
|
32039
33476
|
if (existsSync10(sgPath) && isValidBinary(sgPath)) {
|
|
32040
33477
|
return sgPath;
|
|
@@ -32045,7 +33482,7 @@ function findSgCliPathSync() {
|
|
|
32045
33482
|
try {
|
|
32046
33483
|
const require2 = createRequire3(import.meta.url);
|
|
32047
33484
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
32048
|
-
const pkgDir =
|
|
33485
|
+
const pkgDir = dirname10(pkgPath);
|
|
32049
33486
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
32050
33487
|
const binaryPath = join15(pkgDir, astGrepName);
|
|
32051
33488
|
if (existsSync10(binaryPath) && isValidBinary(binaryPath)) {
|
|
@@ -32055,9 +33492,9 @@ function findSgCliPathSync() {
|
|
|
32055
33492
|
}
|
|
32056
33493
|
if (process.platform === "darwin") {
|
|
32057
33494
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
32058
|
-
for (const
|
|
32059
|
-
if (existsSync10(
|
|
32060
|
-
return
|
|
33495
|
+
for (const path18 of homebrewPaths) {
|
|
33496
|
+
if (existsSync10(path18) && isValidBinary(path18)) {
|
|
33497
|
+
return path18;
|
|
32061
33498
|
}
|
|
32062
33499
|
}
|
|
32063
33500
|
}
|
|
@@ -32074,8 +33511,8 @@ function getSgCliPath() {
|
|
|
32074
33511
|
}
|
|
32075
33512
|
return "sg";
|
|
32076
33513
|
}
|
|
32077
|
-
function setSgCliPath(
|
|
32078
|
-
resolvedCliPath =
|
|
33514
|
+
function setSgCliPath(path18) {
|
|
33515
|
+
resolvedCliPath = path18;
|
|
32079
33516
|
}
|
|
32080
33517
|
var DEFAULT_TIMEOUT_MS = 300000;
|
|
32081
33518
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -32449,9 +33886,9 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
32449
33886
|
parentSessionID,
|
|
32450
33887
|
requested,
|
|
32451
33888
|
resolvedTaskID: job?.taskID,
|
|
32452
|
-
alias: job
|
|
32453
|
-
state: job
|
|
32454
|
-
terminalState: job
|
|
33889
|
+
alias: job ? options.backgroundJobBoard.field(job.taskID, "alias") : undefined,
|
|
33890
|
+
state: job ? options.backgroundJobBoard.field(job.taskID, "state") : undefined,
|
|
33891
|
+
terminalState: job ? options.backgroundJobBoard.field(job.taskID, "terminalState") : undefined,
|
|
32455
33892
|
cancellationRequested: job?.cancellationRequested
|
|
32456
33893
|
});
|
|
32457
33894
|
if (!job) {
|
|
@@ -32464,11 +33901,12 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
32464
33901
|
return unknownTaskOutput(requested, "cannot cancel parent session");
|
|
32465
33902
|
}
|
|
32466
33903
|
const knownJob = options.backgroundJobBoard.get(requested);
|
|
32467
|
-
|
|
33904
|
+
const ownerParentSessionID = options.backgroundJobBoard.getParentSessionID(requested);
|
|
33905
|
+
if (knownJob && ownerParentSessionID !== parentSessionID) {
|
|
32468
33906
|
log("[cancel-task] rejected unowned tracked raw session", {
|
|
32469
33907
|
parentSessionID,
|
|
32470
33908
|
taskID: requested,
|
|
32471
|
-
ownerParentSessionID
|
|
33909
|
+
ownerParentSessionID
|
|
32472
33910
|
});
|
|
32473
33911
|
return unknownTaskOutput(requested, "unknown or unowned background task");
|
|
32474
33912
|
}
|
|
@@ -32493,9 +33931,11 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
32493
33931
|
await abortAndVerifySession(options, job.taskID);
|
|
32494
33932
|
} catch (error) {
|
|
32495
33933
|
const stillRunning = error instanceof SessionStillRunningError;
|
|
33934
|
+
const boardRunning = options.backgroundJobBoard.isRunning(job.taskID);
|
|
32496
33935
|
log("[cancel-task] abort failed", {
|
|
32497
33936
|
taskID: job.taskID,
|
|
32498
33937
|
stillRunning,
|
|
33938
|
+
boardRunning,
|
|
32499
33939
|
error: error instanceof Error ? error.message : String(error)
|
|
32500
33940
|
});
|
|
32501
33941
|
options.backgroundJobBoard.updateStatus({
|
|
@@ -32514,20 +33954,20 @@ Use only for obsolete, wrong, conflicting, or user-requested cancellation. Accep
|
|
|
32514
33954
|
].join(`
|
|
32515
33955
|
`);
|
|
32516
33956
|
}
|
|
32517
|
-
|
|
33957
|
+
options.backgroundJobBoard.markCancelled(job.taskID, args.reason, Date.now(), { force: true });
|
|
33958
|
+
const state = options.backgroundJobBoard.getState(job.taskID);
|
|
32518
33959
|
log("[cancel-task] marked job cancelled after verified abort", {
|
|
32519
33960
|
taskID: job.taskID,
|
|
32520
|
-
alias: job.alias,
|
|
32521
|
-
|
|
32522
|
-
|
|
32523
|
-
cancellationRequested: cancelled?.cancellationRequested
|
|
33961
|
+
alias: options.backgroundJobBoard.field(job.taskID, "alias"),
|
|
33962
|
+
state,
|
|
33963
|
+
cancellationRequested: options.backgroundJobBoard.field(job.taskID, "cancellationRequested")
|
|
32524
33964
|
});
|
|
32525
33965
|
return [
|
|
32526
33966
|
`task_id: ${job.taskID}`,
|
|
32527
|
-
`state: ${
|
|
33967
|
+
`state: ${state ?? "cancelled"}`,
|
|
32528
33968
|
"",
|
|
32529
33969
|
"<task_error>",
|
|
32530
|
-
|
|
33970
|
+
options.backgroundJobBoard.getResultSummary(job.taskID) ?? "cancelled",
|
|
32531
33971
|
"</task_error>"
|
|
32532
33972
|
].join(`
|
|
32533
33973
|
`);
|
|
@@ -32609,10 +34049,10 @@ async function abortAndVerifySession(options, taskID) {
|
|
|
32609
34049
|
statusKeys: statusSnapshot.keys,
|
|
32610
34050
|
stableStoppedSince,
|
|
32611
34051
|
stableStoppedForMs: stableStoppedSince ? Date.now() - stableStoppedSince : 0,
|
|
32612
|
-
boardState: options.backgroundJobBoard.
|
|
32613
|
-
boardLastLiveBusyAt: options.backgroundJobBoard.
|
|
34052
|
+
boardState: options.backgroundJobBoard.getState(taskID),
|
|
34053
|
+
boardLastLiveBusyAt: options.backgroundJobBoard.getLastLiveBusyAt(taskID)
|
|
32614
34054
|
});
|
|
32615
|
-
const boardLastLiveBusyAt = options.backgroundJobBoard.
|
|
34055
|
+
const boardLastLiveBusyAt = options.backgroundJobBoard.getLastLiveBusyAt(taskID);
|
|
32616
34056
|
if (boardLastLiveBusyAt && boardLastLiveBusyAt >= abortStartedAt) {
|
|
32617
34057
|
log("[cancel-task] abort verification saw board busy after abort", {
|
|
32618
34058
|
taskID,
|
|
@@ -32769,7 +34209,7 @@ async function getSessionStatus(client, taskID) {
|
|
|
32769
34209
|
}
|
|
32770
34210
|
}
|
|
32771
34211
|
function delay2(ms) {
|
|
32772
|
-
return new Promise((
|
|
34212
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
32773
34213
|
}
|
|
32774
34214
|
function isSessionID(value) {
|
|
32775
34215
|
return /^ses_[\w-]+$/.test(value);
|
|
@@ -32861,10 +34301,10 @@ Returns the councillor responses with a summary footer.`,
|
|
|
32861
34301
|
const trulyIgnored = hasMaster && !legacyMasterModel ? deprecated : deprecated.filter((f) => f !== "master");
|
|
32862
34302
|
const parts = [];
|
|
32863
34303
|
if (hasMaster && legacyMasterModel) {
|
|
32864
|
-
parts.push(`\`council.master\` is deprecated and will be removed in a future version. Its \`model\` is currently used as a fallback for the council agent
|
|
34304
|
+
parts.push(`\`council.master\` is deprecated and will be removed in a future version. Its \`model\` is currently used as a fallback for the council agent - add a \`council\` entry to your preset to make this explicit.`);
|
|
32865
34305
|
}
|
|
32866
34306
|
if (trulyIgnored.length > 0) {
|
|
32867
|
-
parts.push(`${trulyIgnored.map((f) => `\`council.${f}\``).join(", ")} ${trulyIgnored.length === 1 ? "is" : "are"} deprecated and ignored
|
|
34307
|
+
parts.push(`${trulyIgnored.map((f) => `\`council.${f}\``).join(", ")} ${trulyIgnored.length === 1 ? "is" : "are"} deprecated and ignored - remove ${trulyIgnored.length === 1 ? "it" : "them"} from your config.`);
|
|
32868
34308
|
}
|
|
32869
34309
|
output += `
|
|
32870
34310
|
⚠ Config warning: ${parts.join(" ")}`;
|
|
@@ -32879,15 +34319,15 @@ import * as fs10 from "node:fs";
|
|
|
32879
34319
|
|
|
32880
34320
|
// src/tui-state.ts
|
|
32881
34321
|
import * as fs9 from "node:fs";
|
|
32882
|
-
import * as
|
|
32883
|
-
import * as
|
|
34322
|
+
import * as os6 from "node:os";
|
|
34323
|
+
import * as path18 from "node:path";
|
|
32884
34324
|
var STATE_DIR = "oh-my-opencode-slim";
|
|
32885
34325
|
var STATE_FILE = "tui-state.json";
|
|
32886
34326
|
function dataDir() {
|
|
32887
|
-
return process.env.XDG_DATA_HOME ??
|
|
34327
|
+
return process.env.XDG_DATA_HOME ?? path18.join(os6.homedir(), ".local", "share");
|
|
32888
34328
|
}
|
|
32889
34329
|
function getTuiStatePath() {
|
|
32890
|
-
return
|
|
34330
|
+
return path18.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
|
|
32891
34331
|
}
|
|
32892
34332
|
function emptySnapshot() {
|
|
32893
34333
|
return {
|
|
@@ -32925,7 +34365,7 @@ async function readTuiSnapshotAsync() {
|
|
|
32925
34365
|
function writeTuiSnapshot(snapshot) {
|
|
32926
34366
|
try {
|
|
32927
34367
|
const filePath = getTuiStatePath();
|
|
32928
|
-
fs9.mkdirSync(
|
|
34368
|
+
fs9.mkdirSync(path18.dirname(filePath), { recursive: true });
|
|
32929
34369
|
fs9.writeFileSync(filePath, `${JSON.stringify(snapshot)}
|
|
32930
34370
|
`);
|
|
32931
34371
|
} catch {}
|
|
@@ -32956,11 +34396,11 @@ function recordTuiAgentModel(input) {
|
|
|
32956
34396
|
}
|
|
32957
34397
|
|
|
32958
34398
|
// src/tools/preset-manager.ts
|
|
32959
|
-
var
|
|
34399
|
+
var COMMAND_NAME5 = "preset";
|
|
32960
34400
|
function createPresetManager(ctx, config) {
|
|
32961
34401
|
let activePreset = getActiveRuntimePreset() ?? config.preset ?? null;
|
|
32962
34402
|
async function handleCommandExecuteBefore(input, output) {
|
|
32963
|
-
if (input.command !==
|
|
34403
|
+
if (input.command !== COMMAND_NAME5) {
|
|
32964
34404
|
return;
|
|
32965
34405
|
}
|
|
32966
34406
|
output.parts.length = 0;
|
|
@@ -32979,11 +34419,11 @@ function createPresetManager(ctx, config) {
|
|
|
32979
34419
|
}
|
|
32980
34420
|
function registerCommand(opencodeConfig) {
|
|
32981
34421
|
const configCommand = opencodeConfig.command;
|
|
32982
|
-
if (!configCommand?.[
|
|
34422
|
+
if (!configCommand?.[COMMAND_NAME5]) {
|
|
32983
34423
|
if (!opencodeConfig.command) {
|
|
32984
34424
|
opencodeConfig.command = {};
|
|
32985
34425
|
}
|
|
32986
|
-
opencodeConfig.command[
|
|
34426
|
+
opencodeConfig.command[COMMAND_NAME5] = {
|
|
32987
34427
|
template: "List available presets and switch between them",
|
|
32988
34428
|
description: "Switch agent presets at runtime (e.g., /preset cheap, /preset powerful)"
|
|
32989
34429
|
};
|
|
@@ -33138,15 +34578,15 @@ var BINARY_PREFIXES = [
|
|
|
33138
34578
|
];
|
|
33139
34579
|
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.";
|
|
33140
34580
|
// src/tools/smartfetch/tool.ts
|
|
33141
|
-
import
|
|
33142
|
-
import
|
|
34581
|
+
import os7 from "node:os";
|
|
34582
|
+
import path22 from "node:path";
|
|
33143
34583
|
import {
|
|
33144
34584
|
tool as tool5
|
|
33145
34585
|
} from "@opencode-ai/plugin";
|
|
33146
34586
|
|
|
33147
34587
|
// src/tools/smartfetch/binary.ts
|
|
33148
34588
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
33149
|
-
import
|
|
34589
|
+
import path19 from "node:path";
|
|
33150
34590
|
function extensionForMime(contentType) {
|
|
33151
34591
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
33152
34592
|
const map = {
|
|
@@ -33167,10 +34607,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
33167
34607
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
33168
34608
|
await mkdir2(binaryDir, { recursive: true });
|
|
33169
34609
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
33170
|
-
const parsed =
|
|
34610
|
+
const parsed = path19.parse(initialName);
|
|
33171
34611
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
33172
34612
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
33173
|
-
const file =
|
|
34613
|
+
const file = path19.join(binaryDir, candidateName);
|
|
33174
34614
|
try {
|
|
33175
34615
|
await writeFile2(file, data, { flag: "wx" });
|
|
33176
34616
|
return file;
|
|
@@ -33184,21 +34624,21 @@ async function saveBinary(binaryDir, data, contentType, filename) {
|
|
|
33184
34624
|
throw new Error("Unable to allocate unique filename for binary content");
|
|
33185
34625
|
}
|
|
33186
34626
|
|
|
33187
|
-
// node_modules
|
|
33188
|
-
import { tracingChannel as
|
|
33189
|
-
var S =
|
|
33190
|
-
var W =
|
|
33191
|
-
var C = typeof performance == "object" && performance && typeof performance.now == "function" ? performance : Date;
|
|
34627
|
+
// node_modules/lru-cache/dist/esm/node/index.min.js
|
|
34628
|
+
import { tracingChannel as j, channel as I } from "node:diagnostics_channel";
|
|
34629
|
+
var S = I("lru-cache:metrics");
|
|
34630
|
+
var W = j("lru-cache");
|
|
33192
34631
|
var D = () => S.hasSubscribers || W.hasSubscribers;
|
|
33193
|
-
var
|
|
33194
|
-
var
|
|
34632
|
+
var G = typeof performance == "object" && performance && typeof performance.now == "function" ? performance : Date;
|
|
34633
|
+
var M = new Set;
|
|
34634
|
+
var C = typeof process == "object" && process ? process : {};
|
|
33195
34635
|
var P = (u, e, t, i) => {
|
|
33196
|
-
typeof
|
|
34636
|
+
typeof C.emitWarning == "function" ? C.emitWarning(u, e, t, i) : console.error(`[${t}] ${e}: ${u}`);
|
|
33197
34637
|
};
|
|
33198
|
-
var H = (u) => !
|
|
33199
|
-
var
|
|
34638
|
+
var H = (u) => !M.has(u);
|
|
34639
|
+
var $ = Symbol("type");
|
|
33200
34640
|
var F = (u) => !!u && u === Math.floor(u) && u > 0 && isFinite(u);
|
|
33201
|
-
var
|
|
34641
|
+
var U = (u) => F(u) ? u <= Math.pow(2, 8) ? Uint8Array : u <= Math.pow(2, 16) ? Uint16Array : u <= Math.pow(2, 32) ? Uint32Array : u <= Number.MAX_SAFE_INTEGER ? O : null : null;
|
|
33202
34642
|
var O = class extends Array {
|
|
33203
34643
|
constructor(e) {
|
|
33204
34644
|
super(e), this.fill(0);
|
|
@@ -33209,7 +34649,7 @@ var R = class u {
|
|
|
33209
34649
|
length;
|
|
33210
34650
|
static #o = false;
|
|
33211
34651
|
static create(e) {
|
|
33212
|
-
let t =
|
|
34652
|
+
let t = U(e);
|
|
33213
34653
|
if (!t)
|
|
33214
34654
|
return [];
|
|
33215
34655
|
u.#o = true;
|
|
@@ -33228,11 +34668,11 @@ var R = class u {
|
|
|
33228
34668
|
return this.heap[--this.length];
|
|
33229
34669
|
}
|
|
33230
34670
|
};
|
|
33231
|
-
var
|
|
34671
|
+
var L = class u2 {
|
|
33232
34672
|
#o;
|
|
33233
34673
|
#u;
|
|
33234
34674
|
#w;
|
|
33235
|
-
#
|
|
34675
|
+
#D;
|
|
33236
34676
|
#S;
|
|
33237
34677
|
#M;
|
|
33238
34678
|
#U;
|
|
@@ -33303,7 +34743,7 @@ var M = class u2 {
|
|
|
33303
34743
|
return this.#w;
|
|
33304
34744
|
}
|
|
33305
34745
|
get onInsert() {
|
|
33306
|
-
return this.#
|
|
34746
|
+
return this.#D;
|
|
33307
34747
|
}
|
|
33308
34748
|
get disposeAfter() {
|
|
33309
34749
|
return this.#S;
|
|
@@ -33312,9 +34752,9 @@ var M = class u2 {
|
|
|
33312
34752
|
let { max: t = 0, ttl: i, ttlResolution: s = 1, ttlAutopurge: n, updateAgeOnGet: o, updateAgeOnHas: r, allowStale: h, dispose: l, onInsert: c, disposeAfter: f, noDisposeOnSet: g, noUpdateTTL: p, maxSize: T = 0, maxEntrySize: w = 0, sizeCalculation: y, fetchMethod: a, memoMethod: m, noDeleteOnFetchRejection: _, noDeleteOnStaleGet: b, allowStaleOnFetchRejection: d, allowStaleOnFetchAbort: A, ignoreFetchAbort: z7, perf: x } = e;
|
|
33313
34753
|
if (x !== undefined && typeof x?.now != "function")
|
|
33314
34754
|
throw new TypeError("perf option must have a now() method if specified");
|
|
33315
|
-
if (this.#m = x ??
|
|
34755
|
+
if (this.#m = x ?? G, t !== 0 && !F(t))
|
|
33316
34756
|
throw new TypeError("max option must be a nonnegative integer");
|
|
33317
|
-
let v = t ?
|
|
34757
|
+
let v = t ? U(t) : Array;
|
|
33318
34758
|
if (!v)
|
|
33319
34759
|
throw new Error("invalid max value: " + t);
|
|
33320
34760
|
if (this.#o = t, this.#u = T, this.maxEntrySize = w || this.#u, this.sizeCalculation = y, this.sizeCalculation) {
|
|
@@ -33327,7 +34767,7 @@ var M = class u2 {
|
|
|
33327
34767
|
throw new TypeError("memoMethod must be a function if defined");
|
|
33328
34768
|
if (this.#U = m, a !== undefined && typeof a != "function")
|
|
33329
34769
|
throw new TypeError("fetchMethod must be a function if specified");
|
|
33330
|
-
if (this.#M = a, this.#W = !!a, this.#s = new Map, this.#i = Array.from({ length: t }).fill(undefined), this.#t = Array.from({ length: t }).fill(undefined), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#
|
|
34770
|
+
if (this.#M = a, this.#W = !!a, this.#s = new Map, this.#i = Array.from({ length: t }).fill(undefined), this.#t = Array.from({ length: t }).fill(undefined), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#D = c), typeof f == "function" ? (this.#S = f, this.#r = []) : (this.#S = undefined, this.#r = undefined), this.#T = !!this.#w, this.#j = !!this.#D, this.#f = !!this.#S, this.noDisposeOnSet = !!g, this.noUpdateTTL = !!p, this.noDeleteOnFetchRejection = !!_, this.allowStaleOnFetchRejection = !!d, this.allowStaleOnFetchAbort = !!A, this.ignoreFetchAbort = !!z7, this.maxEntrySize !== 0) {
|
|
33331
34771
|
if (this.#u !== 0 && !F(this.#u))
|
|
33332
34772
|
throw new TypeError("maxSize must be a positive integer if specified");
|
|
33333
34773
|
if (!F(this.maxEntrySize))
|
|
@@ -33343,7 +34783,7 @@ var M = class u2 {
|
|
|
33343
34783
|
throw new TypeError("At least one of max, maxSize, or ttl is required");
|
|
33344
34784
|
if (!this.ttlAutopurge && !this.#o && !this.#u) {
|
|
33345
34785
|
let E = "LRU_CACHE_UNBOUNDED";
|
|
33346
|
-
H(E) && (
|
|
34786
|
+
H(E) && (M.add(E), P("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.", "UnboundedCacheWarning", E, u2));
|
|
33347
34787
|
}
|
|
33348
34788
|
}
|
|
33349
34789
|
getRemainingTTL(e) {
|
|
@@ -33355,7 +34795,7 @@ var M = class u2 {
|
|
|
33355
34795
|
let i = this.ttlAutopurge ? Array.from({ length: this.#o }) : undefined;
|
|
33356
34796
|
this.#g = i, this.#N = (r, h, l = this.#m.now()) => {
|
|
33357
34797
|
t[r] = h !== 0 ? l : 0, e[r] = h, s(r, h);
|
|
33358
|
-
}, this.#
|
|
34798
|
+
}, this.#x = (r) => {
|
|
33359
34799
|
t[r] = e[r] !== 0 ? this.#m.now() : 0, s(r, e[r]);
|
|
33360
34800
|
};
|
|
33361
34801
|
let s = this.ttlAutopurge ? (r, h) => {
|
|
@@ -33399,7 +34839,7 @@ var M = class u2 {
|
|
|
33399
34839
|
return !!l && !!h && (n || o()) - h > l;
|
|
33400
34840
|
};
|
|
33401
34841
|
}
|
|
33402
|
-
#
|
|
34842
|
+
#x = () => {};
|
|
33403
34843
|
#E = () => {};
|
|
33404
34844
|
#N = () => {};
|
|
33405
34845
|
#p = () => false;
|
|
@@ -33565,7 +35005,7 @@ var M = class u2 {
|
|
|
33565
35005
|
return this.#v(e, "set"), h && (h.set = "miss", h.maxEntrySizeExceeded = true), this;
|
|
33566
35006
|
let f = this.#n === 0 ? undefined : this.#s.get(e);
|
|
33567
35007
|
if (f === undefined)
|
|
33568
|
-
f = this.#n === 0 ? this.#h : this.#y.length !== 0 ? this.#y.pop() : this.#n === this.#o ? this.#G(false) : this.#n, this.#i[f] = e, this.#t[f] = t, this.#s.set(e, f), this.#a[this.#h] = f, this.#c[f] = this.#h, this.#h = f, this.#n++, this.#I(f, c, h), h && (h.set = "add"), l = false, this.#j && this.#
|
|
35008
|
+
f = this.#n === 0 ? this.#h : this.#y.length !== 0 ? this.#y.pop() : this.#n === this.#o ? this.#G(false) : this.#n, this.#i[f] = e, this.#t[f] = t, this.#s.set(e, f), this.#a[this.#h] = f, this.#c[f] = this.#h, this.#h = f, this.#n++, this.#I(f, c, h), h && (h.set = "add"), l = false, this.#j && this.#D?.(t, e, "add");
|
|
33569
35009
|
else {
|
|
33570
35010
|
this.#L(f);
|
|
33571
35011
|
let g = this.#t[f];
|
|
@@ -33629,7 +35069,7 @@ var M = class u2 {
|
|
|
33629
35069
|
if (this.#p(n))
|
|
33630
35070
|
s && (s.has = "stale", this.#E(s, n));
|
|
33631
35071
|
else
|
|
33632
|
-
return i && this.#
|
|
35072
|
+
return i && this.#x(n), s && (s.has = "hit", this.#E(s, n)), true;
|
|
33633
35073
|
} else
|
|
33634
35074
|
s && (s.has = "miss");
|
|
33635
35075
|
return false;
|
|
@@ -33687,7 +35127,7 @@ var M = class u2 {
|
|
|
33687
35127
|
let i = W.hasSubscribers, { status: s = D() ? {} : undefined } = t;
|
|
33688
35128
|
t.status = s, s && t.context && (s.context = t.context);
|
|
33689
35129
|
let n = this.#B(e, t);
|
|
33690
|
-
return s && i && (s.trace = true, W.tracePromise(() => n, s).catch(() => {})), n;
|
|
35130
|
+
return s && D() && i && (s.trace = true, W.tracePromise(() => n, s).catch(() => {})), n;
|
|
33691
35131
|
}
|
|
33692
35132
|
async#B(e, t = {}) {
|
|
33693
35133
|
let { allowStale: i = this.allowStale, updateAgeOnGet: s = this.updateAgeOnGet, noDeleteOnStaleGet: n = this.noDeleteOnStaleGet, ttl: o = this.ttl, noDisposeOnSet: r = this.noDisposeOnSet, size: h = 0, sizeCalculation: l = this.sizeCalculation, noUpdateTTL: c = this.noUpdateTTL, noDeleteOnFetchRejection: f = this.noDeleteOnFetchRejection, allowStaleOnFetchRejection: g = this.allowStaleOnFetchRejection, ignoreFetchAbort: p = this.ignoreFetchAbort, allowStaleOnFetchAbort: T = this.allowStaleOnFetchAbort, context: w, forceRefresh: y = false, status: a, signal: m } = t;
|
|
@@ -33706,7 +35146,7 @@ var M = class u2 {
|
|
|
33706
35146
|
}
|
|
33707
35147
|
let A = this.#p(b);
|
|
33708
35148
|
if (!y && !A)
|
|
33709
|
-
return a && (a.fetch = "hit"), this.#L(b), s && this.#
|
|
35149
|
+
return a && (a.fetch = "hit"), this.#L(b), s && this.#x(b), a && this.#E(a, b), d;
|
|
33710
35150
|
let z7 = this.#P(e, b, _, w), v = z7.__staleWhileFetching !== undefined && i;
|
|
33711
35151
|
return a && (a.fetch = A ? "stale" : "refresh", v && A && (a.returnedStale = true)), v ? z7.__staleWhileFetching : z7.__returned = z7;
|
|
33712
35152
|
}
|
|
@@ -33715,7 +35155,7 @@ var M = class u2 {
|
|
|
33715
35155
|
let i = W.hasSubscribers, { status: s = D() ? {} : undefined } = t;
|
|
33716
35156
|
t.status = s, s && t.context && (s.context = t.context);
|
|
33717
35157
|
let n = this.#K(e, t);
|
|
33718
|
-
return s && i && (s.trace = true, W.tracePromise(() => n, s).catch(() => {})), n;
|
|
35158
|
+
return s && D() && i && (s.trace = true, W.tracePromise(() => n, s).catch(() => {})), n;
|
|
33719
35159
|
}
|
|
33720
35160
|
async#K(e, t = {}) {
|
|
33721
35161
|
let i = await this.#B(e, t);
|
|
@@ -33754,7 +35194,7 @@ var M = class u2 {
|
|
|
33754
35194
|
return;
|
|
33755
35195
|
}
|
|
33756
35196
|
let h = this.#t[r], l = this.#e(h);
|
|
33757
|
-
return o && this.#E(o, r), this.#p(r) ? l ? (o && (o.get = "stale-fetching"), i && h.__staleWhileFetching !== undefined ? (o && (o.returnedStale = true), h.__staleWhileFetching) : undefined) : (n || this.#v(e, "expire"), o && (o.get = "stale"), i ? (o && (o.returnedStale = true), h) : undefined) : (o && (o.get = l ? "fetching" : "hit"), this.#L(r), s && this.#
|
|
35197
|
+
return o && this.#E(o, r), this.#p(r) ? l ? (o && (o.get = "stale-fetching"), i && h.__staleWhileFetching !== undefined ? (o && (o.returnedStale = true), h.__staleWhileFetching) : undefined) : (n || this.#v(e, "expire"), o && (o.get = "stale"), i ? (o && (o.returnedStale = true), h) : undefined) : (o && (o.get = l ? "fetching" : "hit"), this.#L(r), s && this.#x(r), l ? h.__staleWhileFetching : h);
|
|
33758
35198
|
}
|
|
33759
35199
|
#$(e, t) {
|
|
33760
35200
|
this.#c[t] = e, this.#a[e] = t;
|
|
@@ -33824,7 +35264,7 @@ var M = class u2 {
|
|
|
33824
35264
|
};
|
|
33825
35265
|
|
|
33826
35266
|
// src/tools/smartfetch/network.ts
|
|
33827
|
-
import
|
|
35267
|
+
import path20 from "node:path";
|
|
33828
35268
|
|
|
33829
35269
|
// src/tools/smartfetch/utils.ts
|
|
33830
35270
|
var import_readability = __toESM(require_readability(), 1);
|
|
@@ -34549,7 +35989,7 @@ function inferFilenameFromUrl(url) {
|
|
|
34549
35989
|
function truncateFilename(name, maxLength = 180) {
|
|
34550
35990
|
if (name.length <= maxLength)
|
|
34551
35991
|
return name;
|
|
34552
|
-
const parsed =
|
|
35992
|
+
const parsed = path20.parse(name);
|
|
34553
35993
|
const ext = parsed.ext || "";
|
|
34554
35994
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
34555
35995
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -34659,7 +36099,7 @@ async function probeLlmsText(url, timeoutMs, signal, fallbackOrigin) {
|
|
|
34659
36099
|
}
|
|
34660
36100
|
|
|
34661
36101
|
// src/tools/smartfetch/cache.ts
|
|
34662
|
-
var CACHE = new
|
|
36102
|
+
var CACHE = new L({
|
|
34663
36103
|
maxSize: 50 * 1024 * 1024,
|
|
34664
36104
|
ttl: 15 * 60 * 1000,
|
|
34665
36105
|
sizeCalculation: (value) => {
|
|
@@ -34721,7 +36161,7 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
34721
36161
|
// src/tools/smartfetch/secondary-model.ts
|
|
34722
36162
|
import { existsSync as existsSync12 } from "node:fs";
|
|
34723
36163
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
34724
|
-
import
|
|
36164
|
+
import path21 from "node:path";
|
|
34725
36165
|
function parseModelRef(value) {
|
|
34726
36166
|
if (!value)
|
|
34727
36167
|
return;
|
|
@@ -34747,7 +36187,7 @@ function pickAgentModelRef(value) {
|
|
|
34747
36187
|
}
|
|
34748
36188
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
34749
36189
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
34750
|
-
const fullPath =
|
|
36190
|
+
const fullPath = path21.join(baseDir, file);
|
|
34751
36191
|
if (existsSync12(fullPath))
|
|
34752
36192
|
return fullPath;
|
|
34753
36193
|
}
|
|
@@ -34764,7 +36204,7 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
34764
36204
|
}
|
|
34765
36205
|
}
|
|
34766
36206
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
34767
|
-
const projectDir =
|
|
36207
|
+
const projectDir = path21.join(directory, ".opencode");
|
|
34768
36208
|
const userDirs = getConfigSearchDirs();
|
|
34769
36209
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
34770
36210
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -34864,7 +36304,7 @@ async function deleteSessionSafely(client, sessionId, directory) {
|
|
|
34864
36304
|
console.warn(`[smartfetch] Failed to clean up secondary session ${sessionId} ` + `after ${SESSION_DELETE_RETRIES} attempts: ` + (error instanceof Error ? error.message : String(error)));
|
|
34865
36305
|
return;
|
|
34866
36306
|
}
|
|
34867
|
-
await new Promise((
|
|
36307
|
+
await new Promise((resolve4) => setTimeout(resolve4, _testConfig.deleteRetryDelayMs));
|
|
34868
36308
|
}
|
|
34869
36309
|
}
|
|
34870
36310
|
}
|
|
@@ -34946,7 +36386,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
34946
36386
|
// src/tools/smartfetch/tool.ts
|
|
34947
36387
|
var z7 = tool5.schema;
|
|
34948
36388
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
34949
|
-
const binaryDir = options.binaryDir ||
|
|
36389
|
+
const binaryDir = options.binaryDir || path22.join(os7.tmpdir(), "opencode-smartfetch");
|
|
34950
36390
|
return tool5({
|
|
34951
36391
|
description: WEBFETCH_DESCRIPTION,
|
|
34952
36392
|
args: {
|
|
@@ -35554,6 +36994,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35554
36994
|
let foregroundFallback;
|
|
35555
36995
|
let deepworkCommandHook;
|
|
35556
36996
|
let reflectCommandHook;
|
|
36997
|
+
let loopCommandHook;
|
|
35557
36998
|
let taskSessionManagerHook;
|
|
35558
36999
|
let backgroundJobBoard;
|
|
35559
37000
|
let interviewManager;
|
|
@@ -35578,8 +37019,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35578
37019
|
}
|
|
35579
37020
|
disabledAgents = getDisabledAgents(config);
|
|
35580
37021
|
rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
|
|
35581
|
-
agentDefs = createAgents(config);
|
|
35582
|
-
agents = getAgentConfigs(config);
|
|
37022
|
+
agentDefs = createAgents(config, { projectDirectory: ctx.directory });
|
|
37023
|
+
agents = getAgentConfigs(config, { projectDirectory: ctx.directory });
|
|
35583
37024
|
modelArrayMap = {};
|
|
35584
37025
|
runtimeChains = {};
|
|
35585
37026
|
for (const agentDef of agentDefs) {
|
|
@@ -35615,7 +37056,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35615
37056
|
readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8
|
|
35616
37057
|
});
|
|
35617
37058
|
multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig, backgroundJobBoard);
|
|
35618
|
-
backgroundJobBoard.
|
|
37059
|
+
backgroundJobBoard.addTerminalStateListener((taskID) => {
|
|
35619
37060
|
multiplexerSessionManager.retryDeferredIdleClose(taskID);
|
|
35620
37061
|
});
|
|
35621
37062
|
autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
|
|
@@ -35635,6 +37076,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35635
37076
|
foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
|
|
35636
37077
|
deepworkCommandHook = createDeepworkCommandHook();
|
|
35637
37078
|
reflectCommandHook = createReflectCommandHook();
|
|
37079
|
+
loopCommandHook = createLoopCommandHook();
|
|
35638
37080
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
35639
37081
|
maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
|
|
35640
37082
|
readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
|
|
@@ -35764,9 +37206,11 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35764
37206
|
const chosen = models[0];
|
|
35765
37207
|
const entry = configAgent[agentName];
|
|
35766
37208
|
if (entry) {
|
|
35767
|
-
entry.model
|
|
35768
|
-
|
|
35769
|
-
|
|
37209
|
+
if (entry.model === undefined) {
|
|
37210
|
+
entry.model = chosen.id;
|
|
37211
|
+
if (chosen.variant) {
|
|
37212
|
+
entry.variant = chosen.variant;
|
|
37213
|
+
}
|
|
35770
37214
|
}
|
|
35771
37215
|
} else {
|
|
35772
37216
|
configAgent[agentName] = {
|
|
@@ -35906,6 +37350,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35906
37350
|
interviewManager.registerCommand(opencodeConfig);
|
|
35907
37351
|
deepworkCommandHook.registerCommand(opencodeConfig);
|
|
35908
37352
|
reflectCommandHook.registerCommand(opencodeConfig);
|
|
37353
|
+
loopCommandHook.registerCommand(opencodeConfig);
|
|
35909
37354
|
presetManager.registerCommand(opencodeConfig);
|
|
35910
37355
|
},
|
|
35911
37356
|
event: async (input) => {
|
|
@@ -35979,6 +37424,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
35979
37424
|
await presetManager.handleCommandExecuteBefore(input, output);
|
|
35980
37425
|
await deepworkCommandHook.handleCommandExecuteBefore(input, output);
|
|
35981
37426
|
await reflectCommandHook.handleCommandExecuteBefore(input, output);
|
|
37427
|
+
await loopCommandHook.handleCommandExecuteBefore(input, output);
|
|
35982
37428
|
},
|
|
35983
37429
|
"chat.headers": chatHeadersHook["chat.headers"],
|
|
35984
37430
|
"chat.message": async (input, output) => {
|