symforge 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.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const childProcess = require("child_process");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+
9
+ function createLauncher(overrides = {}) {
10
+ const fsMod = overrides.fs || fs;
11
+ const pathMod = overrides.path || path;
12
+ const osMod = overrides.os || os;
13
+ const processMod = overrides.process || process;
14
+ const consoleMod = overrides.console || console;
15
+ const spawnSyncFn = overrides.spawnSync || childProcess.spawnSync;
16
+ const execFileSyncFn = overrides.execFileSync || childProcess.execFileSync;
17
+ const packageJson = overrides.packageJson || require("../package.json");
18
+ const installScriptPath = overrides.installScriptPath
19
+ || pathMod.join(__dirname, "..", "scripts", "install.js");
20
+
21
+ function resolveInstallDir() {
22
+ if (overrides.installDir) {
23
+ return overrides.installDir;
24
+ }
25
+ if (processMod.env.SYMFORGE_HOME) {
26
+ return pathMod.join(processMod.env.SYMFORGE_HOME, "bin");
27
+ }
28
+ return pathMod.join(osMod.homedir(), ".symforge", "bin");
29
+ }
30
+
31
+ const ext = processMod.platform === "win32" ? ".exe" : "";
32
+ const installDir = resolveInstallDir();
33
+ const binPath = pathMod.join(installDir, "symforge" + ext);
34
+ const pendingPath = pathMod.join(installDir, "symforge.pending" + ext);
35
+
36
+ function relayInstallerOutput(output) {
37
+ if (!output) {
38
+ return;
39
+ }
40
+ const text = typeof output === "string" ? output : String(output);
41
+ for (const line of text.split(/\r?\n/)) {
42
+ if (line) {
43
+ consoleMod.error(line);
44
+ }
45
+ }
46
+ }
47
+
48
+ function getInstalledVersion() {
49
+ try {
50
+ const output = execFileSyncFn(binPath, ["--version"], {
51
+ encoding: "utf8",
52
+ timeout: 5000,
53
+ env: processMod.env,
54
+ }).trim();
55
+ const match = output.match(/(\d+\.\d+\.\d+)/);
56
+ return match ? match[1] : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function applyPendingUpdate() {
63
+ if (!fsMod.existsSync(pendingPath)) {
64
+ return false;
65
+ }
66
+
67
+ try {
68
+ fsMod.renameSync(pendingPath, binPath);
69
+ consoleMod.error("symforge: applied pending update.");
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function runInstaller() {
77
+ try {
78
+ const stdout = execFileSyncFn(processMod.execPath, [installScriptPath], {
79
+ encoding: "utf8",
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ env: processMod.env,
82
+ });
83
+ relayInstallerOutput(stdout);
84
+ } catch (error) {
85
+ relayInstallerOutput(error.stdout);
86
+ relayInstallerOutput(error.stderr);
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ function detectClients() {
92
+ const clients = [];
93
+ const home = osMod.homedir();
94
+ if (fsMod.existsSync(pathMod.join(home, ".claude"))) clients.push("claude");
95
+ if (fsMod.existsSync(pathMod.join(home, ".codex"))) clients.push("codex");
96
+ if (clients.length === 0 || clients.length >= 2) return "all";
97
+ return clients[0];
98
+ }
99
+
100
+ function runAutoInit() {
101
+ const client = detectClients();
102
+ consoleMod.error(`symforge: auto-configuring for ${client}...`);
103
+ try {
104
+ const output = execFileSyncFn(binPath, ["init", "--client", client], {
105
+ encoding: "utf8",
106
+ timeout: 15000,
107
+ env: processMod.env,
108
+ });
109
+ relayInstallerOutput(output);
110
+ } catch (error) {
111
+ consoleMod.error(
112
+ `symforge: auto-init warning: ${error.message}`
113
+ );
114
+ }
115
+ }
116
+
117
+ function ensureInstalledBinary() {
118
+ const pendingApplied = applyPendingUpdate();
119
+
120
+ const expectedVersion = packageJson.version;
121
+ const hasBinary = fsMod.existsSync(binPath);
122
+ const installedVersion = hasBinary ? getInstalledVersion() : null;
123
+
124
+ if (installedVersion === expectedVersion) {
125
+ // If a pending update was just applied, run init to ensure config matches
126
+ if (pendingApplied) {
127
+ runAutoInit();
128
+ }
129
+ return;
130
+ }
131
+
132
+ if (!hasBinary) {
133
+ consoleMod.error("symforge binary not found. Running install...");
134
+ } else {
135
+ consoleMod.error(
136
+ `symforge binary version ${installedVersion || "unknown"} does not match wrapper version ${expectedVersion}. Running install...`
137
+ );
138
+ }
139
+
140
+ runInstaller();
141
+ applyPendingUpdate();
142
+
143
+ if (!fsMod.existsSync(binPath)) {
144
+ throw new Error("symforge binary is still missing after install.");
145
+ }
146
+ }
147
+
148
+ function main(args) {
149
+ ensureInstalledBinary();
150
+ const result = spawnSyncFn(binPath, args, {
151
+ stdio: "inherit",
152
+ env: processMod.env,
153
+ });
154
+ return result.status ?? 1;
155
+ }
156
+
157
+ return {
158
+ applyPendingUpdate,
159
+ detectClients,
160
+ ensureInstalledBinary,
161
+ getInstalledVersion,
162
+ getBinaryPath: () => binPath,
163
+ getPendingPath: () => pendingPath,
164
+ main,
165
+ runAutoInit,
166
+ };
167
+ }
168
+
169
+ module.exports = { createLauncher };
170
+
171
+ if (require.main === module) {
172
+ process.exit(createLauncher().main(process.argv.slice(2)));
173
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { createLauncher } = require("./launcher.js");
5
+
6
+ try {
7
+ process.exit(createLauncher().main(process.argv.slice(2)));
8
+ } catch (err) {
9
+ console.error(err.message);
10
+ process.exit(1);
11
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "symforge",
3
+ "version": "1.0.0",
4
+ "description": "SymForge — in-memory code intelligence for Claude Code, Codex, and Gemini CLI",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/special-place-administrator/symforge"
9
+ },
10
+ "bin": {
11
+ "symforge": "bin/symforge.js"
12
+ },
13
+ "scripts": {
14
+ "test": "node --test tests/*.test.js",
15
+ "postinstall": "node scripts/install.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "scripts/"
20
+ ],
21
+ "keywords": [
22
+ "mcp",
23
+ "code-indexing",
24
+ "code-retrieval",
25
+ "ai-coding",
26
+ "tree-sitter",
27
+ "claude-code",
28
+ "codex",
29
+ "gemini-cli",
30
+ "hooks"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ }
35
+ }
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const childProcess = require("child_process");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+ const https = require("https");
9
+ const http = require("http");
10
+
11
+ const REPO = "special-place-administrator/tokenizor_agentic_mcp";
12
+
13
+ function createInstaller(overrides = {}) {
14
+ const fsMod = overrides.fs || fs;
15
+ const pathMod = overrides.path || path;
16
+ const osMod = overrides.os || os;
17
+ const processMod = overrides.process || process;
18
+ const consoleMod = overrides.console || console;
19
+ const execSyncFn = overrides.execSync || childProcess.execSync;
20
+ const execFileSyncFn = overrides.execFileSync || childProcess.execFileSync;
21
+ const sleep = overrides.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
22
+ const packageJson = overrides.packageJson || require("../package.json");
23
+
24
+ function resolveInstallDir() {
25
+ if (overrides.installDir) {
26
+ return overrides.installDir;
27
+ }
28
+ if (processMod.env.SYMFORGE_HOME) {
29
+ return pathMod.join(processMod.env.SYMFORGE_HOME, "bin");
30
+ }
31
+ return pathMod.join(osMod.homedir(), ".symforge", "bin");
32
+ }
33
+
34
+ // Binary lives outside node_modules so npm can update the JS wrapper
35
+ // even while the MCP server holds a lock on the running .exe (Windows).
36
+ const installDir = resolveInstallDir();
37
+
38
+ function getPlatformArtifact() {
39
+ const platform = processMod.platform;
40
+ const arch = processMod.arch;
41
+
42
+ if (platform === "win32" && arch === "x64") return "symforge-windows-x64.exe";
43
+ if (platform === "darwin" && arch === "arm64") return "symforge-macos-arm64";
44
+ if (platform === "darwin" && arch === "x64") return "symforge-macos-x64";
45
+ if (platform === "linux" && arch === "x64") return "symforge-linux-x64";
46
+
47
+ consoleMod.error(`Unsupported platform: ${platform}-${arch}`);
48
+ consoleMod.error("Build from source: https://github.com/" + REPO);
49
+ processMod.exit(1);
50
+ }
51
+
52
+ function getVersion() {
53
+ return packageJson.version;
54
+ }
55
+
56
+ function getBinaryPath() {
57
+ const ext = processMod.platform === "win32" ? ".exe" : "";
58
+ return pathMod.join(installDir, "symforge" + ext);
59
+ }
60
+
61
+ function getPendingPath() {
62
+ const ext = processMod.platform === "win32" ? ".exe" : "";
63
+ return pathMod.join(installDir, "symforge.pending" + ext);
64
+ }
65
+
66
+ function download(url) {
67
+ if (overrides.download) {
68
+ return overrides.download(url);
69
+ }
70
+ return new Promise((resolve, reject) => {
71
+ const client = url.startsWith("https") ? https : http;
72
+ client.get(url, { headers: { "User-Agent": "symforge" } }, (res) => {
73
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
74
+ return download(res.headers.location).then(resolve).catch(reject);
75
+ }
76
+ if (res.statusCode !== 200) {
77
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
78
+ }
79
+ const chunks = [];
80
+ res.on("data", (chunk) => chunks.push(chunk));
81
+ res.on("end", () => resolve(Buffer.concat(chunks)));
82
+ res.on("error", reject);
83
+ }).on("error", reject);
84
+ });
85
+ }
86
+
87
+ function getInstalledVersion(binPath) {
88
+ try {
89
+ const output = execFileSyncFn(binPath, ["--version"], {
90
+ encoding: "utf8",
91
+ timeout: 5000,
92
+ }).trim();
93
+ const match = output.match(/(\d+\.\d+\.\d+)/);
94
+ return match ? match[1] : null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function isLockedError(error) {
101
+ return error && (error.code === "EPERM" || error.code === "EBUSY");
102
+ }
103
+
104
+ function removePendingIfPresent(pendingPath) {
105
+ try {
106
+ fsMod.unlinkSync(pendingPath);
107
+ } catch {}
108
+ }
109
+
110
+ function writeInstalledBinary(binPath, pendingPath, data) {
111
+ fsMod.writeFileSync(binPath, data);
112
+ fsMod.chmodSync(binPath, 0o755);
113
+ removePendingIfPresent(pendingPath);
114
+ consoleMod.log(`Installed: ${binPath}`);
115
+ }
116
+
117
+ function stopRunningWindowsProcesses(binPath) {
118
+ if (processMod.platform !== "win32") {
119
+ return [];
120
+ }
121
+
122
+ // NOTE: PowerShell -and operators must stay on the same line as their
123
+ // operands — semicolons are statement terminators, not line joiners.
124
+ const script = [
125
+ "$target = [System.IO.Path]::GetFullPath($env:SYMFORGE_TARGET_BIN)",
126
+ "$comparer = [System.StringComparer]::OrdinalIgnoreCase",
127
+ "$procs = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'symforge.exe' -and $_.ExecutablePath -and $comparer.Equals([System.IO.Path]::GetFullPath($_.ExecutablePath), $target) }",
128
+ "$ids = @($procs | ForEach-Object { [int]$_.ProcessId })",
129
+ "if ($ids.Count -gt 0) { Stop-Process -Id $ids -Force -ErrorAction SilentlyContinue; $ids | ConvertTo-Json -Compress }",
130
+ ].join("; ");
131
+
132
+ try {
133
+ const output = execFileSyncFn(
134
+ "powershell.exe",
135
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
136
+ {
137
+ encoding: "utf8",
138
+ env: { ...processMod.env, SYMFORGE_TARGET_BIN: binPath },
139
+ }
140
+ ).trim();
141
+
142
+ if (!output) {
143
+ return [];
144
+ }
145
+ const parsed = JSON.parse(output);
146
+ return Array.isArray(parsed) ? parsed : [parsed];
147
+ } catch (error) {
148
+ consoleMod.log(
149
+ `Failed to stop running SymForge processes automatically: ${error.message}`
150
+ );
151
+ return [];
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Stop symforge *daemon* processes (the background server), but leave
157
+ * the stdio MCP process alive — it may be actively serving Claude Code.
158
+ *
159
+ * Daemons are identifiable because they were launched with the `daemon` arg,
160
+ * visible in the process command line. On Windows we filter via WMI
161
+ * CommandLine; on Unix we use `pkill -f`.
162
+ *
163
+ * Returns an array of killed PIDs (Windows) or [] (Unix, best-effort).
164
+ */
165
+ function stopDaemonProcesses() {
166
+ if (processMod.platform === "win32") {
167
+ // Match symforge.exe processes whose CommandLine contains " daemon"
168
+ // This avoids killing the MCP stdio process that Claude Code is using.
169
+ // NOTE: PowerShell -and operators must stay on the same line as their
170
+ // operands — semicolons are statement terminators, not line joiners.
171
+ const script = [
172
+ "$procs = Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'symforge.exe' -and $_.CommandLine -and $_.CommandLine -match '\\bdaemon\\b' }",
173
+ "$ids = @($procs | ForEach-Object { [int]$_.ProcessId })",
174
+ "if ($ids.Count -gt 0) { Stop-Process -Id $ids -Force -ErrorAction SilentlyContinue; $ids | ConvertTo-Json -Compress }",
175
+ ].join("; ");
176
+
177
+ try {
178
+ const output = execFileSyncFn(
179
+ "powershell.exe",
180
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
181
+ { encoding: "utf8", env: processMod.env }
182
+ ).trim();
183
+
184
+ if (!output) return [];
185
+ const parsed = JSON.parse(output);
186
+ return Array.isArray(parsed) ? parsed : [parsed];
187
+ } catch (error) {
188
+ consoleMod.log(
189
+ `Note: could not stop daemon processes: ${error.message}`
190
+ );
191
+ return [];
192
+ }
193
+ }
194
+
195
+ // Unix: kill only daemon processes (best-effort)
196
+ try {
197
+ execSyncFn("pkill -f 'symforge daemon' 2>/dev/null || true", {
198
+ encoding: "utf8",
199
+ });
200
+ } catch {
201
+ // Ignore — process may not exist
202
+ }
203
+ return [];
204
+ }
205
+
206
+ async function retryInstallAfterStop(binPath, pendingPath, data) {
207
+ for (let attempt = 0; attempt < 8; attempt += 1) {
208
+ try {
209
+ writeInstalledBinary(binPath, pendingPath, data);
210
+ return true;
211
+ } catch (retryErr) {
212
+ if (!isLockedError(retryErr)) {
213
+ throw retryErr;
214
+ }
215
+ await sleep(250);
216
+ }
217
+ }
218
+ return false;
219
+ }
220
+
221
+ async function installDownloadedBinary(binPath, pendingPath, data) {
222
+ fsMod.mkdirSync(installDir, { recursive: true });
223
+
224
+ try {
225
+ writeInstalledBinary(binPath, pendingPath, data);
226
+ return { status: "installed", stoppedProcessIds: [] };
227
+ } catch (writeErr) {
228
+ if (!isLockedError(writeErr)) {
229
+ throw writeErr;
230
+ }
231
+
232
+ const stoppedProcessIds = stopRunningWindowsProcesses(binPath);
233
+ if (stoppedProcessIds.length > 0) {
234
+ consoleMod.log(
235
+ `Stopping ${stoppedProcessIds.length} running SymForge process(es) so the update can be applied...`
236
+ );
237
+ const installedAfterStop = await retryInstallAfterStop(binPath, pendingPath, data);
238
+ if (installedAfterStop) {
239
+ return { status: "installed", stoppedProcessIds };
240
+ }
241
+ }
242
+
243
+ fsMod.writeFileSync(pendingPath, data);
244
+ fsMod.chmodSync(pendingPath, 0o755);
245
+ consoleMod.log(`Binary is locked (MCP server running). Staged update at: ${pendingPath}`);
246
+ consoleMod.log(`Update will apply automatically on next launch.`);
247
+ return { status: "staged", stoppedProcessIds };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Detect which CLI agents are installed and return the appropriate
253
+ * `--client` flag value for `symforge init`.
254
+ */
255
+ function detectClients() {
256
+ const clients = [];
257
+
258
+ // Claude Code: check for ~/.claude directory
259
+ const claudeDir = pathMod.join(osMod.homedir(), ".claude");
260
+ if (fsMod.existsSync(claudeDir)) {
261
+ clients.push("claude");
262
+ }
263
+
264
+ // Codex: check for ~/.codex directory
265
+ const codexDir = pathMod.join(osMod.homedir(), ".codex");
266
+ if (fsMod.existsSync(codexDir)) {
267
+ clients.push("codex");
268
+ }
269
+
270
+ // Gemini: check for ~/.gemini directory
271
+ const geminiDir = pathMod.join(osMod.homedir(), ".gemini");
272
+ if (fsMod.existsSync(geminiDir)) {
273
+ clients.push("gemini");
274
+ }
275
+
276
+ // If all, none, or more than 1 detected, use "all"
277
+ if (clients.length === 0 || clients.length >= 2) {
278
+ return "all";
279
+ }
280
+ return clients[0];
281
+ }
282
+
283
+ /**
284
+ * Run `symforge init` after successful install to configure
285
+ * hooks and MCP server registration for detected CLI agents.
286
+ */
287
+ function runAutoInit(binPath) {
288
+ const client = detectClients();
289
+ consoleMod.log(`Auto-configuring for detected client(s): ${client}`);
290
+ try {
291
+ const output = execFileSyncFn(binPath, ["init", "--client", client], {
292
+ encoding: "utf8",
293
+ timeout: 15000,
294
+ env: processMod.env,
295
+ });
296
+ if (output) {
297
+ for (const line of output.trim().split(/\r?\n/)) {
298
+ consoleMod.log(line);
299
+ }
300
+ }
301
+ } catch (error) {
302
+ consoleMod.log(
303
+ `Auto-init warning: ${error.message}\nYou can run manually: symforge init --client all`
304
+ );
305
+ }
306
+ }
307
+
308
+ async function main() {
309
+ const binPath = getBinaryPath();
310
+ const pendingPath = getPendingPath();
311
+ const version = getVersion();
312
+
313
+ // Skip only if binary exists AND matches the expected version
314
+ if (fsMod.existsSync(binPath)) {
315
+ const installed = getInstalledVersion(binPath);
316
+ if (installed === version) {
317
+ removePendingIfPresent(pendingPath);
318
+ consoleMod.log(`symforge v${version} already installed at ${binPath}`);
319
+ // Still run init to ensure config is up to date
320
+ runAutoInit(binPath);
321
+ return;
322
+ }
323
+ consoleMod.log(
324
+ `symforge v${installed || "unknown"} found, updating to v${version}...`
325
+ );
326
+ }
327
+
328
+ // Stop the background daemon process before install. The daemon holds a
329
+ // file handle on the binary (Windows), but the MCP stdio process is left
330
+ // alive so Claude Code keeps working. If the binary is still locked by
331
+ // the stdio process, the installer will stage to .pending and the
332
+ // launcher will apply it on next start.
333
+ const stoppedPids = stopDaemonProcesses();
334
+ if (stoppedPids.length > 0) {
335
+ consoleMod.log(
336
+ `Stopped ${stoppedPids.length} symforge daemon process(es) for update`
337
+ );
338
+ // Brief pause to let OS release file handles
339
+ await sleep(500);
340
+ }
341
+
342
+ const artifact = getPlatformArtifact();
343
+ const url = `https://github.com/${REPO}/releases/download/v${version}/${artifact}`;
344
+
345
+ consoleMod.log(
346
+ `Downloading symforge v${version} for ${processMod.platform}-${processMod.arch}...`
347
+ );
348
+ consoleMod.log(` ${url}`);
349
+
350
+ try {
351
+ const data = await download(url);
352
+ const result = await installDownloadedBinary(binPath, pendingPath, data);
353
+
354
+ // Clean up npm cache to reclaim disk space after download.
355
+ // npm cache grows unbounded over time; verify removes stale entries.
356
+ try {
357
+ execSyncFn("npm cache verify --silent", {
358
+ encoding: "utf8",
359
+ timeout: 30000,
360
+ stdio: "ignore",
361
+ });
362
+ consoleMod.log("npm cache verified (stale entries cleaned)");
363
+ } catch {
364
+ // Non-fatal — skip if npm isn't available or verify fails
365
+ }
366
+
367
+ if (result.status === "installed") {
368
+ // Binary replaced in place — run init now.
369
+ runAutoInit(binPath);
370
+ } else if (result.status === "staged") {
371
+ // Binary is staged as .pending because the MCP server is still running.
372
+ // Init will run automatically on next launch via the launcher.
373
+ consoleMod.log(
374
+ "Auto-init deferred: will run on next launch when the pending update is applied."
375
+ );
376
+ }
377
+ } catch (err) {
378
+ consoleMod.error(`Failed to download binary: ${err.message}`);
379
+ consoleMod.error("");
380
+ consoleMod.error("You can build from source instead:");
381
+ consoleMod.error(" git clone https://github.com/" + REPO);
382
+ consoleMod.error(" cd tokenizor_agentic_mcp");
383
+ consoleMod.error(" cargo build --release");
384
+ processMod.exit(1);
385
+ }
386
+ }
387
+
388
+ return {
389
+ getBinaryPath,
390
+ getPendingPath,
391
+ getInstalledVersion,
392
+ installDownloadedBinary,
393
+ isLockedError,
394
+ main,
395
+ stopRunningWindowsProcesses,
396
+ stopDaemonProcesses,
397
+ detectClients,
398
+ runAutoInit,
399
+ };
400
+ }
401
+
402
+ module.exports = { createInstaller };
403
+
404
+ if (require.main === module) {
405
+ createInstaller().main();
406
+ }