chainlesschain 0.45.4 → 0.45.5
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 +54 -0
- package/package.json +1 -1
- package/src/commands/orchestrate.js +513 -0
- package/src/index.js +6 -0
- package/src/lib/agent-router.js +397 -0
- package/src/lib/claude-code-bridge.js +353 -0
- package/src/lib/notifiers/dingtalk.js +99 -0
- package/src/lib/notifiers/feishu.js +112 -0
- package/src/lib/notifiers/index.js +183 -0
- package/src/lib/notifiers/telegram.js +131 -0
- package/src/lib/notifiers/websocket.js +67 -0
- package/src/lib/notifiers/wecom.js +74 -0
- package/src/lib/orchestrator.js +438 -0
- package/src/lib/ws-server.js +87 -0
package/README.md
CHANGED
|
@@ -1037,6 +1037,60 @@ chainlesschain ui --host 0.0.0.0 # Bind to all interfaces (remote access)
|
|
|
1037
1037
|
|
|
1038
1038
|
---
|
|
1039
1039
|
|
|
1040
|
+
## AI Orchestration Layer (v0.45.4)
|
|
1041
|
+
|
|
1042
|
+
### `chainlesschain orchestrate`
|
|
1043
|
+
|
|
1044
|
+
Use ChainlessChain as an orchestration layer — automatically decompose tasks, dispatch to parallel AI coding agents (Claude Code / Codex / Gemini / OpenAI / Ollama), verify with CI/CD, and notify via Telegram / WeCom / DingTalk / Feishu.
|
|
1045
|
+
|
|
1046
|
+
```bash
|
|
1047
|
+
chainlesschain orchestrate "Fix the auth bug" # Auto-detect AI tool and run
|
|
1048
|
+
chainlesschain orchestrate "Refactor payments" \
|
|
1049
|
+
--backends claude,gemini --strategy parallel-all # Multi-backend, parallel
|
|
1050
|
+
chainlesschain orchestrate "Add tests" \
|
|
1051
|
+
--ci "npm run test:unit" --retries 5 # Custom CI + retries
|
|
1052
|
+
chainlesschain orchestrate "task" --no-ci # Skip CI verification
|
|
1053
|
+
chainlesschain orchestrate "task" --json # JSON output (for scripts)
|
|
1054
|
+
chainlesschain orchestrate detect # Detect installed AI CLIs
|
|
1055
|
+
chainlesschain orchestrate --status # Show orchestrator status
|
|
1056
|
+
chainlesschain orchestrate --status --json # JSON status with backends list
|
|
1057
|
+
chainlesschain orchestrate --webhook # Start IM webhook server (port 18820)
|
|
1058
|
+
chainlesschain orchestrate --webhook --webhook-port 9090 # Custom port
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
**Routing strategies** (`--strategy`):
|
|
1062
|
+
|
|
1063
|
+
| Strategy | Description |
|
|
1064
|
+
| -------------- | -------------------------------------------------------------- |
|
|
1065
|
+
| `round-robin` | Weighted round-robin across all backends (default) |
|
|
1066
|
+
| `primary` | Use first backend, auto-fallback on failure |
|
|
1067
|
+
| `parallel-all` | Run all backends simultaneously, pick best result |
|
|
1068
|
+
| `by-type` | Route by task type (`code-generation` / `analysis` / `review`) |
|
|
1069
|
+
|
|
1070
|
+
**Auto-detected backends**: `claude` (CLI), `codex` (CLI), `gemini` / `openai` / `anthropic` (API key env vars), `ollama` (always included as local fallback).
|
|
1071
|
+
|
|
1072
|
+
**Notification channels** (configured via env vars):
|
|
1073
|
+
|
|
1074
|
+
```bash
|
|
1075
|
+
TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=... # Telegram
|
|
1076
|
+
WECOM_WEBHOOK_URL=... # WeCom (企业微信)
|
|
1077
|
+
DINGTALK_WEBHOOK_URL=... DINGTALK_SECRET=... # DingTalk (钉钉)
|
|
1078
|
+
FEISHU_WEBHOOK_URL=... FEISHU_SECRET=... # Feishu (飞书)
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
**Incoming webhooks** — receive task commands from IM platforms:
|
|
1082
|
+
|
|
1083
|
+
```bash
|
|
1084
|
+
chainlesschain orchestrate --webhook --webhook-port 18820
|
|
1085
|
+
# POST /wecom (WeCom XML)
|
|
1086
|
+
# POST /dingtalk (DingTalk JSON)
|
|
1087
|
+
# POST /feishu (Feishu JSON + challenge verification)
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
**WebSocket integration** — trigger via `{ "type": "orchestrate", "task": "...", "cwd": "..." }`, receive real-time `orchestrate:event` progress events and final `orchestrate:done`.
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1040
1094
|
## Global Options
|
|
1041
1095
|
|
|
1042
1096
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrate command — ChainlessChain as orchestration layer,
|
|
3
|
+
* Claude Code / Codex as parallel execution agents.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* cc orchestrate "Fix the auth bug in login.ts"
|
|
7
|
+
* cc orchestrate "Refactor payment service" --agents 5 --ci "npm run test:unit"
|
|
8
|
+
* cc orchestrate --status
|
|
9
|
+
* cc orchestrate --watch --interval 10
|
|
10
|
+
* cc orchestrate detect # Check which AI CLI is installed
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import ora from "ora";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { logger } from "../lib/logger.js";
|
|
18
|
+
|
|
19
|
+
export function registerOrchestrateCommand(program) {
|
|
20
|
+
const cmd = program
|
|
21
|
+
.command("orchestrate [task]")
|
|
22
|
+
.description(
|
|
23
|
+
"Orchestrate AI coding tasks: ChainlessChain → Claude Code/Codex agents → CI/CD → Notify",
|
|
24
|
+
)
|
|
25
|
+
.option("-a, --agents <n>", "Max parallel agents", "3")
|
|
26
|
+
.option(
|
|
27
|
+
"--ci <command>",
|
|
28
|
+
"CI command to run after agents complete",
|
|
29
|
+
"npm test",
|
|
30
|
+
)
|
|
31
|
+
.option("--no-ci", "Skip CI/CD verification step")
|
|
32
|
+
.option(
|
|
33
|
+
"--source <type>",
|
|
34
|
+
"Input source: cli|sentry|github|file|wecom|dingtalk|feishu",
|
|
35
|
+
"cli",
|
|
36
|
+
)
|
|
37
|
+
.option("--file <path>", "Read task from file (use with --source file)")
|
|
38
|
+
.option("--context <text>", "Extra context for the task (e.g. stack trace)")
|
|
39
|
+
.option("--cwd <path>", "Project root directory (default: current dir)")
|
|
40
|
+
.option("--provider <name>", "LLM provider for decomposition")
|
|
41
|
+
.option("--model <name>", "Model for decomposition LLM calls")
|
|
42
|
+
.option("--cli-tool <name>", "Execution CLI: claude|codex (auto-detected)")
|
|
43
|
+
.option(
|
|
44
|
+
"--backends <list>",
|
|
45
|
+
"Agent backends: claude,codex,gemini,openai,ollama (comma-separated)",
|
|
46
|
+
)
|
|
47
|
+
.option(
|
|
48
|
+
"--strategy <name>",
|
|
49
|
+
"Agent routing: round-robin|by-type|parallel-all|primary",
|
|
50
|
+
"round-robin",
|
|
51
|
+
)
|
|
52
|
+
.option("--retries <n>", "Max CI retry cycles", "3")
|
|
53
|
+
.option("--timeout <sec>", "Per-agent timeout in seconds", "300")
|
|
54
|
+
.option("--no-notify", "Disable notifications")
|
|
55
|
+
.option("--status", "Show orchestrator and agent pool status")
|
|
56
|
+
.option("--watch", "Start cron watch mode")
|
|
57
|
+
.option("--interval <min>", "Cron interval in minutes (watch mode)", "10")
|
|
58
|
+
.option("--webhook", "Start HTTP webhook server for IM platform commands")
|
|
59
|
+
.option("--webhook-port <port>", "Webhook server port", "18820")
|
|
60
|
+
.option("--json", "Output as JSON")
|
|
61
|
+
.option("--verbose", "Verbose output");
|
|
62
|
+
|
|
63
|
+
cmd.action(async (task, options) => {
|
|
64
|
+
// Special sub-keyword: cc orchestrate detect
|
|
65
|
+
if (task === "detect") {
|
|
66
|
+
const { detectClaudeCode, detectCodex } =
|
|
67
|
+
await import("../lib/claude-code-bridge.js");
|
|
68
|
+
const claude = detectClaudeCode();
|
|
69
|
+
const codex = detectCodex();
|
|
70
|
+
console.log(chalk.bold("\n\uD83D\uDD0D AI CLI Detection\n"));
|
|
71
|
+
console.log(
|
|
72
|
+
claude.found
|
|
73
|
+
? chalk.green(` \u2713 claude ${claude.version}`)
|
|
74
|
+
: chalk.red(
|
|
75
|
+
" \u2717 claude not found (install: npm i -g @anthropic-ai/claude-code)",
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
console.log(
|
|
79
|
+
codex.found
|
|
80
|
+
? chalk.green(` \u2713 codex ${codex.version}`)
|
|
81
|
+
: chalk.gray(" \u2717 codex not found"),
|
|
82
|
+
);
|
|
83
|
+
if (!claude.found && !codex.found) {
|
|
84
|
+
console.log(
|
|
85
|
+
chalk.yellow(
|
|
86
|
+
"\n \u26A0 No AI CLI found. Install Claude Code:\n npm install -g @anthropic-ai/claude-code\n",
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { Orchestrator, TASK_SOURCE, TASK_STATUS } =
|
|
94
|
+
await import("../lib/orchestrator.js");
|
|
95
|
+
|
|
96
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
97
|
+
|
|
98
|
+
// --status mode
|
|
99
|
+
if (options.status) {
|
|
100
|
+
await _showStatus(cwd, options);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --watch mode
|
|
105
|
+
if (options.watch) {
|
|
106
|
+
await _watchMode(cwd, options);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --webhook mode: start HTTP server to receive commands from IM platforms
|
|
111
|
+
if (options.webhook) {
|
|
112
|
+
await _webhookMode(cwd, options);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Resolve task text
|
|
117
|
+
let taskText = task || "";
|
|
118
|
+
if (options.source === "file" && options.file) {
|
|
119
|
+
try {
|
|
120
|
+
taskText = fs.readFileSync(path.resolve(options.file), "utf-8").trim();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error(`Cannot read file: ${err.message}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!taskText) {
|
|
128
|
+
console.log(
|
|
129
|
+
chalk.yellow('Usage: cc orchestrate "<task description>"') +
|
|
130
|
+
chalk.gray(
|
|
131
|
+
"\n cc orchestrate --status\n cc orchestrate --watch\n cc orchestrate --webhook # receive commands from WeCom/DingTalk/Feishu\n",
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Build agent backends from --backends option
|
|
138
|
+
let agentsConfig;
|
|
139
|
+
if (options.backends) {
|
|
140
|
+
const { BACKEND_TYPE } = await import("../lib/agent-router.js");
|
|
141
|
+
const backendNames = options.backends.split(",").map((s) => s.trim());
|
|
142
|
+
agentsConfig = {
|
|
143
|
+
backends: backendNames.map((type) => ({ type, weight: 1 })),
|
|
144
|
+
strategy: options.strategy || "round-robin",
|
|
145
|
+
};
|
|
146
|
+
} else if (options.strategy && options.strategy !== "round-robin") {
|
|
147
|
+
agentsConfig = { strategy: options.strategy };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Build orchestrator
|
|
151
|
+
const orch = new Orchestrator({
|
|
152
|
+
cwd,
|
|
153
|
+
maxParallel: parseInt(options.agents, 10) || 3,
|
|
154
|
+
maxRetries: parseInt(options.retries, 10) || 3,
|
|
155
|
+
ciCommand: options.ci || "npm test",
|
|
156
|
+
agents: agentsConfig,
|
|
157
|
+
model: options.model || undefined,
|
|
158
|
+
llm: options.provider
|
|
159
|
+
? { provider: options.provider, model: options.model }
|
|
160
|
+
: {},
|
|
161
|
+
verbose: options.verbose,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (options.json) {
|
|
165
|
+
_runJson(orch, taskText, options);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Pretty output
|
|
170
|
+
_runPretty(orch, taskText, options, cwd);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Pretty (interactive) run ────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
async function _runPretty(orch, taskText, options, cwd) {
|
|
177
|
+
const { TASK_STATUS } = await import("../lib/orchestrator.js");
|
|
178
|
+
|
|
179
|
+
console.log(chalk.bold.cyan("\n⚡ ChainlessChain Orchestrator\n"));
|
|
180
|
+
console.log(chalk.gray(` Task: `) + chalk.white(taskText.slice(0, 120)));
|
|
181
|
+
console.log(chalk.gray(` CWD: `) + chalk.white(cwd));
|
|
182
|
+
console.log(
|
|
183
|
+
chalk.gray(` Agents: `) +
|
|
184
|
+
chalk.white(`max ${options.agents} × ${orch.cliCommand}`),
|
|
185
|
+
);
|
|
186
|
+
if (options.ci !== false) {
|
|
187
|
+
console.log(chalk.gray(` CI: `) + chalk.white(orch.ciCommand));
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
|
|
191
|
+
const spinner = ora("Decomposing task...").start();
|
|
192
|
+
|
|
193
|
+
orch.on("task:decomposed", ({ subtasks }) => {
|
|
194
|
+
spinner.text = `Decomposed into ${subtasks.length} subtask(s)`;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
orch.on("agents:dispatched", ({ count }) => {
|
|
198
|
+
spinner.text = `Dispatching ${count} subtask(s) to ${orch.cliCommand} agents...`;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
orch.on("batch:start", ({ count }) => {
|
|
202
|
+
spinner.text = `Running batch of ${count} agent(s)...`;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
orch.on("agent:complete", ({ taskId, success, duration }) => {
|
|
206
|
+
const icon = success ? chalk.green("✓") : chalk.red("✗");
|
|
207
|
+
spinner.text = `Agent done: ${icon} ${taskId} (${Math.round(duration / 1000)}s)`;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
orch.on("ci:checking", ({ attempt }) => {
|
|
211
|
+
spinner.text = `Running CI check (attempt ${attempt + 1})...`;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
orch.on("ci:fail", ({ errors, attempt }) => {
|
|
215
|
+
spinner.text = `CI failed (attempt ${attempt + 1}) — retrying with agents...`;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
orch.on("task:complete", (task) => {
|
|
219
|
+
spinner.succeed(
|
|
220
|
+
chalk.green(`✅ Task completed`) +
|
|
221
|
+
chalk.gray(` [${task.id}] status: ${task.status}`),
|
|
222
|
+
);
|
|
223
|
+
_printSummary(task);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
orch.on("task:failed", ({ task, error }) => {
|
|
227
|
+
spinner.fail(chalk.red(`❌ Task failed: ${error}`));
|
|
228
|
+
_printSummary(task);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
orch.on("log", (msg) => {
|
|
232
|
+
if (options.verbose) spinner.text = msg;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const { TASK_SOURCE } = await import("../lib/orchestrator.js");
|
|
237
|
+
await orch.addTask(taskText, {
|
|
238
|
+
source:
|
|
239
|
+
options.source === "sentry"
|
|
240
|
+
? TASK_SOURCE.SENTRY
|
|
241
|
+
: options.source === "github"
|
|
242
|
+
? TASK_SOURCE.GITHUB
|
|
243
|
+
: options.source === "file"
|
|
244
|
+
? TASK_SOURCE.FILE
|
|
245
|
+
: TASK_SOURCE.CLI,
|
|
246
|
+
context: options.context || "",
|
|
247
|
+
cwd,
|
|
248
|
+
runCI: options.ci !== false,
|
|
249
|
+
notify: options.notify !== false,
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
spinner.fail(chalk.red(`Orchestration error: ${err.message}`));
|
|
253
|
+
if (options.verbose) console.error(err);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── JSON run ────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
async function _runJson(orch, taskText, options) {
|
|
261
|
+
const { TASK_SOURCE } = await import("../lib/orchestrator.js");
|
|
262
|
+
try {
|
|
263
|
+
const task = await orch.addTask(taskText, {
|
|
264
|
+
source: TASK_SOURCE.CLI,
|
|
265
|
+
context: options.context || "",
|
|
266
|
+
runCI: options.ci !== false,
|
|
267
|
+
notify: false,
|
|
268
|
+
});
|
|
269
|
+
console.log(JSON.stringify(task, null, 2));
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.log(JSON.stringify({ error: err.message }, null, 2));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Status ──────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
async function _showStatus(cwd, options) {
|
|
279
|
+
const { detectClaudeCode, detectCodex } =
|
|
280
|
+
await import("../lib/claude-code-bridge.js");
|
|
281
|
+
const claude = detectClaudeCode();
|
|
282
|
+
const codex = detectCodex();
|
|
283
|
+
|
|
284
|
+
const status = {
|
|
285
|
+
cliTools: {
|
|
286
|
+
claude: claude.found ? claude.version : "not found",
|
|
287
|
+
codex: codex.found ? codex.version : "not found",
|
|
288
|
+
},
|
|
289
|
+
activeCliTool: claude.found ? "claude" : codex.found ? "codex" : "none",
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const { AgentRouter } = await import("../lib/agent-router.js");
|
|
293
|
+
const router = AgentRouter.autoDetect();
|
|
294
|
+
const backends = router.summary();
|
|
295
|
+
|
|
296
|
+
if (options.json) {
|
|
297
|
+
console.log(JSON.stringify({ ...status, backends }, null, 2));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(chalk.bold.cyan("\n⚡ Orchestrator Status\n"));
|
|
302
|
+
console.log(chalk.bold(" CLI Tools"));
|
|
303
|
+
console.log(
|
|
304
|
+
` ${chalk.gray("claude:")} ` +
|
|
305
|
+
(claude.found ? chalk.green(claude.version) : chalk.red("not installed")),
|
|
306
|
+
);
|
|
307
|
+
console.log(
|
|
308
|
+
` ${chalk.gray("codex:")} ` +
|
|
309
|
+
(codex.found ? chalk.green(codex.version) : chalk.gray("not installed")),
|
|
310
|
+
);
|
|
311
|
+
console.log();
|
|
312
|
+
console.log(chalk.bold(" Auto-detected Backends"));
|
|
313
|
+
for (const b of backends) {
|
|
314
|
+
const icon = b.kind === "cli" ? "🖥" : "🌐";
|
|
315
|
+
console.log(
|
|
316
|
+
` ${icon} ${chalk.cyan(b.type.padEnd(12))} ${chalk.gray(b.kind)} weight:${b.weight}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(chalk.bold(" Notification Channels"));
|
|
322
|
+
const { NotificationManager } = await import("../lib/notifiers/index.js");
|
|
323
|
+
const nm = NotificationManager.fromEnv();
|
|
324
|
+
const channels = nm.activeChannels;
|
|
325
|
+
if (channels.length === 0) {
|
|
326
|
+
console.log(
|
|
327
|
+
chalk.gray(
|
|
328
|
+
" (none configured — set TELEGRAM_BOT_TOKEN, WECOM_WEBHOOK_URL, etc.)",
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
for (const ch of channels) {
|
|
333
|
+
console.log(` ${chalk.green("✓")} ${ch}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
console.log();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── Webhook mode (receive commands from IM platforms) ─────────────
|
|
340
|
+
|
|
341
|
+
async function _webhookMode(cwd, options) {
|
|
342
|
+
const { createServer } = await import("http");
|
|
343
|
+
const { parseDingTalkIncoming, parseFeishuIncoming, parseWeComIncoming } =
|
|
344
|
+
await import("../lib/notifiers/index.js");
|
|
345
|
+
const { Orchestrator, TASK_SOURCE } = await import("../lib/orchestrator.js");
|
|
346
|
+
|
|
347
|
+
const port = parseInt(options.webhookPort, 10) || 18820;
|
|
348
|
+
|
|
349
|
+
const orch = new Orchestrator({ cwd, verbose: options.verbose });
|
|
350
|
+
|
|
351
|
+
const server = createServer(async (req, res) => {
|
|
352
|
+
if (req.method !== "POST") {
|
|
353
|
+
res.writeHead(405);
|
|
354
|
+
res.end("Method Not Allowed");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let body = "";
|
|
359
|
+
req.on("data", (chunk) => (body += chunk.toString("utf8")));
|
|
360
|
+
req.on("end", async () => {
|
|
361
|
+
let taskText = null;
|
|
362
|
+
let source = TASK_SOURCE.CLI;
|
|
363
|
+
|
|
364
|
+
const url = req.url?.split("?")[0] || "/";
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
if (url === "/wecom") {
|
|
368
|
+
taskText = parseWeComIncoming(body);
|
|
369
|
+
source = TASK_SOURCE.CLI;
|
|
370
|
+
} else if (url === "/dingtalk") {
|
|
371
|
+
const parsed = JSON.parse(body);
|
|
372
|
+
taskText = parseDingTalkIncoming(parsed);
|
|
373
|
+
source = TASK_SOURCE.CLI;
|
|
374
|
+
} else if (url === "/feishu") {
|
|
375
|
+
const parsed = JSON.parse(body);
|
|
376
|
+
// Feishu challenge verification
|
|
377
|
+
if (parsed.challenge) {
|
|
378
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
379
|
+
res.end(JSON.stringify({ challenge: parsed.challenge }));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
taskText = parseFeishuIncoming(parsed);
|
|
383
|
+
source = TASK_SOURCE.CLI;
|
|
384
|
+
} else {
|
|
385
|
+
res.writeHead(404);
|
|
386
|
+
res.end("Not Found");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
} catch (_err) {
|
|
390
|
+
res.writeHead(400);
|
|
391
|
+
res.end("Bad Request");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!taskText) {
|
|
396
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
397
|
+
res.end(JSON.stringify({ ok: true, message: "no task detected" }));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Acknowledge immediately (IM platforms require fast response)
|
|
402
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
403
|
+
res.end(
|
|
404
|
+
JSON.stringify({
|
|
405
|
+
ok: true,
|
|
406
|
+
message: `task queued: ${taskText.slice(0, 60)}`,
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Run orchestration async
|
|
411
|
+
if (!options.json) {
|
|
412
|
+
console.log(
|
|
413
|
+
chalk.cyan(`[webhook] Received task: `) + taskText.slice(0, 80),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
orch
|
|
417
|
+
.addTask(taskText, {
|
|
418
|
+
source,
|
|
419
|
+
cwd,
|
|
420
|
+
runCI: options.ci !== false,
|
|
421
|
+
notify: true,
|
|
422
|
+
})
|
|
423
|
+
.catch((err) =>
|
|
424
|
+
console.error(chalk.red(`[webhook] Error: ${err.message}`)),
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
server.listen(port, "127.0.0.1", () => {
|
|
430
|
+
console.log(chalk.bold.cyan("\n⚡ Orchestrator Webhook Server\n"));
|
|
431
|
+
console.log(
|
|
432
|
+
` ${chalk.gray("WeCom:")} POST http://localhost:${port}/wecom`,
|
|
433
|
+
);
|
|
434
|
+
console.log(
|
|
435
|
+
` ${chalk.gray("DingTalk:")} POST http://localhost:${port}/dingtalk`,
|
|
436
|
+
);
|
|
437
|
+
console.log(
|
|
438
|
+
` ${chalk.gray("Feishu:")} POST http://localhost:${port}/feishu`,
|
|
439
|
+
);
|
|
440
|
+
console.log(chalk.gray("\n Press Ctrl+C to stop\n"));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
process.on("SIGINT", () => {
|
|
444
|
+
server.close();
|
|
445
|
+
orch.stopCronWatch();
|
|
446
|
+
console.log(chalk.gray("\nWebhook server stopped."));
|
|
447
|
+
process.exit(0);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Watch mode ──────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
async function _watchMode(cwd, options) {
|
|
454
|
+
const { Orchestrator } = await import("../lib/orchestrator.js");
|
|
455
|
+
const intervalMs = parseInt(options.interval, 10) * 60_000 || 600_000;
|
|
456
|
+
|
|
457
|
+
const orch = new Orchestrator({ cwd, verbose: options.verbose });
|
|
458
|
+
orch.startCronWatch(intervalMs);
|
|
459
|
+
|
|
460
|
+
orch.on("cron:tick", ({ at }) => {
|
|
461
|
+
if (!options.json) console.log(chalk.gray(`[cron] tick at ${at}`));
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
console.log(
|
|
465
|
+
chalk.cyan(`\n⚡ Orchestrator watch mode started`) +
|
|
466
|
+
chalk.gray(` (interval: ${options.interval}m)`),
|
|
467
|
+
);
|
|
468
|
+
console.log(chalk.gray(" Press Ctrl+C to stop\n"));
|
|
469
|
+
|
|
470
|
+
// Keep alive
|
|
471
|
+
process.on("SIGINT", () => {
|
|
472
|
+
orch.stopCronWatch();
|
|
473
|
+
console.log(chalk.gray("\nOrchestrator stopped."));
|
|
474
|
+
process.exit(0);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ─── Summary printer ─────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
function _printSummary(task) {
|
|
481
|
+
console.log();
|
|
482
|
+
console.log(chalk.bold(" Summary"));
|
|
483
|
+
console.log(chalk.gray(" ─────────────────────────────────"));
|
|
484
|
+
console.log(` ID: ${chalk.cyan(task.id)}`);
|
|
485
|
+
console.log(` Source: ${task.source}`);
|
|
486
|
+
console.log(` Retries: ${task.retries}`);
|
|
487
|
+
console.log(` Status: ${_statusColor(task.status)}`);
|
|
488
|
+
|
|
489
|
+
if (task.subtasks?.length) {
|
|
490
|
+
console.log(` Subtasks: ${task.subtasks.length}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (task.agentResults?.length) {
|
|
494
|
+
const passed = task.agentResults.filter((r) => r.success).length;
|
|
495
|
+
console.log(
|
|
496
|
+
` Agents: ${chalk.green(passed)} passed / ${chalk.red(task.agentResults.length - passed)} failed`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
console.log();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function _statusColor(status) {
|
|
503
|
+
const colors = {
|
|
504
|
+
completed: chalk.green,
|
|
505
|
+
"ci-passed": chalk.green,
|
|
506
|
+
failed: chalk.red,
|
|
507
|
+
"ci-failed": chalk.red,
|
|
508
|
+
retrying: chalk.yellow,
|
|
509
|
+
dispatched: chalk.cyan,
|
|
510
|
+
"ci-checking": chalk.cyan,
|
|
511
|
+
};
|
|
512
|
+
return (colors[status] || chalk.white)(status);
|
|
513
|
+
}
|
package/src/index.js
CHANGED
|
@@ -89,6 +89,9 @@ import { registerServeCommand } from "./commands/serve.js";
|
|
|
89
89
|
// Web UI
|
|
90
90
|
import { registerUiCommand } from "./commands/ui.js";
|
|
91
91
|
|
|
92
|
+
// Orchestration Layer: ChainlessChain → Claude Code/Codex agents → CI → Notify
|
|
93
|
+
import { registerOrchestrateCommand } from "./commands/orchestrate.js";
|
|
94
|
+
|
|
92
95
|
export function createProgram() {
|
|
93
96
|
const program = new Command();
|
|
94
97
|
|
|
@@ -207,5 +210,8 @@ export function createProgram() {
|
|
|
207
210
|
// Web UI
|
|
208
211
|
registerUiCommand(program);
|
|
209
212
|
|
|
213
|
+
// Orchestration Layer
|
|
214
|
+
registerOrchestrateCommand(program);
|
|
215
|
+
|
|
210
216
|
return program;
|
|
211
217
|
}
|