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 +153 -18
- package/package.json +1 -1
- package/src/commands/init.js +60 -6
- package/src/commands/ui.js +169 -0
- package/src/index.js +6 -0
- package/src/lib/web-ui-server.js +1114 -0
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
|
|
511
|
-
chainlesschain
|
|
512
|
-
chainlesschain
|
|
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 (
|
|
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
|
|
992
|
-
|
|
|
993
|
-
| Unit — lib modules
|
|
994
|
-
| Unit — commands
|
|
995
|
-
| Unit — runtime
|
|
996
|
-
|
|
|
997
|
-
|
|
|
998
|
-
|
|
|
999
|
-
|
|
|
1000
|
-
| Integration — WS session
|
|
1001
|
-
|
|
|
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
package/src/commands/init.js
CHANGED
|
@@ -206,7 +206,7 @@ async function pollUntilDone(promptId) {
|
|
|
206
206
|
return { success: false, error: "Timeout waiting for image generation" };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|