macroclaw 0.38.0 → 0.39.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macroclaw",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -167,19 +167,11 @@ describe("install", () => {
167
167
  expect(() => mgr.install()).toThrow("Settings not found. Run `macroclaw setup` first.");
168
168
  });
169
169
 
170
- it("runs global install and resolves bun, claude and macroclaw paths", () => {
170
+ it("runs global install without resolving binary paths on systemd", () => {
171
171
  const tmpHome = `/tmp/macroclaw-test-install-${Date.now()}`;
172
172
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
173
173
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
174
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
175
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
176
174
 
177
- mockExecSync.mockImplementation((cmd: string) => {
178
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
179
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
180
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
181
- return "";
182
- });
183
175
  mockUserInfo.mockImplementation(() => ({ username: "testuser", homedir: tmpHome, uid: 1000, gid: 1000, shell: "/bin/bash" }));
184
176
  // Mock existsSync to handle linger check
185
177
  mockExistsSync.mockImplementation((path: string) => {
@@ -191,41 +183,42 @@ describe("install", () => {
191
183
  rmSync(tmpHome, { recursive: true });
192
184
 
193
185
  expect(mockExecSync).toHaveBeenCalledWith("bun install -g macroclaw", expect.anything());
194
- expect(mockExecSync).toHaveBeenCalledWith("which bun", expect.anything());
195
- expect(mockExecSync).toHaveBeenCalledWith("which claude", expect.anything());
196
- expect(mockExecSync).toHaveBeenCalledWith("bun pm bin -g", expect.anything());
186
+ // systemd no longer resolves paths — bash -lc handles PATH at runtime
187
+ expect(mockExecSync).not.toHaveBeenCalledWith("which bun", expect.anything());
188
+ expect(mockExecSync).not.toHaveBeenCalledWith("which claude", expect.anything());
189
+ expect(mockExecSync).not.toHaveBeenCalledWith("bun pm bin -g", expect.anything());
197
190
  });
198
191
 
199
- it("installs launchd service with PATH and OAuth token", () => {
192
+ it("installs launchd service with bash -lc and OAuth token", () => {
200
193
  const tmpHome = `/tmp/macroclaw-test-launchd-${Date.now()}`;
201
194
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
202
195
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
203
196
  const plistDir = join(tmpHome, "Library/LaunchAgents");
204
197
  mkdirSync(plistDir, { recursive: true });
205
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
206
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
207
198
 
208
- mockExecSync.mockImplementation((cmd: string) => {
209
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
210
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
211
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
212
- return "";
213
- });
214
199
  const mgr = createManager({ platform: "darwin", home: tmpHome });
215
200
  mgr.install("sk-test-token");
216
201
 
217
202
  const plistPath = join(plistDir, "com.macroclaw.plist");
218
203
  expect(existsSync(plistPath)).toBe(true);
219
204
  const writtenContent = readFileSync(plistPath, "utf-8");
220
- expect(writtenContent).toContain(`<string>${tmpHome}/.bun/bin/bun</string>`);
221
- expect(writtenContent).toContain(`<string>${tmpHome}/.bun/bin/macroclaw</string>`);
222
- expect(writtenContent).toContain("<string>start</string>");
205
+ // bash -lc pattern — no hardcoded binary paths
206
+ expect(writtenContent).toContain("<string>/bin/bash</string>");
207
+ expect(writtenContent).toContain("<string>-lc</string>");
208
+ expect(writtenContent).toContain("<string>exec bun macroclaw start</string>");
223
209
  expect(writtenContent).toContain("<key>KeepAlive</key>");
224
210
  expect(writtenContent).toContain(".macroclaw/logs/stdout.log");
225
- expect(writtenContent).toContain("<key>PATH</key>");
211
+ // No PATH/HOME env vars — login shell provides them
212
+ expect(writtenContent).not.toContain("<key>PATH</key>");
213
+ expect(writtenContent).not.toContain("<key>HOME</key>");
214
+ // OAuth token is preserved
226
215
  expect(writtenContent).toContain("<key>CLAUDE_CODE_OAUTH_TOKEN</key>");
227
216
  expect(writtenContent).toContain("<string>sk-test-token</string>");
228
217
  expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl load"), expect.anything());
218
+ // No path resolution calls
219
+ expect(mockExecSync).not.toHaveBeenCalledWith("which bun", expect.anything());
220
+ expect(mockExecSync).not.toHaveBeenCalledWith("which claude", expect.anything());
221
+ expect(mockExecSync).not.toHaveBeenCalledWith("bun pm bin -g", expect.anything());
229
222
  rmSync(tmpHome, { recursive: true });
230
223
  });
231
224
 
@@ -234,19 +227,12 @@ describe("install", () => {
234
227
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
235
228
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
236
229
  mkdirSync(join(tmpHome, "Library/LaunchAgents"), { recursive: true });
237
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
238
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
239
230
 
240
- mockExecSync.mockImplementation((cmd: string) => {
241
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
242
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
243
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
244
- return "";
245
- });
246
231
  const mgr = createManager({ platform: "darwin", home: tmpHome });
247
232
  mgr.install();
248
233
  const writtenContent = readFileSync(join(tmpHome, "Library/LaunchAgents/com.macroclaw.plist"), "utf-8");
249
234
  expect(writtenContent).not.toContain("CLAUDE_CODE_OAUTH_TOKEN");
235
+ expect(writtenContent).not.toContain("<key>EnvironmentVariables</key>");
250
236
  rmSync(tmpHome, { recursive: true });
251
237
  });
252
238
 
@@ -255,15 +241,10 @@ describe("install", () => {
255
241
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
256
242
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
257
243
  mkdirSync(join(tmpHome, "Library/LaunchAgents"), { recursive: true });
258
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
259
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
260
244
 
261
245
  const calls: string[] = [];
262
246
  mockExecSync.mockImplementation((cmd: string) => {
263
247
  calls.push(cmd);
264
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
265
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
266
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
267
248
  if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
268
249
  return "";
269
250
  });
@@ -281,13 +262,8 @@ describe("install", () => {
281
262
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
282
263
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
283
264
  mkdirSync(join(tmpHome, "Library/LaunchAgents"), { recursive: true });
284
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
285
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
286
265
 
287
266
  mockExecSync.mockImplementation((cmd: string) => {
288
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
289
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
290
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
291
267
  if (cmd.startsWith("launchctl list ")) return LAUNCHD_STOPPED;
292
268
  return "";
293
269
  });
@@ -297,19 +273,11 @@ describe("install", () => {
297
273
  rmSync(tmpHome, { recursive: true });
298
274
  });
299
275
 
300
- it("installs systemd user service and writes unit file directly", () => {
276
+ it("installs systemd user service with bash -lc and no hardcoded paths", () => {
301
277
  const tmpHome = `/tmp/macroclaw-test-systemd-${Date.now()}`;
302
278
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
303
279
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
304
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
305
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
306
280
 
307
- mockExecSync.mockImplementation((cmd: string) => {
308
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
309
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
310
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
311
- return "";
312
- });
313
281
  mockUserInfo.mockImplementation(() => ({ username: "testuser", homedir: tmpHome, uid: 1000, gid: 1000, shell: "/bin/bash" }));
314
282
  // Mock existsSync: linger file does not exist (triggers sudo loginctl)
315
283
  mockExistsSync.mockImplementation((path: string) => {
@@ -326,8 +294,11 @@ describe("install", () => {
326
294
  expect(unitContent).toContain("WantedBy=default.target");
327
295
  expect(unitContent).not.toContain("User=");
328
296
  expect(unitContent).not.toContain("Group=");
329
- expect(unitContent).toContain(`Environment=HOME=${tmpHome}`);
330
- expect(unitContent).toContain(`ExecStart=${tmpHome}/.bun/bin/bun ${tmpHome}/.bun/bin/macroclaw start`);
297
+ // bash -lc sources login profile for PATH — no hardcoded Environment lines
298
+ expect(unitContent).not.toContain("Environment=HOME=");
299
+ expect(unitContent).not.toContain("Environment=PATH=");
300
+ expect(unitContent).toContain("WorkingDirectory=%h");
301
+ expect(unitContent).toContain("ExecStart=/bin/bash -lc 'exec bun macroclaw start'");
331
302
 
332
303
  // Lingering enabled via sudo
333
304
  expect(mockExecSync).toHaveBeenCalledWith("sudo loginctl enable-linger testuser", expect.anything());
@@ -346,15 +317,7 @@ describe("install", () => {
346
317
  const tmpHome = `/tmp/macroclaw-test-linger-${Date.now()}`;
347
318
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
348
319
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
349
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
350
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
351
320
 
352
- mockExecSync.mockImplementation((cmd: string) => {
353
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
354
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
355
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
356
- return "";
357
- });
358
321
  mockUserInfo.mockImplementation(() => ({ username: "testuser", homedir: tmpHome, uid: 1000, gid: 1000, shell: "/bin/bash" }));
359
322
  // Linger already enabled
360
323
  mockExistsSync.mockImplementation((path: string) => {
@@ -372,15 +335,7 @@ describe("install", () => {
372
335
  const tmpHome = `/tmp/macroclaw-test-nosudo-${Date.now()}`;
373
336
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
374
337
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
375
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
376
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
377
338
 
378
- mockExecSync.mockImplementation((cmd: string) => {
379
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
380
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
381
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
382
- return "";
383
- });
384
339
  mockUserInfo.mockImplementation(() => ({ username: "testuser", homedir: tmpHome, uid: 1000, gid: 1000, shell: "/bin/bash" }));
385
340
  mockExistsSync.mockImplementation((path: string) => {
386
341
  if (path === "/var/lib/systemd/linger/testuser") return true;
@@ -393,52 +348,12 @@ describe("install", () => {
393
348
  rmSync(tmpHome, { recursive: true });
394
349
  });
395
350
 
396
- it("throws when bun path cannot be resolved", () => {
397
- const tmpHome = `/tmp/macroclaw-test-nobun-${Date.now()}`;
398
- mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
399
- writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
400
-
401
- mockExecSync.mockImplementation((cmd: string) => {
402
- if (cmd === "which bun") throw new Error("not found");
403
- return "";
404
- });
405
- const mgr = createManager({ home: tmpHome });
406
- expect(() => mgr.install()).toThrow("Could not resolve bun path. Is it installed?");
407
- rmSync(tmpHome, { recursive: true });
408
- });
409
-
410
- it("throws when macroclaw not found in global bin", () => {
411
- const tmpHome = `/tmp/macroclaw-test-nomc-${Date.now()}`;
412
- mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
413
- writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
414
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
415
- // Note: NOT creating macroclaw binary
416
-
417
- mockExecSync.mockImplementation((cmd: string) => {
418
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
419
- if (cmd === "which claude") return `${tmpHome}/.local/bin/claude\n`;
420
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
421
- return "";
422
- });
423
- const mgr = createManager({ home: tmpHome });
424
- expect(() => mgr.install()).toThrow(`Could not find macroclaw in ${tmpHome}/.bun/bin`);
425
- rmSync(tmpHome, { recursive: true });
426
- });
427
-
428
351
  it("macOS install does not use sudo", () => {
429
352
  const tmpHome = `/tmp/macroclaw-test-macos-${Date.now()}`;
430
353
  mkdirSync(join(tmpHome, ".macroclaw"), { recursive: true });
431
354
  writeFileSync(join(tmpHome, ".macroclaw/settings.json"), "{}");
432
355
  mkdirSync(join(tmpHome, "Library/LaunchAgents"), { recursive: true });
433
- mkdirSync(join(tmpHome, ".bun/bin"), { recursive: true });
434
- writeFileSync(join(tmpHome, ".bun/bin/macroclaw"), "");
435
356
 
436
- mockExecSync.mockImplementation((cmd: string) => {
437
- if (cmd === "which bun") return `${tmpHome}/.bun/bin/bun\n`;
438
- if (cmd === "which claude") return `${tmpHome}/.bun/bin/claude\n`;
439
- if (cmd === "bun pm bin -g") return `${tmpHome}/.bun/bin\n`;
440
- return "";
441
- });
442
357
  const mgr = createManager({ platform: "darwin", home: tmpHome });
443
358
  mgr.install();
444
359
  for (const call of mockExecSync.mock.calls) {
@@ -1,7 +1,7 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { userInfo as osUserInfo } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { dirname, resolve } from "node:path";
5
5
  import { createLogger } from "./logger";
6
6
 
7
7
  const log = createLogger("service");
@@ -85,11 +85,6 @@ export class SystemServiceManager {
85
85
  }
86
86
 
87
87
  this.#exec("bun install -g macroclaw");
88
- const bunPath = this.#resolvePath("bun");
89
- const claudePath = this.#resolvePath("claude");
90
- const macroclawPath = this.#resolveGlobalBinPath("macroclaw");
91
-
92
- const pathDirs = [...new Set([dirname(bunPath), dirname(claudePath), dirname(macroclawPath)])];
93
88
 
94
89
  const logDir = resolve(this.#home, ".macroclaw/logs");
95
90
  mkdirSync(logDir, { recursive: true });
@@ -97,7 +92,7 @@ export class SystemServiceManager {
97
92
  this.#exec(`launchctl unload ${this.serviceFilePath}`);
98
93
  }
99
94
 
100
- writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(bunPath, macroclawPath, pathDirs, oauthToken));
95
+ writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(oauthToken));
101
96
  log.debug({ filePath: this.serviceFilePath }, "Wrote launchd plist");
102
97
  this.#exec(`launchctl load ${this.serviceFilePath}`);
103
98
  }
@@ -113,11 +108,6 @@ export class SystemServiceManager {
113
108
  }
114
109
 
115
110
  this.#exec("bun install -g macroclaw");
116
- const bunPath = this.#resolvePath("bun");
117
- const claudePath = this.#resolvePath("claude");
118
- const macroclawPath = this.#resolveGlobalBinPath("macroclaw");
119
-
120
- const pathDirs = [...new Set([dirname(bunPath), dirname(claudePath), dirname(macroclawPath)])];
121
111
 
122
112
  // Enable lingering so user services run without an active login session
123
113
  const username = osUserInfo().username;
@@ -125,7 +115,7 @@ export class SystemServiceManager {
125
115
  this.#sudo(`loginctl enable-linger ${username}`);
126
116
  }
127
117
 
128
- const unitContent = this.#generateSystemdUnit(bunPath, macroclawPath, pathDirs);
118
+ const unitContent = this.#generateSystemdUnit();
129
119
  mkdirSync(dirname(this.serviceFilePath), { recursive: true });
130
120
  writeFileSync(this.serviceFilePath, unitContent);
131
121
  log.debug({ filePath: this.serviceFilePath }, "Wrote systemd unit");
@@ -252,23 +242,6 @@ export class SystemServiceManager {
252
242
  return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).toString();
253
243
  }
254
244
 
255
- #resolvePath(binary: string): string {
256
- try {
257
- return this.#exec(`which ${binary}`).trim();
258
- } catch {
259
- throw new Error(`Could not resolve ${binary} path. Is it installed?`);
260
- }
261
- }
262
-
263
- #resolveGlobalBinPath(binary: string): string {
264
- const binDir = this.#exec("bun pm bin -g").trim();
265
- const binPath = join(binDir, binary);
266
- if (!existsSync(binPath)) {
267
- throw new Error(`Could not find ${binary} in ${binDir}. Is it installed?`);
268
- }
269
- return binPath;
270
- }
271
-
272
245
 
273
246
  #getInstalledVersion(): string {
274
247
  try {
@@ -298,9 +271,11 @@ export class SystemServiceManager {
298
271
  this.#exec(`sudo ${cmd}`);
299
272
  }
300
273
 
301
- #generateLaunchdPlist(bunPath: string, macroclawPath: string, pathDirs: string[], oauthToken?: string): string {
274
+ #generateLaunchdPlist(oauthToken?: string): string {
302
275
  const logDir = resolve(this.#home, ".macroclaw/logs");
303
- const tokenEnv = oauthToken ? `\n\t\t<key>CLAUDE_CODE_OAUTH_TOKEN</key>\n\t\t<string>${oauthToken}</string>` : "";
276
+ const tokenEnvBlock = oauthToken
277
+ ? `\n\t<key>EnvironmentVariables</key>\n\t<dict>\n\t\t<key>CLAUDE_CODE_OAUTH_TOKEN</key>\n\t\t<string>${oauthToken}</string>\n\t</dict>`
278
+ : "";
304
279
  return `<?xml version="1.0" encoding="UTF-8"?>
305
280
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
306
281
  <plist version="1.0">
@@ -309,39 +284,30 @@ export class SystemServiceManager {
309
284
  <string>com.macroclaw</string>
310
285
  <key>ProgramArguments</key>
311
286
  <array>
312
- <string>${bunPath}</string>
313
- <string>${macroclawPath}</string>
314
- <string>start</string>
287
+ <string>/bin/bash</string>
288
+ <string>-lc</string>
289
+ <string>exec bun macroclaw start</string>
315
290
  </array>
316
291
  <key>KeepAlive</key>
317
292
  <true/>
318
293
  <key>StandardOutPath</key>
319
294
  <string>${logDir}/stdout.log</string>
320
295
  <key>StandardErrorPath</key>
321
- <string>${logDir}/stderr.log</string>
322
- <key>EnvironmentVariables</key>
323
- <dict>
324
- <key>HOME</key>
325
- <string>${this.#home}</string>
326
- <key>PATH</key>
327
- <string>${pathDirs.join(":")}</string>${tokenEnv}
328
- </dict>
296
+ <string>${logDir}/stderr.log</string>${tokenEnvBlock}
329
297
  </dict>
330
298
  </plist>
331
299
  `;
332
300
  }
333
301
 
334
- #generateSystemdUnit(bunPath: string, macroclawPath: string, pathDirs: string[]): string {
302
+ #generateSystemdUnit(): string {
335
303
  return `[Unit]
336
304
  Description=Macroclaw - Telegram-to-Claude-Code bridge
337
305
  After=network.target
338
306
 
339
307
  [Service]
340
308
  Type=simple
341
- Environment=HOME=${this.#home}
342
- Environment=PATH=${pathDirs.join(":")}
343
- WorkingDirectory=${this.#home}
344
- ExecStart=${bunPath} ${macroclawPath} start
309
+ WorkingDirectory=%h
310
+ ExecStart=/bin/bash -lc 'exec bun macroclaw start'
345
311
  Restart=on-failure
346
312
  RestartSec=5
347
313