opencode-autognosis 0.1.4 → 1.0.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/index.d.ts +3 -15
- package/dist/index.js +233 -28
- package/package.json +10 -11
- package/gemini-extension.json +0 -12
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export default function plugin(): {
|
|
5
|
-
tools: any;
|
|
6
|
-
commands: any;
|
|
7
|
-
slashCommands: {
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
execute: ({ mode, token }: {
|
|
11
|
-
mode: string;
|
|
12
|
-
token?: string;
|
|
13
|
-
}) => Promise<string | undefined>;
|
|
14
|
-
}[];
|
|
15
|
-
};
|
|
1
|
+
import { type Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
export declare const AutognosisPlugin: Plugin;
|
|
3
|
+
export default AutognosisPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,235 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as fsSync from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const PROJECT_ROOT = process.cwd();
|
|
10
|
+
const OPENCODE_DIR = path.join(PROJECT_ROOT, ".opencode");
|
|
11
|
+
const CACHE_DIR = path.join(OPENCODE_DIR, "cache");
|
|
12
|
+
async function runCmd(cmd, cwd = PROJECT_ROOT, timeoutMs = 30000) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
15
|
+
cwd,
|
|
16
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
17
|
+
timeout: timeoutMs
|
|
18
|
+
});
|
|
19
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.signal === 'SIGTERM' && error.code === undefined) {
|
|
23
|
+
return { stdout: "", stderr: `Command timed out after ${timeoutMs}ms`, error, timedOut: true };
|
|
24
|
+
}
|
|
25
|
+
return { stdout: "", stderr: error.message, error };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function checkBinary(bin) {
|
|
29
|
+
const { error } = await runCmd(`${bin} --version`, PROJECT_ROOT, 5000);
|
|
30
|
+
return !error;
|
|
31
|
+
}
|
|
32
|
+
async function ensureCache() {
|
|
33
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
async function cleanCache() {
|
|
36
|
+
try {
|
|
37
|
+
const files = await fs.readdir(CACHE_DIR);
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const MAX_AGE = 7 * 24 * 60 * 60 * 1000;
|
|
40
|
+
let deleted = 0;
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const filePath = path.join(CACHE_DIR, file);
|
|
43
|
+
const stats = await fs.stat(filePath);
|
|
44
|
+
if (now - stats.mtimeMs > MAX_AGE) {
|
|
45
|
+
await fs.unlink(filePath);
|
|
46
|
+
deleted++;
|
|
27
47
|
}
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
}
|
|
49
|
+
return deleted;
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function maintainSymbolIndex() {
|
|
56
|
+
await ensureCache();
|
|
57
|
+
if (!(await checkBinary("ctags"))) {
|
|
58
|
+
return { rebuilt: false, status: "unavailable", reason: "ctags binary missing" };
|
|
59
|
+
}
|
|
60
|
+
const tagsFile = path.join(CACHE_DIR, "tags");
|
|
61
|
+
const fingerprintFile = path.join(CACHE_DIR, "tags.fingerprint");
|
|
62
|
+
const { stdout: head } = await runCmd("git rev-parse HEAD");
|
|
63
|
+
const { stdout: status } = await runCmd("git status --porcelain");
|
|
64
|
+
const currentFingerprint = `${head}\n${status}`;
|
|
65
|
+
let storedFingerprint = "";
|
|
66
|
+
try {
|
|
67
|
+
storedFingerprint = await fs.readFile(fingerprintFile, "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch (e) { }
|
|
70
|
+
if (currentFingerprint !== storedFingerprint || !fsSync.existsSync(tagsFile)) {
|
|
71
|
+
const { error, stderr } = await runCmd(`ctags -R -f ${tagsFile} --languages=TypeScript,JavaScript,Python,Go,Rust,C++,C .`, PROJECT_ROOT);
|
|
72
|
+
if (error) {
|
|
73
|
+
return { rebuilt: false, status: "failed", reason: stderr };
|
|
74
|
+
}
|
|
75
|
+
await fs.writeFile(fingerprintFile, currentFingerprint);
|
|
76
|
+
return { rebuilt: true, status: "ok" };
|
|
77
|
+
}
|
|
78
|
+
return { rebuilt: false, status: "ok" };
|
|
30
79
|
}
|
|
80
|
+
export const AutognosisPlugin = async (_ctx) => {
|
|
81
|
+
let pendingInitToken = null;
|
|
82
|
+
return {
|
|
83
|
+
tool: {
|
|
84
|
+
autognosis_init: tool({
|
|
85
|
+
description: "Initialize the Autognosis environment.",
|
|
86
|
+
args: {
|
|
87
|
+
mode: tool.schema.enum(["plan", "apply"]).optional().default("plan"),
|
|
88
|
+
token: tool.schema.string().optional()
|
|
89
|
+
},
|
|
90
|
+
async execute({ mode, token }) {
|
|
91
|
+
if (mode === "plan") {
|
|
92
|
+
const checks = { rg: await checkBinary("rg"), fd: await checkBinary("fd"), sg: await checkBinary("sg"), ctags: await checkBinary("ctags"), git: await checkBinary("git") };
|
|
93
|
+
const actions = [];
|
|
94
|
+
if (!fsSync.existsSync(CACHE_DIR))
|
|
95
|
+
actions.push(`Create cache directory: ${CACHE_DIR}`);
|
|
96
|
+
pendingInitToken = crypto.randomBytes(4).toString("hex");
|
|
97
|
+
return JSON.stringify({
|
|
98
|
+
status: "PLAN_READY",
|
|
99
|
+
system_checks: checks,
|
|
100
|
+
planned_actions: actions,
|
|
101
|
+
confirm_token: pendingInitToken,
|
|
102
|
+
instruction: "Call autognosis_init with mode='apply' and the confirm_token."
|
|
103
|
+
}, null, 2);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
if (!pendingInitToken || token !== pendingInitToken) {
|
|
107
|
+
return JSON.stringify({ status: "ERROR", message: "Invalid token." });
|
|
108
|
+
}
|
|
109
|
+
await ensureCache();
|
|
110
|
+
pendingInitToken = null;
|
|
111
|
+
return JSON.stringify({ status: "SUCCESS", message: "Autognosis initialized." });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
fast_search: tool({
|
|
116
|
+
description: "Fast search using rg and fd.",
|
|
117
|
+
args: {
|
|
118
|
+
query: tool.schema.string(),
|
|
119
|
+
mode: tool.schema.enum(["filename", "content"]).optional().default("filename"),
|
|
120
|
+
path: tool.schema.string().optional().default(".")
|
|
121
|
+
},
|
|
122
|
+
async execute({ query, mode, path: searchPath }) {
|
|
123
|
+
if (mode === "content") {
|
|
124
|
+
const { stdout } = await runCmd(`rg -n --column "${query}" "${searchPath}"`);
|
|
125
|
+
return stdout.split('\n').slice(0, 50).join('\n') || "No matches.";
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const { stdout } = await runCmd(`fd "${query}" "${searchPath}"`);
|
|
129
|
+
return stdout.split('\n').slice(0, 50).join('\n') || "No files.";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
read_slice: tool({
|
|
134
|
+
description: "Read a specific slice of a file.",
|
|
135
|
+
args: {
|
|
136
|
+
file: tool.schema.string(),
|
|
137
|
+
start_line: tool.schema.number(),
|
|
138
|
+
end_line: tool.schema.number()
|
|
139
|
+
},
|
|
140
|
+
async execute({ file, start_line, end_line }) {
|
|
141
|
+
const { stdout, stderr } = await runCmd(`sed -n '${start_line},${end_line}p;${end_line + 1}q' "${file}"`);
|
|
142
|
+
if (stderr)
|
|
143
|
+
return `Error: ${stderr}`;
|
|
144
|
+
return JSON.stringify({ file, start_line, end_line, content: stdout }, null, 2);
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
symbol_query: tool({
|
|
148
|
+
description: "Query the symbol index.",
|
|
149
|
+
args: {
|
|
150
|
+
symbol: tool.schema.string()
|
|
151
|
+
},
|
|
152
|
+
async execute({ symbol }) {
|
|
153
|
+
const maint = await maintainSymbolIndex();
|
|
154
|
+
if (maint.status === "unavailable")
|
|
155
|
+
return JSON.stringify({ error: maint.reason });
|
|
156
|
+
const tagsFile = path.join(CACHE_DIR, "tags");
|
|
157
|
+
const { stdout: grepOut } = await runCmd(`grep -P "^${symbol}\t" "${tagsFile}"`);
|
|
158
|
+
return JSON.stringify({ matches: grepOut.split('\n').filter(Boolean), metadata: maint }, null, 2);
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
161
|
+
jump_to_symbol: tool({
|
|
162
|
+
description: "Jump to a symbol definition.",
|
|
163
|
+
args: {
|
|
164
|
+
symbol: tool.schema.string()
|
|
165
|
+
},
|
|
166
|
+
async execute({ symbol }) {
|
|
167
|
+
const maint = await maintainSymbolIndex();
|
|
168
|
+
if (maint.status !== "ok")
|
|
169
|
+
return JSON.stringify({ error: maint.reason });
|
|
170
|
+
const tagsFile = path.join(CACHE_DIR, "tags");
|
|
171
|
+
const { stdout: tagLine } = await runCmd(`grep -P "^${symbol}\t" "${tagsFile}" | head -n 1`);
|
|
172
|
+
if (!tagLine)
|
|
173
|
+
return JSON.stringify({ found: false, symbol });
|
|
174
|
+
const file = tagLine.split('\t')[1];
|
|
175
|
+
const { stdout: grepLine } = await runCmd(`grep -n "${symbol}" "${file}" | head -n 1`);
|
|
176
|
+
const line = grepLine ? parseInt(grepLine.split(':')[0], 10) : 1;
|
|
177
|
+
const start = Math.max(1, line - 5);
|
|
178
|
+
const end = line + 15;
|
|
179
|
+
const { stdout: slice } = await runCmd(`sed -n '${start},${end}p;${end + 1}q' "${file}"`);
|
|
180
|
+
return JSON.stringify({ symbol, resolved_location: { file, line }, slice: { start, end, content: slice } }, null, 2);
|
|
181
|
+
}
|
|
182
|
+
}),
|
|
183
|
+
brief_fix_loop: tool({
|
|
184
|
+
description: "Action Planner.",
|
|
185
|
+
args: {
|
|
186
|
+
symbol: tool.schema.string(),
|
|
187
|
+
intent: tool.schema.string()
|
|
188
|
+
},
|
|
189
|
+
async execute({ symbol, intent }) {
|
|
190
|
+
return JSON.stringify({ plan_id: `plan-${Date.now()}`, symbol, intent }, null, 2);
|
|
191
|
+
}
|
|
192
|
+
}),
|
|
193
|
+
prepare_patch: tool({
|
|
194
|
+
description: "Generate a .diff artifact.",
|
|
195
|
+
args: {
|
|
196
|
+
message: tool.schema.string()
|
|
197
|
+
},
|
|
198
|
+
async execute({ message }) {
|
|
199
|
+
await ensureCache();
|
|
200
|
+
const patchPath = path.join(CACHE_DIR, `patch-${Date.now()}.diff`);
|
|
201
|
+
const { stdout } = await runCmd("git diff");
|
|
202
|
+
if (!stdout)
|
|
203
|
+
return "No changes.";
|
|
204
|
+
await fs.writeFile(patchPath, `// MSG: ${message}\n\n${stdout}`);
|
|
205
|
+
return `Patch saved to ${patchPath}`;
|
|
206
|
+
}
|
|
207
|
+
}),
|
|
208
|
+
validate_patch: tool({
|
|
209
|
+
description: "Validate a patch.",
|
|
210
|
+
args: {
|
|
211
|
+
patch_path: tool.schema.string()
|
|
212
|
+
},
|
|
213
|
+
async execute({ patch_path }) {
|
|
214
|
+
const { error } = await runCmd(`git apply --check "${patch_path}"`);
|
|
215
|
+
return error ? `FAILED: ${error.message}` : "SUCCESS.";
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
finalize_plan: tool({
|
|
219
|
+
description: "Finalize a plan.",
|
|
220
|
+
args: {
|
|
221
|
+
plan_id: tool.schema.string(),
|
|
222
|
+
outcome: tool.schema.string()
|
|
223
|
+
},
|
|
224
|
+
async execute({ plan_id, outcome }) {
|
|
225
|
+
await ensureCache();
|
|
226
|
+
const report = { plan_id, outcome, time: new Date().toISOString() };
|
|
227
|
+
await fs.appendFile(path.join(CACHE_DIR, "gaps.jsonl"), JSON.stringify(report) + "\n");
|
|
228
|
+
const deleted = await cleanCache();
|
|
229
|
+
return `Finalized. Deleted ${deleted} items.`;
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
export default AutognosisPlugin;
|
package/package.json
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-autognosis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Transforms OpenCode agents into 'miniature engineers' with deep codebase awareness. Includes fast structural search (ast-grep), instant symbol navigation (ctags), and a disciplined 'Plan → Execute → Patch' workflow.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
},
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
10
8
|
"files": [
|
|
11
9
|
"dist",
|
|
12
10
|
"assets",
|
|
13
11
|
"LICENSE",
|
|
14
|
-
"README.md"
|
|
15
|
-
"gemini-extension.json"
|
|
12
|
+
"README.md"
|
|
16
13
|
],
|
|
17
14
|
"scripts": {
|
|
18
15
|
"build": "tsc -p tsconfig.json",
|
|
19
16
|
"prepublishOnly": "npm run build"
|
|
20
17
|
},
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
18
|
+
"opencode": {
|
|
19
|
+
"type": "plugin",
|
|
20
|
+
"hooks": []
|
|
24
21
|
},
|
|
22
|
+
"dependencies": {},
|
|
25
23
|
"devDependencies": {
|
|
24
|
+
"@opencode-ai/plugin": "^1.0.162",
|
|
26
25
|
"@types/node": "^20.0.0",
|
|
27
26
|
"typescript": "^5.0.0"
|
|
28
27
|
}
|
|
29
|
-
}
|
|
28
|
+
}
|
package/gemini-extension.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "opencode-autognosis",
|
|
3
|
-
"version": "0.1.3",
|
|
4
|
-
"description": "Autognosis extension for Gemini CLI",
|
|
5
|
-
"mcpServers": {
|
|
6
|
-
"autognosis": {
|
|
7
|
-
"command": "node",
|
|
8
|
-
"args": ["${extensionPath}/dist/index.js"],
|
|
9
|
-
"cwd": "${extensionPath}"
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|