macroclaw 0.9.0 → 0.11.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 +4 -2
- package/src/app.ts +1 -2
- package/src/cli.test.ts +23 -0
- package/src/cli.ts +33 -1
- package/src/orchestrator.ts +3 -1
- package/src/service.test.ts +112 -0
- package/src/service.ts +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macroclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Telegram-to-Claude-Code bridge",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "bun run src/main.ts",
|
|
21
21
|
"dev": "bun run --watch src/main.ts",
|
|
22
|
-
"check": "tsc --noEmit && biome check && bun test",
|
|
22
|
+
"check": "tsc --noEmit && biome check && bun test && bun run depcheck",
|
|
23
|
+
"depcheck": "depcruise src --config .dependency-cruiser.json --output-type err",
|
|
23
24
|
"typecheck": "tsc --noEmit",
|
|
24
25
|
"lint": "biome check",
|
|
25
26
|
"lint:fix": "biome check --fix --unsafe",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@biomejs/biome": "^2.4.6",
|
|
43
44
|
"bun-types": "^1.3.10",
|
|
45
|
+
"dependency-cruiser": "^17.3.8",
|
|
44
46
|
"typescript": "^5.9.3"
|
|
45
47
|
}
|
|
46
48
|
}
|
package/src/app.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Bot } from "grammy";
|
|
2
|
-
import type { Claude } from "./claude";
|
|
3
2
|
import { CronScheduler } from "./cron";
|
|
4
3
|
import { createLogger } from "./logger";
|
|
5
|
-
import { Orchestrator, type OrchestratorResponse } from "./orchestrator";
|
|
4
|
+
import { type Claude, Orchestrator, type OrchestratorResponse } from "./orchestrator";
|
|
6
5
|
import { isAvailable as isSttAvailable, transcribe } from "./stt";
|
|
7
6
|
import { createBot, downloadFile, sendFile, sendResponse } from "./telegram";
|
|
8
7
|
|
package/src/cli.test.ts
CHANGED
|
@@ -115,6 +115,8 @@ function mockService(overrides?: Partial<SystemService>): SystemService {
|
|
|
115
115
|
start: mock(() => ""),
|
|
116
116
|
stop: mock(() => {}),
|
|
117
117
|
update: mock(() => ""),
|
|
118
|
+
status: mock(() => ({ installed: false, running: false, platform: "systemd" as const })),
|
|
119
|
+
logs: mock(() => "journalctl -u macroclaw -n 50 --no-pager"),
|
|
118
120
|
...overrides,
|
|
119
121
|
};
|
|
120
122
|
}
|
|
@@ -155,6 +157,27 @@ describe("Cli.service", () => {
|
|
|
155
157
|
expect(update).toHaveBeenCalled();
|
|
156
158
|
});
|
|
157
159
|
|
|
160
|
+
it("runs status action", () => {
|
|
161
|
+
const status = mock(() => ({ installed: true, running: true, platform: "systemd" as const, pid: 42, uptime: "Thu 2026-03-12 10:00:00 UTC" }));
|
|
162
|
+
const cli = new Cli(undefined, mockService({ status }));
|
|
163
|
+
cli.service("status");
|
|
164
|
+
expect(status).toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("runs logs action", () => {
|
|
168
|
+
const logs = mock(() => "journalctl -u macroclaw -n 50 --no-pager");
|
|
169
|
+
const cli = new Cli(undefined, mockService({ logs }));
|
|
170
|
+
cli.service("logs");
|
|
171
|
+
expect(logs).toHaveBeenCalledWith(undefined);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("passes follow flag to logs action", () => {
|
|
175
|
+
const logs = mock(() => "journalctl -u macroclaw -f");
|
|
176
|
+
const cli = new Cli(undefined, mockService({ logs }));
|
|
177
|
+
cli.service("logs", undefined, true);
|
|
178
|
+
expect(logs).toHaveBeenCalledWith(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
158
181
|
it("throws for unknown action", () => {
|
|
159
182
|
const cli = new Cli(undefined, mockService());
|
|
160
183
|
expect(() => cli.service("bogus")).toThrow("Unknown service action: bogus");
|
package/src/cli.ts
CHANGED
|
@@ -83,7 +83,7 @@ export class Cli {
|
|
|
83
83
|
exec(args.join(" "), { cwd: settings.workspace, stdio: "inherit", env: { ...process.env, CLAUDECODE: "" } });
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
service(action: string, token?: string): void {
|
|
86
|
+
service(action: string, token?: string, follow?: boolean): void {
|
|
87
87
|
switch (action) {
|
|
88
88
|
case "install": {
|
|
89
89
|
const logCmd = this.#systemService.install(token);
|
|
@@ -108,6 +108,23 @@ export class Cli {
|
|
|
108
108
|
console.log(`Service updated. Check logs:\n ${logCmd}`);
|
|
109
109
|
break;
|
|
110
110
|
}
|
|
111
|
+
case "status": {
|
|
112
|
+
const s = this.#systemService.status();
|
|
113
|
+
const lines = [
|
|
114
|
+
`Platform: ${s.platform}`,
|
|
115
|
+
`Installed: ${s.installed ? "yes" : "no"}`,
|
|
116
|
+
`Running: ${s.running ? "yes" : "no"}`,
|
|
117
|
+
];
|
|
118
|
+
if (s.pid) lines.push(`PID: ${s.pid}`);
|
|
119
|
+
if (s.uptime) lines.push(`Active since: ${s.uptime}`);
|
|
120
|
+
console.log(lines.join("\n"));
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "logs": {
|
|
124
|
+
const cmd = this.#systemService.logs(follow);
|
|
125
|
+
console.log(cmd);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
111
128
|
default:
|
|
112
129
|
throw new Error(`Unknown service action: ${action}`);
|
|
113
130
|
}
|
|
@@ -169,6 +186,19 @@ const serviceUpdateCommand = defineCommand({
|
|
|
169
186
|
run: () => { try { defaultCli.service("update"); } catch (err) { handleError(err); } },
|
|
170
187
|
});
|
|
171
188
|
|
|
189
|
+
const serviceStatusCommand = defineCommand({
|
|
190
|
+
meta: { name: "status", description: "Show service installation and running status" },
|
|
191
|
+
run: () => { try { defaultCli.service("status"); } catch (err) { handleError(err); } },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const serviceLogsCommand = defineCommand({
|
|
195
|
+
meta: { name: "logs", description: "Print the command to view service logs" },
|
|
196
|
+
args: {
|
|
197
|
+
follow: { type: "boolean", alias: "f", description: "Follow log output in real-time" },
|
|
198
|
+
},
|
|
199
|
+
run: ({ args }) => { try { defaultCli.service("logs", undefined, args.follow); } catch (err) { handleError(err); } },
|
|
200
|
+
});
|
|
201
|
+
|
|
172
202
|
const serviceCommand = defineCommand({
|
|
173
203
|
meta: { name: "service", description: "Manage macroclaw system service" },
|
|
174
204
|
subCommands: {
|
|
@@ -177,6 +207,8 @@ const serviceCommand = defineCommand({
|
|
|
177
207
|
start: serviceStartCommand,
|
|
178
208
|
stop: serviceStopCommand,
|
|
179
209
|
update: serviceUpdateCommand,
|
|
210
|
+
status: serviceStatusCommand,
|
|
211
|
+
logs: serviceLogsCommand,
|
|
180
212
|
},
|
|
181
213
|
});
|
|
182
214
|
|
package/src/orchestrator.ts
CHANGED
|
@@ -12,7 +12,8 @@ import { createLogger } from "./logger";
|
|
|
12
12
|
import { CRON_TIMEOUT, MAIN_TIMEOUT, SYSTEM_PROMPT } from "./prompts";
|
|
13
13
|
import { Queue } from "./queue";
|
|
14
14
|
import { loadSessions, saveSessions } from "./sessions";
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
type ButtonSpec = string | { text: string; data: string };
|
|
16
17
|
|
|
17
18
|
const log = createLogger("orchestrator");
|
|
18
19
|
|
|
@@ -42,6 +43,7 @@ const textResultType = { type: "text" } as const;
|
|
|
42
43
|
// --- Public response type ---
|
|
43
44
|
|
|
44
45
|
export type { ButtonSpec };
|
|
46
|
+
export type { Claude };
|
|
45
47
|
|
|
46
48
|
export interface OrchestratorResponse {
|
|
47
49
|
message: string;
|
package/src/service.test.ts
CHANGED
|
@@ -588,3 +588,115 @@ describe("update", () => {
|
|
|
588
588
|
}
|
|
589
589
|
});
|
|
590
590
|
});
|
|
591
|
+
|
|
592
|
+
describe("status", () => {
|
|
593
|
+
it("returns not installed, not running when service file missing", () => {
|
|
594
|
+
mockExistsSync.mockImplementation(() => false);
|
|
595
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
596
|
+
if (cmd === "systemctl is-active macroclaw") throw new Error("not found");
|
|
597
|
+
return "";
|
|
598
|
+
});
|
|
599
|
+
const mgr = createManager();
|
|
600
|
+
const s = mgr.status();
|
|
601
|
+
expect(s.installed).toBe(false);
|
|
602
|
+
expect(s.running).toBe(false);
|
|
603
|
+
expect(s.platform).toBe("systemd");
|
|
604
|
+
expect(s.pid).toBeUndefined();
|
|
605
|
+
expect(s.uptime).toBeUndefined();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("returns installed but not running for systemd", () => {
|
|
609
|
+
mockExistsSync.mockImplementation(() => true);
|
|
610
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
611
|
+
if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
|
|
612
|
+
return "";
|
|
613
|
+
});
|
|
614
|
+
const mgr = createManager();
|
|
615
|
+
const s = mgr.status();
|
|
616
|
+
expect(s.installed).toBe(true);
|
|
617
|
+
expect(s.running).toBe(false);
|
|
618
|
+
expect(s.pid).toBeUndefined();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("returns pid and uptime for running systemd service", () => {
|
|
622
|
+
mockExistsSync.mockImplementation(() => true);
|
|
623
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
624
|
+
if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
|
|
625
|
+
if (cmd.startsWith("systemctl show macroclaw")) return "MainPID=42\nActiveEnterTimestamp=Thu 2026-03-12 10:00:00 UTC";
|
|
626
|
+
return "";
|
|
627
|
+
});
|
|
628
|
+
const mgr = createManager();
|
|
629
|
+
const s = mgr.status();
|
|
630
|
+
expect(s.installed).toBe(true);
|
|
631
|
+
expect(s.running).toBe(true);
|
|
632
|
+
expect(s.pid).toBe(42);
|
|
633
|
+
expect(s.uptime).toBe("Thu 2026-03-12 10:00:00 UTC");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("returns pid for running launchd service", () => {
|
|
637
|
+
mockExistsSync.mockImplementation(() => true);
|
|
638
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
639
|
+
if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
|
|
640
|
+
return "";
|
|
641
|
+
});
|
|
642
|
+
const mgr = createManager({ platform: "darwin" });
|
|
643
|
+
const s = mgr.status();
|
|
644
|
+
expect(s.installed).toBe(true);
|
|
645
|
+
expect(s.running).toBe(true);
|
|
646
|
+
expect(s.platform).toBe("launchd");
|
|
647
|
+
expect(s.pid).toBe(12345);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("handles systemctl show failure gracefully", () => {
|
|
651
|
+
mockExistsSync.mockImplementation(() => true);
|
|
652
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
653
|
+
if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
|
|
654
|
+
if (cmd.startsWith("systemctl show")) throw new Error("failed");
|
|
655
|
+
return "";
|
|
656
|
+
});
|
|
657
|
+
const mgr = createManager();
|
|
658
|
+
const s = mgr.status();
|
|
659
|
+
expect(s.running).toBe(true);
|
|
660
|
+
expect(s.pid).toBeUndefined();
|
|
661
|
+
expect(s.uptime).toBeUndefined();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("handles launchctl list failure gracefully during status", () => {
|
|
665
|
+
mockExistsSync.mockImplementation(() => true);
|
|
666
|
+
let callCount = 0;
|
|
667
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
668
|
+
if (cmd.startsWith("launchctl list ")) {
|
|
669
|
+
callCount++;
|
|
670
|
+
if (callCount === 1) return LAUNCHD_RUNNING;
|
|
671
|
+
throw new Error("failed");
|
|
672
|
+
}
|
|
673
|
+
return "";
|
|
674
|
+
});
|
|
675
|
+
const mgr = createManager({ platform: "darwin" });
|
|
676
|
+
const s = mgr.status();
|
|
677
|
+
expect(s.running).toBe(true);
|
|
678
|
+
expect(s.pid).toBeUndefined();
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("logs", () => {
|
|
683
|
+
it("returns journalctl command for systemd", () => {
|
|
684
|
+
const mgr = createManager();
|
|
685
|
+
expect(mgr.logs()).toBe("journalctl -u macroclaw -n 50 --no-pager");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("returns journalctl follow command for systemd", () => {
|
|
689
|
+
const mgr = createManager();
|
|
690
|
+
expect(mgr.logs(true)).toBe("journalctl -u macroclaw -f");
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("returns tail command for launchd", () => {
|
|
694
|
+
const mgr = createManager({ platform: "darwin" });
|
|
695
|
+
expect(mgr.logs()).toBe("tail -n 50 /home/testuser/.macroclaw/logs/stdout.log");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("returns tail follow command for launchd", () => {
|
|
699
|
+
const mgr = createManager({ platform: "darwin" });
|
|
700
|
+
expect(mgr.logs(true)).toBe("tail -f /home/testuser/.macroclaw/logs/stdout.log /home/testuser/.macroclaw/logs/stderr.log");
|
|
701
|
+
});
|
|
702
|
+
});
|
package/src/service.ts
CHANGED
|
@@ -50,12 +50,22 @@ interface LinuxUser {
|
|
|
50
50
|
home: string;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface ServiceStatus {
|
|
54
|
+
installed: boolean;
|
|
55
|
+
running: boolean;
|
|
56
|
+
platform: Platform;
|
|
57
|
+
pid?: number;
|
|
58
|
+
uptime?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
export interface SystemService {
|
|
54
62
|
install: (oauthToken?: string) => string;
|
|
55
63
|
uninstall: () => void;
|
|
56
64
|
start: () => string;
|
|
57
65
|
stop: () => void;
|
|
58
66
|
update: () => string;
|
|
67
|
+
status: () => ServiceStatus;
|
|
68
|
+
logs: (follow?: boolean) => string;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
export class ServiceManager implements SystemService {
|
|
@@ -229,6 +239,46 @@ export class ServiceManager implements SystemService {
|
|
|
229
239
|
return this.#logTailCommand();
|
|
230
240
|
}
|
|
231
241
|
|
|
242
|
+
status(): ServiceStatus {
|
|
243
|
+
const result: ServiceStatus = {
|
|
244
|
+
installed: this.isInstalled,
|
|
245
|
+
running: this.isRunning,
|
|
246
|
+
platform: this.#platform,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (result.running) {
|
|
250
|
+
if (this.#platform === "launchd") {
|
|
251
|
+
try {
|
|
252
|
+
const out = this.#deps.execSync(`launchctl list ${LAUNCHD_LABEL}`);
|
|
253
|
+
const pidMatch = /"PID"\s*=\s*(\d+)/.exec(out);
|
|
254
|
+
if (pidMatch) result.pid = Number(pidMatch[1]);
|
|
255
|
+
} catch { /* best effort */ }
|
|
256
|
+
} else {
|
|
257
|
+
try {
|
|
258
|
+
const out = this.#deps.execSync("systemctl show macroclaw --property=MainPID,ActiveEnterTimestamp --no-pager");
|
|
259
|
+
const pidMatch = /MainPID=(\d+)/.exec(out);
|
|
260
|
+
if (pidMatch && pidMatch[1] !== "0") result.pid = Number(pidMatch[1]);
|
|
261
|
+
const tsMatch = /ActiveEnterTimestamp=(.+)/.exec(out);
|
|
262
|
+
if (tsMatch?.[1].trim()) result.uptime = tsMatch[1].trim();
|
|
263
|
+
} catch { /* best effort */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
logs(follow = false): string {
|
|
271
|
+
if (this.#platform === "launchd") {
|
|
272
|
+
const logDir = resolve(this.#deps.home, ".macroclaw/logs");
|
|
273
|
+
return follow
|
|
274
|
+
? `tail -f ${logDir}/stdout.log ${logDir}/stderr.log`
|
|
275
|
+
: `tail -n 50 ${logDir}/stdout.log`;
|
|
276
|
+
}
|
|
277
|
+
return follow
|
|
278
|
+
? "journalctl -u macroclaw -f"
|
|
279
|
+
: "journalctl -u macroclaw -n 50 --no-pager";
|
|
280
|
+
}
|
|
281
|
+
|
|
232
282
|
#resolvePath(binary: string): string {
|
|
233
283
|
try {
|
|
234
284
|
return this.#deps.execSync(`which ${binary}`).trim();
|