opencode-resolve 0.1.6 → 0.1.8

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.
@@ -0,0 +1,56 @@
1
+ export type PermissionValue = "ask" | "allow" | "deny";
2
+ export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "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";
4
+ export type AgentMode = "subagent" | "primary" | "all";
5
+ export type ResolveAgentConfig = {
6
+ enabled?: boolean;
7
+ model?: string;
8
+ mode?: AgentMode;
9
+ description?: string;
10
+ prompt?: string;
11
+ color?: string;
12
+ maxSteps?: number;
13
+ tools?: Record<string, boolean>;
14
+ permission?: {
15
+ edit?: PermissionValue;
16
+ bash?: PermissionValue | Record<string, PermissionValue>;
17
+ webfetch?: PermissionValue;
18
+ doom_loop?: PermissionValue;
19
+ external_directory?: PermissionValue;
20
+ };
21
+ };
22
+ export type ProfileName = "mix" | "glm" | "gpt";
23
+ export type TierName = "bronze" | "silver" | "gold";
24
+ export type ResolveConfig = {
25
+ profile?: ProfileName;
26
+ tier?: TierName;
27
+ enabled?: ResolveAgentName[];
28
+ models?: Partial<Record<ModelAlias, string>>;
29
+ agents?: Partial<Record<ResolveAgentName, ResolveAgentConfig>>;
30
+ preserveNative?: boolean;
31
+ context7?: boolean;
32
+ commands?: boolean;
33
+ autoApprove?: boolean;
34
+ maxParallelSubagents?: number;
35
+ autoUpdate?: boolean;
36
+ };
37
+ export type ResolvePluginOptions = ResolveConfig & {
38
+ config?: string;
39
+ };
40
+ export type UnknownRecord = Record<string, unknown>;
41
+ export type ProjectContext = {
42
+ /** Project knowledge files or directories that exist */
43
+ knowledgeFiles: string[];
44
+ /** Pattern/context documents discovered under committed context directories */
45
+ contextFiles: string[];
46
+ /** Package manager detected (npm, yarn, pnpm, bun) */
47
+ packageManager: string | undefined;
48
+ /** Verification commands available (e.g. "npx tsc --noEmit", "npm run lint") */
49
+ verifyCommands: string[];
50
+ /** Whether this is a TypeScript project */
51
+ hasTypeScript: boolean;
52
+ /** Whether HARNESS.md exists */
53
+ hasHarness: boolean;
54
+ /** Whether AGENTS.md exists */
55
+ hasAgents: boolean;
56
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { ProjectContext, ResolveConfig } from "./types.js";
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
+ export declare function runCommand(command: string, cwd: string, timeoutMs: number): Promise<{
7
+ stdout: string;
8
+ stderr: string;
9
+ exitCode: number;
10
+ }>;
11
+ export declare function truncateOutput(text: string, maxLen: number): string;
12
+ /** Sanitize a string for safe use as a shell argument. Strips dangerous metacharacters. */
13
+ export declare function sanitizeShellArg(input: string): string;
14
+ export declare function isMissingFileError(error: unknown): boolean;
15
+ export declare function formatError(error: unknown): string;
16
+ export declare function classifyBashCommand(pattern: string): "allow" | "deny" | "ask";
17
+ export declare function existsFile(path: string): Promise<boolean>;
18
+ export declare function existsPath(path: string): Promise<boolean>;
19
+ export declare function existsDirectory(path: string): Promise<boolean>;
20
+ export declare function detectProjectContext(directory: string): Promise<ProjectContext>;
21
+ export declare function collectContextFiles(rootDirectory: string, relativeDirectory: string, maxFiles?: number): Promise<string[]>;
22
+ 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
+ export declare function readFirstJson(paths: string[]): Promise<ResolveConfig | undefined>;
30
+ export declare const BANNED_COMMANDS: ReadonlyArray<RegExp>;
31
+ export declare const DANGEROUS_BASH_PATTERNS: ReadonlyArray<RegExp>;
32
+ export declare const ALWAYS_SAFE_COMMANDS: ReadonlyArray<string>;
33
+ export declare const SAFE_BASH_PREFIXES: ReadonlyArray<readonly [string, ReadonlyArray<string>]>;
package/dist/utils.js ADDED
@@ -0,0 +1,371 @@
1
+ import { spawn } from "node:child_process";
2
+ import { stat, readFile, access, readdir } from "node:fs/promises";
3
+ import { join, dirname, extname, relative } from "node:path";
4
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+ import { normalizeResolveConfig } from "./config.js";
8
+ 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
+ export function runCommand(command, cwd, timeoutMs) {
13
+ return new Promise((resolve) => {
14
+ const proc = spawn("sh", ["-c", command], {
15
+ cwd,
16
+ env: { ...process.env, CI: "true", GIT_TERMINAL_PROMPT: "0" },
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ });
19
+ let stdout = "";
20
+ let stderr = "";
21
+ const timer = setTimeout(() => { proc.kill("SIGKILL"); }, timeoutMs);
22
+ proc.stdout.on("data", (d) => { stdout += d.toString(); });
23
+ proc.stderr.on("data", (d) => { stderr += d.toString(); });
24
+ proc.on("close", (code) => {
25
+ clearTimeout(timer);
26
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
27
+ });
28
+ proc.on("error", (err) => {
29
+ clearTimeout(timer);
30
+ resolve({ stdout: "", stderr: err.message, exitCode: 1 });
31
+ });
32
+ });
33
+ }
34
+ export function truncateOutput(text, maxLen) {
35
+ if (text.length <= maxLen)
36
+ return text;
37
+ return text.slice(0, maxLen) + `\n... (${text.length - maxLen} more bytes truncated)`;
38
+ }
39
+ /** Sanitize a string for safe use as a shell argument. Strips dangerous metacharacters. */
40
+ export function sanitizeShellArg(input) {
41
+ return input
42
+ .replace(/[;&|`$(){}[\]!#~<>\\]/g, "") // strip shell metacharacters
43
+ .replace(/'/g, "'\\''") // escape single quotes for single-quoted context
44
+ .slice(0, 500);
45
+ }
46
+ export function isMissingFileError(error) {
47
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
48
+ }
49
+ export function formatError(error) {
50
+ return error instanceof Error ? error.message : String(error);
51
+ }
52
+ export function classifyBashCommand(pattern) {
53
+ const cmd = pattern.trim();
54
+ for (const re of BANNED_COMMANDS) {
55
+ if (re.test(cmd))
56
+ return "deny";
57
+ }
58
+ for (const re of DANGEROUS_BASH_PATTERNS) {
59
+ if (re.test(cmd))
60
+ return "deny";
61
+ }
62
+ const firstToken = cmd.split(/\s+/)[0];
63
+ if (ALWAYS_SAFE_COMMANDS.includes(firstToken))
64
+ return "allow";
65
+ for (const [prefix, subcommands] of SAFE_BASH_PREFIXES) {
66
+ if (firstToken !== prefix)
67
+ continue;
68
+ // If no subcommands listed, the prefix itself is safe (e.g. npx, node)
69
+ if (subcommands.length === 0)
70
+ return "allow";
71
+ const secondToken = cmd.split(/\s+/)[1];
72
+ if (secondToken && subcommands.includes(secondToken))
73
+ return "allow";
74
+ }
75
+ return "ask";
76
+ }
77
+ export async function existsFile(path) {
78
+ try {
79
+ const s = await stat(path);
80
+ return s.isFile();
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ export async function existsPath(path) {
87
+ try {
88
+ await stat(path);
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ export async function existsDirectory(path) {
96
+ try {
97
+ const s = await stat(path);
98
+ return s.isDirectory();
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
104
+ export async function detectProjectContext(directory) {
105
+ const ctx = {
106
+ knowledgeFiles: [],
107
+ contextFiles: [],
108
+ packageManager: undefined,
109
+ verifyCommands: [],
110
+ hasTypeScript: false,
111
+ hasHarness: false,
112
+ hasAgents: false,
113
+ };
114
+ const knowledgeFileCandidates = [
115
+ "HARNESS.md",
116
+ "AGENTS.md",
117
+ "CLAUDE.md",
118
+ "CONVENTIONS.md",
119
+ ];
120
+ for (const candidate of knowledgeFileCandidates) {
121
+ const fullPath = join(directory, candidate);
122
+ if (await existsFile(fullPath)) {
123
+ ctx.knowledgeFiles.push(candidate);
124
+ if (candidate === "HARNESS.md")
125
+ ctx.hasHarness = true;
126
+ if (candidate === "AGENTS.md")
127
+ ctx.hasAgents = true;
128
+ }
129
+ }
130
+ const knowledgeDirectoryCandidates = [
131
+ ".opencode/context",
132
+ ".claude/context",
133
+ "context",
134
+ "thoughts",
135
+ ];
136
+ for (const candidate of knowledgeDirectoryCandidates) {
137
+ const fullPath = join(directory, candidate);
138
+ if (await existsDirectory(fullPath)) {
139
+ ctx.knowledgeFiles.push(candidate);
140
+ ctx.contextFiles.push(...await collectContextFiles(directory, candidate));
141
+ }
142
+ }
143
+ if (await existsFile(join(directory, "pnpm-lock.yaml")))
144
+ ctx.packageManager = "pnpm";
145
+ else if (await existsFile(join(directory, "bun.lockb")) || await existsFile(join(directory, "bun.lock")))
146
+ ctx.packageManager = "bun";
147
+ else if (await existsFile(join(directory, "yarn.lock")))
148
+ ctx.packageManager = "yarn";
149
+ else if (await existsFile(join(directory, "package-lock.json")))
150
+ ctx.packageManager = "npm";
151
+ ctx.hasTypeScript = await existsFile(join(directory, "tsconfig.json"));
152
+ try {
153
+ const pkgRaw = await readFile(join(directory, "package.json"), "utf8");
154
+ const pkg = JSON.parse(pkgRaw);
155
+ const scripts = typeof pkg?.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
156
+ if (typeof scripts["typecheck"] === "string" || typeof scripts["type-check"] === "string") {
157
+ const cmd = scripts["typecheck"] ?? scripts["type-check"];
158
+ ctx.verifyCommands.push(`npm run ${scripts["typecheck"] ? "typecheck" : "type-check"}`);
159
+ }
160
+ else if (ctx.hasTypeScript) {
161
+ ctx.verifyCommands.push("npx tsc --noEmit");
162
+ }
163
+ if (typeof scripts["lint"] === "string") {
164
+ ctx.verifyCommands.push("npm run lint");
165
+ }
166
+ if (typeof scripts["test"] === "string") {
167
+ ctx.verifyCommands.push("npm test");
168
+ }
169
+ }
170
+ catch {
171
+ // no package.json or unreadable — skip
172
+ }
173
+ return ctx;
174
+ }
175
+ export async function collectContextFiles(rootDirectory, relativeDirectory, maxFiles = 40) {
176
+ const allowedExtensions = new Set([".md", ".mdx", ".txt", ".json", ".jsonc", ".yaml", ".yml"]);
177
+ const results = [];
178
+ async function walk(current, depth) {
179
+ if (depth > 3 || results.length >= maxFiles)
180
+ return;
181
+ let entries;
182
+ try {
183
+ entries = await readdir(current, { withFileTypes: true });
184
+ }
185
+ catch {
186
+ return;
187
+ }
188
+ entries.sort((a, b) => a.name.localeCompare(b.name));
189
+ for (const entry of entries) {
190
+ if (results.length >= maxFiles)
191
+ break;
192
+ if (entry.name.startsWith("."))
193
+ continue;
194
+ if (entry.name === "archive")
195
+ continue;
196
+ const fullPath = join(current, entry.name);
197
+ if (entry.isDirectory()) {
198
+ await walk(fullPath, depth + 1);
199
+ continue;
200
+ }
201
+ if (!entry.isFile())
202
+ continue;
203
+ if (!allowedExtensions.has(extname(entry.name).toLowerCase()))
204
+ continue;
205
+ results.push(relative(rootDirectory, fullPath));
206
+ }
207
+ }
208
+ await walk(join(rootDirectory, relativeDirectory), 0);
209
+ return results;
210
+ }
211
+ export function readPluginVersion() {
212
+ try {
213
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
214
+ const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
215
+ return typeof pkg?.version === "string" ? pkg.version : "unknown";
216
+ }
217
+ catch {
218
+ return "unknown";
219
+ }
220
+ }
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
+ try {
250
+ const previous = readUpdateCheckCache();
251
+ if (previous && Date.now() - previous.checkedAt < UPDATE_CHECK_INTERVAL_MS) {
252
+ return;
253
+ }
254
+ }
255
+ catch {
256
+ // ignore corrupt cache and re-check
257
+ }
258
+ let latest;
259
+ try {
260
+ const response = await fetch("https://registry.npmjs.org/opencode-resolve/latest", {
261
+ headers: { Accept: "application/json" },
262
+ signal: AbortSignal.timeout(5000),
263
+ });
264
+ if (!response.ok)
265
+ return;
266
+ const data = (await response.json());
267
+ if (typeof data?.version !== "string")
268
+ return;
269
+ latest = data.version;
270
+ }
271
+ catch {
272
+ return;
273
+ }
274
+ try {
275
+ mkdirSync(dirname(UPDATE_CHECK_FILE), { recursive: true });
276
+ writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ checkedAt: Date.now(), latest }));
277
+ }
278
+ catch {
279
+ // best-effort; don't block on cache write failure
280
+ }
281
+ if (!isNewerVersion(latest, PLUGIN_VERSION))
282
+ return;
283
+ console.log(`[opencode-resolve] new version v${latest} available (current: v${PLUGIN_VERSION}) — refreshing cache in background. Restart OpenCode to activate (current session stays on v${PLUGIN_VERSION}).`);
284
+ try {
285
+ spawn("sh", ["-c", `rm -rf "${PLUGIN_CACHE_DIR}" && opencode plugin opencode-resolve --global --force`], { detached: true, stdio: "ignore" }).unref();
286
+ }
287
+ catch {
288
+ // If spawn fails, the user already saw the notice and can run the command manually.
289
+ }
290
+ }
291
+ export async function readFirstJson(paths) {
292
+ for (const path of paths) {
293
+ try {
294
+ await access(path);
295
+ return normalizeResolveConfig(JSON.parse(await readFile(path, "utf8")), path);
296
+ }
297
+ catch (error) {
298
+ if (isMissingFileError(error))
299
+ continue;
300
+ throw new Error(`Failed to read OpenCode Resolve config at ${path}: ${formatError(error)}`);
301
+ }
302
+ }
303
+ return undefined;
304
+ }
305
+ export const BANNED_COMMANDS = [
306
+ /\b(vim?|nano|emacs|pico|ed)\b/, // interactive editors
307
+ /\b(less|more|most|pg)\b/, // pagers
308
+ /\bman\s/, // man pages
309
+ /\b(python|python3|ipython)\b(\s*$)/, // Python REPL
310
+ /\b(node|bun|deno)\b(\s*$)/, // JS REPL
311
+ /\b(irb|ghci|scala|jshell)\b(\s*$)/, // other REPLs
312
+ /\b(bash|zsh|fish|sh)\s+-i\b/, // interactive shells
313
+ /\bgit\s+add\s+-p\b/, // interactive git add
314
+ /\bgit\s+rebase\s+-i\b/, // interactive rebase
315
+ /\bgit\s+commit\b(?!\s+-m)/, // commit without -m
316
+ /\bscreen\b/, // screen multiplexer
317
+ /\btmux\b(?!.*[|&;])/, // tmux without subcommand pipe
318
+ /\bssh\b(?!\s.*-\w*[oN])/, // ssh without batch flags
319
+ /\bsftp\b/, // sftp interactive
320
+ /\btelnet\b/, // telnet interactive
321
+ /\bnc\b(\s*$)/, // netcat interactive
322
+ /\bsqlite3?\b(\s*$)/, // sqlite interactive
323
+ /\bpsql\b(\s*$)/, // psql interactive
324
+ /\bmysql\b(\s*$)/, // mysql interactive
325
+ // Ralph Loop: dangerous patterns that waste tokens or cause damage
326
+ /\bcurl\b.*\|\s*(ba)?sh\b/, // curl pipe to shell
327
+ /\bwget\b.*\|\s*(ba)?sh\b/, // wget pipe to shell
328
+ /\beval\s/, // eval is dangerous
329
+ /\bchmod\s+(-R\s+)?777\b/, // chmod 777
330
+ /\bchown\s+-R\s+/, // recursive chown
331
+ /\bsudo\s+(rm|chmod|chown|dd|mkfs)/, // sudo + destructive
332
+ /\bgit\s+push\s+--force/, // force push
333
+ /\bgit\s+reset\s+--hard/, // hard reset
334
+ /\brm\s+(-rf?|-fr?)\s+[^.]/, // rm -rf (not dotfiles)
335
+ /\bdd\s+if=/, // dd can destroy disks
336
+ /\b(mkfs|format)\b/, // filesystem format
337
+ ];
338
+ export const DANGEROUS_BASH_PATTERNS = [
339
+ /\brm\s+.*-[rR].*[fF].*\s+\//, // rm -rf /... (absolute path)
340
+ /\bgit\s+push\s+.*(--force|-f\b)/, // force push
341
+ /\bgit\s+reset\s+--hard/, // hard reset
342
+ /\bgit\s+clean\s+-fd/, // clean untracked files
343
+ /\bsudo\s+rm\b/, // sudo rm
344
+ /\bdd\s+.*of=\/dev\//, // dd to device
345
+ /\bchmod\s+-R\s+777\s+\//, // chmod everything
346
+ /\b(DROP|TRUNCATE)\s/i, // SQL destructive
347
+ ];
348
+ export const ALWAYS_SAFE_COMMANDS = [
349
+ "ls", "cat", "head", "tail", "wc", "which", "echo", "pwd", "env",
350
+ "printenv", "whoami", "uname", "date", "df", "du", "free", "top",
351
+ "ps", "grep", "find", "sort", "uniq", "diff", "file", "stat",
352
+ "touch", "mkdir", "cp", "mv", "sed", "awk", "tr", "cut", "xargs",
353
+ "curl", "wget", "dig", "nslookup", "ping",
354
+ ];
355
+ export const SAFE_BASH_PREFIXES = [
356
+ ["npm", ["test", "run", "start", "build", "lint", "typecheck", "check", "info", "list", "view", "outdated", "audit", "pack"]],
357
+ ["npx", []],
358
+ ["node", []],
359
+ ["bun", ["test", "run", "build", "install", "add", "remove"]],
360
+ ["yarn", ["test", "run", "build", "install", "add", "remove", "lint", "typecheck"]],
361
+ ["pnpm", ["test", "run", "build", "install", "add", "remove", "lint", "typecheck"]],
362
+ ["git", ["status", "log", "diff", "branch", "show", "remote", "stash", "tag", "describe"]],
363
+ ["tsc", []],
364
+ ["eslint", []],
365
+ ["prettier", []],
366
+ ["jest", []],
367
+ ["vitest", []],
368
+ ["pytest", []],
369
+ ["cargo", ["test", "check", "build", "clippy", "fmt"]],
370
+ ["make", ["test", "check", "build", "lint", "clean"]],
371
+ ];
@@ -1,5 +1,5 @@
1
1
  {
2
- "enabled": ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner"],
2
+ "profile": "mix",
3
3
  "preserveNative": true,
4
4
  "context7": true,
5
5
  "commands": false,
@@ -9,7 +9,7 @@
9
9
  "agents": {
10
10
  "coder": {
11
11
  "enabled": true,
12
- "mode": "all"
12
+ "mode": "subagent"
13
13
  },
14
14
  "resolver": {
15
15
  "enabled": true
@@ -41,6 +41,9 @@
41
41
  },
42
42
  "researcher": {
43
43
  "enabled": false
44
+ },
45
+ "glm": {
46
+ "enabled": false
44
47
  }
45
48
  }
46
49
  }
@@ -17,10 +17,50 @@
17
17
  // permission values, and wrong types throw an error at load time.
18
18
 
19
19
  {
20
+ // -----------------------------------------------------------------------
21
+ // profile ("mix" | "glm" | "gpt")
22
+ // Activates provider-specific agent configurations.
23
+ // Set automatically by postinstall based on detected providers.
24
+ //
25
+ // "mix" — explicit mixed/default profile. Uses standard resolver prompts,
26
+ // DEFAULT_ENABLED, and whatever model aliases you configure.
27
+ // "glm" — GLM-only, optimized for ZAI coding-plan: reduced maxSteps,
28
+ // token-efficient prompts, no hard concurrency cap by default.
29
+ // No deep-reviewer by default.
30
+ // "gpt" — GPT-only, high-performance: parallel coder dispatch, full agent
31
+ // roster, higher maxSteps.
32
+ //
33
+ // When omitted, the plugin defaults to "mix".
34
+ // -----------------------------------------------------------------------
35
+ "profile": "mix",
36
+
37
+ // -----------------------------------------------------------------------
38
+ // tier ("bronze" | "silver" | "gold")
39
+ // Agent roster preset. Overrides the default enabled list when set.
40
+ //
41
+ // "bronze" — Minimum agents: coder + resolver only. Maximum token savings.
42
+ // "silver" — Standard: coder, resolver, explorer, reviewer, planner.
43
+ // No deep-reviewer. Best balance for most projects.
44
+ // "gold" — Full power: all 8 agents including debugger and researcher.
45
+ // Maximum capability for complex tasks.
46
+ //
47
+ // Postinstall sets this automatically:
48
+ // GLM → silver, GPT → gold, Mix/unknown → no tier (DEFAULT_ENABLED applies).
49
+ //
50
+ // Explicit `enabled` array always wins over tier.
51
+ // -----------------------------------------------------------------------
52
+ // "tier": "silver",
53
+
20
54
  // -----------------------------------------------------------------------
21
55
  // enabled (string[])
22
- // Which resolve agents to inject.
23
- // Default: ["coder", "resolver", "explorer", "reviewer", "deep-reviewer"].
56
+ // Which resolve agents to inject. When omitted, the tier or profile default
57
+ // is used. Explicit `enabled` wins over both tier and profile defaults.
58
+ //
59
+ // Mix/default (no tier): ["coder", "resolver", "explorer", "reviewer",
60
+ // "deep-reviewer", "planner"].
61
+ //
62
+ // GLM profile default: ["coder", "resolver", "explorer", "reviewer",
63
+ // "planner"] — deep-reviewer omitted to save tokens.
24
64
  //
25
65
  // Core path: resolver + coder form the fixed-role verified resolve loop.
26
66
  // Internal specialists: explorer, reviewer, deep-reviewer are injected as
@@ -30,7 +70,7 @@
30
70
  // Native OpenCode agents (plan, build) are always preserved and never
31
71
  // touched by this list. Per-agent `agents.<name>.enabled` overrides this.
32
72
  // -----------------------------------------------------------------------
33
- "enabled": ["coder", "resolver", "explorer", "reviewer", "deep-reviewer"],
73
+ // "enabled": ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner"],
34
74
 
35
75
  // -----------------------------------------------------------------------
36
76
  // preserveNative (boolean)
@@ -55,20 +95,23 @@
55
95
 
56
96
  // -----------------------------------------------------------------------
57
97
  // autoApprove (boolean)
58
- // When true, every default `"ask"` permission on enabled agents is flipped
59
- // to `"allow"`. Never touches `"deny"`. Never overrides a permission key
60
- // you set explicitly under `agents.<name>.permission`. Default: true.
61
- //
62
- // Set to false for the conservative ask-every-time behavior.
98
+ // Documentary flag. The plugin's permission.ask hook automatically
99
+ // classifies bash commands as safe (allow) / dangerous (deny) / unknown (ask).
100
+ // Base agent permissions are already set to the correct defaults (allow for
101
+ // edit/webfetch, ask for bash on write agents, deny for read-only agents).
102
+ // Changing this flag has no effect on behavior — it exists for config
103
+ // readability and future compatibility. Default: true.
63
104
  // -----------------------------------------------------------------------
64
105
  "autoApprove": true,
65
106
 
66
107
  // -----------------------------------------------------------------------
67
108
  // maxParallelSubagents (positive integer)
68
- // Maximum number of subagents of the SAME ROLE the resolver may dispatch
69
- // concurrently. Default: 2 at most two coders in parallel.
109
+ // Optional prompt-level cap for how many subagents of the SAME ROLE the
110
+ // resolver may dispatch concurrently. When omitted, the resolver uses soft
111
+ // fan-out guidance and backs off on rate-limit errors. GLM profile does not
112
+ // impose a hard cap unless you set one.
70
113
  // 1 = strict per-role serial
71
- // 2 = default
114
+ // 2 = up to two coders in parallel
72
115
  // N = up to N of each role
73
116
  // -----------------------------------------------------------------------
74
117
  "maxParallelSubagents": 2,
@@ -94,7 +137,7 @@
94
137
  // When resolve.json is created for the first time by postinstall, the
95
138
  // plugin inspects your OpenCode model config and writes an adaptive
96
139
  // preset:
97
- // - GLM/ZAI detected → mixed GLM + GPT aliases
140
+ // - GLM/ZAI detected → GLM-only aliases (no GPT dependency)
98
141
  // - OpenAI/GPT detected → single-provider GPT aliases using your model
99
142
  // - Other or no model → empty (model-neutral inheritance)
100
143
  // Existing resolve.json files are never overwritten. Delete resolve.json
@@ -111,17 +154,19 @@
111
154
  // "resolver": "strong"
112
155
  // }
113
156
  //
114
- // Example — GLM + GPT mixed preset (auto-generated for GLM/ZAI users):
157
+ // Example — GLM-only preset (auto-generated for GLM/ZAI users):
158
+ // All agents use GLM — no GPT dependency, so token exhaustion on
159
+ // third-party providers never blocks agent execution.
115
160
  // "models": {
116
161
  // "glm": "zai-coding-plan/glm-5.1",
117
- // "gpt": "openai/gpt-5.5",
118
162
  // "fast": "glm",
119
- // "strong": "gpt",
163
+ // "strong": "glm",
120
164
  // "coder": "glm",
121
- // "resolver": "gpt",
122
- // "reviewer": "gpt",
123
- // "deep-reviewer": "gpt",
124
- // "explorer": "fast"
165
+ // "resolver": "glm",
166
+ // "reviewer": "glm",
167
+ // "deep-reviewer": "glm",
168
+ // "explorer": "fast",
169
+ // "planner": "glm"
125
170
  // }
126
171
  //
127
172
  // Legacy aliases (glm, gpt, quick, deep) remain supported for backward
@@ -229,6 +274,20 @@
229
274
  "enabled": false,
230
275
  "mode": "subagent",
231
276
  "maxSteps": 8
277
+ },
278
+
279
+ // -----------------------------------------------------------------
280
+ // glm — GLM-optimized orchestrator (selectable in agent picker)
281
+ // Disabled by default. Enable to get a standalone GLM agent with
282
+ // coding-plan optimized prompts: session limit handling,
283
+ // token-efficient execution, no hard concurrency cap unless configured.
284
+ // Users pick this from OpenCode's agent picker as an alternative
285
+ // to the default resolver.
286
+ // -----------------------------------------------------------------
287
+ "glm": {
288
+ "enabled": false,
289
+ "mode": "all",
290
+ "maxSteps": 30
232
291
  }
233
292
  }
234
293
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-resolve",
3
- "version": "0.1.6",
4
- "description": "OpenCode plugin that adds a small coder/reviewer agent set while preserving native plan/build behavior.",
3
+ "version": "0.1.8",
4
+ "description": "OpenCode plugin that adds a lightweight resolver/coder harness for continuous agentic coding.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/jshsakura/opencode-resolve#readme",
7
7
  "bugs": {
@@ -25,6 +25,7 @@
25
25
  "opencode-resolve.example.json",
26
26
  "opencode-resolve.reference.jsonc",
27
27
  "README.md",
28
+ "README.ko.md",
28
29
  "LICENSE"
29
30
  ],
30
31
  "scripts": {
@@ -39,6 +40,9 @@
39
40
  "opencode",
40
41
  "opencode-plugin",
41
42
  "agents",
43
+ "agentic-coding",
44
+ "autonomous-coding",
45
+ "harness",
42
46
  "taskflow",
43
47
  "resolve",
44
48
  "task-resolution",