llm-party-cli 0.1.2 → 0.2.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/README.md +9 -6
- package/dist/index.js +1330 -138
- package/package.json +1 -1
- package/dist/adapters/base.js +0 -1
- package/dist/adapters/claude.js +0 -64
- package/dist/adapters/codex.js +0 -44
- package/dist/adapters/copilot.js +0 -46
- package/dist/adapters/glm.js +0 -91
- package/dist/config/loader.js +0 -131
- package/dist/orchestrator.js +0 -185
- package/dist/types.js +0 -1
- package/dist/ui/terminal.js +0 -219
package/dist/ui/terminal.js
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import readline from "node:readline/promises";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import { initProjectFolder } from "../config/loader.js";
|
|
6
|
-
export async function runTerminal(orchestrator, options = {}) {
|
|
7
|
-
const rl = readline.createInterface({ input, output });
|
|
8
|
-
const humanName = orchestrator.getHumanName();
|
|
9
|
-
const tags = formatTagHints(orchestrator);
|
|
10
|
-
let lastTargets;
|
|
11
|
-
let knownChangedFiles = await getChangedFiles();
|
|
12
|
-
let projectFolderReady = false;
|
|
13
|
-
process.on("SIGINT", () => {
|
|
14
|
-
rl.close();
|
|
15
|
-
output.write("\n");
|
|
16
|
-
process.exit(0);
|
|
17
|
-
});
|
|
18
|
-
output.write(chalk.cyan(`llm-party Phase 1 started. Commands: /agents, /history, /save <path>, /session, /changes, /exit. Tags: ${tags}\n`));
|
|
19
|
-
output.write(chalk.gray(`Session: ${orchestrator.getSessionId()}\n`));
|
|
20
|
-
output.write(chalk.gray(`Transcript: ${orchestrator.getTranscriptPath()}\n`));
|
|
21
|
-
while (true) {
|
|
22
|
-
let line = "";
|
|
23
|
-
try {
|
|
24
|
-
line = (await rl.question(chalk.green(`${humanName} > `))).trim();
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
const code = error.code;
|
|
28
|
-
if (code === "ERR_USE_AFTER_CLOSE" || code === "ABORT_ERR") {
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
if (!line) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (line === "/exit") {
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
if (line === "/history") {
|
|
40
|
-
const history = orchestrator.getHistory();
|
|
41
|
-
for (const msg of history) {
|
|
42
|
-
output.write(`${chalk.gray(msg.createdAt)} ${chalk.yellow("[" + msg.from + "]")} ${msg.text}\n`);
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (line === "/agents") {
|
|
47
|
-
const agents = orchestrator.listAgents();
|
|
48
|
-
for (const agent of agents) {
|
|
49
|
-
output.write(`${chalk.cyan(agent.name)} tag=@${agent.tag} provider=${agent.provider} model=${agent.model}\n`);
|
|
50
|
-
}
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (line === "/session") {
|
|
54
|
-
output.write(chalk.cyan(`Session: ${orchestrator.getSessionId()}\n`));
|
|
55
|
-
output.write(chalk.cyan(`Transcript: ${orchestrator.getTranscriptPath()}\n`));
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (line === "/changes") {
|
|
59
|
-
const changedFiles = await getChangedFiles();
|
|
60
|
-
if (changedFiles.length === 0) {
|
|
61
|
-
output.write(chalk.cyan("No modified files in git working tree.\n"));
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
output.write(chalk.cyan("Modified files:\n"));
|
|
65
|
-
for (const file of changedFiles) {
|
|
66
|
-
output.write(`- ${file}\n`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (line.startsWith("/save ")) {
|
|
72
|
-
const filePath = line.replace("/save ", "").trim();
|
|
73
|
-
if (!filePath) {
|
|
74
|
-
output.write(chalk.red("Usage: /save <path>\n"));
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
await orchestrator.saveHistory(filePath);
|
|
78
|
-
output.write(chalk.cyan(`Saved history to ${filePath}\n`));
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const routing = parseRouting(line);
|
|
82
|
-
const explicitTargets = routing.targets && routing.targets.length > 0
|
|
83
|
-
? Array.from(new Set(routing.targets.flatMap((target) => orchestrator.resolveTargets(target))))
|
|
84
|
-
: undefined;
|
|
85
|
-
if (routing.targets && routing.targets.length > 0 && (!explicitTargets || explicitTargets.length === 0)) {
|
|
86
|
-
output.write(chalk.red(`No agent matched ${routing.targets.map((target) => `@${target}`).join(", ")}. Use /agents to list names/providers.\n`));
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const targets = explicitTargets ?? lastTargets;
|
|
90
|
-
if (explicitTargets && explicitTargets.length > 0) {
|
|
91
|
-
lastTargets = explicitTargets;
|
|
92
|
-
}
|
|
93
|
-
if (!projectFolderReady) {
|
|
94
|
-
await initProjectFolder(process.cwd());
|
|
95
|
-
projectFolderReady = true;
|
|
96
|
-
}
|
|
97
|
-
const userMessage = orchestrator.addUserMessage(routing.message);
|
|
98
|
-
await orchestrator.appendTranscript(userMessage);
|
|
99
|
-
knownChangedFiles = await dispatchWithHandoffs(orchestrator, output, targets, knownChangedFiles, options.maxAutoHops ?? 6);
|
|
100
|
-
}
|
|
101
|
-
rl.close();
|
|
102
|
-
}
|
|
103
|
-
async function getChangedFiles() {
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
execFile("git", ["status", "--porcelain"], { cwd: process.cwd() }, (error, stdout) => {
|
|
106
|
-
if (error) {
|
|
107
|
-
resolve([]);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
const files = stdout
|
|
111
|
-
.split("\n")
|
|
112
|
-
.filter((line) => line.length >= 4)
|
|
113
|
-
.map((line) => line.slice(3).trim());
|
|
114
|
-
resolve(Array.from(new Set(files)));
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
async function dispatchWithHandoffs(orchestrator, out, initialTargets, previousChangedFiles = [], maxHops = 6) {
|
|
119
|
-
let targets = initialTargets;
|
|
120
|
-
let hops = 0;
|
|
121
|
-
let knownChangedFiles = previousChangedFiles;
|
|
122
|
-
while (true) {
|
|
123
|
-
const targetLabel = targets && targets.length > 0 ? targets.join(",") : "all";
|
|
124
|
-
out.write(chalk.gray(`Dispatching to ${targetLabel}...\n`));
|
|
125
|
-
const batch = [];
|
|
126
|
-
await orchestrator.fanOutWithProgress(targets, (msg) => {
|
|
127
|
-
batch.push(msg);
|
|
128
|
-
out.write(chalk.magenta(`[${msg.from}]`) + ` ${msg.text}\n\n`);
|
|
129
|
-
});
|
|
130
|
-
const latestChangedFiles = await getChangedFiles();
|
|
131
|
-
const newlyChanged = diffChangedFiles(knownChangedFiles, latestChangedFiles);
|
|
132
|
-
if (newlyChanged.length > 0) {
|
|
133
|
-
out.write(chalk.yellow(`LLM modified files at ${new Date().toISOString()}:\n`));
|
|
134
|
-
for (const file of newlyChanged) {
|
|
135
|
-
out.write(chalk.yellow(`- ${file}\n`));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
knownChangedFiles = latestChangedFiles;
|
|
139
|
-
const nextSelectors = extractNextSelectors(batch);
|
|
140
|
-
if (nextSelectors.length === 0) {
|
|
141
|
-
return knownChangedFiles;
|
|
142
|
-
}
|
|
143
|
-
if (nextSelectors.some((selector) => {
|
|
144
|
-
const normalized = selector.toLowerCase();
|
|
145
|
-
return normalized === orchestrator.getHumanTag().toLowerCase()
|
|
146
|
-
|| normalized === orchestrator.getHumanName().toLowerCase();
|
|
147
|
-
})) {
|
|
148
|
-
return knownChangedFiles;
|
|
149
|
-
}
|
|
150
|
-
const resolvedTargets = Array.from(new Set(nextSelectors.flatMap((selector) => orchestrator.resolveTargets(selector))));
|
|
151
|
-
if (resolvedTargets.length === 0) {
|
|
152
|
-
out.write(chalk.yellow(`Ignored @next target(s): ${nextSelectors.join(",")}\n`));
|
|
153
|
-
return knownChangedFiles;
|
|
154
|
-
}
|
|
155
|
-
hops += 1;
|
|
156
|
-
if (Number.isFinite(maxHops) && hops >= maxHops) {
|
|
157
|
-
out.write(chalk.yellow(`Stopped auto-handoff after ${maxHops} hops to prevent loops.\n`));
|
|
158
|
-
return knownChangedFiles;
|
|
159
|
-
}
|
|
160
|
-
out.write(chalk.gray(`Auto handoff via @next to ${resolvedTargets.join(",")}\n`));
|
|
161
|
-
targets = resolvedTargets;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function diffChangedFiles(before, after) {
|
|
165
|
-
const beforeSet = new Set(before);
|
|
166
|
-
return after.filter((file) => !beforeSet.has(file));
|
|
167
|
-
}
|
|
168
|
-
function formatTagHints(orchestrator) {
|
|
169
|
-
const agents = orchestrator.listAgents();
|
|
170
|
-
const tags = new Set();
|
|
171
|
-
tags.add("@all");
|
|
172
|
-
tags.add("@everyone");
|
|
173
|
-
for (const agent of agents) {
|
|
174
|
-
tags.add(`@${agent.tag}`);
|
|
175
|
-
tags.add(`@${agent.provider}`);
|
|
176
|
-
}
|
|
177
|
-
return Array.from(tags).join(", ");
|
|
178
|
-
}
|
|
179
|
-
function extractNextSelectors(messages) {
|
|
180
|
-
const selectors = [];
|
|
181
|
-
for (const msg of messages) {
|
|
182
|
-
const regex = /@next\s*:\s*([A-Za-z0-9_-]+)/gi;
|
|
183
|
-
let match = null;
|
|
184
|
-
while ((match = regex.exec(msg.text)) !== null) {
|
|
185
|
-
selectors.push(match[1]);
|
|
186
|
-
}
|
|
187
|
-
const controlMatch = msg.text.match(/@control[\s\S]*?next\s*:\s*([A-Za-z0-9_-]+)[\s\S]*?@end/i);
|
|
188
|
-
if (controlMatch?.[1]) {
|
|
189
|
-
selectors.push(controlMatch[1]);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return selectors;
|
|
193
|
-
}
|
|
194
|
-
function parseRouting(line) {
|
|
195
|
-
const normalizedStart = line.replace(/^[^A-Za-z0-9@_-]+/, "");
|
|
196
|
-
const startMatch = normalizedStart.match(/^@([A-Za-z0-9_-]+)[\.,:;!?-]*\s+([\s\S]+)$/);
|
|
197
|
-
if (startMatch) {
|
|
198
|
-
return {
|
|
199
|
-
targets: [startMatch[1].toLowerCase()],
|
|
200
|
-
message: startMatch[2].trim()
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
const mentionRegex = /(^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]+)\b/g;
|
|
204
|
-
const targets = [];
|
|
205
|
-
let stripped = line;
|
|
206
|
-
let match = null;
|
|
207
|
-
while ((match = mentionRegex.exec(line)) !== null) {
|
|
208
|
-
targets.push(match[2].toLowerCase());
|
|
209
|
-
}
|
|
210
|
-
if (targets.length === 0) {
|
|
211
|
-
return { message: line };
|
|
212
|
-
}
|
|
213
|
-
stripped = stripped.replace(/(^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]+)\b/g, (full, prefix) => prefix || "");
|
|
214
|
-
stripped = stripped.replace(/\s{2,}/g, " ").trim();
|
|
215
|
-
return {
|
|
216
|
-
targets,
|
|
217
|
-
message: stripped || line
|
|
218
|
-
};
|
|
219
|
-
}
|