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/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 ptyTools = createPtyTools(ptyManager);
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
- budget_tokens: 32000,
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
  };
@@ -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
- console.warn(`[micode] Failed to load mindmodel manifest: ${error}`);
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
- console.warn(`[micode] Failed to load mindmodel example: ${categoryPath}`);
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.info("mindmodel", `Matched categories: ${categories.join(", ")}`);
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.info("mindmodel", `Returning ${examples.length} examples`);
76
+ log.debug("mindmodel", `Returning ${examples.length} examples`);
77
77
 
78
78
  return formatted;
79
79
  } catch (error) {
@@ -1,27 +1,29 @@
1
1
  // src/tools/pty/index.ts
2
- export { PTYManager } from "./manager";
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 {
@@ -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, SpawnOptions, ReadResult, SearchResult } from "./types";
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 = spawn(opts.command, args, {
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
+ }
@@ -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;