opencode-resolve 0.1.7 → 0.1.9
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 +121 -60
- package/README.md +121 -60
- package/dist/agents.d.ts +27 -0
- package/dist/agents.js +392 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +404 -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 +8 -2
- package/opencode-resolve.reference.jsonc +107 -27
- package/package.json +10 -2
- package/scripts/install-git-hooks.mjs +28 -0
- package/scripts/install-local.mjs +64 -4
- package/scripts/postinstall.mjs +552 -34
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type PermissionValue = "ask" | "allow" | "deny";
|
|
2
|
+
export type ResolveAgentName = "coder" | "reviewer" | "resolver" | "codex" | "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";
|
|
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,11 +9,14 @@
|
|
|
9
9
|
"agents": {
|
|
10
10
|
"coder": {
|
|
11
11
|
"enabled": true,
|
|
12
|
-
"mode": "
|
|
12
|
+
"mode": "subagent"
|
|
13
13
|
},
|
|
14
14
|
"resolver": {
|
|
15
15
|
"enabled": true
|
|
16
16
|
},
|
|
17
|
+
"codex": {
|
|
18
|
+
"enabled": false
|
|
19
|
+
},
|
|
17
20
|
"explorer": {
|
|
18
21
|
"enabled": true,
|
|
19
22
|
"mode": "subagent"
|
|
@@ -41,6 +44,9 @@
|
|
|
41
44
|
},
|
|
42
45
|
"researcher": {
|
|
43
46
|
"enabled": false
|
|
47
|
+
},
|
|
48
|
+
"glm": {
|
|
49
|
+
"enabled": false
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
}
|