opencode-resolve 0.1.19 → 0.2.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/dist/state.d.ts CHANGED
@@ -6,6 +6,8 @@ export declare const FAILURE_THRESHOLD = 10;
6
6
  export declare const STRATEGY_PIVOT_THRESHOLD = 20;
7
7
  export declare const EDIT_HOTSPOT_THRESHOLD = 10;
8
8
  export declare const EDIT_HOTSPOT_TTL_MS = 600000;
9
+ export declare const DISPATCH_STOP_THRESHOLD = 3;
10
+ export declare const DISPATCH_PIVOT_THRESHOLD = 6;
9
11
  export interface SessionState {
10
12
  storedConfig?: ResolveConfig;
11
13
  storedProjectContext?: ProjectContext;
@@ -13,6 +15,10 @@ export interface SessionState {
13
15
  errors: number;
14
16
  warnings: number;
15
17
  timestamp: number;
18
+ errorMessages?: {
19
+ line: number;
20
+ message: string;
21
+ }[];
16
22
  }>;
17
23
  failurePatterns: Map<string, {
18
24
  count: number;
@@ -27,9 +33,16 @@ export interface SessionState {
27
33
  }>;
28
34
  totalEdits: number;
29
35
  totalToolCalls: number;
36
+ totalOutputBytes: number;
30
37
  sessionStartTime: number;
31
38
  loopWarnings: string[];
32
39
  lastStrategyHint: string;
40
+ consecutiveDispatchFailures: number;
41
+ lastDispatchAgent?: string;
42
+ lastDispatchSucceeded?: boolean;
43
+ lastDispatchAt?: number;
44
+ awaitingVerify: boolean;
45
+ awaitingVerifyFile?: string;
33
46
  currentAgent?: string;
34
47
  locale: Locale;
35
48
  }
package/dist/state.js CHANGED
@@ -4,6 +4,12 @@ export const FAILURE_THRESHOLD = 10;
4
4
  export const STRATEGY_PIVOT_THRESHOLD = 20;
5
5
  export const EDIT_HOTSPOT_THRESHOLD = 10;
6
6
  export const EDIT_HOTSPOT_TTL_MS = 600_000;
7
+ // Ralph Loop: dispatch lifecycle thresholds. The resolver→coder loop must
8
+ // close: dispatch → verify → (fail: diagnose+redispatch | pass: done).
9
+ // After DISPATCH_STOP_THRESHOLD consecutive failed dispatches, force a STOP &
10
+ // report. After DISPATCH_PIVOT_THRESHOLD, force an architect rethink.
11
+ export const DISPATCH_STOP_THRESHOLD = 3;
12
+ export const DISPATCH_PIVOT_THRESHOLD = 6;
7
13
  export function createSessionState() {
8
14
  return {
9
15
  recentDiagnostics: new Map(),
@@ -13,9 +19,12 @@ export function createSessionState() {
13
19
  editHotspots: new Map(),
14
20
  totalEdits: 0,
15
21
  totalToolCalls: 0,
22
+ totalOutputBytes: 0,
16
23
  sessionStartTime: Date.now(),
17
24
  loopWarnings: [],
18
25
  lastStrategyHint: "",
26
+ consecutiveDispatchFailures: 0,
27
+ awaitingVerify: false,
19
28
  locale: "en"
20
29
  };
21
30
  }
@@ -1,4 +1,5 @@
1
1
  import { SessionState } from "../state.js";
2
+ export declare function summarizeTestFailure(stdout: string, stderr: string, runner: string | undefined): string;
2
3
  export declare function getTools(sessionState: SessionState): {
3
4
  "resolve-verify": {
4
5
  description: string;
@@ -13,9 +14,11 @@ export declare function getTools(sessionState: SessionState): {
13
14
  description: string;
14
15
  args: {
15
16
  path: import("zod").ZodOptional<import("zod").ZodString>;
17
+ context: import("zod").ZodOptional<import("zod").ZodBoolean>;
16
18
  };
17
19
  execute(args: {
18
20
  path?: string | undefined;
21
+ context?: boolean | undefined;
19
22
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
20
23
  };
21
24
  "resolve-context": {
@@ -56,11 +59,13 @@ export declare function getTools(sessionState: SessionState): {
56
59
  file: import("zod").ZodOptional<import("zod").ZodString>;
57
60
  pattern: import("zod").ZodOptional<import("zod").ZodString>;
58
61
  runner: import("zod").ZodOptional<import("zod").ZodString>;
62
+ raw: import("zod").ZodOptional<import("zod").ZodBoolean>;
59
63
  };
60
64
  execute(args: {
61
65
  file?: string | undefined;
62
66
  pattern?: string | undefined;
63
67
  runner?: string | undefined;
68
+ raw?: boolean | undefined;
64
69
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
65
70
  };
66
71
  "resolve-pattern": {
@@ -219,8 +224,12 @@ export declare function getTools(sessionState: SessionState): {
219
224
  };
220
225
  "resolve-session": {
221
226
  description: string;
222
- args: {};
223
- execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
227
+ args: {
228
+ full: import("zod").ZodOptional<import("zod").ZodBoolean>;
229
+ };
230
+ execute(args: {
231
+ full?: boolean | undefined;
232
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
224
233
  };
225
234
  "resolve-audit": {
226
235
  description: string;
@@ -6,8 +6,8 @@ import { fileURLToPath } from "node:url";
6
6
  import { dirname } from "node:path";
7
7
  import { classifyBashCommand, runCommand, sanitizeShellArg, truncateOutput, PLUGIN_VERSION } from "../utils.js";
8
8
  import { DIAGNOSTICS_TTL_MS } from "../state.js";
9
- import { VALID_PROFILES, VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
10
- const WRITE_CAPABLE_AGENTS = new Set(["resolver", "codex", "coder", "glm", "gpt-coder", "debugger"]);
9
+ import { VALID_TIERS, VALID_AGENT_NAME_SET, VALID_AGENT_NAMES } from "../agents.js";
10
+ const WRITE_CAPABLE_AGENTS = new Set(["resolver", "coder", "debugger"]);
11
11
  function canWriteFromTool(ctx) {
12
12
  return typeof ctx.agent !== "string" || WRITE_CAPABLE_AGENTS.has(ctx.agent);
13
13
  }
@@ -23,6 +23,60 @@ function commandExecutionDenied(command) {
23
23
  }
24
24
  return `Command is not allowlisted for direct tool execution: ${command}. Run it through OpenCode bash so the normal permission flow can decide.`;
25
25
  }
26
+ // Phase 1.2: parse Vitest/Jest/Pytest/Go test failures into a compact summary
27
+ // (failing names, expected vs received, 3 stack frames). Always returns output
28
+ // ≤ the previous raw-truncate fallback, so resolve-test is strictly net-positive.
29
+ export function summarizeTestFailure(stdout, stderr, runner) {
30
+ const raw = stderr || stdout;
31
+ const cmd = (runner ?? "").toLowerCase();
32
+ const out = `${stdout}\n${stderr}`;
33
+ // Pytest: "FAILED file::test_name - msg" + "E ..." assertion lines
34
+ if (cmd.includes("pytest") || /FAILED\s+\S+::/.test(raw)) {
35
+ const failed = [...raw.matchAll(/FAILED\s+(\S+?::\S+)\s+-\s+(.+)/g)].slice(0, 10)
36
+ .map((m) => ` ✗ ${m[1]}\n ${m[2].slice(0, 200)}`);
37
+ const asserts = [...raw.matchAll(/^E\s+(.+)/gm)].slice(0, 5).map((m) => m[1].slice(0, 200));
38
+ if (failed.length || asserts.length) {
39
+ const parts = [];
40
+ if (failed.length)
41
+ parts.push(`Failed tests:\n${failed.join("\n")}`);
42
+ if (asserts.length)
43
+ parts.push(`Assertion errors:\n ${asserts.join("\n ")}`);
44
+ return truncateOutput(parts.join("\n"), 1200);
45
+ }
46
+ }
47
+ // Go test: "--- FAIL: TestName" + " file.go:NN: msg"
48
+ if (cmd.includes("go test") || /--- FAIL:/.test(raw)) {
49
+ const failed = [...raw.matchAll(/--- FAIL:\s+(\S+)/g)].slice(0, 10).map((m) => ` ✗ ${m[1]}`);
50
+ const errs = [...raw.matchAll(/^\s+(\S+\.go):(\d+):\s+(.+)/gm)].slice(0, 5)
51
+ .map((m) => ` ${m[1]}:${m[2]}: ${m[3].slice(0, 160)}`);
52
+ if (failed.length) {
53
+ return truncateOutput(`Failed tests:\n${failed.join("\n")}\n${errs.join("\n")}`, 1200);
54
+ }
55
+ }
56
+ // Vitest / Jest
57
+ const failingNames = new Set();
58
+ for (const m of out.matchAll(/●\s+(.+)/g))
59
+ failingNames.add(m[1].trim().slice(0, 120)); // Jest suite › test
60
+ for (const m of out.matchAll(/^\s*×\s+(.+)/gm))
61
+ failingNames.add(m[1].trim().slice(0, 120)); // Vitest × test
62
+ const failFiles = [...out.matchAll(/^FAIL\s+(\S+)/gm)].map((m) => m[1]).slice(0, 10);
63
+ const expected = [...out.matchAll(/Expected[^:\n]*:\s*(.+)/g)].slice(0, 3).map((m) => m[1].slice(0, 150));
64
+ const received = [...out.matchAll(/Received[^:\n]*:\s*(.+)/g)].slice(0, 3).map((m) => m[1].slice(0, 150));
65
+ const stack = [...out.matchAll(/^\s+at\s+(.+)/gm)].slice(0, 3).map((m) => m[1].slice(0, 160));
66
+ const parts = [];
67
+ if (failFiles.length)
68
+ parts.push(`Failed files: ${failFiles.join(", ")}`);
69
+ if (failingNames.size)
70
+ parts.push(`Failed tests:\n${[...failingNames].slice(0, 10).map((n) => ` ✗ ${n}`).join("\n")}`);
71
+ if (expected.length || received.length) {
72
+ parts.push(`Expected: ${expected[0] ?? "-"}\nReceived: ${received[0] ?? "-"}`);
73
+ }
74
+ if (stack.length)
75
+ parts.push(`Stack:\n${stack.map((s) => ` at ${s}`).join("\n")}`);
76
+ // Parsed something useful → compact summary; otherwise fall back to the
77
+ // previous raw-truncate behaviour (byte-identical).
78
+ return parts.length ? truncateOutput(parts.join("\n"), 1200) : truncateOutput(raw, 1500);
79
+ }
26
80
  export function getTools(sessionState) {
27
81
  return {
28
82
  "resolve-verify": tool({
@@ -53,22 +107,53 @@ export function getTools(sessionState) {
53
107
  },
54
108
  }),
55
109
  "resolve-diagnostics": tool({
56
- description: "Get current LSP diagnostics snapshot. Returns errors and warnings per file from the language server.",
110
+ description: "Get current LSP diagnostics snapshot. Returns errors and warnings per file from the language server. When errors are present, includes the message and ±3 lines of source context so the coder can fix without a separate read.",
57
111
  args: {
58
112
  path: tool.schema.string().optional().describe("Specific file path to check. If omitted, returns all files with active diagnostics."),
113
+ context: tool.schema.boolean().optional().describe("Include ±3 lines of source context around each error (default true). Set false for counts only."),
59
114
  },
60
- async execute(args) {
115
+ async execute(args, ctx) {
61
116
  if (sessionState.recentDiagnostics.size === 0) {
62
117
  return "No active LSP diagnostics.";
63
118
  }
64
119
  const now = Date.now();
120
+ const wantContext = args.context !== false;
65
121
  const entries = [];
66
122
  for (const [filePath, diag] of sessionState.recentDiagnostics) {
67
123
  if (now - diag.timestamp > DIAGNOSTICS_TTL_MS)
68
124
  continue;
69
125
  if (args.path && filePath !== args.path)
70
126
  continue;
71
- entries.push(`${filePath}: ${diag.errors} errors, ${diag.warnings} warnings`);
127
+ const head = `${filePath}: ${diag.errors} errors, ${diag.warnings} warnings`;
128
+ // Net-positive: error-free files and context:false calls are byte-identical to before.
129
+ if (!wantContext || diag.errors === 0 || !diag.errorMessages?.length) {
130
+ entries.push(head);
131
+ continue;
132
+ }
133
+ // Errors present: show message + ±3 source lines (bounded to 3 errors/file).
134
+ const blocks = [head];
135
+ let srcLines = null;
136
+ for (const e of diag.errorMessages.slice(0, 3)) {
137
+ const ln = e.line + 1; // LSP lines are 0-based
138
+ if (srcLines === null) {
139
+ try {
140
+ srcLines = (await readFile(resolve(ctx.directory, filePath), "utf8")).split("\n");
141
+ }
142
+ catch {
143
+ srcLines = []; // unreadable: show message without context
144
+ }
145
+ }
146
+ let snippet = "";
147
+ if (srcLines.length > 0) {
148
+ const start = Math.max(0, ln - 4);
149
+ const end = Math.min(srcLines.length, ln + 3);
150
+ snippet = srcLines.slice(start, end)
151
+ .map((l, i) => `${start + i + 1}${start + i + 1 === ln ? ">" : ":"} ${l}`)
152
+ .join("\n");
153
+ }
154
+ blocks.push(` L${ln}: ${e.message}${snippet ? "\n" + snippet : ""}`);
155
+ }
156
+ entries.push(blocks.join("\n"));
72
157
  }
73
158
  if (entries.length === 0) {
74
159
  return args.path ? `No active diagnostics for ${args.path}.` : "No active LSP diagnostics.";
@@ -177,6 +262,7 @@ export function getTools(sessionState) {
177
262
  file: tool.schema.string().optional().describe("Test file path or glob pattern (e.g. 'test/plugin.test.mjs')."),
178
263
  pattern: tool.schema.string().optional().describe("Test name pattern to filter (e.g. 'GLM profile')."),
179
264
  runner: tool.schema.string().optional().describe("Override test runner command (e.g. 'vitest run', 'jest')."),
265
+ raw: tool.schema.boolean().optional().describe("Return the raw untruncated log instead of the parsed failure summary."),
180
266
  },
181
267
  async execute(args, ctx) {
182
268
  const projCtx = sessionState.storedProjectContext;
@@ -215,7 +301,11 @@ export function getTools(sessionState) {
215
301
  if (result.exitCode === 0) {
216
302
  return { output: `✅ Tests passed.\n${truncateOutput(result.stdout, 800)}`, metadata: { exitCode: 0 } };
217
303
  }
218
- return { output: `❌ Tests failed (exit ${result.exitCode}).\n${truncateOutput(result.stderr || result.stdout, 1500)}`, metadata: { exitCode: result.exitCode } };
304
+ // Phase 1.2: parse framework output into a compact summary unless raw requested.
305
+ const detail = args.raw
306
+ ? truncateOutput(result.stderr || result.stdout, 1500)
307
+ : summarizeTestFailure(result.stdout, result.stderr, testCmd);
308
+ return { output: `❌ Tests failed (exit ${result.exitCode}).\n${detail}`, metadata: { exitCode: result.exitCode } };
219
309
  }
220
310
  catch (err) {
221
311
  return `⚠️ Test runner failed: ${err instanceof Error ? err.message : String(err)}`;
@@ -480,8 +570,6 @@ export function getTools(sessionState) {
480
570
  }
481
571
  // Build resolve.json content
482
572
  const resolveConfig = {};
483
- if (sessionState.storedConfig?.profile)
484
- resolveConfig.profile = sessionState.storedConfig.profile;
485
573
  if (sessionState.storedConfig?.tier)
486
574
  resolveConfig.tier = sessionState.storedConfig.tier;
487
575
  if (projCtx?.verifyCommands.length) {
@@ -926,9 +1014,11 @@ export function getTools(sessionState) {
926
1014
  },
927
1015
  }),
928
1016
  "resolve-session": tool({
929
- description: "Show current Ralph Loop session state: profile, tier, edit count, tool call count, failure warnings, loop warnings, and elapsed time. Use when you suspect you're going in circles.",
930
- args: {},
931
- async execute(_args, ctx) {
1017
+ description: "Show current Ralph Loop session state: tier, edit count, tool call count, output bytes, failure warnings, loop warnings, and elapsed time. Use when you suspect you're going in circles.",
1018
+ args: {
1019
+ full: tool.schema.boolean().optional().describe("Include a compact changelog (recent commits + uncommitted count) so you get session + repo state in one call. Default false."),
1020
+ },
1021
+ async execute(args, ctx) {
932
1022
  const lines = [];
933
1023
  const cfg = sessionState.storedConfig;
934
1024
  const projCtx = sessionState.storedProjectContext;
@@ -936,8 +1026,7 @@ export function getTools(sessionState) {
936
1026
  lines.push(`Session duration: ${elapsed}s`);
937
1027
  lines.push(`Tool calls: ${sessionState.totalToolCalls}`);
938
1028
  lines.push(`Edits: ${sessionState.totalEdits}`);
939
- if (cfg?.profile)
940
- lines.push(`Profile: ${cfg.profile}`);
1029
+ lines.push(`Output: ${sessionState.totalOutputBytes} bytes (~${Math.round(sessionState.totalOutputBytes / 1024)}KB)`);
941
1030
  if (cfg?.tier)
942
1031
  lines.push(`Tier: ${cfg.tier}`);
943
1032
  if (projCtx?.hasTypeScript)
@@ -970,6 +1059,25 @@ export function getTools(sessionState) {
970
1059
  lines.push(` 🔄 ${w}`);
971
1060
  }
972
1061
  ctx.metadata({ title: `session: ${sessionState.totalEdits} edits, ${sessionState.totalToolCalls} tools, ${elapsed}s` });
1062
+ // Phase 1.6: opt-in `full` mode folds a compact changelog (recent
1063
+ // commits + uncommitted count) into the same response — 3 calls → 1.
1064
+ // Default (full omitted/false) leaves the output exactly as before.
1065
+ if (args.full) {
1066
+ try {
1067
+ const log = await runCommand("git log --oneline -5", ctx.directory, 5_000);
1068
+ const st = await runCommand("git status --porcelain", ctx.directory, 5_000);
1069
+ lines.push("Recent changes:");
1070
+ if (log.stdout.trim()) {
1071
+ lines.push(log.stdout.trim().split("\n").map((l) => ` ${l}`).join("\n"));
1072
+ }
1073
+ else {
1074
+ lines.push(" (no commits yet)");
1075
+ }
1076
+ const uncommitted = st.stdout.trim().split("\n").filter(Boolean).length;
1077
+ lines.push(`Uncommitted files: ${uncommitted}`);
1078
+ }
1079
+ catch { /* not a git repo */ }
1080
+ }
973
1081
  return lines.join("\n");
974
1082
  },
975
1083
  }),
@@ -1053,17 +1161,17 @@ export function getTools(sessionState) {
1053
1161
  if (!cfg) {
1054
1162
  return "No resolve config loaded. Plugin may not be initialized.";
1055
1163
  }
1056
- // 1. Profile check
1057
- if (cfg.profile) {
1058
- if (VALID_PROFILES.has(cfg.profile)) {
1059
- results.push(`✅ Profile: ${cfg.profile}`);
1164
+ // 1. Tier check
1165
+ if (cfg.tier) {
1166
+ if (VALID_TIERS.has(cfg.tier)) {
1167
+ results.push(`✅ Tier: ${cfg.tier}`);
1060
1168
  }
1061
1169
  else {
1062
- results.push(`🔴 Invalid profile: '${cfg.profile}'. Valid: ${[...VALID_PROFILES].join(", ")}`);
1170
+ results.push(`🔴 Invalid tier: '${cfg.tier}'. Valid: ${[...VALID_TIERS].join(", ")}`);
1063
1171
  }
1064
1172
  }
1065
1173
  else {
1066
- results.push("ℹ️ No profile set (using defaults)");
1174
+ results.push("ℹ️ No tier set (using defaults)");
1067
1175
  }
1068
1176
  // 2. Tier check
1069
1177
  if (cfg.tier) {
@@ -1190,8 +1298,6 @@ export function getTools(sessionState) {
1190
1298
  failures: sessionState.totalFailures,
1191
1299
  elapsedSeconds: Math.round((Date.now() - sessionState.sessionStartTime) / 1000),
1192
1300
  };
1193
- if (sessionState.storedConfig?.profile)
1194
- state.profile = sessionState.storedConfig.profile;
1195
1301
  if (sessionState.storedConfig?.tier)
1196
1302
  state.tier = sessionState.storedConfig.tier;
1197
1303
  if (sessionState.failureWarnings.length > 0)
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type PermissionValue = "ask" | "allow" | "deny";
2
- export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "codex" | "gpt" | "glm" | "architect" | "gpt-coder" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
3
- export type ModelAlias = ResolveAgentName | "glm" | "gpt" | "quick" | "deep" | "fast" | "strong" | "mini" | "codex" | "bronze" | "silver" | "gold" | "gpt-bronze" | "gpt-silver" | "gpt-gold" | "glm-bronze" | "glm-silver" | "glm-gold";
2
+ export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "architect" | "debugger" | "researcher" | "explorer" | "deep-reviewer" | "planner";
3
+ export type ModelAlias = ResolveAgentName | "quick" | "deep" | "fast" | "strong" | "mini" | "bronze" | "silver" | "gold";
4
4
  export type AgentMode = "subagent" | "primary" | "all";
5
5
  export type ResolveAgentConfig = {
6
6
  enabled?: boolean;
@@ -19,22 +19,20 @@ export type ResolveAgentConfig = {
19
19
  external_directory?: PermissionValue;
20
20
  };
21
21
  };
22
- export type ProfileName = "mix" | "glm" | "gpt";
23
22
  export type TierName = "bronze" | "silver" | "gold";
24
23
  export type LanguageSetting = "auto" | "en" | "ko";
25
24
  export type ResolveConfig = {
26
- profile?: ProfileName;
27
25
  tier?: TierName;
28
26
  enabled?: ResolveAgentName[];
29
27
  models?: Partial<Record<ModelAlias, string>>;
30
28
  agents?: Partial<Record<ResolveAgentName, ResolveAgentConfig>>;
31
29
  preserveNative?: boolean;
32
- context7?: boolean;
33
30
  commands?: boolean;
34
31
  autoApprove?: boolean;
35
32
  maxParallelSubagents?: number;
36
33
  autoUpdate?: boolean;
37
34
  language?: LanguageSetting;
35
+ singleAgentMode?: boolean;
38
36
  };
39
37
  export type ResolvePluginOptions = ResolveConfig & {
40
38
  config?: string;
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  import { ProjectContext, ResolveConfig } from "./types.js";
2
2
  export declare const PLUGIN_VERSION: string;
3
- export declare const UPDATE_CHECK_INTERVAL_MS: number;
4
- export declare const UPDATE_CHECK_FILE: string;
5
- export declare const PLUGIN_CACHE_DIR: string;
6
3
  export declare function runCommand(command: string, cwd: string, timeoutMs: number): Promise<{
7
4
  stdout: string;
8
5
  stderr: string;
@@ -20,12 +17,6 @@ export declare function existsDirectory(path: string): Promise<boolean>;
20
17
  export declare function detectProjectContext(directory: string): Promise<ProjectContext>;
21
18
  export declare function collectContextFiles(rootDirectory: string, relativeDirectory: string, maxFiles?: number): Promise<string[]>;
22
19
  export declare function readPluginVersion(): string;
23
- export declare function readUpdateCheckCache(): {
24
- checkedAt: number;
25
- latest: string;
26
- } | undefined;
27
- export declare function isNewerVersion(candidate: string, baseline: string): boolean;
28
- export declare function maybeAutoUpdate(): Promise<void>;
29
20
  export declare function readFirstJson(paths: string[]): Promise<ResolveConfig | undefined>;
30
21
  export declare const BANNED_COMMANDS: ReadonlyArray<RegExp>;
31
22
  export declare const DANGEROUS_BASH_PATTERNS: ReadonlyArray<RegExp>;
package/dist/utils.js CHANGED
@@ -1,14 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { stat, readFile, access, readdir } from "node:fs/promises";
3
3
  import { join, dirname, extname, relative } from "node:path";
4
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
- import { homedir } from "node:os";
4
+ import { readFileSync } from "node:fs";
6
5
  import { fileURLToPath } from "node:url";
7
6
  import { normalizeResolveConfig } from "./config.js";
8
7
  export const PLUGIN_VERSION = readPluginVersion();
9
- export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10
- export const UPDATE_CHECK_FILE = join(homedir(), ".cache", "opencode-resolve", "update-check.json");
11
- export const PLUGIN_CACHE_DIR = join(homedir(), ".cache", "opencode", "packages", "opencode-resolve@latest");
12
8
  export function runCommand(command, cwd, timeoutMs) {
13
9
  return new Promise((resolve) => {
14
10
  const proc = spawn("sh", ["-c", command], {
@@ -218,92 +214,13 @@ export function readPluginVersion() {
218
214
  return "unknown";
219
215
  }
220
216
  }
221
- export function readUpdateCheckCache() {
222
- try {
223
- const raw = readFileSync(UPDATE_CHECK_FILE, "utf8");
224
- const parsed = JSON.parse(raw);
225
- if (typeof parsed?.checkedAt === "number" &&
226
- typeof parsed?.latest === "string") {
227
- return { checkedAt: parsed.checkedAt, latest: parsed.latest };
228
- }
229
- }
230
- catch {
231
- // file missing or unparseable
232
- }
233
- return undefined;
234
- }
235
- export function isNewerVersion(candidate, baseline) {
236
- const a = candidate.split(".").map((n) => Number.parseInt(n, 10));
237
- const b = baseline.split(".").map((n) => Number.parseInt(n, 10));
238
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
239
- const av = Number.isFinite(a[i]) ? a[i] : 0;
240
- const bv = Number.isFinite(b[i]) ? b[i] : 0;
241
- if (av > bv)
242
- return true;
243
- if (av < bv)
244
- return false;
245
- }
246
- return false;
247
- }
248
- export async function maybeAutoUpdate() {
249
- const previous = readUpdateCheckCache();
250
- try {
251
- if (previous && Date.now() - previous.checkedAt < UPDATE_CHECK_INTERVAL_MS) {
252
- if (isNewerVersion(previous.latest, PLUGIN_VERSION)) {
253
- refreshPluginCacheInBackground(previous.latest, "cached");
254
- }
255
- return;
256
- }
257
- }
258
- catch {
259
- // ignore corrupt cache and re-check
260
- }
261
- let latest;
262
- try {
263
- const response = await fetch("https://registry.npmjs.org/opencode-resolve/latest", {
264
- headers: { Accept: "application/json" },
265
- signal: AbortSignal.timeout(5000),
266
- });
267
- if (!response.ok)
268
- return;
269
- const data = (await response.json());
270
- if (typeof data?.version !== "string")
271
- return;
272
- latest = data.version;
273
- }
274
- catch {
275
- return;
276
- }
277
- try {
278
- mkdirSync(dirname(UPDATE_CHECK_FILE), { recursive: true });
279
- writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ checkedAt: Date.now(), latest }));
280
- }
281
- catch {
282
- // best-effort; don't block on cache write failure
283
- }
284
- if (!isNewerVersion(latest, PLUGIN_VERSION))
285
- return;
286
- refreshPluginCacheInBackground(latest, "registry");
287
- }
288
- function refreshPluginCacheInBackground(latest, source) {
289
- const sourceLabel = source === "cached" ? "cached latest" : "registry latest";
290
- console.log(`[opencode-resolve] new version v${latest} available (${sourceLabel}, current: v${PLUGIN_VERSION}) — refreshing OpenCode plugin cache in background. Restart OpenCode to activate it.`);
291
- try {
292
- spawn("sh", ["-c", `rm -rf "${PLUGIN_CACHE_DIR}" && opencode plugin opencode-resolve@latest --global --force`], {
293
- detached: true,
294
- stdio: "ignore",
295
- env: {
296
- ...process.env,
297
- OPENCODE_RESOLVE_REFRESHING_CACHE: "1",
298
- OPENCODE_RESOLVE_SKIP_POSTINSTALL: "1",
299
- OPENCODE_RESOLVE_SKIP_COMPANIONS: "1",
300
- },
301
- }).unref();
302
- }
303
- catch {
304
- // If spawn fails, the user already saw the notice and can run the command manually.
305
- }
306
- }
217
+ // Auto-update was removed: it spawned `opencode plugin opencode-resolve@latest
218
+ // --global --force` per `hooks.config()` call, which created hundreds of parallel
219
+ // OpenCode sessions whenever multiple instances loaded the plugin at once (and
220
+ // once flattened the user's server with 177 sessions during a test run).
221
+ // Users update manually: `npm i -g opencode-resolve` or via the install script.
222
+ // The `autoUpdate` config field is still accepted by the schema for backward
223
+ // compatibility, but it is now a no-op.
307
224
  export async function readFirstJson(paths) {
308
225
  for (const path of paths) {
309
226
  try {
@@ -1,10 +1,7 @@
1
1
  {
2
- "profile": "mix",
3
2
  "preserveNative": true,
4
- "context7": true,
5
3
  "commands": false,
6
4
  "autoApprove": true,
7
- "autoUpdate": true,
8
5
  "language": "auto",
9
6
  "models": {},
10
7
  "agents": {
@@ -15,12 +12,6 @@
15
12
  "resolver": {
16
13
  "enabled": true
17
14
  },
18
- "codex": {
19
- "enabled": false
20
- },
21
- "gpt": {
22
- "enabled": false
23
- },
24
15
  "explorer": {
25
16
  "enabled": true,
26
17
  "mode": "subagent"
@@ -40,17 +31,11 @@
40
31
  "architect": {
41
32
  "enabled": false
42
33
  },
43
- "gpt-coder": {
44
- "enabled": false
45
- },
46
34
  "debugger": {
47
35
  "enabled": false
48
36
  },
49
37
  "researcher": {
50
38
  "enabled": false
51
- },
52
- "glm": {
53
- "enabled": false
54
39
  }
55
40
  }
56
41
  }