@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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../cli/main.js";
3
+
4
+ await main();
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
+ });