@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,380 @@
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 { mkdtemp, readFile } from "node:fs/promises";
6
+ import { main, run } from "./main.js";
7
+
8
+ test("cli main prints the running command and redacts api key", async () => {
9
+ const outputs = [];
10
+ const originalStdoutWrite = process.stdout.write;
11
+ process.stdout.write = (chunk) => {
12
+ outputs.push(String(chunk));
13
+ return true;
14
+ };
15
+
16
+ try {
17
+ await main([
18
+ "--help",
19
+ "--api-key",
20
+ "secret-value",
21
+ "--data-dir",
22
+ "/tmp/demo dir",
23
+ ], {});
24
+ const content = outputs.join("");
25
+ assert.match(content, /运行命令:\s+grix-claude --help --api-key '\*\*\*\*\*\*' --data-dir '\/tmp\/demo dir'/u);
26
+ assert.match(content, /实际入口:\s+.*grix-claude\.js --help --api-key '\*\*\*\*\*\*' --data-dir '\/tmp\/demo dir'/u);
27
+ assert.doesNotMatch(content, /secret-value/u);
28
+ } finally {
29
+ process.stdout.write = originalStdoutWrite;
30
+ }
31
+ });
32
+
33
+ test("cli run routes daemon subcommand", async () => {
34
+ const outputs = [];
35
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-route-cli-"));
36
+ const originalStdoutWrite = process.stdout.write;
37
+ process.stdout.write = (chunk) => {
38
+ outputs.push(String(chunk));
39
+ return true;
40
+ };
41
+
42
+ try {
43
+ const exitCode = await run([
44
+ "daemon",
45
+ "--exit-after-ready",
46
+ "--data-dir",
47
+ tempDir,
48
+ ], {});
49
+ assert.equal(exitCode, 0);
50
+ assert.match(outputs.join(""), /daemon 已启动/u);
51
+ } finally {
52
+ process.stdout.write = originalStdoutWrite;
53
+ }
54
+ });
55
+
56
+ test("cli run routes worker subcommand", async () => {
57
+ const outputs = [];
58
+ const originalStdoutWrite = process.stdout.write;
59
+ process.stdout.write = (chunk) => {
60
+ outputs.push(String(chunk));
61
+ return true;
62
+ };
63
+
64
+ try {
65
+ const exitCode = await run(["worker"], {});
66
+ assert.equal(exitCode, 1);
67
+ assert.match(outputs.join(""), /不要手动运行 worker/u);
68
+ } finally {
69
+ process.stdout.write = originalStdoutWrite;
70
+ }
71
+ });
72
+
73
+ test("default cli path persists daemon config without starting daemon when --no-launch is used", async () => {
74
+ const outputs = [];
75
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-default-cli-"));
76
+ const originalStdoutWrite = process.stdout.write;
77
+ process.stdout.write = (chunk) => {
78
+ outputs.push(String(chunk));
79
+ return true;
80
+ };
81
+
82
+ try {
83
+ const exitCode = await run([
84
+ "--no-launch",
85
+ "--data-dir",
86
+ tempDir,
87
+ "--ws-url",
88
+ "ws://127.0.0.1:8888/ws",
89
+ "--agent-id",
90
+ "agent-default",
91
+ "--api-key",
92
+ "key-default",
93
+ ], {});
94
+ assert.equal(exitCode, 0);
95
+ assert.match(outputs.join(""), /daemon 还没有启动/u);
96
+
97
+ const config = JSON.parse(
98
+ await readFile(path.join(tempDir, "daemon-config.json"), "utf8"),
99
+ );
100
+ assert.equal(config.ws_url, "ws://127.0.0.1:8888/ws");
101
+ assert.equal(config.agent_id, "agent-default");
102
+ assert.equal(config.api_key, "key-default");
103
+ } finally {
104
+ process.stdout.write = originalStdoutWrite;
105
+ }
106
+ });
107
+
108
+ test("default cli path does not leak host process env into daemon config writes", async () => {
109
+ const outputs = [];
110
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-env-cli-"));
111
+ const originalStdoutWrite = process.stdout.write;
112
+ const originalAgentID = process.env.GRIX_CLAUDE_AGENT_ID;
113
+ const originalAPIKey = process.env.GRIX_CLAUDE_API_KEY;
114
+ process.stdout.write = (chunk) => {
115
+ outputs.push(String(chunk));
116
+ return true;
117
+ };
118
+ process.env.GRIX_CLAUDE_AGENT_ID = "stale-agent";
119
+ process.env.GRIX_CLAUDE_API_KEY = "stale-key";
120
+
121
+ try {
122
+ const exitCode = await run([
123
+ "--no-launch",
124
+ "--data-dir",
125
+ tempDir,
126
+ "--ws-url",
127
+ "ws://127.0.0.1:8899/ws",
128
+ "--agent-id",
129
+ "fresh-agent",
130
+ "--api-key",
131
+ "fresh-key",
132
+ ], {});
133
+ assert.equal(exitCode, 0);
134
+ assert.match(outputs.join(""), /fresh-agent/u);
135
+
136
+ const config = JSON.parse(
137
+ await readFile(path.join(tempDir, "daemon-config.json"), "utf8"),
138
+ );
139
+ assert.equal(config.agent_id, "fresh-agent");
140
+ assert.equal(config.api_key, "fresh-key");
141
+ } finally {
142
+ process.stdout.write = originalStdoutWrite;
143
+ if (originalAgentID === undefined) {
144
+ delete process.env.GRIX_CLAUDE_AGENT_ID;
145
+ } else {
146
+ process.env.GRIX_CLAUDE_AGENT_ID = originalAgentID;
147
+ }
148
+ if (originalAPIKey === undefined) {
149
+ delete process.env.GRIX_CLAUDE_API_KEY;
150
+ } else {
151
+ process.env.GRIX_CLAUDE_API_KEY = originalAPIKey;
152
+ }
153
+ }
154
+ });
155
+
156
+ test("default cli path can enable visible Claude debug mode", async () => {
157
+ const outputs = [];
158
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-show-claude-cli-"));
159
+ const originalStdoutWrite = process.stdout.write;
160
+ process.stdout.write = (chunk) => {
161
+ outputs.push(String(chunk));
162
+ return true;
163
+ };
164
+
165
+ try {
166
+ const exitCode = await run([
167
+ "--no-launch",
168
+ "--show-claude",
169
+ "--data-dir",
170
+ tempDir,
171
+ "--ws-url",
172
+ "ws://127.0.0.1:8890/ws",
173
+ "--agent-id",
174
+ "agent-show",
175
+ "--api-key",
176
+ "key-show",
177
+ ], {});
178
+ assert.equal(exitCode, 0);
179
+ assert.match(outputs.join(""), /daemon 还没有启动/u);
180
+ } finally {
181
+ process.stdout.write = originalStdoutWrite;
182
+ }
183
+ });
184
+
185
+ test("default cli path accepts GRIX_CLAUDE_ENDPOINT env for daemon config", async () => {
186
+ const outputs = [];
187
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-endpoint-env-cli-"));
188
+ const originalStdoutWrite = process.stdout.write;
189
+ process.stdout.write = (chunk) => {
190
+ outputs.push(String(chunk));
191
+ return true;
192
+ };
193
+
194
+ try {
195
+ const exitCode = await run([
196
+ "--no-launch",
197
+ "--data-dir",
198
+ tempDir,
199
+ ], {
200
+ GRIX_CLAUDE_ENDPOINT: "ws://127.0.0.1:27189/v1/agent-api/ws?agent_id=2035251418226495488",
201
+ GRIX_CLAUDE_AGENT_ID: "2035251418226495488",
202
+ GRIX_CLAUDE_API_KEY: "ak_2035251418226495488_Gyav9cyaOHbAUP7qrOJ4JHv13FR0XgwB",
203
+ });
204
+ assert.equal(exitCode, 0);
205
+ assert.match(outputs.join(""), /Agent ID: 2035251418226495488/u);
206
+ assert.match(outputs.join(""), /daemon 还没有启动/u);
207
+ } finally {
208
+ process.stdout.write = originalStdoutWrite;
209
+ }
210
+ });
211
+
212
+ test("cli daemon subcommand persists daemon config when options are provided", async () => {
213
+ const outputs = [];
214
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-cli-"));
215
+ const originalStdoutWrite = process.stdout.write;
216
+ process.stdout.write = (chunk) => {
217
+ outputs.push(String(chunk));
218
+ return true;
219
+ };
220
+
221
+ try {
222
+ const exitCode = await run([
223
+ "daemon",
224
+ "--exit-after-ready",
225
+ "--data-dir",
226
+ tempDir,
227
+ "--ws-url",
228
+ "ws://127.0.0.1:7777/ws",
229
+ "--agent-id",
230
+ "agent-1",
231
+ "--api-key",
232
+ "key-1",
233
+ "--chunk-limit",
234
+ "2048",
235
+ ], {});
236
+ assert.equal(exitCode, 0);
237
+ assert.match(outputs.join(""), /已配置: yes/u);
238
+
239
+ const config = JSON.parse(
240
+ await readFile(path.join(tempDir, "daemon-config.json"), "utf8"),
241
+ );
242
+ assert.equal(config.ws_url, "ws://127.0.0.1:7777/ws");
243
+ assert.equal(config.agent_id, "agent-1");
244
+ assert.equal(config.api_key, "key-1");
245
+ assert.equal(config.outbound_text_chunk_limit, 2048);
246
+ } finally {
247
+ process.stdout.write = originalStdoutWrite;
248
+ }
249
+ });
250
+
251
+ test("cli install subcommand prepares config and delegates to service manager", async () => {
252
+ const outputs = [];
253
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-install-cli-"));
254
+ const originalStdoutWrite = process.stdout.write;
255
+ const calls = [];
256
+ process.stdout.write = (chunk) => {
257
+ outputs.push(String(chunk));
258
+ return true;
259
+ };
260
+
261
+ try {
262
+ const exitCode = await run([
263
+ "install",
264
+ "--data-dir",
265
+ tempDir,
266
+ "--ws-url",
267
+ "ws://127.0.0.1:9010/ws",
268
+ "--agent-id",
269
+ "agent-install",
270
+ "--api-key",
271
+ "key-install",
272
+ ], {}, {
273
+ serviceManager: {
274
+ install: async ({ dataDir }) => {
275
+ calls.push({ kind: "install", dataDir });
276
+ return {
277
+ installed: true,
278
+ service_kind: "systemd-user",
279
+ data_dir: dataDir,
280
+ install_state: "current",
281
+ daemon_state: "running",
282
+ service_id: "service-1",
283
+ };
284
+ },
285
+ },
286
+ });
287
+ assert.equal(exitCode, 0);
288
+ assert.deepEqual(calls, [{
289
+ kind: "install",
290
+ dataDir: tempDir,
291
+ }]);
292
+ assert.match(outputs.join(""), /服务已安装: yes/u);
293
+ } finally {
294
+ process.stdout.write = originalStdoutWrite;
295
+ }
296
+ });
297
+
298
+ test("cli status subcommand prints service manager status", async () => {
299
+ const outputs = [];
300
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-status-cli-"));
301
+ const originalStdoutWrite = process.stdout.write;
302
+ process.stdout.write = (chunk) => {
303
+ outputs.push(String(chunk));
304
+ return true;
305
+ };
306
+
307
+ try {
308
+ const exitCode = await run([
309
+ "status",
310
+ "--data-dir",
311
+ tempDir,
312
+ ], {}, {
313
+ serviceManager: {
314
+ status: async ({ dataDir }) => ({
315
+ installed: true,
316
+ service_kind: "launchd",
317
+ data_dir: dataDir,
318
+ install_state: "current",
319
+ daemon_state: "running",
320
+ pid: 1234,
321
+ }),
322
+ },
323
+ });
324
+ assert.equal(exitCode, 0);
325
+ assert.match(outputs.join(""), /进程 PID: 1234/u);
326
+ } finally {
327
+ process.stdout.write = originalStdoutWrite;
328
+ }
329
+ });
330
+
331
+ test("cli start subcommand persists environment config before starting service", async () => {
332
+ const outputs = [];
333
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "grix-daemon-start-env-cli-"));
334
+ const originalStdoutWrite = process.stdout.write;
335
+ const calls = [];
336
+ process.stdout.write = (chunk) => {
337
+ outputs.push(String(chunk));
338
+ return true;
339
+ };
340
+
341
+ try {
342
+ const exitCode = await run([
343
+ "start",
344
+ "--data-dir",
345
+ tempDir,
346
+ ], {
347
+ GRIX_CLAUDE_ENDPOINT: "ws://127.0.0.1:9020/ws",
348
+ GRIX_CLAUDE_AGENT_ID: "agent-start-env",
349
+ GRIX_CLAUDE_API_KEY: "key-start-env",
350
+ }, {
351
+ serviceManager: {
352
+ start: async ({ dataDir }) => {
353
+ calls.push({ kind: "start", dataDir });
354
+ return {
355
+ installed: true,
356
+ service_kind: "launchd",
357
+ data_dir: dataDir,
358
+ install_state: "current",
359
+ daemon_state: "running",
360
+ pid: 4567,
361
+ };
362
+ },
363
+ },
364
+ });
365
+ assert.equal(exitCode, 0);
366
+ assert.deepEqual(calls, [{
367
+ kind: "start",
368
+ dataDir: tempDir,
369
+ }]);
370
+ const config = JSON.parse(
371
+ await readFile(path.join(tempDir, "daemon-config.json"), "utf8"),
372
+ );
373
+ assert.equal(config.ws_url, "ws://127.0.0.1:9020/ws");
374
+ assert.equal(config.agent_id, "agent-start-env");
375
+ assert.equal(config.api_key, "key-start-env");
376
+ assert.match(outputs.join(""), /Agent ID: agent-start-env/u);
377
+ } finally {
378
+ process.stdout.write = originalStdoutWrite;
379
+ }
380
+ });
package/cli/mcp.js ADDED
@@ -0,0 +1,113 @@
1
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const serverName = "grix-claude";
7
+
8
+ function normalizeString(value) {
9
+ return String(value ?? "").trim();
10
+ }
11
+
12
+ function runClaudeCommand({ claudeCommand, args, cwd, env, allowFailure = false }) {
13
+ const result = spawnSync(claudeCommand, args, {
14
+ cwd,
15
+ env,
16
+ encoding: "utf8",
17
+ });
18
+
19
+ if (result.error) {
20
+ if (result.error.code === "ENOENT") {
21
+ throw new Error("没有找到 claude 命令,请先安装并登录 Claude Code。");
22
+ }
23
+ throw result.error;
24
+ }
25
+
26
+ if (result.status !== 0 && !allowFailure) {
27
+ const output = normalizeString(result.stderr || result.stdout);
28
+ throw new Error(output || `Claude 命令执行失败: ${args.join(" ")}`);
29
+ }
30
+
31
+ return result;
32
+ }
33
+
34
+ function sameArgs(left, right) {
35
+ if (left.length !== right.length) {
36
+ return false;
37
+ }
38
+ return left.every((value, index) => value === right[index]);
39
+ }
40
+
41
+ function resolveClaudeConfigPath(env) {
42
+ const homeDir = normalizeString(env.HOME || env.USERPROFILE) || os.homedir();
43
+ return path.join(homeDir, ".claude.json");
44
+ }
45
+
46
+ async function readUserScopedServer(env) {
47
+ try {
48
+ const raw = await readFile(resolveClaudeConfigPath(env), "utf8");
49
+ const parsed = JSON.parse(raw);
50
+ return parsed?.mcpServers?.[serverName] ?? null;
51
+ } catch (error) {
52
+ if (
53
+ error &&
54
+ typeof error === "object" &&
55
+ "code" in error &&
56
+ error.code === "ENOENT"
57
+ ) {
58
+ return null;
59
+ }
60
+ if (error instanceof SyntaxError) {
61
+ return null;
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ export async function ensureUserMcpServer({
68
+ claudeCommand,
69
+ serverCommand,
70
+ serverArgs,
71
+ env = process.env,
72
+ }) {
73
+ const tempCwd = await mkdtemp(path.join(os.tmpdir(), "grix-claude-mcp-"));
74
+
75
+ try {
76
+ const current = await readUserScopedServer(env);
77
+ if (
78
+ current &&
79
+ normalizeString(current.type || "stdio") === "stdio" &&
80
+ normalizeString(current.command) === serverCommand &&
81
+ sameArgs(Array.isArray(current.args) ? current.args : [], serverArgs)
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ runClaudeCommand({
87
+ claudeCommand,
88
+ args: ["mcp", "remove", "-s", "user", serverName],
89
+ cwd: tempCwd,
90
+ env,
91
+ allowFailure: true,
92
+ });
93
+
94
+ runClaudeCommand({
95
+ claudeCommand,
96
+ args: ["mcp", "add", "--scope", "user", serverName, "--", serverCommand, ...serverArgs],
97
+ cwd: tempCwd,
98
+ env,
99
+ });
100
+
101
+ const verifiedDetails = await readUserScopedServer(env);
102
+ if (
103
+ !verifiedDetails ||
104
+ normalizeString(verifiedDetails.type || "stdio") !== "stdio" ||
105
+ normalizeString(verifiedDetails.command) !== serverCommand ||
106
+ !sameArgs(Array.isArray(verifiedDetails.args) ? verifiedDetails.args : [], serverArgs)
107
+ ) {
108
+ throw new Error("用户级 Claude MCP 配置写入后校验失败。");
109
+ }
110
+ } finally {
111
+ await rm(tempCwd, { recursive: true, force: true });
112
+ }
113
+ }
@@ -0,0 +1,7 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ensureUserMcpServer } from "./mcp.js";
4
+
5
+ test("ensureUserMcpServer is exported", () => {
6
+ assert.equal(typeof ensureUserMcpServer, "function");
7
+ });