micode 0.9.0 → 0.10.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/README.md +40 -4
- package/dist/index.js +1614 -530
- package/package.json +2 -1
- package/src/agents/brainstormer.ts +1 -1
- package/src/agents/commander.ts +2 -1
- package/src/agents/index.ts +27 -26
- package/src/config-loader.test.ts +53 -2
- package/src/config-loader.ts +78 -17
- package/src/hooks/fetch-tracker.ts +233 -0
- package/src/index.ts +22 -5
- package/src/mindmodel/loader.ts +3 -2
- package/src/tools/mindmodel-lookup.ts +2 -2
- package/src/tools/pty/index.ts +10 -8
- package/src/tools/pty/manager.ts +33 -3
- package/src/tools/pty/pty-loader.ts +123 -0
- package/src/utils/config.ts +25 -0
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createCommentCheckerHook } from "./hooks/comment-checker";
|
|
|
12
12
|
import { createConstraintReviewerHook } from "./hooks/constraint-reviewer";
|
|
13
13
|
import { createContextInjectorHook } from "./hooks/context-injector";
|
|
14
14
|
import { createContextWindowMonitorHook } from "./hooks/context-window-monitor";
|
|
15
|
+
import { createFetchTrackerHook } from "./hooks/fetch-tracker";
|
|
15
16
|
import { createFileOpsTrackerHook, getFileOps } from "./hooks/file-ops-tracker";
|
|
16
17
|
import { createFragmentInjectorHook, warnUnknownAgents } from "./hooks/fragment-injector";
|
|
17
18
|
import { createLedgerLoaderHook } from "./hooks/ledger-loader";
|
|
@@ -28,7 +29,7 @@ import { milestone_artifact_search } from "./tools/milestone-artifact-search";
|
|
|
28
29
|
import { createMindmodelLookupTool } from "./tools/mindmodel-lookup";
|
|
29
30
|
import { createOcttoTools, createSessionStore } from "./tools/octto";
|
|
30
31
|
// PTY System
|
|
31
|
-
import { createPtyTools, PTYManager } from "./tools/pty";
|
|
32
|
+
import { createPtyTools, loadBunPty, PTYManager } from "./tools/pty";
|
|
32
33
|
import { createSpawnAgentTool } from "./tools/spawn-agent";
|
|
33
34
|
import { log } from "./utils/logger";
|
|
34
35
|
|
|
@@ -101,6 +102,7 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
101
102
|
const commentCheckerHook = createCommentCheckerHook(ctx);
|
|
102
103
|
const artifactAutoIndexHook = createArtifactAutoIndexHook(ctx);
|
|
103
104
|
const fileOpsTrackerHook = createFileOpsTrackerHook(ctx);
|
|
105
|
+
const fetchTrackerHook = createFetchTrackerHook(ctx);
|
|
104
106
|
|
|
105
107
|
// Fragment injector hook - injects user-defined prompt fragments
|
|
106
108
|
const fragmentInjectorHook = createFragmentInjectorHook(ctx, userConfig);
|
|
@@ -174,9 +176,15 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
174
176
|
}
|
|
175
177
|
});
|
|
176
178
|
|
|
177
|
-
// PTY System
|
|
179
|
+
// PTY System - load bun-pty with graceful degradation
|
|
180
|
+
// Sets BUN_PTY_LIB env var to fix path resolution in OpenCode plugin environments
|
|
181
|
+
// See: https://github.com/vtemian/micode/issues/20
|
|
178
182
|
const ptyManager = new PTYManager();
|
|
179
|
-
const
|
|
183
|
+
const bunPty = await loadBunPty();
|
|
184
|
+
if (bunPty) {
|
|
185
|
+
ptyManager.init(bunPty.spawn);
|
|
186
|
+
}
|
|
187
|
+
const ptyTools = ptyManager.available ? createPtyTools(ptyManager) : {};
|
|
180
188
|
|
|
181
189
|
// Spawn agent tool (for subagents to spawn other subagents)
|
|
182
190
|
const spawn_agent = createSpawnAgentTool(ctx);
|
|
@@ -229,7 +237,6 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
229
237
|
edit: "allow",
|
|
230
238
|
bash: "allow",
|
|
231
239
|
webfetch: "allow",
|
|
232
|
-
doom_loop: "allow",
|
|
233
240
|
external_directory: "allow",
|
|
234
241
|
};
|
|
235
242
|
|
|
@@ -313,7 +320,7 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
313
320
|
...output.options,
|
|
314
321
|
thinking: {
|
|
315
322
|
type: "enabled",
|
|
316
|
-
|
|
323
|
+
budgetTokens: 128000,
|
|
317
324
|
},
|
|
318
325
|
};
|
|
319
326
|
}
|
|
@@ -398,6 +405,12 @@ IMPORTANT:
|
|
|
398
405
|
output,
|
|
399
406
|
);
|
|
400
407
|
|
|
408
|
+
// Track fetch operations and cache results
|
|
409
|
+
await fetchTrackerHook["tool.execute.after"](
|
|
410
|
+
{ tool: input.tool, sessionID: input.sessionID, args: input.args },
|
|
411
|
+
output,
|
|
412
|
+
);
|
|
413
|
+
|
|
401
414
|
// Constraint review for Edit/Write
|
|
402
415
|
await constraintReviewerHook["tool.execute.after"](
|
|
403
416
|
{ tool: input.tool, sessionID: input.sessionID, args: input.args },
|
|
@@ -444,6 +457,7 @@ IMPORTANT:
|
|
|
444
457
|
thinkModeState.delete(sessionId);
|
|
445
458
|
ptyManager.cleanupBySession(sessionId);
|
|
446
459
|
constraintReviewerHook.cleanupSession(sessionId);
|
|
460
|
+
fetchTrackerHook.cleanupSession(sessionId);
|
|
447
461
|
|
|
448
462
|
// Cleanup octto sessions
|
|
449
463
|
const octtoSessions = octtoSessionsMap.get(sessionId);
|
|
@@ -464,6 +478,9 @@ IMPORTANT:
|
|
|
464
478
|
|
|
465
479
|
// File ops tracker cleanup
|
|
466
480
|
await fileOpsTrackerHook.event({ event });
|
|
481
|
+
|
|
482
|
+
// Fetch tracker cleanup
|
|
483
|
+
await fetchTrackerHook.event({ event });
|
|
467
484
|
},
|
|
468
485
|
};
|
|
469
486
|
};
|
package/src/mindmodel/loader.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { access, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { config } from "../utils/config";
|
|
6
|
+
import { log } from "../utils/logger";
|
|
6
7
|
import { type MindmodelManifest, parseManifest } from "./types";
|
|
7
8
|
|
|
8
9
|
export interface LoadedMindmodel {
|
|
@@ -36,7 +37,7 @@ export async function loadMindmodel(projectDir: string): Promise<LoadedMindmodel
|
|
|
36
37
|
manifest,
|
|
37
38
|
};
|
|
38
39
|
} catch (error) {
|
|
39
|
-
|
|
40
|
+
log.warn("mindmodel", `Failed to load manifest: ${error}`);
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -58,7 +59,7 @@ export async function loadExamples(mindmodel: LoadedMindmodel, categoryPaths: st
|
|
|
58
59
|
content,
|
|
59
60
|
});
|
|
60
61
|
} catch {
|
|
61
|
-
|
|
62
|
+
log.warn("mindmodel", `Failed to load example: ${categoryPath}`);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -64,7 +64,7 @@ Returns relevant code examples and patterns to follow.`,
|
|
|
64
64
|
return "No specific patterns found for this task. Proceed using general best practices.";
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
log.
|
|
67
|
+
log.debug("mindmodel", `Matched categories: ${categories.join(", ")}`);
|
|
68
68
|
|
|
69
69
|
// Load examples
|
|
70
70
|
const examples = await loadExamples(mindmodel, categories);
|
|
@@ -73,7 +73,7 @@ Returns relevant code examples and patterns to follow.`,
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const formatted = formatExamplesForInjection(examples);
|
|
76
|
-
log.
|
|
76
|
+
log.debug("mindmodel", `Returning ${examples.length} examples`);
|
|
77
77
|
|
|
78
78
|
return formatted;
|
|
79
79
|
} catch (error) {
|
package/src/tools/pty/index.ts
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
// src/tools/pty/index.ts
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
export { RingBuffer } from "./buffer";
|
|
4
|
+
export { PTYManager } from "./manager";
|
|
5
|
+
export { getBunPtyLoadError, isBunPtyAvailable, loadBunPty } from "./pty-loader";
|
|
6
|
+
export { createPtyKillTool } from "./tools/kill";
|
|
7
|
+
export { createPtyListTool } from "./tools/list";
|
|
8
|
+
export { createPtyReadTool } from "./tools/read";
|
|
4
9
|
export { createPtySpawnTool } from "./tools/spawn";
|
|
5
10
|
export { createPtyWriteTool } from "./tools/write";
|
|
6
|
-
export { createPtyReadTool } from "./tools/read";
|
|
7
|
-
export { createPtyListTool } from "./tools/list";
|
|
8
|
-
export { createPtyKillTool } from "./tools/kill";
|
|
9
11
|
export type {
|
|
10
12
|
PTYSession,
|
|
11
13
|
PTYSessionInfo,
|
|
12
14
|
PTYStatus,
|
|
13
|
-
SpawnOptions,
|
|
14
15
|
ReadResult,
|
|
15
16
|
SearchMatch,
|
|
16
17
|
SearchResult,
|
|
18
|
+
SpawnOptions,
|
|
17
19
|
} from "./types";
|
|
18
20
|
|
|
19
21
|
import type { PTYManager } from "./manager";
|
|
22
|
+
import { createPtyKillTool } from "./tools/kill";
|
|
23
|
+
import { createPtyListTool } from "./tools/list";
|
|
24
|
+
import { createPtyReadTool } from "./tools/read";
|
|
20
25
|
import { createPtySpawnTool } from "./tools/spawn";
|
|
21
26
|
import { createPtyWriteTool } from "./tools/write";
|
|
22
|
-
import { createPtyReadTool } from "./tools/read";
|
|
23
|
-
import { createPtyListTool } from "./tools/list";
|
|
24
|
-
import { createPtyKillTool } from "./tools/kill";
|
|
25
27
|
|
|
26
28
|
export function createPtyTools(manager: PTYManager) {
|
|
27
29
|
return {
|
package/src/tools/pty/manager.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// src/tools/pty/manager.ts
|
|
2
|
-
import { spawn, type IPty } from "bun-pty";
|
|
3
2
|
import { RingBuffer } from "./buffer";
|
|
4
|
-
import type { PTYSession, PTYSessionInfo,
|
|
3
|
+
import type { PTYSession, PTYSessionInfo, ReadResult, SearchResult, SpawnOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
// bun-pty types used locally - the actual module is injected via init()
|
|
6
|
+
type IPty = import("bun-pty").IPty;
|
|
7
|
+
type SpawnFn = typeof import("bun-pty").spawn;
|
|
5
8
|
|
|
6
9
|
function generateId(): string {
|
|
7
10
|
const hex = Array.from(crypto.getRandomValues(new Uint8Array(4)))
|
|
@@ -12,8 +15,35 @@ function generateId(): string {
|
|
|
12
15
|
|
|
13
16
|
export class PTYManager {
|
|
14
17
|
private sessions: Map<string, PTYSession> = new Map();
|
|
18
|
+
private spawnFn: SpawnFn | null = null;
|
|
19
|
+
private _available = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize the manager with the bun-pty spawn function.
|
|
23
|
+
* Must be called before spawn(). If not called or called with null,
|
|
24
|
+
* PTY operations will return errors indicating PTY is unavailable.
|
|
25
|
+
*/
|
|
26
|
+
init(spawnFn: SpawnFn): void {
|
|
27
|
+
this.spawnFn = spawnFn;
|
|
28
|
+
this._available = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether PTY functionality is available (bun-pty loaded successfully).
|
|
33
|
+
*/
|
|
34
|
+
get available(): boolean {
|
|
35
|
+
return this._available;
|
|
36
|
+
}
|
|
15
37
|
|
|
16
38
|
spawn(opts: SpawnOptions): PTYSessionInfo {
|
|
39
|
+
if (!this.spawnFn) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"PTY unavailable: bun-pty native library could not be loaded. " +
|
|
42
|
+
"Set BUN_PTY_LIB environment variable to the path of the native library " +
|
|
43
|
+
"(e.g., .opencode/node_modules/bun-pty/rust-pty/target/release/librust_pty.dylib)",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
const id = generateId();
|
|
18
48
|
const args = opts.args ?? [];
|
|
19
49
|
const workdir = opts.workdir ?? process.cwd();
|
|
@@ -22,7 +52,7 @@ export class PTYManager {
|
|
|
22
52
|
|
|
23
53
|
let ptyProcess: IPty;
|
|
24
54
|
try {
|
|
25
|
-
ptyProcess =
|
|
55
|
+
ptyProcess = this.spawnFn(opts.command, args, {
|
|
26
56
|
name: "xterm-256color",
|
|
27
57
|
cols: 120,
|
|
28
58
|
rows: 40,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/tools/pty/pty-loader.ts
|
|
2
|
+
// Resolves bun-pty native library path and loads bun-pty with graceful degradation.
|
|
3
|
+
//
|
|
4
|
+
// bun-pty's resolveLibPath() checks BUN_PTY_LIB env var first, then hardcoded paths
|
|
5
|
+
// relative to import.meta.url. When micode is installed as an OpenCode plugin,
|
|
6
|
+
// the library ends up in .opencode/node_modules/bun-pty/... which isn't in the
|
|
7
|
+
// hardcoded search paths. We fix this by probing likely locations and setting
|
|
8
|
+
// BUN_PTY_LIB before the dynamic import.
|
|
9
|
+
//
|
|
10
|
+
// See: https://github.com/vtemian/micode/issues/20
|
|
11
|
+
// See: https://github.com/anomalyco/opencode/issues/10556
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { log } from "../../utils/logger";
|
|
17
|
+
|
|
18
|
+
type BunPtyModule = typeof import("bun-pty");
|
|
19
|
+
|
|
20
|
+
let cachedModule: BunPtyModule | null = null;
|
|
21
|
+
let loadAttempted = false;
|
|
22
|
+
let loadError: string | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Probe additional paths where the bun-pty native library might live,
|
|
26
|
+
* beyond what bun-pty checks itself. Sets BUN_PTY_LIB if found.
|
|
27
|
+
*/
|
|
28
|
+
function probeBunPtyLib(): void {
|
|
29
|
+
// If already set by user, respect it
|
|
30
|
+
if (process.env.BUN_PTY_LIB) return;
|
|
31
|
+
|
|
32
|
+
const platform = process.platform;
|
|
33
|
+
const arch = process.arch;
|
|
34
|
+
|
|
35
|
+
const filenames =
|
|
36
|
+
platform === "darwin"
|
|
37
|
+
? arch === "arm64"
|
|
38
|
+
? ["librust_pty_arm64.dylib", "librust_pty.dylib"]
|
|
39
|
+
: ["librust_pty.dylib"]
|
|
40
|
+
: platform === "win32"
|
|
41
|
+
? ["rust_pty.dll"]
|
|
42
|
+
: arch === "arm64"
|
|
43
|
+
? ["librust_pty_arm64.so", "librust_pty.so"]
|
|
44
|
+
: ["librust_pty.so"];
|
|
45
|
+
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
|
|
48
|
+
// Paths that bun-pty does NOT check but where the lib may exist
|
|
49
|
+
// when installed as an OpenCode plugin dependency
|
|
50
|
+
const additionalBasePaths = [
|
|
51
|
+
// .opencode/node_modules/bun-pty/... (plugin installed via .opencode/package.json)
|
|
52
|
+
join(cwd, ".opencode", "node_modules", "bun-pty", "rust-pty", "target", "release"),
|
|
53
|
+
// .micode/node_modules/bun-pty/... (if micode has its own node_modules)
|
|
54
|
+
join(cwd, ".micode", "node_modules", "bun-pty", "rust-pty", "target", "release"),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Also try resolving from require.resolve if available
|
|
58
|
+
try {
|
|
59
|
+
const bunPtyMain = require.resolve("bun-pty");
|
|
60
|
+
if (bunPtyMain) {
|
|
61
|
+
// require.resolve gives us something like .../node_modules/bun-pty/src/index.ts
|
|
62
|
+
// Go up to the bun-pty package root
|
|
63
|
+
const pkgDir = dirname(dirname(bunPtyMain));
|
|
64
|
+
additionalBasePaths.unshift(join(pkgDir, "rust-pty", "target", "release"));
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// require.resolve may fail in some environments
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const basePath of additionalBasePaths) {
|
|
71
|
+
for (const filename of filenames) {
|
|
72
|
+
const candidate = join(basePath, filename);
|
|
73
|
+
if (existsSync(candidate)) {
|
|
74
|
+
process.env.BUN_PTY_LIB = candidate;
|
|
75
|
+
log.info("pty.loader", `Auto-resolved BUN_PTY_LIB=${candidate}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Dynamically load bun-pty with graceful degradation.
|
|
84
|
+
* Sets BUN_PTY_LIB env var before import to fix path resolution
|
|
85
|
+
* in OpenCode plugin environments.
|
|
86
|
+
*
|
|
87
|
+
* Returns null if bun-pty cannot be loaded (native library missing, etc.)
|
|
88
|
+
*/
|
|
89
|
+
export async function loadBunPty(): Promise<BunPtyModule | null> {
|
|
90
|
+
if (loadAttempted) return cachedModule;
|
|
91
|
+
loadAttempted = true;
|
|
92
|
+
|
|
93
|
+
// Probe and set BUN_PTY_LIB before importing
|
|
94
|
+
probeBunPtyLib();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
cachedModule = await import("bun-pty");
|
|
98
|
+
log.info("pty.loader", "bun-pty loaded successfully");
|
|
99
|
+
return cachedModule;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
loadError = error instanceof Error ? error.message : String(error);
|
|
102
|
+
// Extract just the first line for a cleaner warning
|
|
103
|
+
const firstLine = loadError.split("\n")[0];
|
|
104
|
+
log.warn("pty.loader", `bun-pty unavailable: ${firstLine}`);
|
|
105
|
+
log.warn("pty.loader", "PTY tools will be disabled. Set BUN_PTY_LIB env var to the native library path to fix.");
|
|
106
|
+
cachedModule = null;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if bun-pty is available (must call loadBunPty first).
|
|
113
|
+
*/
|
|
114
|
+
export function isBunPtyAvailable(): boolean {
|
|
115
|
+
return cachedModule !== null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the load error message, if any.
|
|
120
|
+
*/
|
|
121
|
+
export function getBunPtyLoadError(): string | null {
|
|
122
|
+
return loadError;
|
|
123
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -127,6 +127,14 @@ export const config = {
|
|
|
127
127
|
allowRemoteBind: false,
|
|
128
128
|
},
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Model settings
|
|
132
|
+
*/
|
|
133
|
+
model: {
|
|
134
|
+
/** Plugin fallback model when no opencode.json or micode.json model is configured */
|
|
135
|
+
default: "openai/gpt-5.2-codex",
|
|
136
|
+
},
|
|
137
|
+
|
|
130
138
|
/**
|
|
131
139
|
* Mindmodel v2 settings
|
|
132
140
|
*/
|
|
@@ -140,4 +148,21 @@ export const config = {
|
|
|
140
148
|
/** Category groups for v2 structure */
|
|
141
149
|
categoryGroups: ["stack", "architecture", "patterns", "style", "components", "domain", "ops"] as readonly string[],
|
|
142
150
|
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fetch loop prevention settings
|
|
154
|
+
*/
|
|
155
|
+
fetch: {
|
|
156
|
+
/** Inject warning after this many calls to the same resource */
|
|
157
|
+
warnThreshold: 3,
|
|
158
|
+
/** Hard block after this many calls to the same resource */
|
|
159
|
+
maxCallsPerResource: 5,
|
|
160
|
+
/** Cache TTL in milliseconds (5 minutes) */
|
|
161
|
+
cacheTtlMs: 300_000,
|
|
162
|
+
/** Max cached entries per session (LRU eviction) */
|
|
163
|
+
cacheMaxEntries: 50,
|
|
164
|
+
},
|
|
143
165
|
} as const;
|
|
166
|
+
|
|
167
|
+
/** Plugin fallback model — single source of truth for the default model string */
|
|
168
|
+
export const DEFAULT_MODEL = config.model.default;
|