@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/cli/main.js ADDED
@@ -0,0 +1,369 @@
1
+ import process from "node:process";
2
+ import {
3
+ maskApiKey,
4
+ resolvePackageBinPath,
5
+ validateConfig,
6
+ } from "./config.js";
7
+ import { ConfigStore } from "../server/config-store.js";
8
+ import { resolveDaemonConfigPath, resolveDaemonDataDir } from "../server/daemon/daemon-paths.js";
9
+ import { ServiceManager } from "../server/service/service-manager.js";
10
+
11
+ function usage() {
12
+ return `用法:
13
+ grix-claude [options]
14
+ grix-claude daemon [options]
15
+ grix-claude worker [options]
16
+ grix-claude install [options]
17
+ grix-claude start [options]
18
+ grix-claude stop [options]
19
+ grix-claude restart [options]
20
+ grix-claude status [options]
21
+ grix-claude uninstall [options]
22
+
23
+ 说明:
24
+ 默认命令会写好 daemon 配置,并启动 grix-claude daemon。
25
+ daemon 是唯一对接 Grix 的常驻服务,Claude 会话由 daemon 按需拉起。
26
+
27
+ 选项:
28
+ --ws-url <value> Grix Agent API WebSocket 地址
29
+ --agent-id <value> Agent ID
30
+ --api-key <value> API Key
31
+ --data-dir <path> daemon 数据目录,默认 ~/.claude/grix-claude-daemon
32
+ --chunk-limit <n> 单段文本长度上限,默认 1200
33
+ --show-claude 开发调试时把 Claude 拉到可见的 Terminal 窗口
34
+ --no-launch 只检查并写好配置,不启动 daemon
35
+ --help, -h 显示帮助
36
+
37
+ 第一次运行需要传完整参数。
38
+ 后续再次运行时,如果本地已经保存过配置,可以直接执行: grix-claude
39
+ `;
40
+ }
41
+
42
+ function parseArgs(argv) {
43
+ const options = {
44
+ wsUrl: "",
45
+ agentId: "",
46
+ apiKey: "",
47
+ dataDir: "",
48
+ chunkLimit: "",
49
+ showClaude: false,
50
+ noLaunch: false,
51
+ help: false,
52
+ };
53
+
54
+ for (let index = 0; index < argv.length; index += 1) {
55
+ const current = argv[index];
56
+ if (current === "--help" || current === "-h") {
57
+ options.help = true;
58
+ continue;
59
+ }
60
+ if (current === "--no-launch") {
61
+ options.noLaunch = true;
62
+ continue;
63
+ }
64
+ if (current === "--show-claude") {
65
+ options.showClaude = true;
66
+ continue;
67
+ }
68
+ const next = argv[index + 1];
69
+ if (current === "--ws-url") {
70
+ if (!next) {
71
+ throw new Error("--ws-url 缺少值。");
72
+ }
73
+ options.wsUrl = next;
74
+ index += 1;
75
+ continue;
76
+ }
77
+ if (current === "--agent-id") {
78
+ if (!next) {
79
+ throw new Error("--agent-id 缺少值。");
80
+ }
81
+ options.agentId = next;
82
+ index += 1;
83
+ continue;
84
+ }
85
+ if (current === "--api-key") {
86
+ if (!next) {
87
+ throw new Error("--api-key 缺少值。");
88
+ }
89
+ options.apiKey = next;
90
+ index += 1;
91
+ continue;
92
+ }
93
+ if (current === "--data-dir") {
94
+ if (!next) {
95
+ throw new Error("--data-dir 缺少值。");
96
+ }
97
+ options.dataDir = next;
98
+ index += 1;
99
+ continue;
100
+ }
101
+ if (current === "--chunk-limit") {
102
+ if (!next) {
103
+ throw new Error("--chunk-limit 缺少值。");
104
+ }
105
+ options.chunkLimit = next;
106
+ index += 1;
107
+ continue;
108
+ }
109
+ throw new Error(`未知参数: ${current}`);
110
+ }
111
+
112
+ return options;
113
+ }
114
+
115
+ function print(message) {
116
+ process.stdout.write(`${message}\n`);
117
+ }
118
+
119
+ function printError(message) {
120
+ process.stderr.write(`${message}\n`);
121
+ }
122
+
123
+ function shellQuoteForDisplay(value) {
124
+ const text = String(value ?? "");
125
+ if (!text) {
126
+ return "''";
127
+ }
128
+ if (/^[A-Za-z0-9_./:=@-]+$/u.test(text)) {
129
+ return text;
130
+ }
131
+ return `'${text.replace(/'/g, `'\\''`)}'`;
132
+ }
133
+
134
+ function redactSensitiveArgs(argv) {
135
+ const args = Array.isArray(argv) ? argv.map((item) => String(item ?? "")) : [];
136
+ const redacted = [];
137
+ for (let index = 0; index < args.length; index += 1) {
138
+ const current = args[index];
139
+ if (current === "--api-key") {
140
+ redacted.push("--api-key");
141
+ if (index + 1 < args.length) {
142
+ redacted.push("******");
143
+ index += 1;
144
+ }
145
+ continue;
146
+ }
147
+ if (current.startsWith("--api-key=")) {
148
+ redacted.push("--api-key=******");
149
+ continue;
150
+ }
151
+ redacted.push(current);
152
+ }
153
+ return redacted;
154
+ }
155
+
156
+ function formatRunningCommand(argv) {
157
+ const command = ["grix-claude", ...redactSensitiveArgs(argv)];
158
+ return command.map((item) => shellQuoteForDisplay(item)).join(" ");
159
+ }
160
+
161
+ function formatRuntimeEntryCommand(argv) {
162
+ const command = [process.execPath, resolvePackageBinPath(), ...redactSensitiveArgs(argv)];
163
+ return command.map((item) => shellQuoteForDisplay(item)).join(" ");
164
+ }
165
+
166
+ function createServiceManager(env = process.env) {
167
+ return new ServiceManager({ env });
168
+ }
169
+
170
+ function buildRuntimeEnv(options, env) {
171
+ return {
172
+ ...env,
173
+ ...(options.dataDir ? { GRIX_CLAUDE_DAEMON_DATA_DIR: options.dataDir } : {}),
174
+ ...(options.showClaude ? { GRIX_CLAUDE_SHOW_CLAUDE_WINDOW: "1" } : {}),
175
+ };
176
+ }
177
+
178
+ function buildDaemonStatus(configStore) {
179
+ const config = configStore.get();
180
+ validateConfig(config);
181
+ return {
182
+ config,
183
+ configPath: configStore.filePath,
184
+ };
185
+ }
186
+
187
+ async function prepareDaemonConfig(options, env = process.env, { persistResolvedConfig = false } = {}) {
188
+ const runtimeEnv = buildRuntimeEnv(options, env);
189
+ const configStore = new ConfigStore(resolveDaemonConfigPath(runtimeEnv), {
190
+ env: runtimeEnv,
191
+ });
192
+ await configStore.load();
193
+ const hasCliOverrides = Boolean(
194
+ options.wsUrl
195
+ || options.agentId
196
+ || options.apiKey
197
+ || options.chunkLimit,
198
+ );
199
+ if (hasCliOverrides) {
200
+ await configStore.update({
201
+ ...(options.wsUrl ? { ws_url: options.wsUrl } : {}),
202
+ ...(options.agentId ? { agent_id: options.agentId } : {}),
203
+ ...(options.apiKey ? { api_key: options.apiKey } : {}),
204
+ ...(options.chunkLimit ? { outbound_text_chunk_limit: Number(options.chunkLimit) } : {}),
205
+ });
206
+ } else if (persistResolvedConfig) {
207
+ await configStore.update(configStore.get());
208
+ }
209
+ return {
210
+ runtimeEnv,
211
+ dataDir: resolveDaemonDataDir(runtimeEnv),
212
+ ...buildDaemonStatus(configStore),
213
+ };
214
+ }
215
+
216
+ async function runDefault(argv, env = process.env) {
217
+ const options = parseArgs(argv);
218
+ if (options.help) {
219
+ print(usage());
220
+ return 0;
221
+ }
222
+
223
+ const { runtimeEnv, dataDir, config, configPath } = await prepareDaemonConfig(options, env);
224
+
225
+ print(`配置已准备好。`);
226
+ print(`数据目录: ${dataDir}`);
227
+ print(`配置文件: ${configPath}`);
228
+ print(`Agent ID: ${config.agent_id}`);
229
+ print(`API Key: ${maskApiKey(config.api_key)}`);
230
+
231
+ if (options.noLaunch) {
232
+ print(`daemon 还没有启动。需要时直接执行 grix-claude 即可。`);
233
+ return 0;
234
+ }
235
+
236
+ print(`正在启动 daemon...`);
237
+ const { run } = await import("../server/daemon/main.js");
238
+ return run([], runtimeEnv);
239
+ }
240
+
241
+ function formatServiceStatus(status) {
242
+ const lines = [
243
+ `服务已安装: ${status.installed ? "yes" : "no"}`,
244
+ `服务类型: ${status.service_kind || ""}`,
245
+ `数据目录: ${status.data_dir || ""}`,
246
+ `安装状态: ${status.install_state || ""}`,
247
+ `daemon 状态: ${status.daemon_state || ""}`,
248
+ ];
249
+ if (status.service_id) {
250
+ lines.push(`服务标识: ${status.service_id}`);
251
+ }
252
+ if (status.pid) {
253
+ lines.push(`进程 PID: ${status.pid}`);
254
+ }
255
+ if (status.connection_state) {
256
+ lines.push(`连接状态: ${status.connection_state}`);
257
+ }
258
+ if (status.bridge_url) {
259
+ lines.push(`Bridge: ${status.bridge_url}`);
260
+ }
261
+ if (status.definition_path) {
262
+ lines.push(`启动项: ${status.definition_path}`);
263
+ }
264
+ return lines.join("\n");
265
+ }
266
+
267
+ async function runServiceSubcommand(name, argv, env, deps = {}) {
268
+ const options = parseArgs(argv);
269
+ if (options.help) {
270
+ print(usage());
271
+ return 0;
272
+ }
273
+ const runtimeEnv = buildRuntimeEnv(options, env);
274
+ const dataDir = resolveDaemonDataDir(runtimeEnv);
275
+ const manager = deps.serviceManager || createServiceManager(runtimeEnv);
276
+ const shouldPrepareConfig = ["install", "start", "restart"].includes(name);
277
+ const prepared = shouldPrepareConfig
278
+ ? await prepareDaemonConfig(options, env, { persistResolvedConfig: true })
279
+ : null;
280
+ if (name === "install") {
281
+ const { config, configPath } = prepared;
282
+ const status = await manager.install({ dataDir });
283
+ print(`配置文件: ${configPath}`);
284
+ print(`Agent ID: ${config.agent_id}`);
285
+ print(`API Key: ${maskApiKey(config.api_key)}`);
286
+ print(formatServiceStatus(status));
287
+ return 0;
288
+ }
289
+ if (name === "start") {
290
+ const { config, configPath } = prepared;
291
+ const status = await manager.start({ dataDir });
292
+ print(`配置文件: ${configPath}`);
293
+ print(`Agent ID: ${config.agent_id}`);
294
+ print(`API Key: ${maskApiKey(config.api_key)}`);
295
+ print(formatServiceStatus(status));
296
+ return 0;
297
+ }
298
+ if (name === "stop") {
299
+ const status = await manager.stop({ dataDir });
300
+ print(formatServiceStatus(status));
301
+ return 0;
302
+ }
303
+ if (name === "restart") {
304
+ const { config, configPath } = prepared;
305
+ const status = await manager.restart({ dataDir });
306
+ print(`配置文件: ${configPath}`);
307
+ print(`Agent ID: ${config.agent_id}`);
308
+ print(`API Key: ${maskApiKey(config.api_key)}`);
309
+ print(formatServiceStatus(status));
310
+ return 0;
311
+ }
312
+ if (name === "status") {
313
+ const status = await manager.status({ dataDir });
314
+ print(formatServiceStatus(status));
315
+ return 0;
316
+ }
317
+ if (name === "uninstall") {
318
+ const status = await manager.uninstall({ dataDir });
319
+ print(formatServiceStatus(status));
320
+ return 0;
321
+ }
322
+ throw new Error(`未知子命令: ${name}`);
323
+ }
324
+
325
+ async function runSubcommand(name, argv, env, deps = {}) {
326
+ if (name === "daemon") {
327
+ const { run } = await import("../server/daemon/main.js");
328
+ return run(argv, env);
329
+ }
330
+ if (name === "worker") {
331
+ const { run } = await import("../server/worker/main.js");
332
+ return run(argv, env);
333
+ }
334
+ if (["install", "start", "stop", "restart", "status", "uninstall"].includes(name)) {
335
+ return runServiceSubcommand(name, argv, env, deps);
336
+ }
337
+ throw new Error(`未知子命令: ${name}`);
338
+ }
339
+
340
+ export async function run(argv, env = process.env, deps = {}) {
341
+ const [first = ""] = argv;
342
+ if ([
343
+ "daemon",
344
+ "worker",
345
+ "install",
346
+ "start",
347
+ "stop",
348
+ "restart",
349
+ "status",
350
+ "uninstall",
351
+ ].includes(first)) {
352
+ return runSubcommand(first, argv.slice(1), env, deps);
353
+ }
354
+ return runDefault(argv, env);
355
+ }
356
+
357
+ export async function main(argv = process.argv.slice(2), env = process.env) {
358
+ try {
359
+ print(`运行命令: ${formatRunningCommand(argv)}`);
360
+ print(`实际入口: ${formatRuntimeEntryCommand(argv)}`);
361
+ const exitCode = await run(argv, env);
362
+ process.exitCode = exitCode;
363
+ } catch (error) {
364
+ printError(error instanceof Error ? error.message : String(error));
365
+ printError("");
366
+ printError(usage());
367
+ process.exitCode = 1;
368
+ }
369
+ }