macroclaw 0.42.0 → 0.43.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.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,6 +10,7 @@ const existsSync = realExistsSync;
10
10
  const DEFAULT_LOGIN_PATH = "/usr/local/bin:/usr/bin:/bin";
11
11
  const DEFAULT_BUN_GLOBAL_BIN = "/home/testuser/.bun/bin";
12
12
  const DEFAULT_SERVICE_PATH = `${DEFAULT_BUN_GLOBAL_BIN}:${DEFAULT_LOGIN_PATH}`;
13
+ const DEFAULT_EXECUTABLE_PATH = `${DEFAULT_BUN_GLOBAL_BIN}/macroclaw`;
13
14
  const mockExecSync = mock((cmd: string, _opts?: object): string => {
14
15
  if (cmd === "/bin/bash -lc 'printf %s \"$PATH\"'") return `${DEFAULT_LOGIN_PATH}\n`;
15
16
  if (cmd === "bun pm bin -g") return `${DEFAULT_BUN_GLOBAL_BIN}\n`;
@@ -236,7 +237,7 @@ describe("install", () => {
236
237
  const plistPath = join(plistDir, "com.macroclaw.plist");
237
238
  expect(existsSync(plistPath)).toBe(true);
238
239
  const writtenContent = readFileSync(plistPath, "utf-8");
239
- expect(writtenContent).toContain("<string>macroclaw</string>");
240
+ expect(writtenContent).toContain(`<string>${DEFAULT_EXECUTABLE_PATH}</string>`);
240
241
  expect(writtenContent).toContain("<string>start</string>");
241
242
  expect(writtenContent).toContain("<key>KeepAlive</key>");
242
243
  expect(writtenContent).toContain(".macroclaw/logs/stdout.log");
@@ -368,7 +369,7 @@ describe("install", () => {
368
369
  expect(unitContent).not.toContain("Environment=HOME=");
369
370
  expect(unitContent).toContain(`Environment=PATH=${DEFAULT_SERVICE_PATH}`);
370
371
  expect(unitContent).toContain("WorkingDirectory=%h");
371
- expect(unitContent).toContain("ExecStart=macroclaw start");
372
+ expect(unitContent).toContain(`ExecStart=${DEFAULT_EXECUTABLE_PATH} start`);
372
373
 
373
374
  // Lingering enabled via sudo
374
375
  expect(mockExecSync).toHaveBeenCalledWith("sudo loginctl enable-linger testuser", expect.anything());
@@ -801,6 +802,7 @@ describe("refresh", () => {
801
802
  expect(mockExecSync).toHaveBeenCalledWith("/bin/bash -lc 'printf %s \"$PATH\"'", expect.anything());
802
803
  expect(mockExecSync).toHaveBeenCalledWith("bun pm bin -g", expect.anything());
803
804
  expect(plist).toContain("<string>/home/testuser/.bun/bin:/custom/bin:/usr/bin:/bin</string>");
805
+ expect(plist).toContain("<string>/home/testuser/.bun/bin/macroclaw</string>");
804
806
  expect(plist).toContain("<key>CLAUDE_CODE_OAUTH_TOKEN</key>");
805
807
  expect(plist).toContain("<string>sk-test-token</string>");
806
808
  rmSync(tmpHome, { recursive: true });
@@ -826,6 +828,7 @@ describe("refresh", () => {
826
828
  expect(mockExecSync).toHaveBeenCalledWith("bun pm bin -g", expect.anything());
827
829
  expect(mockExecSync).toHaveBeenCalledWith("systemctl --user daemon-reload", expect.anything());
828
830
  expect(unitContent).toContain("Environment=PATH=/home/testuser/.bun/bin:/custom/bin:/usr/bin:/bin");
831
+ expect(unitContent).toContain("ExecStart=/home/testuser/.bun/bin/macroclaw start");
829
832
  rmSync(tmpHome, { recursive: true });
830
833
  });
831
834
 
@@ -845,8 +848,30 @@ describe("refresh", () => {
845
848
  mgr.refresh();
846
849
  const unitContent = readFileSync(join(unitDir, "macroclaw.service"), "utf-8");
847
850
 
848
- expect(unitContent).toContain("Environment=PATH=/usr/local/bin:/home/testuser/.bun/bin:/usr/bin:/bin");
849
- expect(unitContent).not.toContain("Environment=PATH=/home/testuser/.bun/bin:/usr/local/bin:/home/testuser/.bun/bin:/usr/bin:/bin");
851
+ expect(unitContent).toContain("Environment=PATH=/home/testuser/.bun/bin:/usr/local/bin:/usr/bin:/bin");
852
+ expect(unitContent).not.toContain("Environment=PATH=/usr/local/bin:/home/testuser/.bun/bin:/usr/local/bin:/usr/bin:/bin");
853
+ rmSync(tmpHome, { recursive: true });
854
+ });
855
+
856
+ it("dedupes repeated PATH entries while keeping bun global bin first", () => {
857
+ const tmpHome = `/tmp/macroclaw-test-refresh-systemd-path-dedup-${Date.now()}`;
858
+ const unitDir = join(tmpHome, ".config/systemd/user");
859
+ mkdirSync(unitDir, { recursive: true });
860
+ writeFileSync(join(unitDir, "macroclaw.service"), "test");
861
+
862
+ mockExecSync.mockImplementation((cmd: string) => {
863
+ if (cmd === "/bin/bash -lc 'printf %s \"$PATH\"'") {
864
+ return "/home/testuser/.bun/bin:/usr/local/bin:/usr/bin:/usr/local/bin:/usr/bin:/bin:/home/testuser/.bun/bin\n";
865
+ }
866
+ if (cmd === "bun pm bin -g") return "/home/testuser/.bun/bin\n";
867
+ return "";
868
+ });
869
+
870
+ const mgr = createManager({ platform: "linux", home: tmpHome });
871
+ mgr.refresh();
872
+ const unitContent = readFileSync(join(unitDir, "macroclaw.service"), "utf-8");
873
+
874
+ expect(unitContent).toContain("Environment=PATH=/home/testuser/.bun/bin:/usr/local/bin:/usr/bin:/bin");
850
875
  rmSync(tmpHome, { recursive: true });
851
876
  });
852
877
  });
@@ -87,6 +87,7 @@ export class SystemServiceManager {
87
87
 
88
88
  this.#exec("bun install -g macroclaw");
89
89
  const servicePath = this.#getServicePath();
90
+ const executablePath = this.#getMacroclawExecutablePath();
90
91
 
91
92
  const logDir = resolve(this.#home, ".macroclaw/logs");
92
93
  mkdirSync(logDir, { recursive: true });
@@ -94,7 +95,7 @@ export class SystemServiceManager {
94
95
  this.#exec(`launchctl unload ${this.serviceFilePath}`);
95
96
  }
96
97
 
97
- writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(servicePath, oauthToken));
98
+ writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(servicePath, executablePath, oauthToken));
98
99
  log.debug({ filePath: this.serviceFilePath }, "Wrote launchd plist");
99
100
  this.#exec(`launchctl load ${this.serviceFilePath}`);
100
101
  }
@@ -111,6 +112,7 @@ export class SystemServiceManager {
111
112
 
112
113
  this.#exec("bun install -g macroclaw");
113
114
  const servicePath = this.#getServicePath();
115
+ const executablePath = this.#getMacroclawExecutablePath();
114
116
 
115
117
  // Enable lingering so user services run without an active login session
116
118
  const username = osUserInfo().username;
@@ -118,7 +120,7 @@ export class SystemServiceManager {
118
120
  this.#sudo(`loginctl enable-linger ${username}`);
119
121
  }
120
122
 
121
- const unitContent = this.#generateSystemdUnit(servicePath);
123
+ const unitContent = this.#generateSystemdUnit(servicePath, executablePath);
122
124
  mkdirSync(dirname(this.serviceFilePath), { recursive: true });
123
125
  writeFileSync(this.serviceFilePath, unitContent);
124
126
  log.debug({ filePath: this.serviceFilePath }, "Wrote systemd unit");
@@ -211,13 +213,14 @@ export class SystemServiceManager {
211
213
  refresh(): void {
212
214
  this.#requireInstalled();
213
215
  const servicePath = this.#getServicePath();
216
+ const executablePath = this.#getMacroclawExecutablePath();
214
217
  if (this.#platform === "systemd") {
215
- writeFileSync(this.serviceFilePath, this.#generateSystemdUnit(servicePath));
218
+ writeFileSync(this.serviceFilePath, this.#generateSystemdUnit(servicePath, executablePath));
216
219
  this.#exec("systemctl --user daemon-reload");
217
220
  return;
218
221
  }
219
222
  const oauthToken = this.#getLaunchdOauthToken();
220
- writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(servicePath, oauthToken));
223
+ writeFileSync(this.serviceFilePath, this.#generateLaunchdPlist(servicePath, executablePath, oauthToken));
221
224
  }
222
225
 
223
226
  status(): ServiceStatus {
@@ -271,9 +274,12 @@ export class SystemServiceManager {
271
274
  #getServicePath(): string {
272
275
  const shellPath = this.#getLoginShellPath();
273
276
  const bunGlobalBin = this.#exec("bun pm bin -g").trim();
274
- const entries = shellPath.split(":").filter(Boolean);
275
- if (entries.includes(bunGlobalBin)) return shellPath;
276
- return [bunGlobalBin, ...entries].join(":");
277
+ const entries = [bunGlobalBin, ...shellPath.split(":").filter(Boolean)];
278
+ return [...new Set(entries)].join(":");
279
+ }
280
+
281
+ #getMacroclawExecutablePath(): string {
282
+ return resolve(this.#exec("bun pm bin -g").trim(), "macroclaw");
277
283
  }
278
284
 
279
285
  #getLaunchdOauthToken(): string | undefined {
@@ -311,7 +317,7 @@ export class SystemServiceManager {
311
317
  this.#exec(`sudo ${cmd}`);
312
318
  }
313
319
 
314
- #generateLaunchdPlist(servicePath: string, oauthToken?: string): string {
320
+ #generateLaunchdPlist(servicePath: string, executablePath: string, oauthToken?: string): string {
315
321
  const logDir = resolve(this.#home, ".macroclaw/logs");
316
322
  const envVars = [`\n\t<key>PATH</key>\n\t\t<string>${servicePath}</string>`];
317
323
  if (oauthToken) {
@@ -326,7 +332,7 @@ export class SystemServiceManager {
326
332
  <string>com.macroclaw</string>
327
333
  <key>ProgramArguments</key>
328
334
  <array>
329
- <string>macroclaw</string>
335
+ <string>${executablePath}</string>
330
336
  <string>start</string>
331
337
  </array>
332
338
  <key>KeepAlive</key>
@@ -340,7 +346,7 @@ export class SystemServiceManager {
340
346
  `;
341
347
  }
342
348
 
343
- #generateSystemdUnit(servicePath: string): string {
349
+ #generateSystemdUnit(servicePath: string, executablePath: string): string {
344
350
  return `[Unit]
345
351
  Description=Macroclaw - Telegram-to-Claude-Code bridge
346
352
  After=network.target
@@ -349,7 +355,7 @@ After=network.target
349
355
  Type=simple
350
356
  WorkingDirectory=%h
351
357
  Environment=PATH=${servicePath}
352
- ExecStart=macroclaw start
358
+ ExecStart=${executablePath} start
353
359
  Restart=on-failure
354
360
  RestartSec=5
355
361