chainlesschain 0.43.4 → 0.45.0

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/README.md CHANGED
@@ -206,11 +206,12 @@ Agent slash commands: `/plan` (plan mode), `/plan interactive <request>` (LLM-dr
206
206
 
207
207
  ### `chainlesschain skill <action>`
208
208
 
209
- Manage and run 138 built-in AI skills.
209
+ Manage and run 138 built-in AI skills across a 4-layer system: bundled < marketplace < managed (global) < workspace (project).
210
210
 
211
211
  ```bash
212
212
  chainlesschain skill list # List all skills grouped by category
213
213
  chainlesschain skill list --category automation
214
+ chainlesschain skill list --category cli-direct # CLI command skill packs
214
215
  chainlesschain skill list --tag code --runnable
215
216
  chainlesschain skill list --json # JSON output
216
217
  chainlesschain skill categories # Show category breakdown
@@ -218,7 +219,45 @@ chainlesschain skill info code-review # Detailed skill info + docs
218
219
  chainlesschain skill info code-review --json
219
220
  chainlesschain skill search "browser" # Search by keyword
220
221
  chainlesschain skill run code-review "Review this function..."
221
- ```
222
+ chainlesschain skill add my-skill # Create custom project skill
223
+ chainlesschain skill remove my-skill # Remove custom skill
224
+ chainlesschain skill sources # Show skill layer paths and counts
225
+ ```
226
+
227
+ #### CLI Command Skill Packs
228
+
229
+ Automatically wraps 63 CLI commands into 9 Agent-callable domain skill packs:
230
+
231
+ ```bash
232
+ chainlesschain skill sync-cli # Generate/update all 9 CLI skill packs
233
+ chainlesschain skill sync-cli --force # Force regenerate all packs
234
+ chainlesschain skill sync-cli --dry-run # Preview changes without writing
235
+ chainlesschain skill sync-cli --remove # Remove all CLI packs
236
+ chainlesschain skill sync-cli --json # JSON output
237
+
238
+ # Run CLI commands via skill packs (Agent can call these directly)
239
+ chainlesschain skill run cli-knowledge-pack "note list"
240
+ chainlesschain skill run cli-identity-pack "did create"
241
+ chainlesschain skill run cli-infra-pack "services up"
242
+ chainlesschain skill run cli-ai-query-pack "ask what is RAG"
243
+ chainlesschain skill run cli-agent-mode-pack "agent"
244
+ chainlesschain skill run cli-web3-pack "wallet assets"
245
+ chainlesschain skill run cli-security-pack "encrypt file secret.txt"
246
+ chainlesschain skill run cli-enterprise-pack "org list"
247
+ chainlesschain skill run cli-integration-pack "mcp servers"
248
+ ```
249
+
250
+ | Pack | Mode | Commands |
251
+ | ---------------------- | --------- | -------------------------------------------------------- |
252
+ | `cli-knowledge-pack` | direct | note, search, memory, session, import, export |
253
+ | `cli-identity-pack` | direct | did, auth, audit |
254
+ | `cli-infra-pack` | direct | setup, start, stop, status, services, config, doctor, db |
255
+ | `cli-ai-query-pack` | llm-query | ask, llm, instinct, tokens |
256
+ | `cli-agent-mode-pack` | agent | agent, chat, cowork |
257
+ | `cli-web3-pack` | direct | wallet, p2p, sync, did |
258
+ | `cli-security-pack` | direct | encrypt, decrypt, audit, pqc |
259
+ | `cli-enterprise-pack` | direct | org, plugin, lowcode, compliance |
260
+ | `cli-integration-pack` | hybrid | mcp, browse, cli-anything, serve, ui |
222
261
 
223
262
  ---
224
263
 
@@ -504,12 +543,73 @@ chainlesschain plugin summary # Installation summary
504
543
 
505
544
  ### `chainlesschain init`
506
545
 
507
- Initialize a new ChainlessChain project.
546
+ Initialize a new ChainlessChain project with a `.chainlesschain/` directory, workspace skills, and an optional AI persona.
547
+
548
+ ```bash
549
+ chainlesschain init # Interactive template selection
550
+ chainlesschain init --bare # Minimal project structure
551
+ chainlesschain init --template code-project --yes # Software project (code-review, refactor, unit-test)
552
+ chainlesschain init --template data-science --yes # Data science / ML project
553
+ chainlesschain init --template devops --yes # DevOps / infrastructure project
554
+ chainlesschain init --template medical-triage --yes # Medical triage assistant (with Persona)
555
+ chainlesschain init --template agriculture-expert --yes # Agriculture expert (with Persona)
556
+ chainlesschain init --template general-assistant --yes # General-purpose assistant (with Persona)
557
+ chainlesschain init --template ai-media-creator --yes # AI media creator (ComfyUI/AnimateDiff/TTS)
558
+ chainlesschain init --template ai-doc-creator --yes # AI doc creator (LibreOffice/pandoc/doc-edit)
559
+ chainlesschain init --template empty --yes # Bare project
560
+ ```
561
+
562
+ #### AI Media Creator Template (`ai-media-creator`)
563
+
564
+ Generates 3 workspace skills for AI image/video/audio creation:
565
+
566
+ ```bash
567
+ chainlesschain skill run comfyui-image "a sunset over mountains, oil painting style"
568
+ chainlesschain skill run comfyui-video '{"prompt":"a cat walking","workflow":"workflows/animatediff.json"}'
569
+ chainlesschain skill run audio-gen "你好,欢迎使用 ChainlessChain"
570
+ ```
571
+
572
+ | Skill | Description |
573
+ | --------------- | --------------------------------------------------------------------- |
574
+ | `comfyui-image` | ComfyUI REST API image generation (txt2img/img2img, custom workflows) |
575
+ | `comfyui-video` | ComfyUI + AnimateDiff video generation (requires workflow JSON) |
576
+ | `audio-gen` | AI TTS: auto-selects edge-tts → piper-tts → ElevenLabs → OpenAI |
577
+
578
+ Also creates a `workflows/` directory with README for saving ComfyUI workflow JSON files.
579
+
580
+ #### AI Doc Creator Template (`ai-doc-creator`)
581
+
582
+ Generates 3 workspace skills for AI document creation and editing:
583
+
584
+ ```bash
585
+ chainlesschain skill run doc-generate "2026年技术趋势分析报告"
586
+ chainlesschain skill run doc-generate '{"topic":"项目方案","format":"docx","style":"proposal"}'
587
+ chainlesschain skill run libre-convert "report.docx"
588
+ chainlesschain skill run libre-convert '{"input_file":"slides.pptx","format":"pdf"}'
589
+ chainlesschain skill run doc-edit '{"input_file":"report.md","instruction":"优化摘要部分"}'
590
+ chainlesschain skill run doc-edit '{"input_file":"data.xlsx","instruction":"首字母大写"}'
591
+ ```
592
+
593
+ | Skill | Description |
594
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
595
+ | `doc-generate` | AI-generated structured documents: md/html/docx/pdf, 4 styles (report/proposal/manual/readme) |
596
+ | `libre-convert` | LibreOffice headless conversion: docx/pdf/html/odt/pptx/xlsx/png |
597
+ | `doc-edit` | AI edit existing docs: md/txt/html (direct LLM), docx (pandoc/soffice), xlsx (openpyxl, formulas preserved), pptx (python-pptx, charts preserved) |
598
+
599
+ Requirements: `winget install pandoc` (for docx), `winget install LibreOffice.LibreOffice` (for PDF/format conversion).
600
+
601
+ Also creates a `templates/` directory with README for document templates.
602
+
603
+ ### `chainlesschain persona <action>`
604
+
605
+ Manage the AI persona for the current project (set by `init` templates or manually).
508
606
 
509
607
  ```bash
510
- chainlesschain init # Interactive project init
511
- chainlesschain init --bare # Minimal project structure
512
- chainlesschain init --template code-project --yes # Use template, skip prompts
608
+ chainlesschain persona show # Show current project persona
609
+ chainlesschain persona set --name "Bot" --role "Helper" # Set persona name and role
610
+ chainlesschain persona set -b "Always respond in English" # Add behavior constraint
611
+ chainlesschain persona set --tools-disabled run_shell # Disable specific tools
612
+ chainlesschain persona reset # Remove persona, restore default
513
613
  ```
514
614
 
515
615
  ### `chainlesschain cowork <action>`
@@ -907,6 +1007,36 @@ chainlesschain serve --project /path/to/project # Default project root f
907
1007
 
908
1008
  ---
909
1009
 
1010
+ ## Web Management Interface (v0.45.0)
1011
+
1012
+ ### `chainlesschain ui`
1013
+
1014
+ Open a browser-based Web management interface — no extra software required.
1015
+
1016
+ ```bash
1017
+ chainlesschain ui # Auto-detect mode, open browser
1018
+ chainlesschain ui --port 18810 # Custom HTTP port
1019
+ chainlesschain ui --ws-port 18800 # Custom WebSocket port
1020
+ chainlesschain ui --no-open # Start server without opening browser
1021
+ chainlesschain ui --token <secret> # Enable WebSocket auth token
1022
+ chainlesschain ui --host 0.0.0.0 # Bind to all interfaces (remote access)
1023
+ ```
1024
+
1025
+ **Two modes** (auto-detected based on current directory):
1026
+
1027
+ | Mode | Trigger | Description |
1028
+ | ---------------- | -------------------------------------------- | -------------------------------------------------------------- |
1029
+ | **Project mode** | Run from a directory with `.chainlesschain/` | AI automatically loads project context (rules, skills, config) |
1030
+ | **Global mode** | Run from any non-project directory | General-purpose AI management panel |
1031
+
1032
+ **Features**: streaming Markdown output, session management (new/switch/history), Agent/Chat mode toggle, slot-filling interactive dialogs, auto-reconnect (3s), Token auth.
1033
+
1034
+ **Ports**: HTTP 18810 (Web UI page), WebSocket 18800 (reuses `chainlesschain serve` infrastructure).
1035
+
1036
+ **Security**: JSON config embedded with XSS-safe Unicode escaping (`\u003c`/`\u003e`); Token auth via `--token`.
1037
+
1038
+ ---
1039
+
910
1040
  ## Global Options
911
1041
 
912
1042
  ```bash
@@ -980,7 +1110,7 @@ Configuration is stored at `~/.chainlesschain/config.json`. The CLI creates and
980
1110
  ```bash
981
1111
  cd packages/cli
982
1112
  npm install
983
- npm test # Run all tests (2748 tests across 124 files)
1113
+ npm test # Run all tests (3050+ tests across 130+ files)
984
1114
  npm run test:unit # Unit tests only
985
1115
  npm run test:integration # Integration tests
986
1116
  npm run test:e2e # End-to-end tests
@@ -988,17 +1118,22 @@ npm run test:e2e # End-to-end tests
988
1118
 
989
1119
  ### Test Coverage
990
1120
 
991
- | Category | Files | Tests | Status |
992
- | ------------------------ | ------- | -------- | --------------- |
993
- | Unit — lib modules | 63 | 1380+ | All passing |
994
- | Unit — commands | 15 | 350+ | All passing |
995
- | Unit — runtime | 1 | 6 | All passing |
996
- | Integration | 6 | 40+ | All passing |
997
- | E2E | 15 | 160+ | All passing |
998
- | Core packages (external) | | 118 | All passing |
999
- | Unit — WS sessions | 9 | 156 | All passing |
1000
- | Integration — WS session | 1 | 12 | All passing |
1001
- | **CLI Total** | **124** | **2748** | **All passing** |
1121
+ | Category | Files | Tests | Status |
1122
+ | ------------------------- | ------- | -------- | --------------- |
1123
+ | Unit — lib modules | 70 | 1700+ | All passing |
1124
+ | Unit — commands | 17 | 400+ | All passing |
1125
+ | Unit — runtime | 1 | 6 | All passing |
1126
+ | Unit — WS sessions | 9 | 156 | All passing |
1127
+ | Unit — Skill Packs | 2 | 57+ | All passing |
1128
+ | Unit AI Templates | 2 | 130+ | All passing |
1129
+ | Integration | 13 | 230+ | All passing |
1130
+ | Integration — WS session | 1 | 12 | All passing |
1131
+ | Integration AI Handlers | 2 | 100+ | All passing |
1132
+ | E2E | 15 | 260+ | All passing |
1133
+ | E2E — Skill Packs | 1 | 23+ | All passing |
1134
+ | E2E — AI Templates | 4 | 65+ | All passing |
1135
+ | Core packages (external) | — | 118 | All passing |
1136
+ | **CLI Total** | **132** | **3056** | **All passing** |
1002
1137
 
1003
1138
  ## License
1004
1139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.43.4",
3
+ "version": "0.45.0",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -206,7 +206,7 @@ async function pollUntilDone(promptId) {
206
206
  return { success: false, error: "Timeout waiting for image generation" };
207
207
  }
208
208
 
209
- module.exports = async function comfyuiImageHandler(params) {
209
+ async function comfyuiImageHandler(params) {
210
210
  const { prompt, negative_prompt, width, height, steps, workflow } = params;
211
211
 
212
212
  if (!prompt) {
@@ -278,7 +278,16 @@ module.exports = async function comfyuiImageHandler(params) {
278
278
  message: \`Generated \${result.images.length} image(s). Open URLs to download:\`,
279
279
  urls: result.images.map((i) => i.url),
280
280
  };
281
+ }
282
+
283
+ comfyuiImageHandler.execute = async (task, _ctx, _skill) => {
284
+ const input = typeof task === "string" ? task : (task.input || task.params?.input || "");
285
+ let p = {};
286
+ try { p = input.trim().startsWith("{") ? JSON.parse(input) : { prompt: input }; }
287
+ catch { p = { prompt: input }; }
288
+ return comfyuiImageHandler(p);
281
289
  };
290
+ module.exports = comfyuiImageHandler;
282
291
  `,
283
292
  },
