macroclaw 0.2.0 → 0.4.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/README.md CHANGED
@@ -63,26 +63,22 @@ the bot.
63
63
  - [Bun](https://bun.sh/) runtime
64
64
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and logged in
65
65
 
66
- ## Setup
66
+ ## Quick Start
67
67
 
68
68
  ```bash
69
- # Run directly (no install needed)
70
- bunx macroclaw
71
-
72
- # Or install globally
73
- bun install -g macroclaw
74
- macroclaw
69
+ bunx macroclaw setup
75
70
  ```
76
71
 
77
- On first launch, an interactive setup wizard guides you through configuration.
72
+ This runs the setup wizard, which:
73
+ 1. Asks for your **Telegram bot token** (from [@BotFather](https://t.me/BotFather))
74
+ 2. Starts the bot temporarily so you can send `/chatid` to discover your chat ID
75
+ 3. Asks for your **chat ID**, **model** preference, **workspace path**, and optional **OpenAI API key**
76
+ 4. Saves settings to `~/.macroclaw/settings.json`
77
+ 5. Offers to install as a system service — this installs macroclaw globally (`bun install -g`), registers it as a **launchd** agent (macOS) or **systemd** unit (Linux), and starts the bridge automatically
78
78
 
79
- The setup wizard will:
80
- 1. Ask for your **Telegram bot token** (from [@BotFather](https://t.me/BotFather))
81
- 2. Start the bot temporarily so you can send `/chatid` to discover your chat ID
82
- 3. Ask for your **chat ID**, **model** preference, **workspace path**, and optional **OpenAI API key**
83
- 4. Save settings to `~/.macroclaw/settings.json`
79
+ No separate install step needed — answering yes to the service prompt handles everything.
84
80
 
85
- On subsequent runs, settings are loaded from the file. Environment variables override file settings (see `.env.example`).
81
+ On subsequent runs, settings are pre-filled from the file.
86
82
 
87
83
  ### Configuration
88
84
 
@@ -102,31 +98,41 @@ Env vars take precedence over settings file values. On startup, a masked setting
102
98
 
103
99
  Session state (Claude session IDs) is stored separately in `~/.macroclaw/sessions.json`.
104
100
 
105
- ## Usage
101
+ ## Commands
106
102
 
107
- Run inside a tmux session so it survives SSH disconnects:
103
+ | Command | Description |
104
+ |---------|-------------|
105
+ | `macroclaw start` | Start the bridge |
106
+ | `macroclaw setup` | Run the interactive setup wizard |
107
+ | `macroclaw claude` | Open Claude Code CLI in the main session |
108
+ | `macroclaw service install` | Install globally and register as a system service |
109
+ | `macroclaw service uninstall` | Stop and remove the system service |
110
+ | `macroclaw service start` | Start the system service |
111
+ | `macroclaw service stop` | Stop the system service |
112
+ | `macroclaw service update` | Reinstall latest version and restart |
108
113
 
109
- ```bash
110
- tmux new -s macroclaw # start session
111
- macroclaw # run the bot
114
+ ### Running as a service
115
+
116
+ The recommended path is `bunx macroclaw setup` and answering yes to the service prompt (see [Quick Start](#quick-start)).
112
117
 
113
- # Ctrl+B, D — detach (bot keeps running)
114
- # tmux attach -t macroclaw — reattach later
115
- # tmux kill-session -t macroclaw — stop everything
118
+ If macroclaw is already installed and configured, you can also install the service directly:
119
+
120
+ ```bash
121
+ macroclaw service install
116
122
  ```
117
123
 
124
+ Both paths install macroclaw globally via `bun install -g`, register it as a **launchd** agent (macOS) or **systemd** unit (Linux) with auto-restart, and start the bridge.
125
+
126
+ On Linux, the command runs as a normal user. Only the privileged operations (writing to `/etc/systemd/system/`, systemctl commands) are elevated via `sudo`, which prompts for a password when needed. Package installation and path resolution stay in the user's environment.
127
+
118
128
  ## Development
119
129
 
120
130
  ```bash
121
131
  git clone git@github.com:macrosak/macroclaw.git
122
132
  cd macroclaw
123
- cp .env.example .env # fill in real values
133
+ cp .env.example .env # fill in real values (see .env.example for available vars)
124
134
  bun install --frozen-lockfile
125
- ```
126
135
 
127
- ## Development
128
-
129
- ```bash
130
136
  bun run dev # start with watch mode
131
137
  bun test # run tests (100% coverage enforced)
132
138
  bun run claude # open Claude Code CLI in current main session
package/bin/macroclaw.js CHANGED
@@ -3,4 +3,6 @@ if (typeof Bun === "undefined") {
3
3
  console.error("macroclaw requires Bun. Install it: https://bun.sh");
4
4
  process.exit(1);
5
5
  }
6
- await import("../src/index.ts");
6
+ const { runMain } = await import("citty");
7
+ const { main } = await import("../src/cli.ts");
8
+ await runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macroclaw",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,8 +17,8 @@
17
17
  "url": "https://github.com/macrosak/macroclaw"
18
18
  },
19
19
  "scripts": {
20
- "start": "bun run src/index.ts",
21
- "dev": "bun run --watch src/index.ts",
20
+ "start": "bun run src/cli.ts",
21
+ "dev": "bun run --watch src/cli.ts",
22
22
  "check": "tsc --noEmit && biome check && bun test",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "lint": "biome check",
@@ -28,6 +28,7 @@
28
28
  "claude": "set -a && . ./.env && set +a && cd ${WORKSPACE:-~/.macroclaw-workspace} && claude --resume $(node -p \"require('$HOME/.macroclaw/settings.json').sessionId\") --model ${MODEL:-sonnet}"
29
29
  },
30
30
  "dependencies": {
31
+ "citty": "^0.2.1",
31
32
  "cron-parser": "^5.5.0",
32
33
  "grammy": "^1.39.3",
33
34
  "openai": "^6.27.0",
package/src/claude.ts CHANGED
@@ -133,4 +133,4 @@ export class Claude {
133
133
  completion,
134
134
  };
135
135
  }
136
- }
136
+ }
@@ -0,0 +1,293 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import { runCommand } from "citty";
3
+ import { Cli, handleError, loadRawSettings, type SetupDeps } from "./cli";
4
+ import type { SystemService } from "./service";
5
+
6
+ // Only mock ./index — safe since no other test imports it
7
+ const mockStart = mock(async () => {});
8
+ mock.module("./index", () => ({ start: mockStart }));
9
+
10
+ const { main } = await import("./cli");
11
+
12
+ function createMockSetupDeps(): SetupDeps & { saved: { settings: unknown; dir: string } | null } {
13
+ const result: SetupDeps & { saved: { settings: unknown; dir: string } | null } = {
14
+ saved: null,
15
+ initLogger: async () => {},
16
+ saveSettings: () => {},
17
+ loadRawSettings: () => null,
18
+ runSetupWizard: async (io) => {
19
+ await io.ask("test?");
20
+ io.write("done");
21
+ io.close?.();
22
+ return { botToken: "tok", chatId: "123" };
23
+ },
24
+ createReadlineInterface: () => ({
25
+ question: (_q: string, cb: (a: string) => void) => cb("answer"),
26
+ close: () => {},
27
+ }),
28
+ resolveDir: () => "/home/test/.macroclaw",
29
+ };
30
+ result.saveSettings = (settings: unknown, dir: string) => { result.saved = { settings, dir }; };
31
+ return result;
32
+ }
33
+
34
+ describe("CLI routing", () => {
35
+ it("requires a subcommand when no args given", async () => {
36
+ await expect(runCommand(main, { rawArgs: [] })).rejects.toThrow("No command specified");
37
+ });
38
+
39
+ it("routes 'start' subcommand to start", async () => {
40
+ mockStart.mockClear();
41
+ await runCommand(main, { rawArgs: ["start"] });
42
+ expect(mockStart).toHaveBeenCalled();
43
+ });
44
+
45
+ it("has all subcommands defined", () => {
46
+ expect(main.subCommands).toBeDefined();
47
+ const subs = main.subCommands as Record<string, unknown>;
48
+ expect(subs.start).toBeDefined();
49
+ expect(subs.setup).toBeDefined();
50
+ expect(subs.claude).toBeDefined();
51
+ expect(subs.service).toBeDefined();
52
+ });
53
+
54
+ it("has correct meta", () => {
55
+ expect(main.meta).toEqual({
56
+ name: "macroclaw",
57
+ description: "Telegram-to-Claude-Code bridge",
58
+ version: "0.0.0-dev",
59
+ });
60
+ });
61
+ });
62
+
63
+ describe("Cli.setup", () => {
64
+ it("runs wizard and saves settings", async () => {
65
+ const deps = createMockSetupDeps();
66
+ const cli = new Cli(deps);
67
+ await cli.setup();
68
+ expect(deps.saved).toEqual({
69
+ settings: { botToken: "tok", chatId: "123" },
70
+ dir: "/home/test/.macroclaw",
71
+ });
72
+ });
73
+
74
+ it("calls initLogger", async () => {
75
+ const deps = createMockSetupDeps();
76
+ const mockInit = mock(async () => {});
77
+ deps.initLogger = mockInit;
78
+ const cli = new Cli(deps);
79
+ await cli.setup();
80
+ expect(mockInit).toHaveBeenCalled();
81
+ });
82
+
83
+ it("passes existing settings as defaults to wizard", async () => {
84
+ const existing = { botToken: "old-tok", chatId: "999" };
85
+ let receivedDefaults: unknown = null;
86
+ const deps = createMockSetupDeps();
87
+ deps.loadRawSettings = () => existing;
88
+ deps.runSetupWizard = async (io, opts) => {
89
+ receivedDefaults = opts?.defaults;
90
+ io.close?.();
91
+ return { botToken: "tok", chatId: "123" };
92
+ };
93
+ const cli = new Cli(deps);
94
+ await cli.setup();
95
+ expect(receivedDefaults).toEqual(existing);
96
+ });
97
+
98
+ it("closes readline after wizard completes", async () => {
99
+ const deps = createMockSetupDeps();
100
+ const mockClose = mock(() => {});
101
+ deps.createReadlineInterface = () => ({
102
+ question: (_q: string, cb: (a: string) => void) => cb("answer"),
103
+ close: mockClose,
104
+ });
105
+ const cli = new Cli(deps);
106
+ await cli.setup();
107
+ expect(mockClose).toHaveBeenCalled();
108
+ });
109
+ });
110
+
111
+ function mockService(overrides?: Partial<SystemService>): SystemService {
112
+ return {
113
+ install: mock(() => ""),
114
+ uninstall: mock(() => {}),
115
+ start: mock(() => ""),
116
+ stop: mock(() => {}),
117
+ update: mock(() => ""),
118
+ ...overrides,
119
+ };
120
+ }
121
+
122
+ describe("Cli.service", () => {
123
+ it("runs install action", () => {
124
+ const install = mock(() => "tail -f /logs");
125
+ const cli = new Cli(undefined, mockService({ install }));
126
+ cli.service("install");
127
+ expect(install).toHaveBeenCalled();
128
+ });
129
+
130
+ it("runs uninstall action", () => {
131
+ const uninstall = mock(() => {});
132
+ const cli = new Cli(undefined, mockService({ uninstall }));
133
+ cli.service("uninstall");
134
+ expect(uninstall).toHaveBeenCalled();
135
+ });
136
+
137
+ it("runs start action", () => {
138
+ const start = mock(() => "tail -f /logs");
139
+ const cli = new Cli(undefined, mockService({ start }));
140
+ cli.service("start");
141
+ expect(start).toHaveBeenCalled();
142
+ });
143
+
144
+ it("runs stop action", () => {
145
+ const stop = mock(() => {});
146
+ const cli = new Cli(undefined, mockService({ stop }));
147
+ cli.service("stop");
148
+ expect(stop).toHaveBeenCalled();
149
+ });
150
+
151
+ it("runs update action", () => {
152
+ const update = mock(() => "tail -f /logs");
153
+ const cli = new Cli(undefined, mockService({ update }));
154
+ cli.service("update");
155
+ expect(update).toHaveBeenCalled();
156
+ });
157
+
158
+ it("throws for unknown action", () => {
159
+ const cli = new Cli(undefined, mockService());
160
+ expect(() => cli.service("bogus")).toThrow("Unknown service action: bogus");
161
+ });
162
+
163
+ it("throws service errors", () => {
164
+ const cli = new Cli(undefined, mockService({ install: () => { throw new Error("Settings not found."); } }));
165
+ expect(() => cli.service("install")).toThrow("Settings not found.");
166
+ });
167
+ });
168
+
169
+ describe("Cli.claude", () => {
170
+ it("builds claude command with session and model", async () => {
171
+ const fs = await import("node:fs");
172
+ const dir = `/tmp/macroclaw-test-claude-${Date.now()}`;
173
+ fs.mkdirSync(dir, { recursive: true });
174
+ fs.writeFileSync(`${dir}/settings.json`, JSON.stringify({ botToken: "tok", chatId: "123", model: "opus", workspace: "/tmp" }));
175
+ fs.writeFileSync(`${dir}/sessions.json`, JSON.stringify({ mainSessionId: "sess-123" }));
176
+
177
+ let capturedCmd = "";
178
+ let capturedOpts: any = {};
179
+ const exec = mock((cmd: string, opts: object) => { capturedCmd = cmd; capturedOpts = opts; });
180
+
181
+ const deps = createMockSetupDeps();
182
+ deps.resolveDir = () => dir;
183
+ const cli = new Cli(deps);
184
+ cli.claude(exec);
185
+
186
+ fs.rmSync(dir, { recursive: true });
187
+
188
+ expect(capturedCmd).toBe("claude --resume sess-123 --model opus");
189
+ expect(capturedOpts.cwd).toBe("/tmp");
190
+ expect(capturedOpts.stdio).toBe("inherit");
191
+ expect(capturedOpts.env.CLAUDECODE).toBe("");
192
+ });
193
+
194
+ it("omits --resume when no session exists", async () => {
195
+ const fs = await import("node:fs");
196
+ const dir = `/tmp/macroclaw-test-claude-${Date.now()}`;
197
+ fs.mkdirSync(dir, { recursive: true });
198
+ fs.writeFileSync(`${dir}/settings.json`, JSON.stringify({ botToken: "tok", chatId: "123", model: "sonnet", workspace: "/tmp" }));
199
+ fs.writeFileSync(`${dir}/sessions.json`, JSON.stringify({}));
200
+
201
+ let capturedCmd = "";
202
+ const exec = mock((cmd: string, _opts: object) => { capturedCmd = cmd; });
203
+
204
+ const deps = createMockSetupDeps();
205
+ deps.resolveDir = () => dir;
206
+ const cli = new Cli(deps);
207
+ cli.claude(exec);
208
+
209
+ fs.rmSync(dir, { recursive: true });
210
+
211
+ expect(capturedCmd).toBe("claude --model sonnet");
212
+ });
213
+
214
+ it("throws when settings are missing", () => {
215
+ const deps = createMockSetupDeps();
216
+ deps.resolveDir = () => "/nonexistent/path";
217
+ const cli = new Cli(deps);
218
+ expect(() => cli.claude(mock())).toThrow("Settings not found");
219
+ });
220
+ });
221
+
222
+ describe("loadRawSettings", () => {
223
+ it("returns null when file does not exist", () => {
224
+ expect(loadRawSettings("/nonexistent/path")).toBeNull();
225
+ });
226
+
227
+ it("reads and parses valid settings file", async () => {
228
+ const dir = await import("node:fs").then(fs => {
229
+ const d = `/tmp/macroclaw-test-${Date.now()}`;
230
+ fs.mkdirSync(d, { recursive: true });
231
+ fs.writeFileSync(`${d}/settings.json`, JSON.stringify({ botToken: "tok", chatId: "123" }));
232
+ return d;
233
+ });
234
+ const result = loadRawSettings(dir);
235
+ expect(result).toEqual({ botToken: "tok", chatId: "123" });
236
+ await import("node:fs").then(fs => fs.rmSync(dir, { recursive: true }));
237
+ });
238
+
239
+ it("returns null for invalid JSON", async () => {
240
+ const dir = await import("node:fs").then(fs => {
241
+ const d = `/tmp/macroclaw-test-${Date.now()}`;
242
+ fs.mkdirSync(d, { recursive: true });
243
+ fs.writeFileSync(`${d}/settings.json`, "not json");
244
+ return d;
245
+ });
246
+ expect(loadRawSettings(dir)).toBeNull();
247
+ await import("node:fs").then(fs => fs.rmSync(dir, { recursive: true }));
248
+ });
249
+
250
+ it("returns null for non-object JSON", async () => {
251
+ const dir = await import("node:fs").then(fs => {
252
+ const d = `/tmp/macroclaw-test-${Date.now()}`;
253
+ fs.mkdirSync(d, { recursive: true });
254
+ fs.writeFileSync(`${d}/settings.json`, '"just a string"');
255
+ return d;
256
+ });
257
+ expect(loadRawSettings(dir)).toBeNull();
258
+ await import("node:fs").then(fs => fs.rmSync(dir, { recursive: true }));
259
+ });
260
+ });
261
+
262
+ describe("handleError", () => {
263
+ it("prints error message and exits", () => {
264
+ const mockExit = mock((_code?: number) => { throw new Error("exit"); });
265
+ const mockConsoleError = mock((..._args: unknown[]) => {});
266
+ const origExit = process.exit;
267
+ const origError = console.error;
268
+ process.exit = mockExit as typeof process.exit;
269
+ console.error = mockConsoleError;
270
+ try {
271
+ handleError(new Error("Settings not found."));
272
+ } catch { /* exit throws */ }
273
+ process.exit = origExit;
274
+ console.error = origError;
275
+ expect(mockConsoleError).toHaveBeenCalledWith("Settings not found.");
276
+ expect(mockExit).toHaveBeenCalledWith(1);
277
+ });
278
+
279
+ it("handles non-Error values", () => {
280
+ const mockExit = mock((_code?: number) => { throw new Error("exit"); });
281
+ const mockConsoleError = mock((..._args: unknown[]) => {});
282
+ const origExit = process.exit;
283
+ const origError = console.error;
284
+ process.exit = mockExit as typeof process.exit;
285
+ console.error = mockConsoleError;
286
+ try {
287
+ handleError("string error");
288
+ } catch { /* exit throws */ }
289
+ process.exit = origExit;
290
+ console.error = origError;
291
+ expect(mockConsoleError).toHaveBeenCalledWith("string error");
292
+ });
293
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,186 @@
1
+ import {execSync} from "node:child_process";
2
+ import {existsSync, readFileSync} from "node:fs";
3
+ import {join, resolve} from "node:path";
4
+ import {createInterface} from "node:readline";
5
+ import {defineCommand} from "citty";
6
+ import {initLogger} from "./logger";
7
+ import {ServiceManager, type SystemService} from "./service";
8
+ import {loadSessions} from "./sessions";
9
+ import {loadSettings, saveSettings} from "./settings";
10
+ import {runSetupWizard} from "./setup";
11
+
12
+ export interface SetupDeps {
13
+ initLogger: () => Promise<void>;
14
+ saveSettings: (settings: unknown, dir: string) => void;
15
+ loadRawSettings: (dir: string) => Record<string, unknown> | null;
16
+ runSetupWizard: (io: { ask: (q: string) => Promise<string>; write: (m: string) => void; close?: () => void }, opts?: { defaults?: Record<string, unknown>; onSettingsReady?: (settings: unknown) => void }) => Promise<unknown>;
17
+ createReadlineInterface: () => { question: (q: string, cb: (a: string) => void) => void; close: () => void };
18
+ resolveDir: () => string;
19
+ }
20
+
21
+ export function loadRawSettings(dir: string): Record<string, unknown> | null {
22
+ const path = join(dir, "settings.json");
23
+ if (!existsSync(path)) return null;
24
+ try {
25
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
26
+ return typeof raw === "object" && raw !== null ? raw : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function defaultSetupDeps(): SetupDeps {
33
+ return {
34
+ initLogger,
35
+ saveSettings: saveSettings as (settings: unknown, dir: string) => void,
36
+ loadRawSettings,
37
+ runSetupWizard: runSetupWizard as SetupDeps["runSetupWizard"],
38
+ createReadlineInterface: () => createInterface({ input: process.stdin, output: process.stdout }),
39
+ resolveDir: () => resolve(process.env.HOME || "~", ".macroclaw"),
40
+ };
41
+ }
42
+
43
+ function defaultSystemService(): SystemService {
44
+ return new ServiceManager();
45
+ }
46
+
47
+ export class Cli {
48
+ readonly #setupDeps: SetupDeps;
49
+ readonly #systemService: SystemService;
50
+
51
+ constructor(setupDeps?: Partial<SetupDeps>, systemService?: SystemService) {
52
+ this.#setupDeps = { ...defaultSetupDeps(), ...setupDeps };
53
+ this.#systemService = systemService ?? defaultSystemService();
54
+ }
55
+
56
+ async setup(): Promise<void> {
57
+ await this.#setupDeps.initLogger();
58
+ const rl = this.#setupDeps.createReadlineInterface();
59
+ const io = {
60
+ ask: (question: string): Promise<string> =>
61
+ new Promise((res) => rl.question(question, (answer: string) => res(answer.trim()))),
62
+ write: (msg: string) => process.stdout.write(msg),
63
+ close: () => rl.close(),
64
+ };
65
+ const dir = this.#setupDeps.resolveDir();
66
+ const defaults = this.#setupDeps.loadRawSettings(dir) ?? undefined;
67
+ const settings = await this.#setupDeps.runSetupWizard(io, {
68
+ defaults,
69
+ onSettingsReady: (s) => this.#setupDeps.saveSettings(s, dir),
70
+ });
71
+ this.#setupDeps.saveSettings(settings, dir);
72
+ }
73
+
74
+ claude(exec: (cmd: string, opts: object) => void = (cmd, opts) => execSync(cmd, opts)): void {
75
+ const dir = this.#setupDeps.resolveDir();
76
+ const settings = loadSettings(dir);
77
+ if (!settings) throw new Error("Settings not found. Run `macroclaw setup` first.");
78
+ const sessions = loadSessions(dir);
79
+ const args = ["claude"];
80
+ if (sessions.mainSessionId) args.push("--resume", sessions.mainSessionId);
81
+ args.push("--model", settings.model);
82
+ exec(args.join(" "), { cwd: settings.workspace, stdio: "inherit", env: { ...process.env, CLAUDECODE: "" } });
83
+ }
84
+
85
+ service(action: string, token?: string): void {
86
+ switch (action) {
87
+ case "install": {
88
+ const logCmd = this.#systemService.install(token);
89
+ console.log(`Service installed and started. Check logs:\n ${logCmd}`);
90
+ break;
91
+ }
92
+ case "uninstall":
93
+ this.#systemService.uninstall();
94
+ console.log("Service uninstalled.");
95
+ break;
96
+ case "start": {
97
+ const logCmd = this.#systemService.start();
98
+ console.log(`Service started. Check logs:\n ${logCmd}`);
99
+ break;
100
+ }
101
+ case "stop":
102
+ this.#systemService.stop();
103
+ console.log("Service stopped.");
104
+ break;
105
+ case "update": {
106
+ const logCmd = this.#systemService.update();
107
+ console.log(`Service updated. Check logs:\n ${logCmd}`);
108
+ break;
109
+ }
110
+ default:
111
+ throw new Error(`Unknown service action: ${action}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ export function handleError(err: unknown): never {
117
+ console.error(err instanceof Error ? err.message : String(err));
118
+ process.exit(1);
119
+ }
120
+
121
+ async function runStart(): Promise<void> {
122
+ const { start } = await import("./index");
123
+ await start();
124
+ }
125
+
126
+ const defaultCli = new Cli();
127
+
128
+ const startCommand = defineCommand({
129
+ meta: { name: "start", description: "Start the macroclaw bridge" },
130
+ run: () => runStart().catch(handleError),
131
+ });
132
+
133
+ const setupCommand = defineCommand({
134
+ meta: { name: "setup", description: "Run the interactive setup wizard" },
135
+ run: () => defaultCli.setup().catch(handleError),
136
+ });
137
+
138
+ const claudeCommand = defineCommand({
139
+ meta: { name: "claude", description: "Open Claude Code CLI in the main session" },
140
+ run: () => { try { defaultCli.claude(); } catch (err) { handleError(err); } },
141
+ });
142
+
143
+ const serviceInstallCommand = defineCommand({
144
+ meta: { name: "install", description: "Install and start macroclaw as a system service" },
145
+ args: {
146
+ token: { type: "string", description: "Claude OAuth token from `claude setup-token` (required on macOS)" },
147
+ },
148
+ run: ({ args }) => { try { defaultCli.service("install", args.token); } catch (err) { handleError(err); } },
149
+ });
150
+
151
+ const serviceUninstallCommand = defineCommand({
152
+ meta: { name: "uninstall", description: "Stop and remove the system service" },
153
+ run: () => { try { defaultCli.service("uninstall"); } catch (err) { handleError(err); } },
154
+ });
155
+
156
+ const serviceStartCommand = defineCommand({
157
+ meta: { name: "start", description: "Start the system service" },
158
+ run: () => { try { defaultCli.service("start"); } catch (err) { handleError(err); } },
159
+ });
160
+
161
+ const serviceStopCommand = defineCommand({
162
+ meta: { name: "stop", description: "Stop the system service" },
163
+ run: () => { try { defaultCli.service("stop"); } catch (err) { handleError(err); } },
164
+ });
165
+
166
+ const serviceUpdateCommand = defineCommand({
167
+ meta: { name: "update", description: "Reinstall latest version and restart the service" },
168
+ run: () => { try { defaultCli.service("update"); } catch (err) { handleError(err); } },
169
+ });
170
+
171
+ const serviceCommand = defineCommand({
172
+ meta: { name: "service", description: "Manage macroclaw system service" },
173
+ subCommands: {
174
+ install: serviceInstallCommand,
175
+ uninstall: serviceUninstallCommand,
176
+ start: serviceStartCommand,
177
+ stop: serviceStopCommand,
178
+ update: serviceUpdateCommand,
179
+ },
180
+ });
181
+
182
+ export const main = defineCommand({
183
+ meta: { name: "macroclaw", description: "Telegram-to-Claude-Code bridge", version: "0.0.0-dev" },
184
+ subCommands: { start: startCommand, setup: setupCommand, claude: claudeCommand, service: serviceCommand },
185
+ });
186
+