@dhf-claude/grix 0.1.8 → 0.1.9

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
@@ -1,35 +1,43 @@
1
1
  # @dhf-claude/grix
2
2
 
3
- This integration connects Claude to Grix ([https://grix.dhf.pub/](https://grix.dhf.pub/)) so you can manage Claude from the website, with mobile PWA support.
3
+ Connect local Claude Code to Grix so you can use Claude from the Grix website or mobile PWA.
4
4
 
5
- ## Quick Start
5
+ ## Before You Start
6
+
7
+ Make sure this machine already has:
6
8
 
7
- ### 1. Install globally
9
+ - Claude Code installed, and the `claude` command works in your terminal
10
+ - A valid Claude login on this machine
11
+ - The 3 connection values from the Grix console:
12
+ - `wsUrl`
13
+ - `agentId`
14
+ - `apiKey`
15
+
16
+ If Claude is not logged in yet, run:
8
17
 
9
18
  ```bash
10
- npm install -g @dhf-claude/grix
19
+ claude auth login
11
20
  ```
12
21
 
13
- ### 2. Install the background service (first time)
22
+ ## Quick Start
14
23
 
15
- Get these 3 values from the Grix console first:
24
+ ### 1. Install the package
16
25
 
17
- - `wsUrl`
18
- - `agentId`
19
- - `apiKey`
26
+ ```bash
27
+ npm install -g @dhf-claude/grix
28
+ ```
20
29
 
21
- Then run:
30
+ ### 2. Install and start the background service
22
31
 
23
32
  ```bash
24
33
  grix-claude install --ws-url <ws_url> --agent-id <agent_id> --api-key <api_key>
25
34
  ```
26
35
 
27
- This command will automatically:
36
+ This will:
28
37
 
29
- - Save connection settings
38
+ - Save your connection settings locally
30
39
  - Install a user-level background service
31
- - Start the local `daemon` immediately
32
- - Let `daemon` handle session startup, resume, and message relay
40
+ - Start the local service immediately
33
41
 
34
42
  Supported background service managers:
35
43
 
@@ -37,7 +45,25 @@ Supported background service managers:
37
45
  - Linux: `systemd --user`
38
46
  - Windows: Task Scheduler
39
47
 
40
- ## Commands you will usually use
48
+ ### 3. Start a Claude session from Grix
49
+
50
+ Open the related Grix private chat and do either of these:
51
+
52
+ - If Grix shows an open-workspace card, use that card
53
+ - Or send:
54
+
55
+ ```text
56
+ /grix open <your_working_directory>
57
+ ```
58
+
59
+ The background service will start or resume the Claude session for that directory.
60
+
61
+ Important:
62
+
63
+ - One Grix private chat is bound to one working directory
64
+ - After a chat is bound, that same chat cannot switch to another directory
65
+
66
+ ## Commands You Will Usually Use
41
67
 
42
68
  ```bash
43
69
  grix-claude status
@@ -47,98 +73,105 @@ grix-claude start
47
73
  grix-claude uninstall
48
74
  ```
49
75
 
50
- - `status` checks service and connection status
51
- - `restart` restarts after config changes
52
- - `stop` temporarily stops the background service
53
- - `start` starts the background service again
54
- - `uninstall` removes the background startup entry
76
+ - `status`: show service and connection status
77
+ - `restart`: restart after config changes or troubleshooting
78
+ - `stop`: stop the background service
79
+ - `start`: start it again
80
+ - `uninstall`: remove the background startup entry
55
81
 
56
- ## If you only want a temporary foreground run
82
+ ## Commands in Grix Chat
57
83
 
58
- You can run without installing a background service:
84
+ Use these in the related Grix private chat:
59
85
 
60
- ```bash
61
- grix-claude --ws-url <ws_url> --agent-id <agent_id> --api-key <api_key>
62
- ```
86
+ | Command | Purpose |
87
+ | --- | --- |
88
+ | `/grix open <working_directory>` | Start or resume a Claude session for that directory |
89
+ | `/grix status` | Show current session status |
90
+ | `/grix where` | Show the current bound directory |
91
+ | `/grix stop` | Stop the current Claude session |
63
92
 
64
- If config is already saved locally, you can also just run:
93
+ ## Commands You Run Inside Claude
65
94
 
66
- ```bash
67
- grix-claude
68
- ```
95
+ If you are already inside the Claude terminal session, use the same `/grix ...` command family:
69
96
 
70
- ## How to start a Claude session
97
+ | Command | Purpose |
98
+ | --- | --- |
99
+ | `/grix status` | Show current connection and status hints |
100
+ | `/grix access` | Show current access control state |
101
+ | `/grix access pair <code>` | Approve a pairing code |
102
+ | `/grix access deny <code>` | Reject a pairing code |
103
+ | `/grix access allow <sender_id>` | Add a sender to the allowlist |
104
+ | `/grix access remove <sender_id>` | Remove a sender from the allowlist |
105
+ | `/grix access policy <allowlist\|open\|disabled>` | Change access policy |
71
106
 
72
- Send this in the related Grix private chat:
107
+ Access changes should be typed by you in the Claude terminal, not driven by messages from other people in chat.
73
108
 
74
- ```text
75
- open <your_working_directory>
76
- ```
109
+ ## Approvals and Questions
77
110
 
78
- `daemon` will start or resume the matching Claude session for that directory.
111
+ When Claude needs confirmation or needs more information, Grix will show interactive cards.
79
112
 
80
- If you are already inside Claude, run:
113
+ - For approvals, click the card buttons to approve or reject
114
+ - For questions, fill the card and submit
115
+ - For browser-based sign-in steps, open the link from the card and then return to the card to finish or cancel
81
116
 
82
- ```text
83
- /grix:status
84
- ```
117
+ These cards are the normal user flow. Legacy text fallback commands are not part of normal use anymore.
85
118
 
86
- If the worker is attached to daemon, the link is healthy.
119
+ ## Temporary Foreground Run
87
120
 
88
- ## Common commands inside Claude
121
+ If you do not want to install a background service, you can run in the foreground:
89
122
 
90
- | Command | Purpose |
91
- | --- | --- |
92
- | `/grix:status` | Check current connection status |
93
- | `/grix:access` | Check current access control |
94
- | `/grix:access pair <code>` | Allow a new private-chat sender |
95
- | `/grix:access policy <allowlist\|open\|disabled>` | Switch access policy |
123
+ ```bash
124
+ grix-claude --ws-url <ws_url> --agent-id <agent_id> --api-key <api_key>
125
+ ```
96
126
 
97
- Connection parameters are now managed only through local CLI, not from inside Claude sessions.
127
+ If config is already saved locally, you can also just run:
98
128
 
99
- ## Approvals and questions
129
+ ```bash
130
+ grix-claude
131
+ ```
100
132
 
101
- When Claude needs your confirmation or more information, messages are sent back to Grix.
133
+ ## File Sending
102
134
 
103
- Interactive cards are used by default:
135
+ Claude can send local files back to Grix.
104
136
 
105
- - For approvals, click approve/reject on the card
106
- - For questions, fill the card and submit
137
+ - Maximum size per file: `50MB`
138
+ - Common image, video, document, archive, and text formats are supported
107
139
 
108
- Text commands are still available as fallback:
140
+ ## Troubleshooting
109
141
 
110
- ```text
111
- yes <request_id>
112
- no <request_id>
113
- /grix-question <request_id> your_answer
142
+ ### Check service status
143
+
144
+ ```bash
145
+ grix-claude status
114
146
  ```
115
147
 
116
- - Use manual text input only for debugging, troubleshooting, or when cards are unavailable
148
+ ### If Claude login expired
117
149
 
118
- ## File sending
150
+ ```bash
151
+ claude auth login
152
+ ```
119
153
 
120
- Claude can send local files back to Grix. Maximum file size is 50MB, and only common image/video/document formats are supported.
154
+ Then retry from Grix.
121
155
 
122
- ## Log troubleshooting
156
+ ### Session log path
123
157
 
124
- Each AIBot session ID has an independent log file:
158
+ Each Grix chat session has its own log file:
125
159
 
126
160
  ```text
127
161
  ~/.claude/grix-claude-daemon/sessions/<aibot_session_id>/logs/daemon-session.log
128
162
  ```
129
163
 
130
- This log records full Claude scheduling flow for that session, including:
164
+ This log is the best place to check when:
131
165
 
132
- - Worker process state changes and PID
133
- - Process relaunch after exit
134
- - Message delivery and result callbacks
135
- - Connectivity probes and timeout decisions
166
+ - Claude does not reply
167
+ - Claude keeps restarting
168
+ - Messages are not delivered back to Grix
136
169
 
137
- Full troubleshooting steps:
170
+ More details:
138
171
 
139
172
  - `docs/session-log-troubleshooting.md`
140
173
 
141
- ## CLI commands
174
+ ## CLI Reference
142
175
 
143
176
  ```text
144
177
  grix-claude install [options]
@@ -150,9 +183,12 @@ grix-claude uninstall [options]
150
183
  grix-claude [options]
151
184
  ```
152
185
 
153
- `install` is the recommended default. The plain `grix-claude [options]` command is better for temporary foreground runs or debugging.
186
+ Recommended default:
154
187
 
155
- ## Common options
188
+ - Use `install` for normal long-running use
189
+ - Use plain `grix-claude` only for temporary foreground runs or debugging
190
+
191
+ ## Common Options
156
192
 
157
193
  ```text
158
194
  --ws-url <value> Grix Agent API WebSocket URL
@@ -165,35 +201,46 @@ grix-claude [options]
165
201
  --help, -h show help
166
202
  ```
167
203
 
168
- - On first `install` or first foreground run, pass full connection parameters
169
- - If config has already been saved locally, you can omit connection parameters
170
- - Use `--data-dir` to isolate data directories across environments
204
+ Notes:
205
+
206
+ - On first `install` or first foreground run, pass the full connection parameters
207
+ - If config is already saved locally, you can omit connection parameters
208
+ - Use `--data-dir` if you want a separate data directory for another environment
171
209
  - `--show-claude` currently supports macOS Terminal only
172
210
 
173
- If Claude seems stuck on the startup confirmation page during development, add `--show-claude` so daemon opens the Claude session in a visible Terminal window.
211
+ ## For Developers
174
212
 
175
- ## Auto-build during development
213
+ If you are changing code in this repository:
176
214
 
177
- If you are changing code in this repository, run:
215
+ ```bash
216
+ npm run dev:build
217
+ ```
218
+
219
+ This keeps local build artifacts up to date.
220
+
221
+ To start `grix-claude` against the local development environment:
178
222
 
179
223
  ```bash
180
224
  npm run dev
181
225
  ```
182
226
 
183
- It continuously watches source changes and builds the latest artifacts to:
227
+ This uses a separate local data directory so it does not overwrite the production daemon state.
184
228
 
185
- - `dist/index.js`
186
- - `dist/daemon.js`
229
+ To start `grix-claude` against the current production environment:
187
230
 
188
- For local integration testing, run this in another terminal:
231
+ ```bash
232
+ npm run prod
233
+ ```
234
+
235
+ This uses the standard production daemon state directory.
236
+
237
+ To run the daemon locally in another terminal:
189
238
 
190
239
  ```bash
191
240
  npm run daemon
192
241
  ```
193
242
 
194
- Then both the daemon process and the worker loaded in Claude sessions will use the latest local build artifacts.
195
-
196
- If you want `npm run daemon` to read connection parameters directly from environment variables, run:
243
+ If you want `npm run daemon` to read connection values from environment variables:
197
244
 
198
245
  ```bash
199
246
  GRIX_CLAUDE_ENDPOINT='ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=<agent_id>' \
@@ -202,4 +249,4 @@ GRIX_CLAUDE_API_KEY='<api_key>' \
202
249
  npm run daemon -- --no-launch
203
250
  ```
204
251
 
205
- `GRIX_CLAUDE_WS_URL` is still supported; if both are provided, daemon prefers environment variable values.
252
+ `GRIX_CLAUDE_WS_URL` is still supported. If both are provided, the daemon prefers the newer environment variable values.
@@ -0,0 +1,163 @@
1
+ import os from "node:os";
2
+ import process from "node:process";
3
+ import { run as runCli } from "./main.js";
4
+ import { ServiceManager } from "../server/service/service-manager.js";
5
+ import { inspectDaemonProcessState } from "../server/daemon/process-state.js";
6
+ import { terminateProcessTree, waitForProcessExit } from "../server/process-control.js";
7
+ import {
8
+ buildRuntimeArgs,
9
+ createManagedCommandEnv,
10
+ resolveRuntimeTarget,
11
+ } from "./runtime-targets.js";
12
+
13
+ const PROCESS_EXIT_TIMEOUT_MS = 5000;
14
+
15
+ function printLine(message, print = (line) => process.stdout.write(`${line}\n`)) {
16
+ if (typeof print === "function") {
17
+ print(String(message ?? ""));
18
+ }
19
+ }
20
+
21
+ function didTargetStop(result) {
22
+ return Boolean(result?.serviceStopped || result?.processTerminated);
23
+ }
24
+
25
+ export async function stopRuntimeTarget(
26
+ target,
27
+ {
28
+ serviceManager,
29
+ inspectDaemonProcessStateImpl = inspectDaemonProcessState,
30
+ terminateProcessTreeImpl = terminateProcessTree,
31
+ waitForProcessExitImpl = waitForProcessExit,
32
+ platform = process.platform,
33
+ } = {},
34
+ ) {
35
+ if (!target?.dataDir) {
36
+ throw new Error("runtime target dataDir is required");
37
+ }
38
+ if (!serviceManager) {
39
+ throw new Error("serviceManager is required");
40
+ }
41
+
42
+ const status = await serviceManager.status({
43
+ dataDir: target.dataDir,
44
+ });
45
+ const serviceStopped = Boolean(status?.installed);
46
+ if (serviceStopped) {
47
+ await serviceManager.stop({
48
+ dataDir: target.dataDir,
49
+ });
50
+ }
51
+
52
+ const state = await inspectDaemonProcessStateImpl({
53
+ dataDir: target.dataDir,
54
+ });
55
+ const pid = Number(state?.pid ?? 0);
56
+ if (!state?.running || !Number.isFinite(pid) || pid <= 0) {
57
+ return {
58
+ target,
59
+ serviceStopped,
60
+ processTerminated: false,
61
+ pid,
62
+ running: Boolean(state?.running),
63
+ };
64
+ }
65
+
66
+ await terminateProcessTreeImpl(pid, {
67
+ platform,
68
+ });
69
+ const exited = await waitForProcessExitImpl(pid, {
70
+ timeoutMs: PROCESS_EXIT_TIMEOUT_MS,
71
+ });
72
+ if (!exited) {
73
+ throw new Error(`failed to stop ${target.name} daemon pid=${pid}`);
74
+ }
75
+
76
+ return {
77
+ target,
78
+ serviceStopped,
79
+ processTerminated: true,
80
+ pid,
81
+ running: false,
82
+ };
83
+ }
84
+
85
+ export async function runProdSwitch(
86
+ env = process.env,
87
+ {
88
+ runCliImpl = runCli,
89
+ serviceManager = null,
90
+ homeDir = os.homedir(),
91
+ print = (line) => process.stdout.write(`${line}\n`),
92
+ inspectDaemonProcessStateImpl = inspectDaemonProcessState,
93
+ terminateProcessTreeImpl = terminateProcessTree,
94
+ waitForProcessExitImpl = waitForProcessExit,
95
+ platform = process.platform,
96
+ } = {},
97
+ ) {
98
+ const runtimeEnv = createManagedCommandEnv(env);
99
+ const manager = serviceManager || new ServiceManager({
100
+ env: runtimeEnv,
101
+ homeDir,
102
+ });
103
+ const devTarget = resolveRuntimeTarget("dev", { homeDir });
104
+ const prodTarget = resolveRuntimeTarget("prod", { homeDir });
105
+
106
+ printLine("正在停止 dev...", print);
107
+ const devStopResult = await stopRuntimeTarget(devTarget, {
108
+ serviceManager: manager,
109
+ inspectDaemonProcessStateImpl,
110
+ terminateProcessTreeImpl,
111
+ waitForProcessExitImpl,
112
+ platform,
113
+ });
114
+ printLine(
115
+ didTargetStop(devStopResult)
116
+ ? "dev 已停止。"
117
+ : "dev 没有运行,无需处理。",
118
+ print,
119
+ );
120
+
121
+ printLine("正在清理旧的 prod 进程...", print);
122
+ const prodStopResult = await stopRuntimeTarget(prodTarget, {
123
+ serviceManager: manager,
124
+ inspectDaemonProcessStateImpl,
125
+ terminateProcessTreeImpl,
126
+ waitForProcessExitImpl,
127
+ platform,
128
+ });
129
+ printLine(
130
+ didTargetStop(prodStopResult)
131
+ ? "旧的 prod 进程已清理。"
132
+ : "没有发现旧的 prod 进程。",
133
+ print,
134
+ );
135
+
136
+ const prodStatus = await manager.status({
137
+ dataDir: prodTarget.dataDir,
138
+ });
139
+ const subcommand = prodStatus.installed ? "start" : "install";
140
+ printLine(
141
+ subcommand === "install"
142
+ ? "正在安装并启动后台服务..."
143
+ : "正在启动后台服务...",
144
+ print,
145
+ );
146
+ const exitCode = await runCliImpl([
147
+ subcommand,
148
+ ...buildRuntimeArgs(prodTarget),
149
+ ], runtimeEnv, {
150
+ serviceManager: manager,
151
+ });
152
+ if (Number(exitCode ?? 0) !== 0) {
153
+ throw new Error(`prod command failed with exit code ${exitCode}`);
154
+ }
155
+ printLine("prod 已切到后台运行。", print);
156
+
157
+ return {
158
+ devStopResult,
159
+ prodStopResult,
160
+ prodTarget,
161
+ subcommand,
162
+ };
163
+ }
@@ -0,0 +1,195 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import path from "node:path";
4
+ import {
5
+ buildRuntimeArgs,
6
+ createManagedCommandEnv,
7
+ resolveRuntimeTarget,
8
+ } from "./runtime-targets.js";
9
+ import { runProdSwitch, stopRuntimeTarget } from "./prod-runner.js";
10
+
11
+ test("createManagedCommandEnv removes connection overrides", () => {
12
+ const env = createManagedCommandEnv({
13
+ KEEP_ME: "1",
14
+ GRIX_CLAUDE_WS_URL: "ws://stale",
15
+ GRIX_CLAUDE_ENDPOINT: "ws://stale-endpoint",
16
+ GRIX_CLAUDE_AGENT_ID: "stale-agent",
17
+ GRIX_CLAUDE_API_KEY: "stale-key",
18
+ GRIX_CLAUDE_OUTBOUND_TEXT_CHUNK_LIMIT: "4096",
19
+ GRIX_CLAUDE_TEXT_CHUNK_LIMIT: "4096",
20
+ });
21
+
22
+ assert.equal(env.KEEP_ME, "1");
23
+ assert.equal("GRIX_CLAUDE_WS_URL" in env, false);
24
+ assert.equal("GRIX_CLAUDE_ENDPOINT" in env, false);
25
+ assert.equal("GRIX_CLAUDE_AGENT_ID" in env, false);
26
+ assert.equal("GRIX_CLAUDE_API_KEY" in env, false);
27
+ assert.equal("GRIX_CLAUDE_OUTBOUND_TEXT_CHUNK_LIMIT" in env, false);
28
+ assert.equal("GRIX_CLAUDE_TEXT_CHUNK_LIMIT" in env, false);
29
+ });
30
+
31
+ test("resolveRuntimeTarget builds the expected prod args", () => {
32
+ const target = resolveRuntimeTarget("prod", {
33
+ homeDir: "/tmp/grix-home",
34
+ });
35
+
36
+ assert.deepEqual(buildRuntimeArgs(target), [
37
+ "--data-dir",
38
+ path.join("/tmp/grix-home", ".claude", "grix-claude-daemon"),
39
+ "--ws-url",
40
+ "wss://clawpool.dhf.pub/v1/agent-api/ws?agent_id=2035513096339984384",
41
+ "--agent-id",
42
+ "2035513096339984384",
43
+ "--api-key",
44
+ "ak_2035513096339984384_wyIkUkt9FyEZFJGHyvWHPu7ZOXkpj0KM",
45
+ ]);
46
+ });
47
+
48
+ test("stopRuntimeTarget stops installed service before killing a remaining daemon pid", async () => {
49
+ const calls = [];
50
+ const target = {
51
+ name: "dev",
52
+ dataDir: "/tmp/grix-dev",
53
+ };
54
+
55
+ const result = await stopRuntimeTarget(target, {
56
+ serviceManager: {
57
+ async status({ dataDir }) {
58
+ calls.push(["status", dataDir]);
59
+ return { installed: true };
60
+ },
61
+ async stop({ dataDir }) {
62
+ calls.push(["stop", dataDir]);
63
+ },
64
+ },
65
+ async inspectDaemonProcessStateImpl({ dataDir }) {
66
+ calls.push(["inspect", dataDir]);
67
+ return { running: true, pid: 4321 };
68
+ },
69
+ async terminateProcessTreeImpl(pid, { platform }) {
70
+ calls.push(["terminate", pid, platform]);
71
+ },
72
+ async waitForProcessExitImpl(pid, { timeoutMs }) {
73
+ calls.push(["wait", pid, timeoutMs]);
74
+ return true;
75
+ },
76
+ platform: "darwin",
77
+ });
78
+
79
+ assert.deepEqual(calls, [
80
+ ["status", "/tmp/grix-dev"],
81
+ ["stop", "/tmp/grix-dev"],
82
+ ["inspect", "/tmp/grix-dev"],
83
+ ["terminate", 4321, "darwin"],
84
+ ["wait", 4321, 5000],
85
+ ]);
86
+ assert.equal(result.serviceStopped, true);
87
+ assert.equal(result.processTerminated, true);
88
+ });
89
+
90
+ test("runProdSwitch stops dev and installs prod service when prod is not installed", async () => {
91
+ const serviceCalls = [];
92
+ const cliCalls = [];
93
+ const prints = [];
94
+ const prodDir = path.join("/tmp/grix-home", ".claude", "grix-claude-daemon");
95
+ const devDir = path.join("/tmp/grix-home", ".claude", "grix-claude-daemon-dev");
96
+
97
+ await runProdSwitch({
98
+ GRIX_CLAUDE_AGENT_ID: "stale-agent",
99
+ KEEP_ME: "1",
100
+ }, {
101
+ homeDir: "/tmp/grix-home",
102
+ print(line) {
103
+ prints.push(String(line));
104
+ },
105
+ serviceManager: {
106
+ async status({ dataDir }) {
107
+ serviceCalls.push(["status", dataDir]);
108
+ return { installed: dataDir === devDir ? false : false };
109
+ },
110
+ },
111
+ async inspectDaemonProcessStateImpl({ dataDir }) {
112
+ serviceCalls.push(["inspect", dataDir]);
113
+ if (dataDir === devDir) {
114
+ return { running: true, pid: 2222 };
115
+ }
116
+ return { running: false, pid: 0 };
117
+ },
118
+ async terminateProcessTreeImpl(pid) {
119
+ serviceCalls.push(["terminate", pid]);
120
+ },
121
+ async waitForProcessExitImpl(pid) {
122
+ serviceCalls.push(["wait", pid]);
123
+ return true;
124
+ },
125
+ async runCliImpl(argv, env, deps) {
126
+ cliCalls.push({ argv, env, deps });
127
+ return 0;
128
+ },
129
+ });
130
+
131
+ assert.deepEqual(serviceCalls, [
132
+ ["status", devDir],
133
+ ["inspect", devDir],
134
+ ["terminate", 2222],
135
+ ["wait", 2222],
136
+ ["status", prodDir],
137
+ ["inspect", prodDir],
138
+ ["status", prodDir],
139
+ ]);
140
+ assert.equal(cliCalls.length, 1);
141
+ assert.equal(cliCalls[0].argv[0], "install");
142
+ assert.equal(cliCalls[0].argv.includes(prodDir), true);
143
+ assert.equal("GRIX_CLAUDE_AGENT_ID" in cliCalls[0].env, false);
144
+ assert.equal(cliCalls[0].env.KEEP_ME, "1");
145
+ assert.ok(cliCalls[0].deps.serviceManager);
146
+ assert.match(prints.join("\n"), /dev 已停止/u);
147
+ assert.match(prints.join("\n"), /安装并启动后台服务/u);
148
+ });
149
+
150
+ test("runProdSwitch starts existing prod service after cleaning old prod runtime", async () => {
151
+ const cliCalls = [];
152
+ const prodDir = path.join("/tmp/grix-home-2", ".claude", "grix-claude-daemon");
153
+ const devDir = path.join("/tmp/grix-home-2", ".claude", "grix-claude-daemon-dev");
154
+ const installStateByDir = new Map([
155
+ [devDir, true],
156
+ [prodDir, true],
157
+ ]);
158
+
159
+ await runProdSwitch({}, {
160
+ homeDir: "/tmp/grix-home-2",
161
+ print() {},
162
+ serviceManager: {
163
+ async status({ dataDir }) {
164
+ return { installed: installStateByDir.get(dataDir) === true };
165
+ },
166
+ async stop() {},
167
+ },
168
+ async inspectDaemonProcessStateImpl({ dataDir }) {
169
+ if (dataDir === prodDir) {
170
+ return { running: true, pid: 3333 };
171
+ }
172
+ return { running: false, pid: 0 };
173
+ },
174
+ async terminateProcessTreeImpl() {},
175
+ async waitForProcessExitImpl() {
176
+ return true;
177
+ },
178
+ async runCliImpl(argv) {
179
+ cliCalls.push(argv);
180
+ return 0;
181
+ },
182
+ });
183
+
184
+ assert.deepEqual(cliCalls, [[
185
+ "start",
186
+ "--data-dir",
187
+ prodDir,
188
+ "--ws-url",
189
+ "wss://clawpool.dhf.pub/v1/agent-api/ws?agent_id=2035513096339984384",
190
+ "--agent-id",
191
+ "2035513096339984384",
192
+ "--api-key",
193
+ "ak_2035513096339984384_wyIkUkt9FyEZFJGHyvWHPu7ZOXkpj0KM",
194
+ ]]);
195
+ });