@poncho-ai/cli 0.37.0 → 0.38.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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +225 -0
- package/dist/{chunk-GUGBKAIM.js → chunk-U643TWFX.js} +6188 -4727
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +186 -128
- package/dist/index.js +111 -5
- package/dist/run-interactive-ink-CE7U47S5.js +679 -0
- package/package.json +4 -4
- package/src/cron-helpers.ts +174 -0
- package/src/http-utils.ts +220 -0
- package/src/index.ts +998 -4741
- package/src/logger.ts +9 -0
- package/src/mcp-commands.ts +283 -0
- package/src/project-init.ts +150 -0
- package/src/run-commands.ts +145 -0
- package/src/scaffolding.ts +528 -0
- package/src/skills.ts +372 -0
- package/src/templates.ts +563 -0
- package/src/testing.ts +108 -0
- package/src/web-ui-client.ts +865 -94
- package/src/web-ui-styles.ts +278 -1
- package/src/web-ui.ts +24 -0
- package/test/cli.test.ts +52 -1
- package/dist/run-interactive-ink-75GKYSEC.js +0 -2115
- package/test/run-orchestration.test.ts +0 -171
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import {
|
|
2
|
+
consumeFirstRunIntro,
|
|
3
|
+
getMascotLines,
|
|
4
|
+
inferConversationTitle,
|
|
5
|
+
resolveHarnessEnvironment
|
|
6
|
+
} from "./chunk-U643TWFX.js";
|
|
7
|
+
|
|
8
|
+
// src/run-interactive-ink.ts
|
|
9
|
+
import * as readline from "readline";
|
|
10
|
+
import { readFile } from "fs/promises";
|
|
11
|
+
import { resolve, basename } from "path";
|
|
12
|
+
import { stdout } from "process";
|
|
13
|
+
import {
|
|
14
|
+
parseAgentFile
|
|
15
|
+
} from "@poncho-ai/harness";
|
|
16
|
+
var C = {
|
|
17
|
+
reset: "\x1B[0m",
|
|
18
|
+
bold: "\x1B[1m",
|
|
19
|
+
dim: "\x1B[2m",
|
|
20
|
+
cyan: "\x1B[36m",
|
|
21
|
+
green: "\x1B[32m",
|
|
22
|
+
yellow: "\x1B[33m",
|
|
23
|
+
red: "\x1B[31m",
|
|
24
|
+
gray: "\x1B[90m",
|
|
25
|
+
magenta: "\x1B[35m"
|
|
26
|
+
};
|
|
27
|
+
var green = (s) => `${C.green}${s}${C.reset}`;
|
|
28
|
+
var yellow = (s) => `${C.yellow}${s}${C.reset}`;
|
|
29
|
+
var red = (s) => `${C.red}${s}${C.reset}`;
|
|
30
|
+
var gray = (s) => `${C.gray}${s}${C.reset}`;
|
|
31
|
+
var magenta = (s) => `${C.magenta}${s}${C.reset}`;
|
|
32
|
+
var FAUX_TOOL_LOG_PATTERN = /Tool Used:|Tool Result:|\blist_skills\b|\bcreate_skill\b|\bedit_skill\b/i;
|
|
33
|
+
var formatDuration = (ms) => ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
34
|
+
var stringifyValue = (v) => {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(v);
|
|
37
|
+
} catch {
|
|
38
|
+
return String(v);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var truncate = (v, max) => v.length <= max ? v : `${v.slice(0, Math.max(0, max - 3))}...`;
|
|
42
|
+
var compactPreview = (v, max = 120) => truncate(stringifyValue(v).replace(/\s+/g, " "), max);
|
|
43
|
+
var loadMetadata = async (workingDir) => {
|
|
44
|
+
let agentName = "agent";
|
|
45
|
+
let model = "unknown";
|
|
46
|
+
let provider = "unknown";
|
|
47
|
+
try {
|
|
48
|
+
const parsed = await parseAgentFile(workingDir);
|
|
49
|
+
agentName = parsed.frontmatter.name ?? agentName;
|
|
50
|
+
model = parsed.frontmatter.model?.name ?? model;
|
|
51
|
+
provider = parsed.frontmatter.model?.provider ?? provider;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
agentName,
|
|
56
|
+
model,
|
|
57
|
+
provider,
|
|
58
|
+
workingDir,
|
|
59
|
+
environment: resolveHarnessEnvironment()
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
var ask = (rl, prompt) => new Promise((res) => {
|
|
63
|
+
rl.question(prompt, (answer) => res(answer));
|
|
64
|
+
});
|
|
65
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
66
|
+
var streamTextAsTokens = async (text) => {
|
|
67
|
+
const tokens = text.match(/\S+\s*|\n/g) ?? [text];
|
|
68
|
+
for (const token of tokens) {
|
|
69
|
+
stdout.write(token);
|
|
70
|
+
const trimmed = token.trim();
|
|
71
|
+
const delay = trimmed.length === 0 ? 0 : Math.max(4, Math.min(18, Math.floor(trimmed.length / 2)));
|
|
72
|
+
await sleep(delay);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var EXT_MIME = {
|
|
76
|
+
jpg: "image/jpeg",
|
|
77
|
+
jpeg: "image/jpeg",
|
|
78
|
+
png: "image/png",
|
|
79
|
+
gif: "image/gif",
|
|
80
|
+
webp: "image/webp",
|
|
81
|
+
svg: "image/svg+xml",
|
|
82
|
+
pdf: "application/pdf",
|
|
83
|
+
mp4: "video/mp4",
|
|
84
|
+
webm: "video/webm",
|
|
85
|
+
mp3: "audio/mpeg",
|
|
86
|
+
wav: "audio/wav",
|
|
87
|
+
txt: "text/plain",
|
|
88
|
+
json: "application/json",
|
|
89
|
+
csv: "text/csv",
|
|
90
|
+
html: "text/html"
|
|
91
|
+
};
|
|
92
|
+
var extToMime = (ext) => EXT_MIME[ext] ?? "application/octet-stream";
|
|
93
|
+
var readPendingFiles = async (files) => {
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
try {
|
|
97
|
+
const buf = await readFile(f.resolved);
|
|
98
|
+
const ext = f.resolved.split(".").pop()?.toLowerCase() ?? "";
|
|
99
|
+
results.push({
|
|
100
|
+
data: buf.toString("base64"),
|
|
101
|
+
mediaType: extToMime(ext),
|
|
102
|
+
filename: basename(f.path)
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
console.log(`${C.yellow}warn: could not read ${f.path}, skipping${C.reset}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
};
|
|
110
|
+
var OWNER_ID = "local-owner";
|
|
111
|
+
var computeTurn = (messages) => Math.max(1, Math.floor(messages.length / 2) + 1);
|
|
112
|
+
var formatDate = (value) => {
|
|
113
|
+
try {
|
|
114
|
+
return new Date(value).toLocaleString();
|
|
115
|
+
} catch {
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var handleSlash = async (command, state, conversationStore, harness) => {
|
|
120
|
+
const [rawCommand, ...args] = command.trim().split(/\s+/);
|
|
121
|
+
const norm = rawCommand.toLowerCase();
|
|
122
|
+
if (norm === "/help") {
|
|
123
|
+
console.log(
|
|
124
|
+
gray(
|
|
125
|
+
"commands> /help /clear /exit /tools /attach <path> /files /list /open <id> /new [title] /delete [id] /continue /compact [focus] /reset [all]"
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
return { shouldExit: false };
|
|
129
|
+
}
|
|
130
|
+
if (norm === "/clear") {
|
|
131
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
132
|
+
return { shouldExit: false };
|
|
133
|
+
}
|
|
134
|
+
if (norm === "/exit") {
|
|
135
|
+
return { shouldExit: true };
|
|
136
|
+
}
|
|
137
|
+
if (norm === "/list") {
|
|
138
|
+
const conversations = await conversationStore.list(OWNER_ID);
|
|
139
|
+
if (conversations.length === 0) {
|
|
140
|
+
console.log(gray("conversations> none"));
|
|
141
|
+
return { shouldExit: false };
|
|
142
|
+
}
|
|
143
|
+
console.log(gray("conversations>"));
|
|
144
|
+
for (const conversation of conversations) {
|
|
145
|
+
const activeMarker = state.activeConversationId === conversation.conversationId ? "*" : " ";
|
|
146
|
+
const maxTitleLen = 40;
|
|
147
|
+
const title = conversation.title.length > maxTitleLen ? conversation.title.slice(0, maxTitleLen - 1) + "\u2026" : conversation.title;
|
|
148
|
+
console.log(
|
|
149
|
+
gray(
|
|
150
|
+
`${activeMarker} ${conversation.conversationId} | ${title} | ${formatDate(conversation.updatedAt)}`
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return { shouldExit: false };
|
|
155
|
+
}
|
|
156
|
+
if (norm === "/open") {
|
|
157
|
+
const conversationId = args[0];
|
|
158
|
+
if (!conversationId) {
|
|
159
|
+
console.log(yellow("usage> /open <conversationId>"));
|
|
160
|
+
return { shouldExit: false };
|
|
161
|
+
}
|
|
162
|
+
const conversation = await conversationStore.get(conversationId);
|
|
163
|
+
if (!conversation) {
|
|
164
|
+
console.log(yellow(`conversations> not found: ${conversationId}`));
|
|
165
|
+
return { shouldExit: false };
|
|
166
|
+
}
|
|
167
|
+
state.activeConversationId = conversation.conversationId;
|
|
168
|
+
state.messages = [...conversation.messages];
|
|
169
|
+
state.turn = computeTurn(state.messages);
|
|
170
|
+
console.log(gray(`conversations> opened ${conversation.conversationId}`));
|
|
171
|
+
return { shouldExit: false };
|
|
172
|
+
}
|
|
173
|
+
if (norm === "/new") {
|
|
174
|
+
const title = args.join(" ").trim();
|
|
175
|
+
const conversation = await conversationStore.create(OWNER_ID, title || void 0);
|
|
176
|
+
state.activeConversationId = conversation.conversationId;
|
|
177
|
+
state.messages = [];
|
|
178
|
+
state.turn = 1;
|
|
179
|
+
console.log(gray(`conversations> new ${conversation.conversationId}`));
|
|
180
|
+
return { shouldExit: false };
|
|
181
|
+
}
|
|
182
|
+
if (norm === "/delete") {
|
|
183
|
+
const targetConversationId = args[0] ?? state.activeConversationId ?? "";
|
|
184
|
+
if (!targetConversationId) {
|
|
185
|
+
console.log(yellow("usage> /delete <conversationId>"));
|
|
186
|
+
return { shouldExit: false };
|
|
187
|
+
}
|
|
188
|
+
const removed = await conversationStore.delete(targetConversationId);
|
|
189
|
+
if (!removed) {
|
|
190
|
+
console.log(yellow(`conversations> not found: ${targetConversationId}`));
|
|
191
|
+
return { shouldExit: false };
|
|
192
|
+
}
|
|
193
|
+
if (state.activeConversationId === targetConversationId) {
|
|
194
|
+
state.activeConversationId = null;
|
|
195
|
+
state.messages = [];
|
|
196
|
+
state.turn = 1;
|
|
197
|
+
}
|
|
198
|
+
console.log(gray(`conversations> deleted ${targetConversationId}`));
|
|
199
|
+
return { shouldExit: false };
|
|
200
|
+
}
|
|
201
|
+
if (norm === "/continue") {
|
|
202
|
+
const conversations = await conversationStore.list(OWNER_ID);
|
|
203
|
+
const latest = conversations[0];
|
|
204
|
+
if (!latest) {
|
|
205
|
+
console.log(yellow("conversations> no conversations to continue"));
|
|
206
|
+
return { shouldExit: false };
|
|
207
|
+
}
|
|
208
|
+
state.activeConversationId = latest.conversationId;
|
|
209
|
+
state.messages = [...latest.messages];
|
|
210
|
+
state.turn = computeTurn(state.messages);
|
|
211
|
+
console.log(gray(`conversations> continued ${latest.conversationId}`));
|
|
212
|
+
return { shouldExit: false };
|
|
213
|
+
}
|
|
214
|
+
if (norm === "/reset") {
|
|
215
|
+
if (args[0]?.toLowerCase() === "all") {
|
|
216
|
+
const conversations = await conversationStore.list(OWNER_ID);
|
|
217
|
+
for (const conversation2 of conversations) {
|
|
218
|
+
await conversationStore.delete(conversation2.conversationId);
|
|
219
|
+
}
|
|
220
|
+
state.activeConversationId = null;
|
|
221
|
+
state.messages = [];
|
|
222
|
+
state.turn = 1;
|
|
223
|
+
console.log(gray("conversations> reset all"));
|
|
224
|
+
return { shouldExit: false };
|
|
225
|
+
}
|
|
226
|
+
if (!state.activeConversationId) {
|
|
227
|
+
state.messages = [];
|
|
228
|
+
state.turn = 1;
|
|
229
|
+
console.log(gray("conversations> current session reset"));
|
|
230
|
+
return { shouldExit: false };
|
|
231
|
+
}
|
|
232
|
+
const conversation = await conversationStore.get(state.activeConversationId);
|
|
233
|
+
if (!conversation) {
|
|
234
|
+
state.activeConversationId = null;
|
|
235
|
+
state.messages = [];
|
|
236
|
+
state.turn = 1;
|
|
237
|
+
console.log(yellow("conversations> active conversation no longer exists"));
|
|
238
|
+
return { shouldExit: false };
|
|
239
|
+
}
|
|
240
|
+
await conversationStore.update({
|
|
241
|
+
...conversation,
|
|
242
|
+
messages: []
|
|
243
|
+
});
|
|
244
|
+
state.messages = [];
|
|
245
|
+
state.turn = 1;
|
|
246
|
+
console.log(gray(`conversations> reset ${conversation.conversationId}`));
|
|
247
|
+
return { shouldExit: false };
|
|
248
|
+
}
|
|
249
|
+
if (norm === "/attach") {
|
|
250
|
+
const filePath = args.join(" ").trim();
|
|
251
|
+
if (!filePath) {
|
|
252
|
+
console.log(yellow("usage> /attach <path>"));
|
|
253
|
+
return { shouldExit: false };
|
|
254
|
+
}
|
|
255
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
256
|
+
try {
|
|
257
|
+
await readFile(resolvedPath);
|
|
258
|
+
state.pendingFiles.push({ path: filePath, resolved: resolvedPath });
|
|
259
|
+
console.log(gray(`attached> ${filePath} [${state.pendingFiles.length} file(s) queued]`));
|
|
260
|
+
} catch {
|
|
261
|
+
console.log(yellow(`attach> file not found: ${filePath}`));
|
|
262
|
+
}
|
|
263
|
+
return { shouldExit: false };
|
|
264
|
+
}
|
|
265
|
+
if (norm === "/files") {
|
|
266
|
+
if (state.pendingFiles.length === 0) {
|
|
267
|
+
console.log(gray("files> none attached"));
|
|
268
|
+
} else {
|
|
269
|
+
console.log(gray("files>"));
|
|
270
|
+
for (const f of state.pendingFiles) {
|
|
271
|
+
console.log(gray(` ${f.path}`));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { shouldExit: false };
|
|
275
|
+
}
|
|
276
|
+
if (norm === "/compact") {
|
|
277
|
+
if (state.messages.length < 4) {
|
|
278
|
+
console.log(gray("compact> not enough messages to compact"));
|
|
279
|
+
return { shouldExit: false };
|
|
280
|
+
}
|
|
281
|
+
const focusHint = args.join(" ").trim() || void 0;
|
|
282
|
+
console.log(gray("compact> compacting conversation..."));
|
|
283
|
+
const result = await harness.compact(
|
|
284
|
+
state.messages,
|
|
285
|
+
focusHint ? { instructions: focusHint } : void 0
|
|
286
|
+
);
|
|
287
|
+
if (result.compacted) {
|
|
288
|
+
state.messages = result.messages;
|
|
289
|
+
console.log(
|
|
290
|
+
gray(
|
|
291
|
+
`compact> done (${result.messagesBefore} \u2192 ${result.messagesAfter} messages)`
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
console.log(yellow(`compact> ${result.warning ?? "compaction skipped"}`));
|
|
296
|
+
}
|
|
297
|
+
return { shouldExit: false };
|
|
298
|
+
}
|
|
299
|
+
console.log(yellow(`Unknown command: ${command}`));
|
|
300
|
+
return { shouldExit: false };
|
|
301
|
+
};
|
|
302
|
+
var runInteractiveInk = async ({
|
|
303
|
+
harness,
|
|
304
|
+
params,
|
|
305
|
+
workingDir,
|
|
306
|
+
config,
|
|
307
|
+
conversationStore
|
|
308
|
+
}) => {
|
|
309
|
+
const metadata = await loadMetadata(workingDir);
|
|
310
|
+
const rl = readline.createInterface({
|
|
311
|
+
input: process.stdin,
|
|
312
|
+
output: process.stdout,
|
|
313
|
+
terminal: true
|
|
314
|
+
});
|
|
315
|
+
console.log("");
|
|
316
|
+
for (const line of getMascotLines()) {
|
|
317
|
+
console.log(line);
|
|
318
|
+
}
|
|
319
|
+
console.log(`${C.bold}${C.cyan} poncho${C.reset}`);
|
|
320
|
+
console.log("");
|
|
321
|
+
console.log(
|
|
322
|
+
gray(
|
|
323
|
+
` ${metadata.agentName} \xB7 ${metadata.provider}/${metadata.model} \xB7 ${metadata.environment}`
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
console.log(gray(' Type "exit" to quit, "/help" for commands'));
|
|
327
|
+
console.log(gray(" Press Ctrl+C during a run to stop streaming output."));
|
|
328
|
+
console.log(
|
|
329
|
+
gray(" Conversation: /list /open <id> /new [title] /delete [id] /continue /reset [all]")
|
|
330
|
+
);
|
|
331
|
+
console.log(
|
|
332
|
+
gray(" Files: /attach <path> /files\n")
|
|
333
|
+
);
|
|
334
|
+
const intro = await consumeFirstRunIntro(workingDir, {
|
|
335
|
+
agentName: metadata.agentName,
|
|
336
|
+
provider: metadata.provider,
|
|
337
|
+
model: metadata.model,
|
|
338
|
+
config
|
|
339
|
+
});
|
|
340
|
+
if (intro) {
|
|
341
|
+
console.log(green("assistant>"));
|
|
342
|
+
await streamTextAsTokens(intro);
|
|
343
|
+
stdout.write("\n\n");
|
|
344
|
+
}
|
|
345
|
+
let messages = intro ? [{ role: "assistant", content: intro }] : [];
|
|
346
|
+
let turn = 1;
|
|
347
|
+
let activeConversationId = null;
|
|
348
|
+
let showToolPayloads = false;
|
|
349
|
+
let activeRunAbortController = null;
|
|
350
|
+
let pendingFiles = [];
|
|
351
|
+
rl.on("SIGINT", () => {
|
|
352
|
+
if (activeRunAbortController && !activeRunAbortController.signal.aborted) {
|
|
353
|
+
activeRunAbortController.abort();
|
|
354
|
+
process.stdout.write("\n");
|
|
355
|
+
console.log(gray("stop> cancelling current run..."));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
rl.close();
|
|
359
|
+
});
|
|
360
|
+
while (true) {
|
|
361
|
+
const filesTag = pendingFiles.length > 0 ? `${C.dim}[${pendingFiles.length} file(s)] ${C.reset}` : "";
|
|
362
|
+
const prompt = `${filesTag}${C.cyan}you> ${C.reset}`;
|
|
363
|
+
let task;
|
|
364
|
+
try {
|
|
365
|
+
task = await ask(rl, prompt);
|
|
366
|
+
} catch {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
const trimmed = task.trim();
|
|
370
|
+
if (!trimmed) continue;
|
|
371
|
+
if (trimmed.toLowerCase() === "exit") break;
|
|
372
|
+
if (trimmed.startsWith("/")) {
|
|
373
|
+
if (trimmed.toLowerCase() === "/exit") break;
|
|
374
|
+
if (trimmed.toLowerCase() === "/tools") {
|
|
375
|
+
showToolPayloads = !showToolPayloads;
|
|
376
|
+
console.log(gray(`tool payloads: ${showToolPayloads ? "on" : "off"}`));
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const interactiveState = {
|
|
380
|
+
messages,
|
|
381
|
+
turn,
|
|
382
|
+
activeConversationId,
|
|
383
|
+
pendingFiles
|
|
384
|
+
};
|
|
385
|
+
const slashResult = await handleSlash(
|
|
386
|
+
trimmed,
|
|
387
|
+
interactiveState,
|
|
388
|
+
conversationStore,
|
|
389
|
+
harness
|
|
390
|
+
);
|
|
391
|
+
if (slashResult.shouldExit) {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
messages = interactiveState.messages;
|
|
395
|
+
turn = interactiveState.turn;
|
|
396
|
+
activeConversationId = interactiveState.activeConversationId;
|
|
397
|
+
pendingFiles = interactiveState.pendingFiles;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
console.log(gray(`
|
|
401
|
+
--- turn ${turn} ---`));
|
|
402
|
+
process.stdout.write(gray("thinking..."));
|
|
403
|
+
let thinkingCleared = false;
|
|
404
|
+
const clearThinking = () => {
|
|
405
|
+
if (thinkingCleared) return;
|
|
406
|
+
thinkingCleared = true;
|
|
407
|
+
readline.clearLine(process.stdout, 0);
|
|
408
|
+
readline.cursorTo(process.stdout, 0);
|
|
409
|
+
};
|
|
410
|
+
let responseText = "";
|
|
411
|
+
let streamedText = "";
|
|
412
|
+
let committedText = false;
|
|
413
|
+
let sawChunk = false;
|
|
414
|
+
let toolEvents = 0;
|
|
415
|
+
const toolTimeline = [];
|
|
416
|
+
const sections = [];
|
|
417
|
+
let currentText = "";
|
|
418
|
+
let currentTools = [];
|
|
419
|
+
let runFailed = false;
|
|
420
|
+
let runCancelled = false;
|
|
421
|
+
let usage;
|
|
422
|
+
let latestRunId = "";
|
|
423
|
+
const startedAt = Date.now();
|
|
424
|
+
activeRunAbortController = new AbortController();
|
|
425
|
+
const turnFiles = pendingFiles.length > 0 ? await readPendingFiles(pendingFiles) : [];
|
|
426
|
+
if (pendingFiles.length > 0) {
|
|
427
|
+
console.log(gray(` sending ${turnFiles.length} file(s)`));
|
|
428
|
+
pendingFiles = [];
|
|
429
|
+
}
|
|
430
|
+
let eventSource = harness.run({
|
|
431
|
+
task: trimmed,
|
|
432
|
+
parameters: params,
|
|
433
|
+
messages,
|
|
434
|
+
files: turnFiles.length > 0 ? turnFiles : void 0,
|
|
435
|
+
abortSignal: activeRunAbortController.signal
|
|
436
|
+
});
|
|
437
|
+
try {
|
|
438
|
+
while (true) {
|
|
439
|
+
let checkpointEvent = null;
|
|
440
|
+
for await (const event of eventSource) {
|
|
441
|
+
if (event.type === "run:started") {
|
|
442
|
+
latestRunId = event.runId;
|
|
443
|
+
}
|
|
444
|
+
if (event.type === "model:chunk") {
|
|
445
|
+
sawChunk = true;
|
|
446
|
+
if (currentTools.length > 0) {
|
|
447
|
+
sections.push({ type: "tools", content: currentTools });
|
|
448
|
+
currentTools = [];
|
|
449
|
+
if (responseText.length > 0 && !/\s$/.test(responseText)) {
|
|
450
|
+
responseText += " ";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
responseText += event.content;
|
|
454
|
+
streamedText += event.content;
|
|
455
|
+
currentText += event.content;
|
|
456
|
+
if (!thinkingCleared) {
|
|
457
|
+
clearThinking();
|
|
458
|
+
process.stdout.write(`${C.green}assistant> ${C.reset}`);
|
|
459
|
+
}
|
|
460
|
+
process.stdout.write(event.content);
|
|
461
|
+
} else if (event.type === "tool:started" || event.type === "tool:completed" || event.type === "tool:error" || event.type === "tool:approval:required") {
|
|
462
|
+
if (streamedText.length > 0) {
|
|
463
|
+
committedText = true;
|
|
464
|
+
streamedText = "";
|
|
465
|
+
process.stdout.write("\n");
|
|
466
|
+
}
|
|
467
|
+
clearThinking();
|
|
468
|
+
if (event.type === "tool:started") {
|
|
469
|
+
if (currentText.length > 0) {
|
|
470
|
+
sections.push({ type: "text", content: currentText });
|
|
471
|
+
currentText = "";
|
|
472
|
+
}
|
|
473
|
+
const preview2 = showToolPayloads ? compactPreview(event.input, 400) : compactPreview(event.input, 100);
|
|
474
|
+
console.log(yellow(`tools> start ${event.tool} input=${preview2}`));
|
|
475
|
+
const toolText = `- start \`${event.tool}\``;
|
|
476
|
+
toolTimeline.push(toolText);
|
|
477
|
+
currentTools.push(toolText);
|
|
478
|
+
toolEvents += 1;
|
|
479
|
+
} else if (event.type === "tool:completed") {
|
|
480
|
+
const preview2 = showToolPayloads ? compactPreview(event.output, 400) : compactPreview(event.output, 100);
|
|
481
|
+
console.log(
|
|
482
|
+
yellow(
|
|
483
|
+
`tools> done ${event.tool} in ${formatDuration(event.duration)}`
|
|
484
|
+
)
|
|
485
|
+
);
|
|
486
|
+
if (showToolPayloads) {
|
|
487
|
+
console.log(yellow(`tools> output ${preview2}`));
|
|
488
|
+
}
|
|
489
|
+
const toolText = `- done \`${event.tool}\` in ${formatDuration(event.duration)}`;
|
|
490
|
+
toolTimeline.push(toolText);
|
|
491
|
+
currentTools.push(toolText);
|
|
492
|
+
} else if (event.type === "tool:error") {
|
|
493
|
+
console.log(
|
|
494
|
+
red(`tools> error ${event.tool}: ${event.error}`)
|
|
495
|
+
);
|
|
496
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
497
|
+
toolTimeline.push(toolText);
|
|
498
|
+
currentTools.push(toolText);
|
|
499
|
+
} else if (event.type === "tool:approval:required") {
|
|
500
|
+
console.log(
|
|
501
|
+
magenta(`tools> approval required for ${event.tool}`)
|
|
502
|
+
);
|
|
503
|
+
const toolText = `- approval required \`${event.tool}\``;
|
|
504
|
+
toolTimeline.push(toolText);
|
|
505
|
+
currentTools.push(toolText);
|
|
506
|
+
}
|
|
507
|
+
} else if (event.type === "tool:approval:checkpoint") {
|
|
508
|
+
checkpointEvent = event;
|
|
509
|
+
} else if (event.type === "run:error") {
|
|
510
|
+
clearThinking();
|
|
511
|
+
runFailed = true;
|
|
512
|
+
console.log(red(`error> ${event.error.message}`));
|
|
513
|
+
} else if (event.type === "run:cancelled") {
|
|
514
|
+
clearThinking();
|
|
515
|
+
runCancelled = true;
|
|
516
|
+
} else if (event.type === "compaction:completed") {
|
|
517
|
+
clearThinking();
|
|
518
|
+
if (event.compactedMessages) {
|
|
519
|
+
messages.length = 0;
|
|
520
|
+
messages.push(...event.compactedMessages);
|
|
521
|
+
}
|
|
522
|
+
console.log(
|
|
523
|
+
gray(
|
|
524
|
+
`compact> ${event.messagesBefore} \u2192 ${event.messagesAfter} messages (${event.tokensBefore.toLocaleString()} \u2192 ${event.tokensAfter.toLocaleString()} tokens est.)`
|
|
525
|
+
)
|
|
526
|
+
);
|
|
527
|
+
} else if (event.type === "compaction:warning") {
|
|
528
|
+
clearThinking();
|
|
529
|
+
console.log(gray(`compact> ${event.reason}`));
|
|
530
|
+
} else if (event.type === "model:response") {
|
|
531
|
+
usage = event.usage;
|
|
532
|
+
} else if (event.type === "run:completed" && !sawChunk) {
|
|
533
|
+
clearThinking();
|
|
534
|
+
responseText = event.result.response ?? "";
|
|
535
|
+
if (responseText.length > 0) {
|
|
536
|
+
process.stdout.write(
|
|
537
|
+
`${C.green}assistant> ${C.reset}${responseText}
|
|
538
|
+
`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (!checkpointEvent) break;
|
|
544
|
+
if (streamedText.length > 0) {
|
|
545
|
+
process.stdout.write("\n");
|
|
546
|
+
streamedText = "";
|
|
547
|
+
}
|
|
548
|
+
clearThinking();
|
|
549
|
+
const preview = compactPreview(checkpointEvent.input, 100);
|
|
550
|
+
const answer = await ask(
|
|
551
|
+
rl,
|
|
552
|
+
`${C.yellow}${C.bold}Tool "${checkpointEvent.tool}" requires approval${C.reset}
|
|
553
|
+
${C.gray}input: ${preview}${C.reset}
|
|
554
|
+
${C.yellow}approve? (y/n): ${C.reset}`
|
|
555
|
+
);
|
|
556
|
+
const approved = answer.trim().toLowerCase() === "y";
|
|
557
|
+
console.log(
|
|
558
|
+
approved ? green(` approved ${checkpointEvent.tool}`) : magenta(` denied ${checkpointEvent.tool}`)
|
|
559
|
+
);
|
|
560
|
+
const approvalText = approved ? `- approval granted (${checkpointEvent.approvalId})` : `- approval denied (${checkpointEvent.approvalId})`;
|
|
561
|
+
toolTimeline.push(approvalText);
|
|
562
|
+
currentTools.push(approvalText);
|
|
563
|
+
let toolResults;
|
|
564
|
+
if (approved) {
|
|
565
|
+
const execResults = await harness.executeTools(
|
|
566
|
+
[{ id: checkpointEvent.toolCallId, name: checkpointEvent.tool, input: checkpointEvent.input }],
|
|
567
|
+
{ runId: latestRunId, agentId: "interactive", step: 0, workingDir, parameters: params }
|
|
568
|
+
);
|
|
569
|
+
toolResults = execResults.map((r) => ({
|
|
570
|
+
callId: r.callId,
|
|
571
|
+
toolName: r.tool,
|
|
572
|
+
result: r.output,
|
|
573
|
+
error: r.error
|
|
574
|
+
}));
|
|
575
|
+
for (const r of execResults) {
|
|
576
|
+
if (r.error) {
|
|
577
|
+
console.log(red(`tools> error ${r.tool}: ${r.error}`));
|
|
578
|
+
const toolText = `- error \`${r.tool}\`: ${r.error}`;
|
|
579
|
+
toolTimeline.push(toolText);
|
|
580
|
+
currentTools.push(toolText);
|
|
581
|
+
} else {
|
|
582
|
+
console.log(yellow(`tools> done ${r.tool}`));
|
|
583
|
+
const toolText = `- done \`${r.tool}\``;
|
|
584
|
+
toolTimeline.push(toolText);
|
|
585
|
+
currentTools.push(toolText);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
toolResults = [{
|
|
590
|
+
callId: checkpointEvent.toolCallId,
|
|
591
|
+
toolName: checkpointEvent.tool,
|
|
592
|
+
error: "Tool execution denied by user"
|
|
593
|
+
}];
|
|
594
|
+
}
|
|
595
|
+
const fullMessages = [...messages, ...checkpointEvent.checkpointMessages];
|
|
596
|
+
eventSource = harness.continueFromToolResult({
|
|
597
|
+
messages: fullMessages,
|
|
598
|
+
toolResults,
|
|
599
|
+
abortSignal: activeRunAbortController.signal
|
|
600
|
+
});
|
|
601
|
+
process.stdout.write(gray("thinking..."));
|
|
602
|
+
thinkingCleared = false;
|
|
603
|
+
}
|
|
604
|
+
} catch (error) {
|
|
605
|
+
clearThinking();
|
|
606
|
+
if (activeRunAbortController?.signal.aborted) {
|
|
607
|
+
runCancelled = true;
|
|
608
|
+
} else {
|
|
609
|
+
runFailed = true;
|
|
610
|
+
console.log(
|
|
611
|
+
red(
|
|
612
|
+
`error> ${error instanceof Error ? error.message : "Unknown error"}`
|
|
613
|
+
)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
} finally {
|
|
617
|
+
activeRunAbortController = null;
|
|
618
|
+
}
|
|
619
|
+
if (sawChunk && streamedText.length > 0) {
|
|
620
|
+
process.stdout.write("\n");
|
|
621
|
+
} else if (!sawChunk && !runFailed && !runCancelled && responseText.length === 0) {
|
|
622
|
+
clearThinking();
|
|
623
|
+
console.log(green("assistant> (no response)"));
|
|
624
|
+
}
|
|
625
|
+
const fullResponse = responseText || streamedText;
|
|
626
|
+
if (!runFailed && toolEvents === 0 && FAUX_TOOL_LOG_PATTERN.test(fullResponse)) {
|
|
627
|
+
console.log(
|
|
628
|
+
magenta(
|
|
629
|
+
"warning> assistant described tool execution but no real tool events occurred."
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
const durationMs = Date.now() - startedAt;
|
|
634
|
+
console.log(
|
|
635
|
+
gray(`meta> ${formatDuration(durationMs)} | tools: ${toolEvents}
|
|
636
|
+
`)
|
|
637
|
+
);
|
|
638
|
+
if (!activeConversationId) {
|
|
639
|
+
const created = await conversationStore.create(
|
|
640
|
+
OWNER_ID,
|
|
641
|
+
inferConversationTitle(trimmed)
|
|
642
|
+
);
|
|
643
|
+
activeConversationId = created.conversationId;
|
|
644
|
+
}
|
|
645
|
+
if (currentTools.length > 0) {
|
|
646
|
+
sections.push({ type: "tools", content: currentTools });
|
|
647
|
+
}
|
|
648
|
+
if (currentText.length > 0) {
|
|
649
|
+
sections.push({ type: "text", content: currentText });
|
|
650
|
+
}
|
|
651
|
+
messages.push({ role: "user", content: trimmed });
|
|
652
|
+
const hasAssistantContent = responseText.length > 0 || toolTimeline.length > 0 || sections.length > 0;
|
|
653
|
+
if (hasAssistantContent) {
|
|
654
|
+
messages.push({
|
|
655
|
+
role: "assistant",
|
|
656
|
+
content: responseText,
|
|
657
|
+
metadata: toolTimeline.length > 0 || sections.length > 0 ? {
|
|
658
|
+
toolActivity: toolTimeline,
|
|
659
|
+
sections: sections.length > 0 ? sections : void 0
|
|
660
|
+
} : void 0
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
turn = computeTurn(messages);
|
|
664
|
+
const conversation = await conversationStore.get(activeConversationId);
|
|
665
|
+
if (conversation) {
|
|
666
|
+
const maybeTitle = conversation.messages.length === 0 && (conversation.title === "New conversation" || conversation.title.trim().length === 0) ? inferConversationTitle(trimmed) : conversation.title;
|
|
667
|
+
await conversationStore.update({
|
|
668
|
+
...conversation,
|
|
669
|
+
title: maybeTitle,
|
|
670
|
+
messages: [...messages],
|
|
671
|
+
runtimeRunId: latestRunId || conversation.runtimeRunId
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
rl.close();
|
|
676
|
+
};
|
|
677
|
+
export {
|
|
678
|
+
runInteractiveInk
|
|
679
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "CLI for building and deploying AI agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"react": "^19.2.4",
|
|
29
29
|
"react-devtools-core": "^6.1.5",
|
|
30
30
|
"yaml": "^2.8.1",
|
|
31
|
-
"@poncho-ai/harness": "0.
|
|
32
|
-
"@poncho-ai/messaging": "0.8.
|
|
33
|
-
"@poncho-ai/sdk": "1.
|
|
31
|
+
"@poncho-ai/harness": "0.39.0",
|
|
32
|
+
"@poncho-ai/messaging": "0.8.4",
|
|
33
|
+
"@poncho-ai/sdk": "1.9.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/busboy": "^1.5.4",
|