grepmax 0.12.2 → 0.12.3

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.
@@ -21,46 +21,62 @@ const node_util_1 = require("node:util");
21
21
  const commander_1 = require("commander");
22
22
  const shell = process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/sh");
23
23
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
24
- const SKILL = `
25
- ---
26
- name: gmax
27
- description: Semantic code search and call-graph tracing via gmax.
28
- ---
29
-
30
- ## ⚠️ CRITICAL: Handling "Indexing" State
31
- If the tool output says **"Indexing"**, **"Building"**, or **"Syncing"**:
32
- 1. **STOP.** Do not hallucinate results.
33
- 2. **INFORM** the user: "The semantic index is still building. Results are partial."
34
- 3. **ASK** if they want to proceed or wait.
35
-
36
- ## Commands
37
- - Search: \`gmax "auth logic" --compact\`
38
- - Trace: \`gmax trace "AuthService"\`
39
- `;
24
+ const AGENTS_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".codex", "AGENTS.md");
25
+ const SKILL_START = "<!-- gmax:start -->";
26
+ const SKILL_END = "<!-- gmax:end -->";
27
+ function getPackageRoot() {
28
+ return node_path_1.default.resolve(__dirname, "../..");
29
+ }
30
+ function loadSkill() {
31
+ const skillPath = node_path_1.default.join(getPackageRoot(), "plugins", "grepmax", "skills", "grepmax", "SKILL.md");
32
+ try {
33
+ return node_fs_1.default.readFileSync(skillPath, "utf-8");
34
+ }
35
+ catch (_a) {
36
+ return [
37
+ "---",
38
+ "name: gmax",
39
+ "description: Semantic code search. Use alongside grep - grep for exact strings, gmax for concepts.",
40
+ "---",
41
+ "",
42
+ 'Use `gmax "query" --agent` for semantic search.',
43
+ ].join("\n");
44
+ }
45
+ }
46
+ function writeSkillToAgents(skill) {
47
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(AGENTS_PATH), { recursive: true });
48
+ const block = `${SKILL_START}\n${skill.trim()}\n${SKILL_END}`;
49
+ if (!node_fs_1.default.existsSync(AGENTS_PATH)) {
50
+ node_fs_1.default.writeFileSync(AGENTS_PATH, block);
51
+ return;
52
+ }
53
+ const content = node_fs_1.default.readFileSync(AGENTS_PATH, "utf-8");
54
+ // Check if file has any gmax content (markers or legacy)
55
+ if (content.includes("gmax")) {
56
+ // Remove all gmax content and rewrite with just our block
57
+ const markerRe = new RegExp(`\n?${SKILL_START}[\\s\\S]*?${SKILL_END}\n?`, "g");
58
+ const cleaned = content.replace(markerRe, "");
59
+ // Remove legacy content (everything between --- blocks mentioning gmax)
60
+ const withoutLegacy = cleaned.replace(/---[\s\S]*?(?:gmax|--compact)[\s\S]*?(?=\n<!-- |$)/, "").trim();
61
+ node_fs_1.default.writeFileSync(AGENTS_PATH, withoutLegacy ? `${withoutLegacy}\n\n${block}` : block);
62
+ }
63
+ else {
64
+ node_fs_1.default.writeFileSync(AGENTS_PATH, `${content.trim()}\n\n${block}`);
65
+ }
66
+ }
40
67
  function installPlugin() {
41
68
  return __awaiter(this, void 0, void 0, function* () {
42
69
  try {
43
- // 1. Register the MCP Tool
44
- // 'gmax mcp' acts as the server.
70
+ // 1. Register MCP tool
45
71
  yield execAsync("codex mcp add gmax gmax mcp", {
46
72
  shell,
47
73
  env: process.env,
48
74
  });
49
75
  console.log("✅ gmax MCP tool registered with Codex");
50
- // 2. Add Instructions to AGENTS.md
51
- const destPath = node_path_1.default.join(node_os_1.default.homedir(), ".codex", "AGENTS.md");
52
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(destPath), { recursive: true });
53
- const content = node_fs_1.default.existsSync(destPath)
54
- ? node_fs_1.default.readFileSync(destPath, "utf-8")
55
- : "";
56
- // Only append if not present
57
- if (!content.includes("name: gmax")) {
58
- node_fs_1.default.appendFileSync(destPath, `\n${SKILL}`);
59
- console.log("✅ gmax skill instructions added to Codex");
60
- }
61
- else {
62
- console.log("ℹ️ gmax skill instructions already present");
63
- }
76
+ // 2. Write SKILL to AGENTS.md (idempotent)
77
+ const skill = loadSkill();
78
+ writeSkillToAgents(skill);
79
+ console.log("✅ gmax skill instructions written to", AGENTS_PATH);
64
80
  }
65
81
  catch (error) {
66
82
  console.error(`❌ Error installing Codex plugin: ${error}`);
@@ -74,17 +90,16 @@ function uninstallPlugin() {
74
90
  yield execAsync("codex mcp remove gmax", { shell, env: process.env });
75
91
  console.log("✅ gmax MCP tool removed");
76
92
  }
77
- catch (_e) {
93
+ catch (_a) {
78
94
  /* ignore if not found */
79
95
  }
80
- const destPath = node_path_1.default.join(node_os_1.default.homedir(), ".codex", "AGENTS.md");
81
- if (node_fs_1.default.existsSync(destPath)) {
82
- let content = node_fs_1.default.readFileSync(destPath, "utf-8");
83
- // Naive removal: strictly matches the block we added.
84
- // For robust removal, you might need regex, but this works if the string is exact.
85
- if (content.includes(SKILL)) {
86
- content = content.replace(SKILL, "").trim();
87
- node_fs_1.default.writeFileSync(destPath, content);
96
+ if (node_fs_1.default.existsSync(AGENTS_PATH)) {
97
+ let content = node_fs_1.default.readFileSync(AGENTS_PATH, "utf-8");
98
+ // Remove marked block
99
+ const markerRe = new RegExp(`\n?${SKILL_START}[\\s\\S]*?${SKILL_END}\n?`, "g");
100
+ if (markerRe.test(content)) {
101
+ content = content.replace(markerRe, "").trim();
102
+ node_fs_1.default.writeFileSync(AGENTS_PATH, content || "");
88
103
  console.log("✅ gmax instructions removed from AGENTS.md");
89
104
  }
90
105
  }
@@ -13,57 +13,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.uninstallDroid = exports.installDroid = void 0;
16
+ const node_child_process_1 = require("node:child_process");
16
17
  const node_fs_1 = __importDefault(require("node:fs"));
17
18
  const node_os_1 = __importDefault(require("node:os"));
18
19
  const node_path_1 = __importDefault(require("node:path"));
19
20
  const commander_1 = require("commander");
20
- const SKILL = `
21
- ---
22
- name: gmax
23
- description: Semantic code search and call-graph tracing for AI agents. Finds code by concept, surfaces roles (ORCHESTRATION vs DEFINITION), and traces dependencies.
24
- allowed-tools: "Bash(gmax:*), Read"
25
- license: Apache-2.0
26
- ---
27
-
28
- ## ⚠️ CRITICAL: Handling "Indexing" State
29
- If any \`gmax\` command returns a status indicating **"Indexing"**, **"Building"**, or **"Syncing"**:
30
- 1. **STOP** your current train of thought.
31
- 2. **INFORM** the user: "The semantic index is currently building. Search results will be incomplete."
32
- 3. **ASK**: "Do you want me to proceed with partial results, or wait for indexing to finish?"
33
- *(Do not assume you should proceed without confirmation).*
34
-
35
- ## Core Commands
36
- - Search: \`gmax "how does auth work" --compact\`
37
- - Trace: \`gmax trace "AuthService"\`
38
- - Symbols: \`gmax symbols "Auth"\`
39
-
40
- ## Output (Default = Compact TSV)
41
- - One line per hit: \`path\\tlines\\tscore\\trole\\tconf\\tdefined\\tpreview\`
42
- - Roles: \`ORCH\` (Orchestration), \`DEF\` (Definition), \`IMPL\` (Implementation).
43
- - **Note:** If output is empty but valid, say "No semantic matches found."
44
-
45
- ## Typical Workflow
46
-
47
- 1. **Discover**
48
- \`\`\`bash
49
- gmax "worker pool lifecycle" --compact
50
- \`\`\`
51
-
52
- 2. **Explore**
53
- \`\`\`bash
54
- gmax symbols Worker
55
- \`\`\`
56
-
57
- 3. **Trace**
58
- \`\`\`bash
59
- gmax trace WorkerPool
60
- \`\`\`
61
-
62
- 4. **Read**
63
- \`\`\`bash
64
- Read src/lib/workers/pool.ts:112-186
65
- \`\`\`
66
- `;
67
21
  function resolveDroidRoot() {
68
22
  const root = node_path_1.default.join(node_os_1.default.homedir(), ".factory");
69
23
  if (!node_fs_1.default.existsSync(root)) {
@@ -71,6 +25,37 @@ function resolveDroidRoot() {
71
25
  }
72
26
  return root;
73
27
  }
28
+ function resolveGmaxBin() {
29
+ try {
30
+ return (0, node_child_process_1.execSync)("which gmax", { encoding: "utf-8", stdio: "pipe" }).trim();
31
+ }
32
+ catch (_a) {
33
+ const binDir = node_path_1.default.dirname(process.argv[1]);
34
+ const candidate = node_path_1.default.join(binDir, "gmax");
35
+ if (node_fs_1.default.existsSync(candidate))
36
+ return candidate;
37
+ return "gmax";
38
+ }
39
+ }
40
+ function getPackageRoot() {
41
+ return node_path_1.default.resolve(__dirname, "../..");
42
+ }
43
+ function loadSkill() {
44
+ const skillPath = node_path_1.default.join(getPackageRoot(), "plugins", "grepmax", "skills", "grepmax", "SKILL.md");
45
+ try {
46
+ return node_fs_1.default.readFileSync(skillPath, "utf-8");
47
+ }
48
+ catch (_a) {
49
+ return [
50
+ "---",
51
+ "name: gmax",
52
+ "description: Semantic code search.",
53
+ "---",
54
+ "",
55
+ 'Use `gmax "query" --agent` for semantic search.',
56
+ ].join("\n");
57
+ }
58
+ }
74
59
  function writeFileIfChanged(filePath, content) {
75
60
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
76
61
  const already = node_fs_1.default.existsSync(filePath)
@@ -105,7 +90,6 @@ function mergeHooks(existing, incoming) {
105
90
  for (const [event, entries] of Object.entries(incoming)) {
106
91
  const current = merged[event] || [];
107
92
  for (const entry of entries) {
108
- // Simple duplicate check based on command string
109
93
  const cmd = entry.hooks[0].command;
110
94
  if (!current.some((c) => c.hooks[0].command === cmd)) {
111
95
  current.push(entry);
@@ -115,37 +99,56 @@ function mergeHooks(existing, incoming) {
115
99
  }
116
100
  return merged;
117
101
  }
118
- // --- MAIN INSTALLER ---
102
+ // --- Installer ---
119
103
  function installPlugin() {
120
104
  return __awaiter(this, void 0, void 0, function* () {
121
105
  const root = resolveDroidRoot();
106
+ const gmaxBin = resolveGmaxBin();
122
107
  const hooksDir = node_path_1.default.join(root, "hooks", "gmax");
123
108
  const skillsDir = node_path_1.default.join(root, "skills", "gmax");
124
109
  const settingsPath = node_path_1.default.join(root, "settings.json");
125
- // 1. Install Hook Scripts (Start/Stop Daemon)
126
- // We expect these files to exist in your dist/hooks folder
127
- const startJsPath = node_path_1.default.join(hooksDir, "gmax_start.js");
128
- const stopJsPath = node_path_1.default.join(hooksDir, "gmax_stop.js");
129
- // Create these scripts dynamically if we don't want to read from dist
110
+ // 1. Install hook scripts (start/stop daemon)
130
111
  const startScript = `
131
- const { spawn } = require("child_process");
112
+ const { execFileSync } = require("child_process");
132
113
  const fs = require("fs");
133
114
  const path = require("path");
134
- const logDir = path.join(require("os").homedir(), ".gmax", "logs");
135
- fs.mkdirSync(logDir, { recursive: true });
136
- const out = fs.openSync(path.join(logDir, "gmax.log"), "a");
137
- const child = spawn("gmax", ["serve"], { detached: true, stdio: ["ignore", out, out] });
138
- child.unref();
139
- `;
115
+ const os = require("os");
116
+
117
+ const BIN = "${gmaxBin}";
118
+
119
+ function resolveGmax() {
120
+ if (fs.existsSync(BIN)) return BIN;
121
+ try {
122
+ return require("child_process").execSync("which gmax", { encoding: "utf-8" }).trim();
123
+ } catch { return null; }
124
+ }
125
+
126
+ function isProjectRegistered() {
127
+ try {
128
+ const projectsPath = path.join(os.homedir(), ".gmax", "projects.json");
129
+ const projects = JSON.parse(fs.readFileSync(projectsPath, "utf-8"));
130
+ const cwd = process.cwd();
131
+ return projects.some((p) => cwd.startsWith(p.root));
132
+ } catch { return false; }
133
+ }
134
+
135
+ const bin = resolveGmax();
136
+ if (bin && isProjectRegistered()) {
137
+ try { execFileSync(bin, ["watch", "--daemon", "-b"], { timeout: 5000, stdio: "ignore" }); } catch {}
138
+ }
139
+ `.trim();
140
140
  const stopScript = `
141
- const { spawnSync, execSync } = require("child_process");
142
- try { execSync("gmax serve stop", { stdio: "ignore" }); } catch {}
143
- `;
144
- writeFileIfChanged(startJsPath, startScript.trim());
145
- writeFileIfChanged(stopJsPath, stopScript.trim());
146
- // 2. Install Skill (with Indexing Warning)
147
- writeFileIfChanged(node_path_1.default.join(skillsDir, "SKILL.md"), SKILL.trimStart());
148
- // 3. Configure Settings
141
+ const { execFileSync } = require("child_process");
142
+ try { execFileSync("gmax", ["watch", "stop", "--all"], { stdio: "ignore", timeout: 5000 }); } catch {}
143
+ `.trim();
144
+ const startJsPath = node_path_1.default.join(hooksDir, "gmax_start.js");
145
+ const stopJsPath = node_path_1.default.join(hooksDir, "gmax_stop.js");
146
+ writeFileIfChanged(startJsPath, startScript);
147
+ writeFileIfChanged(stopJsPath, stopScript);
148
+ // 2. Install SKILL (read from package root)
149
+ const skill = loadSkill();
150
+ writeFileIfChanged(node_path_1.default.join(skillsDir, "SKILL.md"), skill.trim());
151
+ // 3. Configure settings
149
152
  const hookConfig = {
150
153
  SessionStart: [
151
154
  {
@@ -168,7 +171,7 @@ try { execSync("gmax serve stop", { stdio: "ignore" }); } catch {}
168
171
  settings.allowBackgroundProcesses = true;
169
172
  settings.hooks = mergeHooks(settings.hooks, hookConfig);
170
173
  saveSettings(settingsPath, settings);
171
- console.log(`✅ gmax installed for Factory Droid (Hooks + Skill)`);
174
+ console.log("✅ gmax installed for Factory Droid (Hooks + Skill)");
172
175
  });
173
176
  }
174
177
  function uninstallPlugin() {
@@ -107,7 +107,7 @@ function getClients() {
107
107
  isInstalled: () => {
108
108
  const p = path.join(os.homedir(), ".codex", "AGENTS.md");
109
109
  try {
110
- return fs.existsSync(p) && fs.readFileSync(p, "utf-8").includes("name: gmax");
110
+ return fs.existsSync(p) && fs.readFileSync(p, "utf-8").includes("gmax");
111
111
  }
112
112
  catch (_a) {
113
113
  return false;
@@ -394,7 +394,18 @@ class Searcher {
394
394
  ftsSearchFailed = true;
395
395
  this.ftsAvailable = false;
396
396
  const msg = e instanceof Error ? e.message : String(e);
397
- console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
397
+ if (msg.includes("position")) {
398
+ // FTS index lacks positional data — rebuild it
399
+ try {
400
+ yield this.db.createFTSIndex(true);
401
+ this.ftsAvailable = true;
402
+ console.warn("[Searcher] Rebuilt FTS index with position support — retry search");
403
+ }
404
+ catch (_k) { }
405
+ }
406
+ else {
407
+ console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
408
+ }
398
409
  }
399
410
  }
400
411
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -260,18 +260,37 @@ class VectorDB {
260
260
  });
261
261
  }
262
262
  createFTSIndex() {
263
- return __awaiter(this, void 0, void 0, function* () {
263
+ return __awaiter(this, arguments, void 0, function* (rebuild = false) {
264
264
  const table = yield this.ensureTable();
265
+ if (rebuild) {
266
+ try {
267
+ yield table.dropIndex("content_idx");
268
+ }
269
+ catch (_a) { }
270
+ }
265
271
  try {
266
272
  yield table.createIndex("content", {
267
- config: lancedb.Index.fts(),
273
+ config: lancedb.Index.fts({ withPosition: true }),
268
274
  });
269
275
  }
270
276
  catch (e) {
271
277
  const msg = e instanceof Error ? e.message : String(e);
272
- if (!msg.includes("already exists")) {
273
- console.warn("Failed to create FTS index:", e);
278
+ if (msg.includes("already exists")) {
279
+ return;
280
+ }
281
+ // If position error, try dropping and recreating
282
+ if (msg.includes("position")) {
283
+ try {
284
+ yield table.dropIndex("content_idx");
285
+ yield table.createIndex("content", {
286
+ config: lancedb.Index.fts({ withPosition: true }),
287
+ });
288
+ (0, logger_1.log)("vectordb", "Rebuilt FTS index with position support");
289
+ return;
290
+ }
291
+ catch (_b) { }
274
292
  }
293
+ console.warn("Failed to create FTS index:", e);
275
294
  }
276
295
  });
277
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -1,78 +1,121 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Postinstall: sync plugin files (SKILL.md, hooks, plugin.json) to
4
- * the Claude Code plugin cache if it exists. This ensures `npm update -g grepmax`
5
- * automatically updates the skill instructions without needing `gmax install-claude-code`.
3
+ * Postinstall: sync plugin files to all installed integrations.
4
+ * Runs after `npm install -g grepmax@latest` to automatically update
5
+ * skills, hooks, and configs without manual re-installation.
6
+ *
7
+ * Supported integrations:
8
+ * - Claude Code: sync skills/hooks to plugin cache
9
+ * - OpenCode: re-run installer (regenerates tool shim + plugin)
10
+ * - Codex: re-run installer (updates AGENTS.md + MCP registration)
11
+ * - Factory Droid: re-run installer (updates skills + hooks)
6
12
  */
7
13
  const fs = require("node:fs");
8
14
  const path = require("node:path");
9
15
  const os = require("node:os");
16
+ const { execSync } = require("node:child_process");
10
17
 
11
- const pluginCacheBase = path.join(os.homedir(), ".claude", "plugins", "cache", "grepmax", "grepmax");
12
18
  const sourcePlugin = path.join(__dirname, "..", "plugins", "grepmax");
13
19
 
14
- if (!fs.existsSync(pluginCacheBase) || !fs.existsSync(sourcePlugin)) {
15
- // Plugin not installed via Claude Code — skip silently
16
- process.exit(0);
17
- }
20
+ // --- Claude Code: sync files to plugin cache ---
21
+ const pluginCacheBase = path.join(
22
+ os.homedir(),
23
+ ".claude",
24
+ "plugins",
25
+ "cache",
26
+ "grepmax",
27
+ "grepmax",
28
+ );
18
29
 
19
- // Find installed version directories
20
- let entries;
21
- try {
22
- entries = fs.readdirSync(pluginCacheBase, { withFileTypes: true });
23
- } catch {
24
- process.exit(0);
25
- }
30
+ if (fs.existsSync(pluginCacheBase) && fs.existsSync(sourcePlugin)) {
31
+ let entries;
32
+ try {
33
+ entries = fs.readdirSync(pluginCacheBase, { withFileTypes: true });
34
+ } catch {
35
+ entries = [];
36
+ }
26
37
 
27
- const versionDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
28
- if (versionDirs.length === 0) process.exit(0);
38
+ const versionDirs = entries
39
+ .filter((e) => e.isDirectory())
40
+ .map((e) => e.name);
29
41
 
30
- // Sync files to each installed version
31
- function copyRecursive(src, dest) {
32
- if (!fs.existsSync(src)) return;
33
- const stat = fs.statSync(src);
34
- if (stat.isDirectory()) {
35
- fs.mkdirSync(dest, { recursive: true });
36
- for (const entry of fs.readdirSync(src)) {
37
- copyRecursive(path.join(src, entry), path.join(dest, entry));
42
+ function copyRecursive(src, dest) {
43
+ if (!fs.existsSync(src)) return;
44
+ const stat = fs.statSync(src);
45
+ if (stat.isDirectory()) {
46
+ fs.mkdirSync(dest, { recursive: true });
47
+ for (const entry of fs.readdirSync(src)) {
48
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
49
+ }
50
+ } else {
51
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
52
+ fs.copyFileSync(src, dest);
38
53
  }
39
- } else {
40
- fs.mkdirSync(path.dirname(dest), { recursive: true });
41
- fs.copyFileSync(src, dest);
42
54
  }
43
- }
44
55
 
45
- for (const ver of versionDirs) {
46
- const destDir = path.join(pluginCacheBase, ver);
47
- try {
48
- // Sync skills
49
- copyRecursive(
50
- path.join(sourcePlugin, "skills"),
51
- path.join(destDir, "skills"),
52
- );
53
- // Sync hooks
54
- copyRecursive(
55
- path.join(sourcePlugin, "hooks"),
56
- path.join(destDir, "hooks"),
57
- );
58
- // Sync hooks.json
59
- const hooksJson = path.join(sourcePlugin, "hooks.json");
60
- if (fs.existsSync(hooksJson)) {
61
- fs.copyFileSync(hooksJson, path.join(destDir, "hooks.json"));
56
+ for (const ver of versionDirs) {
57
+ const destDir = path.join(pluginCacheBase, ver);
58
+ try {
59
+ copyRecursive(
60
+ path.join(sourcePlugin, "skills"),
61
+ path.join(destDir, "skills"),
62
+ );
63
+ copyRecursive(
64
+ path.join(sourcePlugin, "hooks"),
65
+ path.join(destDir, "hooks"),
66
+ );
67
+ const hooksJson = path.join(sourcePlugin, "hooks.json");
68
+ if (fs.existsSync(hooksJson)) {
69
+ fs.copyFileSync(hooksJson, path.join(destDir, "hooks.json"));
70
+ }
71
+ } catch {
72
+ // Best-effort
62
73
  }
63
- } catch {
64
- // Best-effort — don't fail the install
65
74
  }
66
75
  }
67
76
 
68
- // Sync OpenCode: re-run installer if tool shim or plugin exists
69
- const ocToolPath = path.join(os.homedir(), ".config", "opencode", "tool", "gmax.ts");
70
- const ocPluginPath = path.join(os.homedir(), ".config", "opencode", "plugins", "gmax.ts");
77
+ // --- OpenCode: re-run installer if tool shim or plugin exists ---
78
+ const ocToolPath = path.join(
79
+ os.homedir(),
80
+ ".config",
81
+ "opencode",
82
+ "tool",
83
+ "gmax.ts",
84
+ );
85
+ const ocPluginPath = path.join(
86
+ os.homedir(),
87
+ ".config",
88
+ "opencode",
89
+ "plugins",
90
+ "gmax.ts",
91
+ );
71
92
  if (fs.existsSync(ocToolPath) || fs.existsSync(ocPluginPath)) {
72
93
  try {
73
- const { execSync: exec } = require("node:child_process");
74
- exec("gmax install-opencode", { stdio: "ignore", timeout: 10000 });
75
- } catch {
76
- // Best-effort — don't fail the install
77
- }
94
+ execSync("gmax install-opencode", { stdio: "ignore", timeout: 10000 });
95
+ } catch {}
96
+ }
97
+
98
+ // --- Codex: re-run installer if AGENTS.md has gmax skill ---
99
+ const codexAgentsPath = path.join(os.homedir(), ".codex", "AGENTS.md");
100
+ if (fs.existsSync(codexAgentsPath)) {
101
+ try {
102
+ const content = fs.readFileSync(codexAgentsPath, "utf-8");
103
+ if (content.includes("gmax")) {
104
+ execSync("gmax install-codex", { stdio: "ignore", timeout: 10000 });
105
+ }
106
+ } catch {}
107
+ }
108
+
109
+ // --- Factory Droid: re-run installer if skill exists ---
110
+ const droidSkillPath = path.join(
111
+ os.homedir(),
112
+ ".factory",
113
+ "skills",
114
+ "gmax",
115
+ "SKILL.md",
116
+ );
117
+ if (fs.existsSync(droidSkillPath)) {
118
+ try {
119
+ execSync("gmax install-droid", { stdio: "ignore", timeout: 10000 });
120
+ } catch {}
78
121
  }