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.
- package/README.ko.md +98 -53
- package/README.md +98 -53
- package/dist/agents.d.ts +26 -0
- package/dist/agents.js +355 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +398 -0
- package/dist/hooks/index.d.ts +18 -0
- package/dist/hooks/index.js +493 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -702
- package/dist/state.d.ts +33 -0
- package/dist/state.js +20 -0
- package/dist/tools/index.d.ts +252 -0
- package/dist/tools/index.js +1209 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +371 -0
- package/opencode-resolve.example.json +5 -2
- package/opencode-resolve.reference.jsonc +78 -19
- package/package.json +6 -2
- package/scripts/postinstall.mjs +311 -31
package/dist/types.d.ts
ADDED
|
@@ -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 {};
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
-
"
|
|
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": "
|
|
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
|
-
//
|
|
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
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
-
//
|
|
69
|
-
//
|
|
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 =
|
|
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 →
|
|
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
|
|
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": "
|
|
163
|
+
// "strong": "glm",
|
|
120
164
|
// "coder": "glm",
|
|
121
|
-
// "resolver": "
|
|
122
|
-
// "reviewer": "
|
|
123
|
-
// "deep-reviewer": "
|
|
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.
|
|
4
|
-
"description": "OpenCode plugin that adds a
|
|
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",
|