@dhf-claude/grix 0.1.8 → 0.1.10
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/.claude-plugin/plugin.json +1 -1
- package/README.md +133 -86
- package/cli/prod-runner.js +163 -0
- package/cli/prod-runner.test.js +195 -0
- package/cli/runtime-targets.js +66 -0
- package/dist/daemon.js +2059 -905
- package/dist/index.js +1364 -2033
- package/hooks/hooks.json +34 -1
- package/package.json +8 -6
- package/scripts/dev-start.js +14 -0
- package/scripts/elicitation-hook.js +34 -27
- package/scripts/lifecycle-hook.js +3 -0
- package/scripts/notification-hook.js +0 -8
- package/scripts/prod-start.js +7 -0
- package/scripts/user-prompt-submit-hook.js +2 -1
- package/skills/grix/SKILL.md +121 -0
- package/skills/access/SKILL.md +0 -129
- package/skills/status/SKILL.md +0 -11
package/README.md
CHANGED
|
@@ -1,35 +1,43 @@
|
|
|
1
1
|
# @dhf-claude/grix
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect local Claude Code to Grix so you can use Claude from the Grix website or mobile PWA.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Before You Start
|
|
6
|
+
|
|
7
|
+
Make sure this machine already has:
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
19
|
+
claude auth login
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
## Quick Start
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
### 1. Install the package
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @dhf-claude/grix
|
|
28
|
+
```
|
|
20
29
|
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
51
|
-
- `restart
|
|
52
|
-
- `stop
|
|
53
|
-
- `start
|
|
54
|
-
- `uninstall
|
|
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
|
-
##
|
|
82
|
+
## Commands in Grix Chat
|
|
57
83
|
|
|
58
|
-
|
|
84
|
+
Use these in the related Grix private chat:
|
|
59
85
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
93
|
+
## Commands You Run Inside Claude
|
|
65
94
|
|
|
66
|
-
|
|
67
|
-
grix-claude
|
|
68
|
-
```
|
|
95
|
+
If you are already inside the Claude terminal session, use the same `/grix ...` command family:
|
|
69
96
|
|
|
70
|
-
|
|
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
|
-
|
|
107
|
+
Access changes should be typed by you in the Claude terminal, not driven by messages from other people in chat.
|
|
73
108
|
|
|
74
|
-
|
|
75
|
-
open <your_working_directory>
|
|
76
|
-
```
|
|
109
|
+
## Approvals and Questions
|
|
77
110
|
|
|
78
|
-
|
|
111
|
+
When Claude needs confirmation or needs more information, Grix will show interactive cards.
|
|
79
112
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
## Temporary Foreground Run
|
|
87
120
|
|
|
88
|
-
|
|
121
|
+
If you do not want to install a background service, you can run in the foreground:
|
|
89
122
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
127
|
+
If config is already saved locally, you can also just run:
|
|
98
128
|
|
|
99
|
-
|
|
129
|
+
```bash
|
|
130
|
+
grix-claude
|
|
131
|
+
```
|
|
100
132
|
|
|
101
|
-
|
|
133
|
+
## File Sending
|
|
102
134
|
|
|
103
|
-
|
|
135
|
+
Claude can send local files back to Grix.
|
|
104
136
|
|
|
105
|
-
-
|
|
106
|
-
-
|
|
137
|
+
- Maximum size per file: `50MB`
|
|
138
|
+
- Common image, video, document, archive, and text formats are supported
|
|
107
139
|
|
|
108
|
-
|
|
140
|
+
## Troubleshooting
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
### Check service status
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
grix-claude status
|
|
114
146
|
```
|
|
115
147
|
|
|
116
|
-
|
|
148
|
+
### If Claude login expired
|
|
117
149
|
|
|
118
|
-
|
|
150
|
+
```bash
|
|
151
|
+
claude auth login
|
|
152
|
+
```
|
|
119
153
|
|
|
120
|
-
|
|
154
|
+
Then retry from Grix.
|
|
121
155
|
|
|
122
|
-
|
|
156
|
+
### Session log path
|
|
123
157
|
|
|
124
|
-
Each
|
|
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
|
|
164
|
+
This log is the best place to check when:
|
|
131
165
|
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
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
|
-
|
|
170
|
+
More details:
|
|
138
171
|
|
|
139
172
|
- `docs/session-log-troubleshooting.md`
|
|
140
173
|
|
|
141
|
-
## CLI
|
|
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
|
-
|
|
186
|
+
Recommended default:
|
|
154
187
|
|
|
155
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
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
|
-
|
|
211
|
+
## For Developers
|
|
174
212
|
|
|
175
|
-
|
|
213
|
+
If you are changing code in this repository:
|
|
176
214
|
|
|
177
|
-
|
|
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
|
-
|
|
227
|
+
This uses a separate local data directory so it does not overwrite the production daemon state.
|
|
184
228
|
|
|
185
|
-
|
|
186
|
-
- `dist/daemon.js`
|
|
229
|
+
To start `grix-claude` against the current production environment:
|
|
187
230
|
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|