284
293
  "comfyui-video": {
@@ -429,7 +438,7 @@ async function pollUntilDone(promptId) {
429
438
  return { success: false, error: "Timeout waiting for video generation" };
430
439
  }
431
440
 
432
- module.exports = async function comfyuiVideoHandler(params) {
441
+ async function comfyuiVideoHandler(params) {
433
442
  const { prompt, frames, fps, workflow } = params;
434
443
 
435
444
  if (!prompt) {
@@ -510,7 +519,16 @@ module.exports = async function comfyuiVideoHandler(params) {
510
519
  message: \`Generated \${result.outputs.length} output(s). URLs:\`,
511
520
  urls: result.outputs.map((o) => o.url),
512
521
  };
522
+ }
523
+
524
+ comfyuiVideoHandler.execute = async (task, _ctx, _skill) => {
525
+ const input = typeof task === "string" ? task : (task.input || task.params?.input || "");
526
+ let p = {};
527
+ try { p = input.trim().startsWith("{") ? JSON.parse(input) : { prompt: input }; }
528
+ catch { p = { prompt: input }; }
529
+ return comfyuiVideoHandler(p);
513
530
  };
531
+ module.exports = comfyuiVideoHandler;
514
532
  `,
515
533
  },
516
534
  "audio-gen": {
@@ -680,7 +698,7 @@ function callOpenAITTS(text, voice, outputPath, apiKey) {
680
698
  });
681
699
  }
682
700
 
683
- module.exports = async function audioGenHandler(params) {
701
+ async function audioGenHandler(params) {
684
702
  const { text, voice, type, output } = params;
685
703
 
686
704
  if (!text) {
@@ -780,6 +798,15 @@ async function checkPythonModule(moduleName) {
780
798
  return false;
781
799
  }
782
800
  }
801
+
802
+ audioGenHandler.execute = async (task, _ctx, _skill) => {
803
+ const input = typeof task === "string" ? task : (task.input || task.params?.input || "");
804
+ let p = {};
805
+ try { p = input.trim().startsWith("{") ? JSON.parse(input) : { text: input }; }
806
+ catch { p = { text: input }; }
807
+ return audioGenHandler(p);
808
+ };
809
+ module.exports = audioGenHandler;
783
810
  `,
784
811
  },
785
812
  };
@@ -1097,7 +1124,7 @@ function generateContent(topic, style, outline) {
1097
1124
 
1098
1125
  // ─── Main handler ─────────────────────────────────────────────────
1099
1126
 
1100
- module.exports = async function docGenerateHandler(params) {
1127
+ async function docGenerateHandler(params) {
1101
1128
  const { topic, format, outline, style, output } = params;
1102
1129
 
1103
1130
  if (!topic) {
@@ -1206,7 +1233,16 @@ module.exports = async function docGenerateHandler(params) {
1206
1233
  }
1207
1234
 
1208
1235
  return { error: \`Unsupported format: \${fmt}\`, hint: "Supported formats: md, html, docx, pdf" };
1236
+ }
1237
+
1238
+ docGenerateHandler.execute = async (task, _ctx, _skill) => {
1239
+ const input = typeof task === "string" ? task : (task.input || task.params?.input || "");
1240
+ let p = {};
1241
+ try { p = input.trim().startsWith("{") ? JSON.parse(input) : { topic: input }; }
1242
+ catch { p = { topic: input }; }
1243
+ return docGenerateHandler(p);
1209
1244
  };
1245
+ module.exports = docGenerateHandler;
1210
1246
  `,
1211
1247
  },
1212
1248
 
@@ -1337,7 +1373,7 @@ function findSoffice() {
1337
1373
  return null;
1338
1374
  }
1339
1375
 
1340
- module.exports = async function libreConvertHandler(params) {
1376
+ async function libreConvertHandler(params) {
1341
1377
  const { input_file, format, outdir } = params;
1342
1378
 
1343
1379
  if (!input_file) {
@@ -1442,7 +1478,16 @@ module.exports = async function libreConvertHandler(params) {
1442
1478
  format: targetFormat,
1443
1479
  message: \`Converted to \${targetFormat}: \${outputFile}\`,
1444
1480
  };
1481
+ }
1482
+
1483
+ libreConvertHandler.execute = async (task, _ctx, _skill) => {
1484
+ const input = typeof task === "string" ? task : (task.input || task.params?.input || "");
1485
+ let p = {};
1486
+ try { p = input.trim().startsWith("{") ? JSON.parse(input) : { input_file: input }; }
1487
+ catch { p = { input_file: input }; }
1488
+ return libreConvertHandler(p);
1445
1489
  };
1490
+ module.exports = libreConvertHandler;
1446
1491
  `,
1447
1492
  },
1448
1493
  });
@@ -1948,7 +1993,7 @@ prs.save(r"""\${outputFile}""")
1948
1993
  }
1949
1994
 
1950
1995
  // ── Main export ───────────────────────────────────────────────────────────────
1951
- module.exports = async function docEdit(params) {
1996
+ async function docEdit(params) {
1952
1997
  const { input_file, instruction, action = "edit", section, output_dir } = params || {};
1953
1998
 
1954
1999
  if (!input_file) {
@@ -1986,7 +2031,16 @@ module.exports = async function docEdit(params) {
1986
2031
  error: \`不支持的格式: \${ext}\`,
1987
2032
  hint: "支持的格式: md, txt, html, docx, xlsx, pptx",
1988
2033
  };
2034
+ }
2035
+
2036
+ docEdit.execute = async (task, _ctx, _skill) => {
2037
+ const input = typeof task === "string" ? task : (task.input || task.params?.input || "");
2038
+ let p = {};
2039
+ try { p = input.trim().startsWith("{") ? JSON.parse(input) : { input_file: input }; }
2040
+ catch { p = { input_file: input }; }
2041
+ return docEdit(p);
1989
2042
  };
2043
+ module.exports = docEdit;
1990
2044
  `,
1991
2045
  },
1992
2046
  });
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ui command — start a local web management UI
3
+ * chainlesschain ui [--port] [--ws-port] [--host] [--no-open] [--token]
4
+ *
5
+ * Project mode (run from a dir with .chainlesschain/): project-scoped chat UI
6
+ * Global mode (run from any other dir): global management panel
7
+ */
8
+
9
+ import { execSync } from "child_process";
10
+ import path from "path";
11
+ import chalk from "chalk";
12
+ import { logger } from "../lib/logger.js";
13
+ import { ChainlessChainWSServer } from "../lib/ws-server.js";
14
+ import { WSSessionManager } from "../lib/ws-session-manager.js";
15
+ import { createWebUIServer } from "../lib/web-ui-server.js";
16
+ import { bootstrap } from "../runtime/bootstrap.js";
17
+ import { findProjectRoot, loadProjectConfig } from "../lib/project-detector.js";
18
+
19
+ /**
20
+ * Open a URL in the system default browser (cross-platform).
21
+ */
22
+ function openBrowser(url) {
23
+ try {
24
+ const platform = process.platform;
25
+ if (platform === "win32") {
26
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
27
+ } else if (platform === "darwin") {
28
+ execSync(`open "${url}"`, { stdio: "ignore" });
29
+ } else {
30
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
31
+ }
32
+ } catch (_err) {
33
+ // Non-critical — user can open manually
34
+ }
35
+ }
36
+
37
+ export function registerUiCommand(program) {
38
+ program
39
+ .command("ui")
40
+ .description("Start a local web management UI (project or global mode)")
41
+ .option("-p, --port <port>", "HTTP server port", "18810")
42
+ .option("--ws-port <port>", "WebSocket server port", "18800")
43
+ .option("-H, --host <host>", "Bind host", "127.0.0.1")
44
+ .option("--no-open", "Do not open browser automatically")
45
+ .option(
46
+ "--token <token>",
47
+ "Authentication token for WebSocket (recommended for security)",
48
+ )
49
+ .action(async (opts) => {
50
+ const httpPort = parseInt(opts.port, 10);
51
+ const wsPort = parseInt(opts.wsPort, 10);
52
+ const host = opts.host;
53
+
54
+ if (isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
55
+ logger.error("Invalid --port. Must be between 1 and 65535.");
56
+ process.exit(1);
57
+ }
58
+ if (isNaN(wsPort) || wsPort < 1 || wsPort > 65535) {
59
+ logger.error("Invalid --ws-port. Must be between 1 and 65535.");
60
+ process.exit(1);
61
+ }
62
+
63
+ // ── Detect project context ────────────────────────────────────────────
64
+ const projectRoot = findProjectRoot(process.cwd());
65
+ const projectConfig = projectRoot ? loadProjectConfig(projectRoot) : null;
66
+ const projectName =
67
+ projectConfig?.name ||
68
+ (projectRoot ? path.basename(projectRoot) : null);
69
+ const mode = projectRoot ? "project" : "global";
70
+
71
+ // ── Bootstrap headless runtime ────────────────────────────────────────
72
+ let db = null;
73
+ try {
74
+ const ctx = await bootstrap({ skipDb: false });
75
+ db = ctx.db?.getDb?.() || null;
76
+ } catch (_err) {
77
+ logger.log(
78
+ chalk.yellow(
79
+ " Warning: Database not available, sessions will be in-memory only",
80
+ ),
81
+ );
82
+ }
83
+
84
+ // ── Start WebSocket server ────────────────────────────────────────────
85
+ const sessionManager = new WSSessionManager({
86
+ db,
87
+ defaultProjectRoot: projectRoot || process.cwd(),
88
+ });
89
+
90
+ const wsServer = new ChainlessChainWSServer({
91
+ port: wsPort,
92
+ host,
93
+ token: opts.token || null,
94
+ maxConnections: 20,
95
+ timeout: 60000,
96
+ sessionManager,
97
+ });
98
+
99
+ try {
100
+ await wsServer.start();
101
+ } catch (err) {
102
+ logger.error(`Failed to start WebSocket server: ${err.message}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ // ── Start HTTP server ─────────────────────────────────────────────────
107
+ const httpServer = createWebUIServer({
108
+ wsPort,
109
+ wsToken: opts.token || null,
110
+ wsHost: host === "0.0.0.0" ? "127.0.0.1" : host,
111
+ projectRoot,
112
+ projectName,
113
+ mode,
114
+ });
115
+
116
+ try {
117
+ await new Promise((resolve, reject) => {
118
+ httpServer.listen(httpPort, host, () => resolve());
119
+ httpServer.on("error", reject);
120
+ });
121
+ } catch (err) {
122
+ logger.error(`Failed to start HTTP server: ${err.message}`);
123
+ process.exit(1);
124
+ }
125
+
126
+ // ── Print startup info ────────────────────────────────────────────────
127
+ const uiUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${httpPort}`;
128
+
129
+ logger.log("");
130
+ logger.log(chalk.bold(" ChainlessChain Web UI"));
131
+ logger.log("");
132
+ if (mode === "project") {
133
+ logger.log(
134
+ ` Mode: ${chalk.cyan("project")} ${chalk.dim(projectRoot)}`,
135
+ );
136
+ if (projectName) {
137
+ logger.log(` Project: ${chalk.green(projectName)}`);
138
+ }
139
+ } else {
140
+ logger.log(` Mode: ${chalk.cyan("global")}`);
141
+ }
142
+ logger.log(` UI: ${chalk.cyan(uiUrl)}`);
143
+ logger.log(` WS: ${chalk.dim(`ws://${host}:${wsPort}`)}`);
144
+ logger.log(
145
+ ` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
146
+ );
147
+ logger.log("");
148
+ logger.log(chalk.dim(" Press Ctrl+C to stop"));
149
+ logger.log("");
150
+
151
+ // ── Open browser ──────────────────────────────────────────────────────
152
+ if (opts.open !== false) {
153
+ openBrowser(uiUrl);
154
+ }
155
+
156
+ // ── Graceful shutdown ─────────────────────────────────────────────────
157
+ const shutdown = async () => {
158
+ logger.log("\n" + chalk.yellow("Shutting down UI server..."));
159
+ await Promise.all([
160
+ new Promise((resolve) => httpServer.close(resolve)),
161
+ wsServer.stop(),
162
+ ]);
163
+ process.exit(0);
164
+ };
165
+
166
+ process.on("SIGINT", shutdown);
167
+ process.on("SIGTERM", shutdown);
168
+ });
169
+ }
package/src/index.js CHANGED
@@ -86,6 +86,9 @@ import { registerCliAnythingCommand } from "./commands/cli-anything.js";
86
86
  // WebSocket Server Interface
87
87
  import { registerServeCommand } from "./commands/serve.js";
88
88
 
89
+ // Web UI
90
+ import { registerUiCommand } from "./commands/ui.js";
91
+
89
92
  export function createProgram() {
90
93
  const program = new Command();
91
94
 
@@ -201,5 +204,8 @@ export function createProgram() {
201
204
  // WebSocket Server Interface
202
205
  registerServeCommand(program);
203
206
 
207
+ // Web UI
208
+ registerUiCommand(program);
209
+
204
210
  return program;
205
211
  }