skyloom 1.12.0 → 1.13.1
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/.github/workflows/ci.yml +36 -36
- package/README.md +137 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -155
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +127 -74
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts +52 -19
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +198 -265
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +417 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +174 -223
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -0
- package/vitest.config.ts +17 -17
package/src/cli/main.ts
CHANGED
|
@@ -1,372 +1,417 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 天空织机 CLI — Skyloom Terminal Interface
|
|
4
|
-
*/
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
import * as fs from "fs";
|
|
7
|
-
import * as readline from "readline";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
import { createSystemContext, orchestrateTask } from "../core/factory";
|
|
10
|
-
import { loadConfig, USER_CONFIG_DIR } from "../core/config";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
["/
|
|
29
|
-
["/
|
|
30
|
-
["/
|
|
31
|
-
["/
|
|
32
|
-
["/
|
|
33
|
-
["/
|
|
34
|
-
["/
|
|
35
|
-
["/
|
|
36
|
-
["/
|
|
37
|
-
["/
|
|
38
|
-
["/
|
|
39
|
-
["/
|
|
40
|
-
["/
|
|
41
|
-
["/
|
|
42
|
-
["/
|
|
43
|
-
["/
|
|
44
|
-
["/
|
|
45
|
-
["/
|
|
46
|
-
["/
|
|
47
|
-
["/
|
|
48
|
-
["/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
program.command("
|
|
73
|
-
.
|
|
74
|
-
program.command("
|
|
75
|
-
.action(
|
|
76
|
-
program.command("
|
|
77
|
-
|
|
78
|
-
program.command("
|
|
79
|
-
program.command("
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const pad = " ".repeat(Math.max(0, Math.floor((w - 34) / 2)));
|
|
95
|
-
process.stdout.write("\n" + pad +
|
|
96
|
-
process.stdout.write(pad + chalk.dim("S K Y L O O M\n\n"));
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
process.stdout.write(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
await
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 天空织机 CLI — Skyloom Terminal Interface
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as readline from "readline";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { createSystemContext, orchestrateTask } from "../core/factory";
|
|
10
|
+
import { loadConfig, USER_CONFIG_DIR } from "../core/config";
|
|
11
|
+
import { listProviders, modelsFor, providerLabel, validateModel } from "../core/catalog";
|
|
12
|
+
import { agentTheme } from "../core/theme";
|
|
13
|
+
import { classify } from "../core/router";
|
|
14
|
+
import { InteractiveMode, ModeController } from "./mode";
|
|
15
|
+
import { readLine, renderPalette, StreamRenderer } from "./tui";
|
|
16
|
+
|
|
17
|
+
const MODE = new ModeController();
|
|
18
|
+
const VERSION = (() => { try { return require("../../package.json").version; } catch { return "1.5.2"; } })();
|
|
19
|
+
|
|
20
|
+
const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
|
|
21
|
+
|
|
22
|
+
/* ═══════════════════════════════════════
|
|
23
|
+
Slash commands registry
|
|
24
|
+
═══════════════════════════════════════ */
|
|
25
|
+
const SLASH_CMDS: [string, string][] = [
|
|
26
|
+
["/help", "Show all commands"],
|
|
27
|
+
["/clear", "Clear screen"],
|
|
28
|
+
["/status", "Agent overview"],
|
|
29
|
+
["/cost", "Usage & cost"],
|
|
30
|
+
["/cost reset", "Reset usage stats"],
|
|
31
|
+
["/compact", "Compress context"],
|
|
32
|
+
["/retry", "Resend last msg"],
|
|
33
|
+
["/memory", "Memory stats"],
|
|
34
|
+
["/memory clear", "Clear short-term memory"],
|
|
35
|
+
["/sessions", "Session list"],
|
|
36
|
+
["/workspace", "Workspace info"],
|
|
37
|
+
["/model", "Model info"],
|
|
38
|
+
["/mcp", "MCP server status"],
|
|
39
|
+
["/version", "Version info"],
|
|
40
|
+
["/task <goal>", "Multi-agent orchestrate"],
|
|
41
|
+
["/fog", "≋ Fog — research insight"],
|
|
42
|
+
["/rain", "⸽ Rain — creation codegen"],
|
|
43
|
+
["/frost", "✱ Frost — review quality"],
|
|
44
|
+
["/snow", "❉ Snow — planning architect"],
|
|
45
|
+
["/dew", "∘ Dew — devops reliability"],
|
|
46
|
+
["/fair", "☼ Fair — companion warmth"],
|
|
47
|
+
["/quit", "Exit chat"],
|
|
48
|
+
["/exit", "Exit chat"],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function showPopup(cmds: [string, string][], selIdx: number) {
|
|
52
|
+
const w = process.stdout.columns || 80;
|
|
53
|
+
const start = Math.max(0, Math.min(selIdx - 4, cmds.length - 8));
|
|
54
|
+
const end = Math.min(cmds.length, start + 8);
|
|
55
|
+
process.stdout.write(chalk.dim(" ┌─ commands (↑↓ pick · type letter to filter · tab/enter select) ─┐\n"));
|
|
56
|
+
for (let i = start; i < end; i++) {
|
|
57
|
+
const [cmd, desc] = cmds[i];
|
|
58
|
+
const marker = i === selIdx ? chalk.cyan(" ▶ ") : " ";
|
|
59
|
+
process.stdout.write(` │${marker}${chalk.cyan(cmd.padEnd(24))}${chalk.dim(desc)}${" ".repeat(Math.max(0, 50 - desc.length))}│\n`);
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(chalk.dim(` └${"─".repeat(60)}┘\n`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ═══════════════════════════════════════
|
|
65
|
+
Commander
|
|
66
|
+
═══════════════════════════════════════ */
|
|
67
|
+
const program = new Command()
|
|
68
|
+
.name("sky").description("天空织机 Skyloom").version(VERSION);
|
|
69
|
+
|
|
70
|
+
program.command("chat").argument("[agent]", "agent name", "fog")
|
|
71
|
+
.option("-m,--model <m>", "model").action(async (a: string, o: { model?: string }) => { await chat(a, o.model); });
|
|
72
|
+
program.command("task").argument("[goal]", "task goal")
|
|
73
|
+
.action(async (g?: string) => { if (g) await runTask(g); });
|
|
74
|
+
program.command("web").option("-p,--port <p>", "port", "3000")
|
|
75
|
+
.action((o: { port?: string }) => { import("../web/server").then(m => m.startWebServer(parseInt(o.port || "3000"))); });
|
|
76
|
+
program.command("mcp").action(() => { import("../core/mcp_server").then(m => m.startMCPServer()); });
|
|
77
|
+
program.command("config").action(() => { const c = loadConfig(); process.stdout.write(chalk.cyan("\nConfig: ") + USER_CONFIG_DIR + "\n"); for (const [n, a] of Object.entries(c.agents || {})) process.stdout.write(` ${chalk.bold(n)}: ${(a as any).model || "default"}\n`); });
|
|
78
|
+
program.command("init").action(() => { if (!fs.existsSync(USER_CONFIG_DIR)) fs.mkdirSync(USER_CONFIG_DIR, { recursive: true }); process.stdout.write(chalk.green("✓ ") + USER_CONFIG_DIR + "\n"); });
|
|
79
|
+
program.command("apikey").description("Manage API keys (persisted to ~/.skyloom/config.yaml)")
|
|
80
|
+
.argument("[action]", "set|list").argument("[provider]", "e.g. deepseek").argument("[key]", "API key")
|
|
81
|
+
.action((action?: string, provider?: string, key?: string) => {
|
|
82
|
+
if (action === "set" && provider && key) { saveApiKey(provider, key); process.stdout.write(chalk.green("✓ Saved " + provider + " API key\n")); }
|
|
83
|
+
else { process.stdout.write(chalk.dim("Usage: sky apikey set deepseek YOUR_KEY\n")); }
|
|
84
|
+
});
|
|
85
|
+
program.command("version").action(() => { process.stdout.write(`Skyloom v${VERSION}\n`); });
|
|
86
|
+
|
|
87
|
+
/* ═══════════════════════════════════════
|
|
88
|
+
Welcome
|
|
89
|
+
═══════════════════════════════════════ */
|
|
90
|
+
function welcome(agent: any) {
|
|
91
|
+
const w = process.stdout.columns || 80;
|
|
92
|
+
const active = agentTheme(agent.name);
|
|
93
|
+
const seal = chalk.hex(active.hex);
|
|
94
|
+
const pad = " ".repeat(Math.max(0, Math.floor((w - 34) / 2)));
|
|
95
|
+
process.stdout.write("\n" + pad + seal("✦ 天 空 织 机 ✦\n"));
|
|
96
|
+
process.stdout.write(pad + chalk.dim("S K Y L O O M\n\n"));
|
|
97
|
+
// Six shuttles, each in its own mineral pigment; active one bolded with a seal.
|
|
98
|
+
const parts: string[] = [];
|
|
99
|
+
for (const n of AGENT_NAMES) {
|
|
100
|
+
const t = agentTheme(n);
|
|
101
|
+
const isActive = n === agent.name;
|
|
102
|
+
const label = `${t.symbol} ${t.kanji}`;
|
|
103
|
+
parts.push(isActive ? chalk.bold.hex(t.hex)(`▣ ${label}`) : chalk.hex(t.hex).dim(label));
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(" " + parts.join(chalk.dim(" · ")) + "\n");
|
|
106
|
+
process.stdout.write(" " + chalk.dim.italic(active.poem) + "\n\n");
|
|
107
|
+
process.stdout.write(chalk.dim(" /help for commands · /quit to exit\n\n"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function statusBar(agent: any, ctx: any): string {
|
|
111
|
+
try {
|
|
112
|
+
const cu = agent.contextUsage();
|
|
113
|
+
const pct = cu.pct || 0;
|
|
114
|
+
const bar = pct < 50 ? chalk.green : pct < 80 ? chalk.yellow : chalk.red;
|
|
115
|
+
const filled = Math.round(pct / 10);
|
|
116
|
+
const ctxBar = `${bar("█".repeat(filled) + "░".repeat(10 - filled))} ${pct}%`;
|
|
117
|
+
const cost = formatCost(ctx.llm.getTotalCost());
|
|
118
|
+
return chalk.dim(`${ctxBar} · ${cost} · ${cu.model || "?"}`);
|
|
119
|
+
} catch { return ""; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatCost(c: number): string {
|
|
123
|
+
if (c >= 1) return chalk.yellow(`$${c.toFixed(2)}`);
|
|
124
|
+
if (c >= 0.01) return chalk.yellow(`$${c.toFixed(4)}`);
|
|
125
|
+
if (c > 0) return chalk.green(`${(c * 100).toFixed(2)}¢`);
|
|
126
|
+
return "$0";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ═══════════════════════════════════════
|
|
130
|
+
Streaming renderer — consumes agent.chatStream()
|
|
131
|
+
═══════════════════════════════════════ */
|
|
132
|
+
/**
|
|
133
|
+
* Render a streamed turn live: reasoning in faint ink, content in mineral
|
|
134
|
+
* pigment, tool calls as pulsing weather events. Replaces the old blocking
|
|
135
|
+
* chat() + fake render. Tokens appear as they arrive.
|
|
136
|
+
*/
|
|
137
|
+
async function streamResponse(agent: any, input: string): Promise<void> {
|
|
138
|
+
const theme = agentTheme(agent.name);
|
|
139
|
+
const pigment = chalk.hex(theme.hex);
|
|
140
|
+
const out = process.stdout;
|
|
141
|
+
|
|
142
|
+
// ── Thinking spinner (animates until the first token lands; TTY only) ──
|
|
143
|
+
const isTTY = !!out.isTTY;
|
|
144
|
+
const frames = ["· ", "·· ", " ··", " ·"];
|
|
145
|
+
let fi = 0, spinning = true;
|
|
146
|
+
const draw = () => { if (spinning && isTTY) out.write(`\r ${pigment(theme.symbol)} ${chalk.dim("思忖 " + frames[fi++ % frames.length])}`); };
|
|
147
|
+
const timer = isTTY ? setInterval(draw, 140) : null; draw();
|
|
148
|
+
const stopSpinner = () => { if (spinning) { spinning = false; if (timer) clearInterval(timer); if (isTTY) out.write("\r" + " ".repeat(20) + "\r"); } };
|
|
149
|
+
|
|
150
|
+
let headerShown = false;
|
|
151
|
+
let mode: "none" | "reasoning" | "content" = "none";
|
|
152
|
+
let renderer: StreamRenderer | null = null;
|
|
153
|
+
const header = () => { if (!headerShown) { out.write("\n " + chalk.bold.hex(theme.hex)(`${theme.symbol} ${theme.kanji}`) + chalk.hex(theme.hex)(` ${theme.name}`) + "\n\n"); headerShown = true; } };
|
|
154
|
+
const endBlock = () => { if (renderer) { renderer.flush(); renderer = null; out.write("\n"); } };
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
for await (const ev of agent.chatStream(input)) {
|
|
158
|
+
switch (ev.type) {
|
|
159
|
+
case "reasoning":
|
|
160
|
+
stopSpinner();
|
|
161
|
+
if (mode !== "reasoning") { out.write(chalk.dim(" ◦ 思考 ")); mode = "reasoning"; }
|
|
162
|
+
out.write(chalk.dim.italic(String(ev.text).replace(/\s+/g, " ")));
|
|
163
|
+
break;
|
|
164
|
+
case "content":
|
|
165
|
+
stopSpinner();
|
|
166
|
+
if (mode === "reasoning") out.write("\n");
|
|
167
|
+
if (mode !== "content") { header(); renderer = new StreamRenderer(out, { gutter: " " }); mode = "content"; }
|
|
168
|
+
renderer!.write(String(ev.text));
|
|
169
|
+
break;
|
|
170
|
+
case "tool_status":
|
|
171
|
+
stopSpinner();
|
|
172
|
+
endBlock();
|
|
173
|
+
out.write("\n " + pigment(`${theme.symbol} ${ev.tool_name}`) + (ev.label ? chalk.dim(` ${ev.label}`) : "") + chalk.dim(" …") + "\n");
|
|
174
|
+
mode = "none";
|
|
175
|
+
break;
|
|
176
|
+
case "tool_done":
|
|
177
|
+
out.write(" " + (ev.success ? chalk.hex("#3a7a6e")("✓") : chalk.hex("#b3342d")("✗")) + " " + chalk.dim(String(ev.tool_name)) + "\n");
|
|
178
|
+
mode = "none";
|
|
179
|
+
break;
|
|
180
|
+
case "truncated":
|
|
181
|
+
endBlock();
|
|
182
|
+
out.write(chalk.yellow(`\n ⚠ ${ev.reason}\n`));
|
|
183
|
+
break;
|
|
184
|
+
case "done":
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
stopSpinner();
|
|
190
|
+
endBlock();
|
|
191
|
+
}
|
|
192
|
+
out.write("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ═══════════════════════════════════════
|
|
196
|
+
Chat loop
|
|
197
|
+
═══════════════════════════════════════ */
|
|
198
|
+
/* API key persistence — read from config file too */
|
|
199
|
+
function checkApiKeys(): string | null {
|
|
200
|
+
// Check env vars
|
|
201
|
+
const envKeys = ["DEEPSEEK_API_KEY","OPENAI_API_KEY","ANTHROPIC_API_KEY","GROQ_API_KEY","OPENROUTER_API_KEY"];
|
|
202
|
+
for (const k of envKeys) { if (process.env[k]) return "env:" + k; }
|
|
203
|
+
// Check config file
|
|
204
|
+
try {
|
|
205
|
+
const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
|
|
206
|
+
const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
|
|
207
|
+
if (fs.existsSync(cfgPath)) {
|
|
208
|
+
const cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {};
|
|
209
|
+
const keys = cfg.api_keys || {};
|
|
210
|
+
for (const [p, k] of Object.entries(keys)) { if (k) return "cfg:" + p; }
|
|
211
|
+
}
|
|
212
|
+
} catch { /* ignore */ }
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Save API key to config file */
|
|
217
|
+
function saveApiKey(provider: string, key: string): void {
|
|
218
|
+
const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
|
|
219
|
+
const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
|
|
220
|
+
const dir = path.dirname(cfgPath);
|
|
221
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
222
|
+
let cfg: any = {};
|
|
223
|
+
if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
|
|
224
|
+
if (!cfg.api_keys) cfg.api_keys = {};
|
|
225
|
+
cfg.api_keys[provider] = key;
|
|
226
|
+
fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ═══════════════════════════════════════
|
|
230
|
+
Interactive setup wizard
|
|
231
|
+
═══════════════════════════════════════ */
|
|
232
|
+
async function setupWizard(): Promise<{ provider: string; key: string; model: string } | null> {
|
|
233
|
+
// Derived from the single-source model catalog (config/models.yaml).
|
|
234
|
+
// Every listed model is callable — no hardcoded/fictional entries.
|
|
235
|
+
const providers = listProviders().map((id) => ({
|
|
236
|
+
id,
|
|
237
|
+
name: providerLabel(id),
|
|
238
|
+
models: modelsFor(id).map((m) => m.id),
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
process.stdout.write("\n" + chalk.cyan(" ✦ API Key 设置向导 ✦\n\n"));
|
|
242
|
+
process.stdout.write(chalk.dim(" 选择 Provider(Key 保存在 ~/.skyloom/config.yaml):\n\n"));
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < providers.length; i++) {
|
|
245
|
+
process.stdout.write(chalk.dim(` ${String(i+1).padStart(2)}. ${providers[i].name.padEnd(22)} ${providers[i].models.slice(0,3).join(", ")}\n`));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
249
|
+
const ask = (q: string): Promise<string> => new Promise(r => rl.question(q, r));
|
|
250
|
+
|
|
251
|
+
const choice = await ask(chalk.cyan("\n 编号 (1-"+providers.length+", q退出): "));
|
|
252
|
+
if (choice === "q") { rl.close(); return null; }
|
|
253
|
+
const idx = parseInt(choice) - 1;
|
|
254
|
+
if (isNaN(idx) || idx < 0 || idx >= providers.length) { rl.close(); process.stdout.write(chalk.dim(" 已取消\n")); return null; }
|
|
255
|
+
|
|
256
|
+
const prov = providers[idx];
|
|
257
|
+
const key = await ask(chalk.cyan(` ${prov.name} API Key: `));
|
|
258
|
+
if (!key.trim()) { rl.close(); return null; }
|
|
259
|
+
|
|
260
|
+
saveApiKey(prov.id, key.trim());
|
|
261
|
+
|
|
262
|
+
process.stdout.write(chalk.dim("\n 可用模型:\n"));
|
|
263
|
+
for (let i = 0; i < prov.models.length; i++) process.stdout.write(chalk.dim(` ${i+1}. ${prov.models[i]}\n`));
|
|
264
|
+
|
|
265
|
+
const mc = await ask(chalk.cyan("\n 选择模型 (1-"+prov.models.length+", 默认1): ")) || "1";
|
|
266
|
+
const mi = (parseInt(mc) || 1) - 1;
|
|
267
|
+
const model = prov.models[Math.max(0, Math.min(mi, prov.models.length - 1))];
|
|
268
|
+
|
|
269
|
+
// Save to config
|
|
270
|
+
const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
|
|
271
|
+
const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
|
|
272
|
+
let cfg: any = {}; if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
|
|
273
|
+
cfg.default_model = model; cfg.default_provider = prov.id;
|
|
274
|
+
fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
|
|
275
|
+
|
|
276
|
+
rl.close();
|
|
277
|
+
process.stdout.write(chalk.green(`\n ✓ ${prov.name} · ${model} · 就绪!\n\n`));
|
|
278
|
+
return { provider: prov.id, key: key.trim(), model };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function chat(agentName: string, modelOverride?: string): Promise<void> {
|
|
282
|
+
const haveKey = checkApiKeys();
|
|
283
|
+
if (!haveKey) {
|
|
284
|
+
process.stdout.write("\n" + chalk.cyan(" ✦ 天空织机 Skyloom ✦\n"));
|
|
285
|
+
process.stdout.write(chalk.dim(" 检测到未配置 API Key,进入设置向导...\n\n"));
|
|
286
|
+
const result = await setupWizard();
|
|
287
|
+
if (!result) { process.stdout.write(chalk.red(" 设置未完成,请重新运行 sky 配置。\n")); process.exit(0); }
|
|
288
|
+
process.stdout.write(chalk.green(` ✓ ${result.provider} 已就绪 · 模型: ${result.model}\n\n`));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const ctx = createSystemContext();
|
|
292
|
+
let agent = ctx.agentMap.get(agentName);
|
|
293
|
+
if (!agent) { process.stdout.write(chalk.red("Unknown agent: " + agentName) + "\n"); return; }
|
|
294
|
+
|
|
295
|
+
// Validate the active model is real — catches stale/fictional configs
|
|
296
|
+
// before they 404 mid-request.
|
|
297
|
+
try {
|
|
298
|
+
const cfg = loadConfig();
|
|
299
|
+
const activeModel = cfg.agents?.[agentName]?.model || (cfg as any).llm?.default_model;
|
|
300
|
+
const v = validateModel(activeModel);
|
|
301
|
+
if (!v.ok) {
|
|
302
|
+
process.stdout.write(chalk.yellow(`\n ⚠ 配置的模型 "${activeModel || "(未设置)"}" 不在可用目录中。\n`));
|
|
303
|
+
process.stdout.write(chalk.dim(` 可选: ${v.suggestions.join(", ")}\n`));
|
|
304
|
+
process.stdout.write(chalk.dim(` 运行 /setup 重新选择,或编辑 ~/.skyloom/config.yaml。\n\n`));
|
|
305
|
+
}
|
|
306
|
+
} catch { /* validation is best-effort */ }
|
|
307
|
+
|
|
308
|
+
await agent.init();
|
|
309
|
+
|
|
310
|
+
// Wire up security approval — prompt user for HIGH/CRITICAL operations
|
|
311
|
+
try {
|
|
312
|
+
const { getSecurity, DangerLevel } = require("../core/security");
|
|
313
|
+
const sec = getSecurity();
|
|
314
|
+
sec.setApprovalCallback(async (tool: string, args: Record<string, any>, level: number) => {
|
|
315
|
+
process.stdout.write(chalk.yellow(`\n ⚠ ${tool} ( danger level ${level} )\n`));
|
|
316
|
+
process.stdout.write(chalk.dim(` args: ${JSON.stringify(args).slice(0, 80)}\n`));
|
|
317
|
+
const answer = await new Promise<string>(resolve => {
|
|
318
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
319
|
+
rl2.question(chalk.red(" Approve? [y/N] "), (a: string) => { rl2.close(); resolve(a.trim().toLowerCase()); });
|
|
320
|
+
});
|
|
321
|
+
return answer === "y" || answer === "yes";
|
|
322
|
+
});
|
|
323
|
+
} catch { /* security module optional */ }
|
|
324
|
+
|
|
325
|
+
// eslint-disable-next-line prefer-const
|
|
326
|
+
let currentAgent = agent; // mutable for agent switching
|
|
327
|
+
welcome(agent);
|
|
328
|
+
|
|
329
|
+
process.stdout.write(chalk.dim(" · 输入 / 看命令(Tab 补全)· ↑↓ 翻历史 · Ctrl-C 退出\n\n"));
|
|
330
|
+
|
|
331
|
+
while (true) {
|
|
332
|
+
const inp = await readLine(currentAgent.name);
|
|
333
|
+
if (!inp) continue;
|
|
334
|
+
|
|
335
|
+
// Bare "/" → show the inline command palette
|
|
336
|
+
if (inp === "/") { process.stdout.write("\n" + renderPalette("") + "\n"); continue; }
|
|
337
|
+
|
|
338
|
+
const cmdL = inp.toLowerCase();
|
|
339
|
+
|
|
340
|
+
// Agent switch — stamp a mineral seal on change
|
|
341
|
+
let switched = false;
|
|
342
|
+
for (const n of AGENT_NAMES) {
|
|
343
|
+
if (cmdL === "/" + n) {
|
|
344
|
+
const a = ctx.agentMap.get(n);
|
|
345
|
+
if (a) {
|
|
346
|
+
await a.init(); currentAgent = a;
|
|
347
|
+
const t = agentTheme(n);
|
|
348
|
+
process.stdout.write("\n " + chalk.bold.hex(t.hex)(`▣ ${t.kanji} ${t.pigment}`) + chalk.dim(` · ${t.specialty}`) + "\n");
|
|
349
|
+
process.stdout.write(" " + chalk.dim.italic(t.poem) + "\n\n");
|
|
350
|
+
}
|
|
351
|
+
switched = true; break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (switched) continue;
|
|
355
|
+
if (cmdL === "/quit" || cmdL === "/exit") break;
|
|
356
|
+
if (cmdL === "/clear") { console.clear(); continue; }
|
|
357
|
+
if (cmdL === "/help") { process.stdout.write("\n" + renderPalette("") + "\n"); continue; }
|
|
358
|
+
if (cmdL === "/version") { process.stdout.write(" Skyloom v" + VERSION + "\n"); continue; }
|
|
359
|
+
if (cmdL === "/status") { process.stdout.write(chalk.bold("\n " + currentAgent.displayName + " (" + currentAgent.name + ")\n") + chalk.dim(" State: " + currentAgent.state + " · Memory: " + currentAgent.memory.shortTerm.length + " msgs\n\n")); continue; }
|
|
360
|
+
if (cmdL === "/cost") { process.stdout.write(chalk.bold("\n Total: " + formatCost(ctx.llm.getTotalCost()) + "\n\n")); continue; }
|
|
361
|
+
if (cmdL === "/cost reset") { (ctx.llm as any).resetUsageStats?.(); process.stdout.write(chalk.dim(" Stats reset\n")); continue; }
|
|
362
|
+
if (cmdL === "/compact") { const r = await currentAgent.compact(); process.stdout.write(chalk.green(" ✓ " + r + "\n\n")); continue; }
|
|
363
|
+
if (cmdL === "/memory") { process.stdout.write(chalk.dim(" Short-term: " + currentAgent.memory.shortTerm.length + " msgs · Working: " + Object.keys(currentAgent.memory.working).length + " keys\n")); continue; }
|
|
364
|
+
if (cmdL === "/memory clear") { await currentAgent.memory.clearShortTerm(); process.stdout.write(chalk.dim(" Memory cleared\n")); continue; }
|
|
365
|
+
if (cmdL === "/workspace") { process.stdout.write(chalk.dim(" " + (ctx.workspacePath || "default") + "\n")); continue; }
|
|
366
|
+
if (cmdL === "/sessions") { const ss = await currentAgent.memory.listSessions(); process.stdout.write(chalk.bold("\n Sessions:\n")); for (const s of ss.slice(0, 10)) process.stdout.write(chalk.dim(" " + s.id?.slice(0, 10) + "... " + s.preview + " (" + s.messageCount + " msgs)\n")); continue; }
|
|
367
|
+
if (cmdL === "/mcp") { process.stdout.write(chalk.dim(" " + (ctx.mcpStatus?.join(", ") || "none") + "\n")); continue; }
|
|
368
|
+
if (cmdL.startsWith("/apikey set ")) { const p = inp.split(/\s+/); if (p.length >= 4) { saveApiKey(p[2], p[3]); process.stdout.write(chalk.green(" ✓ Saved " + p[2] + " API key\n")); } else { process.stdout.write(chalk.yellow(" Usage: /apikey set <provider> <key>\n")); } continue; }
|
|
369
|
+
if (cmdL === "/apikey") { process.stdout.write(chalk.bold("\n API Keys:\n")); for (const p of ["openai","deepseek","anthropic","groq","openrouter"]) { process.stdout.write(chalk.dim(" " + p.padEnd(14) + (!!process.env[p.toUpperCase() + "_API_KEY"] ? chalk.green("env") : chalk.dim("—")) + "\n")); } process.stdout.write("\n"); continue; }
|
|
370
|
+
if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); continue; }
|
|
371
|
+
if (cmdL === "/setup") { const r = await setupWizard(); if (r) process.stdout.write(chalk.green(` ${r.provider} · ${r.model} — Ready!\n`)); continue; }
|
|
372
|
+
if (cmdL.startsWith("/model")) { process.stdout.write(chalk.dim(" Run /setup to reconfigure models\n")); continue; }
|
|
373
|
+
if (inp.startsWith("/")) { process.stdout.write("\n" + chalk.dim(` 未知命令 ${inp.split(" ")[0]}\n`) + renderPalette(cmdL.split(" ")[0]) + "\n"); continue; }
|
|
374
|
+
|
|
375
|
+
// ── Chat (real streaming) ──
|
|
376
|
+
try {
|
|
377
|
+
await streamResponse(currentAgent, inp);
|
|
378
|
+
} catch (e: any) {
|
|
379
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
380
|
+
process.stdout.write(chalk.red(" ✗ " + (e.message || e) + "\n\n"));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
process.stdout.write(chalk.dim("\n Session ended\n"));
|
|
385
|
+
await ctx.closeAll();
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* ═══════════════════════════════════════
|
|
390
|
+
Task
|
|
391
|
+
═══════════════════════════════════════ */
|
|
392
|
+
async function runTask(goal: string): Promise<void> {
|
|
393
|
+
const ctx = createSystemContext();
|
|
394
|
+
await ctx.initAll();
|
|
395
|
+
const [, results, summary] = await orchestrateTask(goal, ctx.agentMap);
|
|
396
|
+
for (const r of results) process.stdout.write(` ${r.success ? chalk.green("✓") : chalk.red("✗")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}\n`);
|
|
397
|
+
process.stdout.write(chalk.bold("\n " + summary.slice(0, 800) + "\n\n"));
|
|
398
|
+
await ctx.closeAll();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
/* ═══════════════════════════════════════
|
|
403
|
+
Entry
|
|
404
|
+
═══════════════════════════════════════ */
|
|
405
|
+
async function main() {
|
|
406
|
+
const args = process.argv.slice(2);
|
|
407
|
+
if (args.length === 0) { await chat("fog"); return; }
|
|
408
|
+
if ((AGENT_NAMES as readonly string[]).includes(args[0])) {
|
|
409
|
+
let m: string | undefined;
|
|
410
|
+
for (let i = 1; i < args.length; i++) if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
|
|
411
|
+
await chat(args[0], m); return;
|
|
412
|
+
}
|
|
413
|
+
if (!["chat", "task", "web", "config", "init", "version", "mcp", "help"].includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
|
|
414
|
+
program.parse(process.argv);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main().catch(e => { process.stderr.write(chalk.red(`Fatal: ${(e as Error).message}\n`)); process.exit(1); });
|