mulmoclaude 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/bin/mulmoclaude.js +50 -5
  2. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { execSync, spawn } from "child_process";
9
9
  import { existsSync } from "fs";
10
10
  import { get as httpGet } from "http";
11
11
  import { createRequire } from "module";
12
+ import net from "net";
12
13
  import { join, dirname } from "path";
13
14
  import { fileURLToPath } from "url";
14
15
 
@@ -44,6 +45,27 @@ function pickOpenCommand() {
44
45
  return "xdg-open";
45
46
  }
46
47
 
48
+ function isPortFree(portNum) {
49
+ return new Promise((resolve) => {
50
+ const server = net.createServer();
51
+ server.once("error", () => resolve(false));
52
+ server.once("listening", () => {
53
+ server.close(() => resolve(true));
54
+ });
55
+ server.listen(portNum, "127.0.0.1");
56
+ });
57
+ }
58
+
59
+ // Walk forward from `start` to find a free port. `MAX_PORT_PROBES` caps
60
+ // the scan so an accidentally-saturated system doesn't spin forever.
61
+ const MAX_PORT_PROBES = 20;
62
+ async function findFreePort(start) {
63
+ for (let candidate = start; candidate < start + MAX_PORT_PROBES; candidate++) {
64
+ if (await isPortFree(candidate)) return candidate;
65
+ }
66
+ return null;
67
+ }
68
+
47
69
  // Poll the server until it answers an HTTP request, then call `onReady`.
48
70
  // Gives up after ~15s so the launcher doesn't hang forever on a crash loop.
49
71
  function waitUntilReady(portNum, onReady) {
@@ -99,16 +121,16 @@ Options:
99
121
  }
100
122
 
101
123
  if (args.includes("--version")) {
102
- console.log("mulmoclaude 0.1.1");
124
+ console.log("mulmoclaude 0.1.2");
103
125
  process.exit(0);
104
126
  }
105
127
 
106
- const port = resolvePort();
128
+ const { requestedPort, portExplicit } = parsePortArg();
107
129
  const noOpen = args.includes("--no-open");
108
130
 
109
- function resolvePort() {
131
+ function parsePortArg() {
110
132
  const idx = args.indexOf("--port");
111
- if (idx === -1) return DEFAULT_PORT;
133
+ if (idx === -1) return { requestedPort: DEFAULT_PORT, portExplicit: false };
112
134
  const raw = args[idx + 1];
113
135
  if (raw === undefined) {
114
136
  error("--port requires a value (integer 1..65535)");
@@ -119,7 +141,7 @@ function resolvePort() {
119
141
  error(`Invalid --port value: "${raw}" (expected integer 1..65535)`);
120
142
  process.exit(1);
121
143
  }
122
- return parsed;
144
+ return { requestedPort: parsed, portExplicit: true };
123
145
  }
124
146
 
125
147
  // ── Pre-flight checks ───────────────────────────────────────
@@ -142,6 +164,29 @@ if (!existsSync(SERVER_ENTRY)) {
142
164
  process.exit(1);
143
165
  }
144
166
 
167
+ // ── Resolve a usable port ───────────────────────────────────
168
+
169
+ // Check the requested port before spawning the server — an explicit
170
+ // --port that's taken is a hard error (respect the user's choice),
171
+ // while the default can walk forward to the next free slot so casual
172
+ // double-launches don't crash.
173
+ const port = await chooseAvailablePort(requestedPort, portExplicit);
174
+
175
+ async function chooseAvailablePort(requested, explicit) {
176
+ if (await isPortFree(requested)) return requested;
177
+ if (explicit) {
178
+ error(`Port ${requested} is already in use. Stop the other process or pick a different --port.`);
179
+ process.exit(1);
180
+ }
181
+ const fallback = await findFreePort(requested + 1);
182
+ if (fallback === null) {
183
+ error(`Port ${requested} is in use and no free port found in ${requested}..${requested + MAX_PORT_PROBES - 1}.`);
184
+ process.exit(1);
185
+ }
186
+ log(`Port ${requested} busy → using ${fallback} instead. (Pass --port <N> to pin.)`);
187
+ return fallback;
188
+ }
189
+
145
190
  // ── Start server ────────────────────────────────────────────
146
191
 
147
192
  log(`Starting MulmoClaude on port ${port}...`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmoclaude",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "MulmoClaude — GUI-chat with Claude Code + long-term memory. One command to start.",
5
5
  "type": "module",
6
6
  "bin": {