macroclaw 0.37.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
|
@@ -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
|
|
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
|
-
|
|
195
|
-
expect(mockExecSync).toHaveBeenCalledWith("which
|
|
196
|
-
expect(mockExecSync).toHaveBeenCalledWith("
|
|
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
|
|
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
|
-
|
|
221
|
-
expect(writtenContent).toContain(
|
|
222
|
-
expect(writtenContent).toContain("<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
|
-
|
|
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
|
|
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
|
-
|
|
330
|
-
expect(unitContent).toContain(
|
|
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) {
|
package/src/system-service.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
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(
|
|
274
|
+
#generateLaunchdPlist(oauthToken?: string): string {
|
|
302
275
|
const logDir = resolve(this.#home, ".macroclaw/logs");
|
|
303
|
-
const
|
|
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
|
|
313
|
-
<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(
|
|
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
|
-
|
|
342
|
-
|
|
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
|
|
|
@@ -73,7 +73,7 @@ Two job types, discriminated by field:
|
|
|
73
73
|
|
|
74
74
|
| Field | Required | Description |
|
|
75
75
|
|-------|----------|-------------|
|
|
76
|
-
| `name` | yes | Short kebab-case identifier (e.g. `dentist-reminder`). Appears in the
|
|
76
|
+
| `name` | yes | Short kebab-case identifier (e.g. `dentist-reminder`). Appears in the `<schedule name="...">` element and event name when fired. |
|
|
77
77
|
| `cron` | for recurring | Standard cron expression (local time). See reference below. |
|
|
78
78
|
| `fireAt` | for one-time | ISO 8601 timestamp (e.g. `2026-03-15T08:00:00`). Can include a timezone offset (e.g. `2026-03-15T08:00:00+01:00`); without one, the time is interpreted in the configured timezone. |
|
|
79
79
|
| `prompt` | yes | The message sent to the agent when the event fires. Write it as a natural instruction. |
|
|
@@ -105,4 +105,4 @@ Common patterns:
|
|
|
105
105
|
- Changes are hot-reloaded — no restart needed
|
|
106
106
|
- File location: `<workspace>/data/schedule.json`
|
|
107
107
|
- One-shot events (`fireAt`) are cleaned up automatically after firing
|
|
108
|
-
- Missed one-shot events (e.g. service was down) are fired
|
|
108
|
+
- Missed one-shot events (e.g. service was down) are fired when the service restarts (up to 7 days late) with `missed-by` and `scheduled-at` attributes on the `<schedule>` element
|