@dhf-claude/grix 0.1.8
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 +18 -0
- package/README.md +205 -0
- package/bin/grix-claude.js +4 -0
- package/cli/config.js +136 -0
- package/cli/config.test.js +63 -0
- package/cli/main.js +369 -0
- package/cli/main.test.js +380 -0
- package/cli/mcp.js +113 -0
- package/cli/mcp.test.js +7 -0
- package/dist/daemon.js +21739 -0
- package/dist/index.js +26480 -0
- package/hooks/hooks.json +77 -0
- package/package.json +51 -0
- package/scripts/dev-build.js +52 -0
- package/scripts/elicitation-hook.js +195 -0
- package/scripts/lifecycle-hook.js +33 -0
- package/scripts/notification-hook.js +31 -0
- package/scripts/npm-publish.exp +21 -0
- package/scripts/user-prompt-submit-hook.js +53 -0
- package/skills/access/SKILL.md +129 -0
- package/skills/status/SKILL.md +11 -0
- package/start.sh +6 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "grix-claude-daemon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code channel plugin for Aibot Grix over the Agent API WebSocket.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Aibot"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://grix.dhf.pub",
|
|
9
|
+
"repository": "https://github.com/askie/clawpool-claude",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"plugin",
|
|
14
|
+
"channel",
|
|
15
|
+
"grix",
|
|
16
|
+
"aibot"
|
|
17
|
+
]
|
|
18
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# @dhf-claude/grix
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Install globally
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @dhf-claude/grix
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Install the background service (first time)
|
|
14
|
+
|
|
15
|
+
Get these 3 values from the Grix console first:
|
|
16
|
+
|
|
17
|
+
- `wsUrl`
|
|
18
|
+
- `agentId`
|
|
19
|
+
- `apiKey`
|
|
20
|
+
|
|
21
|
+
Then run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
grix-claude install --ws-url <ws_url> --agent-id <agent_id> --api-key <api_key>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This command will automatically:
|
|
28
|
+
|
|
29
|
+
- Save connection settings
|
|
30
|
+
- Install a user-level background service
|
|
31
|
+
- Start the local `daemon` immediately
|
|
32
|
+
- Let `daemon` handle session startup, resume, and message relay
|
|
33
|
+
|
|
34
|
+
Supported background service managers:
|
|
35
|
+
|
|
36
|
+
- macOS: `launchd`
|
|
37
|
+
- Linux: `systemd --user`
|
|
38
|
+
- Windows: Task Scheduler
|
|
39
|
+
|
|
40
|
+
## Commands you will usually use
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
grix-claude status
|
|
44
|
+
grix-claude restart
|
|
45
|
+
grix-claude stop
|
|
46
|
+
grix-claude start
|
|
47
|
+
grix-claude uninstall
|
|
48
|
+
```
|
|
49
|
+
|
|
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
|
|
55
|
+
|
|
56
|
+
## If you only want a temporary foreground run
|
|
57
|
+
|
|
58
|
+
You can run without installing a background service:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
grix-claude --ws-url <ws_url> --agent-id <agent_id> --api-key <api_key>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If config is already saved locally, you can also just run:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
grix-claude
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How to start a Claude session
|
|
71
|
+
|
|
72
|
+
Send this in the related Grix private chat:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
open <your_working_directory>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`daemon` will start or resume the matching Claude session for that directory.
|
|
79
|
+
|
|
80
|
+
If you are already inside Claude, run:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
/grix:status
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If the worker is attached to daemon, the link is healthy.
|
|
87
|
+
|
|
88
|
+
## Common commands inside Claude
|
|
89
|
+
|
|
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 |
|
|
96
|
+
|
|
97
|
+
Connection parameters are now managed only through local CLI, not from inside Claude sessions.
|
|
98
|
+
|
|
99
|
+
## Approvals and questions
|
|
100
|
+
|
|
101
|
+
When Claude needs your confirmation or more information, messages are sent back to Grix.
|
|
102
|
+
|
|
103
|
+
Interactive cards are used by default:
|
|
104
|
+
|
|
105
|
+
- For approvals, click approve/reject on the card
|
|
106
|
+
- For questions, fill the card and submit
|
|
107
|
+
|
|
108
|
+
Text commands are still available as fallback:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
yes <request_id>
|
|
112
|
+
no <request_id>
|
|
113
|
+
/grix-question <request_id> your_answer
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- Use manual text input only for debugging, troubleshooting, or when cards are unavailable
|
|
117
|
+
|
|
118
|
+
## File sending
|
|
119
|
+
|
|
120
|
+
Claude can send local files back to Grix. Maximum file size is 50MB, and only common image/video/document formats are supported.
|
|
121
|
+
|
|
122
|
+
## Log troubleshooting
|
|
123
|
+
|
|
124
|
+
Each AIBot session ID has an independent log file:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
~/.claude/grix-claude-daemon/sessions/<aibot_session_id>/logs/daemon-session.log
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This log records full Claude scheduling flow for that session, including:
|
|
131
|
+
|
|
132
|
+
- Worker process state changes and PID
|
|
133
|
+
- Process relaunch after exit
|
|
134
|
+
- Message delivery and result callbacks
|
|
135
|
+
- Connectivity probes and timeout decisions
|
|
136
|
+
|
|
137
|
+
Full troubleshooting steps:
|
|
138
|
+
|
|
139
|
+
- `docs/session-log-troubleshooting.md`
|
|
140
|
+
|
|
141
|
+
## CLI commands
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
grix-claude install [options]
|
|
145
|
+
grix-claude start [options]
|
|
146
|
+
grix-claude stop [options]
|
|
147
|
+
grix-claude restart [options]
|
|
148
|
+
grix-claude status [options]
|
|
149
|
+
grix-claude uninstall [options]
|
|
150
|
+
grix-claude [options]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`install` is the recommended default. The plain `grix-claude [options]` command is better for temporary foreground runs or debugging.
|
|
154
|
+
|
|
155
|
+
## Common options
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
--ws-url <value> Grix Agent API WebSocket URL
|
|
159
|
+
--agent-id <value> Agent ID
|
|
160
|
+
--api-key <value> API Key
|
|
161
|
+
--data-dir <path> daemon data directory
|
|
162
|
+
--chunk-limit <n> max text chunk length
|
|
163
|
+
--show-claude show Claude in a visible Terminal window for debugging
|
|
164
|
+
--no-launch validate and save config only, do not start daemon
|
|
165
|
+
--help, -h show help
|
|
166
|
+
```
|
|
167
|
+
|
|
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
|
|
171
|
+
- `--show-claude` currently supports macOS Terminal only
|
|
172
|
+
|
|
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.
|
|
174
|
+
|
|
175
|
+
## Auto-build during development
|
|
176
|
+
|
|
177
|
+
If you are changing code in this repository, run:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm run dev
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
It continuously watches source changes and builds the latest artifacts to:
|
|
184
|
+
|
|
185
|
+
- `dist/index.js`
|
|
186
|
+
- `dist/daemon.js`
|
|
187
|
+
|
|
188
|
+
For local integration testing, run this in another terminal:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm run daemon
|
|
192
|
+
```
|
|
193
|
+
|
|
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:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
GRIX_CLAUDE_ENDPOINT='ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=<agent_id>' \
|
|
200
|
+
GRIX_CLAUDE_AGENT_ID='<agent_id>' \
|
|
201
|
+
GRIX_CLAUDE_API_KEY='<api_key>' \
|
|
202
|
+
npm run daemon -- --no-launch
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`GRIX_CLAUDE_WS_URL` is still supported; if both are provided, daemon prefers environment variable values.
|
package/cli/config.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT } from "../server/config-store.js";
|
|
7
|
+
import { readConnectionEnv } from "../server/connection-env.js";
|
|
8
|
+
|
|
9
|
+
const serverName = "grix-claude";
|
|
10
|
+
const configFileName = "grix-claude-config.json";
|
|
11
|
+
|
|
12
|
+
function normalizeString(value) {
|
|
13
|
+
return String(value ?? "").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
17
|
+
const numeric = Number(value);
|
|
18
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
19
|
+
return fallbackValue;
|
|
20
|
+
}
|
|
21
|
+
return Math.floor(numeric);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveDefaultDataDir() {
|
|
25
|
+
return path.join(os.homedir(), ".claude", serverName);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolvePackageRoot() {
|
|
29
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolvePackageBinPath() {
|
|
33
|
+
return path.join(resolvePackageRoot(), "bin", "grix-claude.js");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveServerEntryPath(packageRoot = resolvePackageRoot()) {
|
|
37
|
+
const sourceEntryPath = path.join(packageRoot, "server", "main.js");
|
|
38
|
+
if (existsSync(sourceEntryPath)) {
|
|
39
|
+
return sourceEntryPath;
|
|
40
|
+
}
|
|
41
|
+
const distEntryPath = path.join(packageRoot, "dist", "index.js");
|
|
42
|
+
if (existsSync(distEntryPath)) {
|
|
43
|
+
return distEntryPath;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`没有找到 grix-claude server 入口: ${packageRoot}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveDataDir(input) {
|
|
49
|
+
const explicitDir = normalizeString(input.dataDir);
|
|
50
|
+
if (explicitDir) {
|
|
51
|
+
return explicitDir;
|
|
52
|
+
}
|
|
53
|
+
const envDir = normalizeString(input.env?.CLAUDE_PLUGIN_DATA || input.env?.GRIX_CLAUDE_DATA_DIR);
|
|
54
|
+
if (envDir) {
|
|
55
|
+
return envDir;
|
|
56
|
+
}
|
|
57
|
+
return resolveDefaultDataDir();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveConfigPath(dataDir) {
|
|
61
|
+
return path.join(dataDir, configFileName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readStoredConfig(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(filePath, "utf8");
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (
|
|
70
|
+
error &&
|
|
71
|
+
typeof error === "object" &&
|
|
72
|
+
"code" in error &&
|
|
73
|
+
error.code === "ENOENT"
|
|
74
|
+
) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof SyntaxError) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function loadConfig({ dataDir, env = process.env, args = {} }) {
|
|
85
|
+
const configPath = resolveConfigPath(dataDir);
|
|
86
|
+
const stored = await readStoredConfig(configPath);
|
|
87
|
+
const connectionEnv = readConnectionEnv(env);
|
|
88
|
+
return {
|
|
89
|
+
schema_version: 1,
|
|
90
|
+
ws_url: normalizeString(connectionEnv.ws_url || args.wsUrl || stored.ws_url),
|
|
91
|
+
agent_id: normalizeString(connectionEnv.agent_id || args.agentId || stored.agent_id),
|
|
92
|
+
api_key: normalizeString(connectionEnv.api_key || args.apiKey || stored.api_key),
|
|
93
|
+
outbound_text_chunk_limit: normalizePositiveInteger(
|
|
94
|
+
connectionEnv.outbound_text_chunk_limit
|
|
95
|
+
|| args.chunkLimit
|
|
96
|
+
|| stored.outbound_text_chunk_limit,
|
|
97
|
+
DEFAULT_OUTBOUND_TEXT_CHUNK_LIMIT,
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function validateConfig(config) {
|
|
103
|
+
if (!config.ws_url) {
|
|
104
|
+
throw new Error("缺少 ws 地址,请传 --ws-url。");
|
|
105
|
+
}
|
|
106
|
+
if (!/^wss?:\/\//i.test(config.ws_url)) {
|
|
107
|
+
throw new Error("ws 地址必须以 ws:// 或 wss:// 开头。");
|
|
108
|
+
}
|
|
109
|
+
if (!config.agent_id) {
|
|
110
|
+
throw new Error("缺少 agent ID,请传 --agent-id。");
|
|
111
|
+
}
|
|
112
|
+
if (!config.api_key) {
|
|
113
|
+
throw new Error("缺少 API Key,请传 --api-key。");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function writeConfig({ dataDir, config }) {
|
|
118
|
+
await mkdir(dataDir, { recursive: true, mode: 0o700 });
|
|
119
|
+
const configPath = resolveConfigPath(dataDir);
|
|
120
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
mode: 0o600,
|
|
123
|
+
});
|
|
124
|
+
return configPath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function maskApiKey(apiKey) {
|
|
128
|
+
const normalized = normalizeString(apiKey);
|
|
129
|
+
if (!normalized) {
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
if (normalized.length <= 8) {
|
|
133
|
+
return `${normalized.slice(0, 2)}***`;
|
|
134
|
+
}
|
|
135
|
+
return `${normalized.slice(0, 4)}***${normalized.slice(-2)}`;
|
|
136
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
6
|
+
import { loadConfig, resolveDataDir, resolveServerEntryPath } from "./config.js";
|
|
7
|
+
|
|
8
|
+
test("resolveDataDir prefers explicit input over environment", () => {
|
|
9
|
+
assert.equal(
|
|
10
|
+
resolveDataDir({
|
|
11
|
+
dataDir: "/tmp/custom-data",
|
|
12
|
+
env: {
|
|
13
|
+
CLAUDE_PLUGIN_DATA: "/tmp/env-data",
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
"/tmp/custom-data",
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("loadConfig merges stored values with cli input and environment", async () => {
|
|
21
|
+
const tempDir = path.join(process.cwd(), "tmp-config-test");
|
|
22
|
+
const config = await loadConfig({
|
|
23
|
+
dataDir: tempDir,
|
|
24
|
+
env: {
|
|
25
|
+
GRIX_CLAUDE_ENDPOINT: "ws://env.example/ws",
|
|
26
|
+
GRIX_CLAUDE_API_KEY: "env-key",
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
wsUrl: "wss://example.com/ws",
|
|
30
|
+
agentId: "agent-1",
|
|
31
|
+
chunkLimit: "2048",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(config, {
|
|
36
|
+
schema_version: 1,
|
|
37
|
+
ws_url: "ws://env.example/ws",
|
|
38
|
+
agent_id: "agent-1",
|
|
39
|
+
api_key: "env-key",
|
|
40
|
+
outbound_text_chunk_limit: 2048,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("resolveServerEntryPath prefers source entry when source and dist both exist", async () => {
|
|
45
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-config-source-entry-"));
|
|
46
|
+
const sourceDir = path.join(tempDir, "server");
|
|
47
|
+
const distDir = path.join(tempDir, "dist");
|
|
48
|
+
await mkdir(sourceDir, { recursive: true });
|
|
49
|
+
await mkdir(distDir, { recursive: true });
|
|
50
|
+
await writeFile(path.join(sourceDir, "main.js"), "export default 1;\n", "utf8");
|
|
51
|
+
await writeFile(path.join(distDir, "index.js"), "export default 2;\n", "utf8");
|
|
52
|
+
|
|
53
|
+
assert.equal(resolveServerEntryPath(tempDir), path.join(sourceDir, "main.js"));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("resolveServerEntryPath falls back to dist entry when source is missing", async () => {
|
|
57
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-config-dist-entry-"));
|
|
58
|
+
const distDir = path.join(tempDir, "dist");
|
|
59
|
+
await mkdir(distDir, { recursive: true });
|
|
60
|
+
await writeFile(path.join(distDir, "index.js"), "export default 2;\n", "utf8");
|
|
61
|
+
|
|
62
|
+
assert.equal(resolveServerEntryPath(tempDir), path.join(distDir, "index.js"));
|
|
63
|
+
});
|