mini-coder 0.5.13 → 0.6.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 +25 -108
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/package.json +17 -22
- package/src/agent.ts +242 -915
- package/src/args.ts +289 -0
- package/src/headless.ts +43 -385
- package/src/index.ts +29 -836
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +227 -276
- package/src/session.ts +57 -961
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-task.ts +114 -0
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +262 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +236 -0
- package/src/types.ts +160 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -4
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/cli.ts +0 -134
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -393
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -371
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-grep.ts +0 -606
- package/src/tool-read.ts +0 -313
- package/src/tool-shell.ts +0 -1001
- package/src/tools.ts +0 -854
- package/src/ui/agent.ts +0 -317
- package/src/ui/commands.test.ts +0 -913
- package/src/ui/commands.ts +0 -834
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
package/src/args.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
type Api,
|
|
4
|
+
getModels,
|
|
5
|
+
getProviders,
|
|
6
|
+
type Model,
|
|
7
|
+
type ThinkingLevel,
|
|
8
|
+
} from "@mariozechner/pi-ai";
|
|
9
|
+
import { Value } from "typebox/value";
|
|
10
|
+
import { getAvailableProviders, isOAuthProvider, loginOAuth } from "./oauth";
|
|
11
|
+
import { DATA_DIR, SETTINGS_PATH } from "./shared.ts";
|
|
12
|
+
import {
|
|
13
|
+
type CliOptions,
|
|
14
|
+
CliOptionsSchema,
|
|
15
|
+
type Settings,
|
|
16
|
+
SettingsSchema,
|
|
17
|
+
} from "./types.ts";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PROVIDER = "openai-codex";
|
|
20
|
+
const DEFAULT_MODEL_ID = "gpt-5.5";
|
|
21
|
+
const DEFAULT_EFFORT: ThinkingLevel = "xhigh";
|
|
22
|
+
|
|
23
|
+
function formatValidationError(
|
|
24
|
+
label: string,
|
|
25
|
+
error: ReturnType<typeof Value.Errors>[number] | undefined,
|
|
26
|
+
) {
|
|
27
|
+
if (!error) {
|
|
28
|
+
return `Invalid ${label}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const path = error.instancePath || "/";
|
|
32
|
+
return `Invalid ${label}: ${path} ${error.message}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseSettings(value: unknown, label: string): Settings {
|
|
36
|
+
if (Value.Check(SettingsSchema, value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(
|
|
41
|
+
formatValidationError(label, Value.Errors(SettingsSchema, value)[0]),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseCliOptions(value: unknown): CliOptions {
|
|
46
|
+
if (Value.Check(CliOptionsSchema, value)) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error(
|
|
51
|
+
formatValidationError(
|
|
52
|
+
"CLI options",
|
|
53
|
+
Value.Errors(CliOptionsSchema, value)[0],
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getSettings(): Promise<Settings | undefined> {
|
|
59
|
+
const file = Bun.file(SETTINGS_PATH);
|
|
60
|
+
|
|
61
|
+
if (!(await file.exists())) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const jsonText = await file.text();
|
|
66
|
+
const settings = JSON.parse(jsonText) as unknown;
|
|
67
|
+
|
|
68
|
+
return parseSettings(settings, "settings");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function saveSettings(s: Settings) {
|
|
72
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
73
|
+
const file = Bun.file(SETTINGS_PATH);
|
|
74
|
+
await Bun.write(file, JSON.stringify(s, null, 4));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requireValue(argv: string[], index: number, flag: string) {
|
|
78
|
+
const value = argv[index + 1];
|
|
79
|
+
|
|
80
|
+
if (!value || value.startsWith("-")) {
|
|
81
|
+
throw new Error(`${flag} requires a value`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findModelConfig(
|
|
88
|
+
modelId: string,
|
|
89
|
+
provider: string,
|
|
90
|
+
customProviders?: Model<Api>[],
|
|
91
|
+
) {
|
|
92
|
+
const knownProviders = getProviders() as string[];
|
|
93
|
+
if (knownProviders.includes(provider)) {
|
|
94
|
+
const models = getModels(provider as any);
|
|
95
|
+
if (models.length === 0) throw new Error("Provider has no models");
|
|
96
|
+
return models.find((m) => m.id === modelId);
|
|
97
|
+
}
|
|
98
|
+
return customProviders?.find(
|
|
99
|
+
(m) => m.provider === provider && m.id === modelId,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getFirstModelConfig(
|
|
104
|
+
provider: string,
|
|
105
|
+
customProviders?: Model<Api>[],
|
|
106
|
+
): Model<Api> {
|
|
107
|
+
const knownProviders = getProviders() as string[];
|
|
108
|
+
if (knownProviders.includes(provider)) {
|
|
109
|
+
const models = getModels(provider as any);
|
|
110
|
+
if (models.length > 0) return models[0];
|
|
111
|
+
}
|
|
112
|
+
const custom = customProviders?.find((m) => m.provider === provider);
|
|
113
|
+
if (custom) return custom;
|
|
114
|
+
throw new Error("Provider has no models");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getFallbackModel(
|
|
118
|
+
provider: string,
|
|
119
|
+
explicitModel: boolean,
|
|
120
|
+
providerChanged: boolean,
|
|
121
|
+
customProviders?: Model<Api>[],
|
|
122
|
+
): Model<Api> {
|
|
123
|
+
if (explicitModel) {
|
|
124
|
+
throw new Error("Model not found");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!providerChanged) {
|
|
128
|
+
throw new Error("Model not found");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return getFirstModelConfig(provider, customProviders);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function handleArgv(argv: string[]): Promise<CliOptions> {
|
|
135
|
+
const settings = await getSettings();
|
|
136
|
+
const customProviders = settings?.customProviders ?? [];
|
|
137
|
+
const builtInProviders = await getAvailableProviders();
|
|
138
|
+
const availableProviders = [
|
|
139
|
+
...new Set([
|
|
140
|
+
...builtInProviders,
|
|
141
|
+
...customProviders.map((cp) => cp.provider),
|
|
142
|
+
]),
|
|
143
|
+
];
|
|
144
|
+
const defaultProvider = availableProviders[0] ?? DEFAULT_PROVIDER;
|
|
145
|
+
const settingsProvider = settings?.provider ?? defaultProvider;
|
|
146
|
+
const settingsModelId =
|
|
147
|
+
settings?.model ??
|
|
148
|
+
(settingsProvider === DEFAULT_PROVIDER
|
|
149
|
+
? DEFAULT_MODEL_ID
|
|
150
|
+
: getFirstModelConfig(settingsProvider, customProviders).id);
|
|
151
|
+
const settingsEffort = settings?.effort ?? DEFAULT_EFFORT;
|
|
152
|
+
let provider: string = settingsProvider;
|
|
153
|
+
let modelId = settingsModelId;
|
|
154
|
+
let effort: string = settingsEffort;
|
|
155
|
+
let prompt: string | undefined;
|
|
156
|
+
let providerChanged = false;
|
|
157
|
+
let explicitProvider = false;
|
|
158
|
+
let explicitModel = false;
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < argv.length; i++) {
|
|
161
|
+
const flag = argv[i];
|
|
162
|
+
|
|
163
|
+
// Settings
|
|
164
|
+
if (flag === "--provider") {
|
|
165
|
+
provider = parseSettings(
|
|
166
|
+
{
|
|
167
|
+
provider: requireValue(argv, i, flag),
|
|
168
|
+
model: modelId,
|
|
169
|
+
effort,
|
|
170
|
+
},
|
|
171
|
+
"CLI settings",
|
|
172
|
+
).provider;
|
|
173
|
+
providerChanged = provider !== settingsProvider;
|
|
174
|
+
explicitProvider = true;
|
|
175
|
+
i++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (flag === "--model") {
|
|
180
|
+
modelId = requireValue(argv, i, flag);
|
|
181
|
+
explicitModel = true;
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (flag === "--effort") {
|
|
187
|
+
effort = parseSettings(
|
|
188
|
+
{
|
|
189
|
+
provider,
|
|
190
|
+
model: modelId,
|
|
191
|
+
effort: requireValue(argv, i, flag),
|
|
192
|
+
},
|
|
193
|
+
"CLI settings",
|
|
194
|
+
).effort;
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// headless api
|
|
200
|
+
if (flag === "--prompt" || flag === "-p") {
|
|
201
|
+
prompt = requireValue(argv, i, flag);
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (flag === "--login" || flag === "-l") {
|
|
207
|
+
await loginOAuth(requireValue(argv, i, flag));
|
|
208
|
+
// Rebuild available providers after login
|
|
209
|
+
const refreshedProviders = await getAvailableProviders();
|
|
210
|
+
availableProviders.splice(
|
|
211
|
+
0,
|
|
212
|
+
availableProviders.length,
|
|
213
|
+
...new Set([
|
|
214
|
+
...refreshedProviders,
|
|
215
|
+
...customProviders.map((cp) => cp.provider),
|
|
216
|
+
]),
|
|
217
|
+
);
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let cliSettings = parseSettings(
|
|
223
|
+
{
|
|
224
|
+
provider,
|
|
225
|
+
model: modelId,
|
|
226
|
+
effort,
|
|
227
|
+
},
|
|
228
|
+
"CLI settings",
|
|
229
|
+
);
|
|
230
|
+
const providerAvailable = availableProviders.includes(cliSettings.provider);
|
|
231
|
+
|
|
232
|
+
if (explicitProvider) {
|
|
233
|
+
if (!providerAvailable && !isOAuthProvider(cliSettings.provider)) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Provider "${cliSettings.provider}" is not logged in and no API key was found`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
} else if (!providerAvailable && availableProviders.length) {
|
|
239
|
+
const fallbackProvider = availableProviders[0];
|
|
240
|
+
provider = fallbackProvider;
|
|
241
|
+
providerChanged = provider !== settingsProvider;
|
|
242
|
+
if (!explicitModel) {
|
|
243
|
+
modelId = getFirstModelConfig(fallbackProvider, customProviders).id;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
cliSettings = parseSettings(
|
|
247
|
+
{
|
|
248
|
+
provider,
|
|
249
|
+
model: modelId,
|
|
250
|
+
effort,
|
|
251
|
+
},
|
|
252
|
+
"CLI settings",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const selectedModel = findModelConfig(
|
|
257
|
+
cliSettings.model,
|
|
258
|
+
cliSettings.provider,
|
|
259
|
+
customProviders,
|
|
260
|
+
);
|
|
261
|
+
const model =
|
|
262
|
+
selectedModel ??
|
|
263
|
+
getFallbackModel(
|
|
264
|
+
cliSettings.provider,
|
|
265
|
+
explicitModel,
|
|
266
|
+
providerChanged,
|
|
267
|
+
customProviders,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const options = parseCliOptions({
|
|
271
|
+
provider: cliSettings.provider,
|
|
272
|
+
model,
|
|
273
|
+
effort: cliSettings.effort,
|
|
274
|
+
prompt,
|
|
275
|
+
customProviders,
|
|
276
|
+
});
|
|
277
|
+
const nextSettings = parseSettings(
|
|
278
|
+
{
|
|
279
|
+
provider: options.provider,
|
|
280
|
+
model: options.model.id,
|
|
281
|
+
effort: options.effort,
|
|
282
|
+
customProviders,
|
|
283
|
+
},
|
|
284
|
+
"settings",
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await saveSettings(nextSettings);
|
|
288
|
+
return options;
|
|
289
|
+
}
|
package/src/headless.ts
CHANGED
|
@@ -1,392 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./text.ts";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Types
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
type HeadlessStopReason = "stop" | "length" | "error" | "aborted";
|
|
26
|
-
|
|
27
|
-
/** Options for a headless NDJSON run. */
|
|
28
|
-
export interface HeadlessRunOptions {
|
|
29
|
-
/** Optional line writer for completed NDJSON event output. */
|
|
30
|
-
writeLine?: (line: string) => void | Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Options for a headless final-text run. */
|
|
34
|
-
export interface HeadlessTextRunOptions {
|
|
35
|
-
/** Optional writer for lightweight assistant-activity snippets. */
|
|
36
|
-
writeActivity?: (text: string) => void | Promise<void>;
|
|
37
|
-
/** Optional writer for the final assistant text output. */
|
|
38
|
-
writeText?: (text: string) => void | Promise<void>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface HeadlessOutputController {
|
|
42
|
-
/** Queue text for one output stream with broken-pipe handling. */
|
|
43
|
-
write(text: string): void;
|
|
44
|
-
/** Attach error handlers for the active run. */
|
|
45
|
-
attach(): void;
|
|
46
|
-
/** Remove error handlers after the run. */
|
|
47
|
-
detach(): void;
|
|
48
|
-
/** Wait for queued writes and resolve the final stop reason. */
|
|
49
|
-
finalize(stopReason: HeadlessStopReason): Promise<HeadlessStopReason>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface HeadlessProcessStream {
|
|
53
|
-
/** Register an output-stream error handler. */
|
|
54
|
-
on(event: "error", listener: (error: unknown) => void): void;
|
|
55
|
-
/** Remove an output-stream error handler. */
|
|
56
|
-
off(event: "error", listener: (error: unknown) => void): void;
|
|
57
|
-
/** Write a text chunk to the stream. */
|
|
58
|
-
write(text: string, callback?: () => void): boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const HEADLESS_ACTIVITY_MAX_CHARS = 160;
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Helpers
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
function defaultWrite(
|
|
68
|
-
stream: HeadlessProcessStream,
|
|
69
|
-
text: string,
|
|
70
|
-
): Promise<void> {
|
|
71
|
-
return new Promise((resolve, reject) => {
|
|
72
|
-
let settled = false;
|
|
73
|
-
|
|
74
|
-
const cleanup = (): void => {
|
|
75
|
-
stream.off("error", handleError);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const settle = (callback: () => void): void => {
|
|
79
|
-
if (settled) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
settled = true;
|
|
83
|
-
cleanup();
|
|
84
|
-
callback();
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const handleError = (error: unknown): void => {
|
|
88
|
-
settle(() => {
|
|
89
|
-
reject(error);
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
stream.on("error", handleError);
|
|
94
|
-
try {
|
|
95
|
-
stream.write(text, () => {
|
|
96
|
-
settle(resolve);
|
|
97
|
-
});
|
|
98
|
-
} catch (error) {
|
|
99
|
-
settle(() => {
|
|
100
|
-
reject(error);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isBrokenPipeError(error: unknown): boolean {
|
|
107
|
-
return (
|
|
108
|
-
typeof error === "object" &&
|
|
109
|
-
error !== null &&
|
|
110
|
-
(("code" in error && error.code === "EPIPE") ||
|
|
111
|
-
("message" in error &&
|
|
112
|
-
typeof error.message === "string" &&
|
|
113
|
-
error.message.includes("broken pipe")))
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function createSigintHandler(state: AppState): () => void {
|
|
118
|
-
return () => {
|
|
119
|
-
state.abortController?.abort();
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function buildCommandError(command: string): Error {
|
|
124
|
-
return new Error(
|
|
125
|
-
`Headless mode does not support slash commands: /${command}`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function resolveHeadlessContent(
|
|
130
|
-
state: AppState,
|
|
131
|
-
rawInput: string,
|
|
132
|
-
): UserMessage["content"] {
|
|
133
|
-
const resolved = resolveRawInput(rawInput, state);
|
|
134
|
-
switch (resolved.type) {
|
|
135
|
-
case "empty":
|
|
136
|
-
throw new Error("Headless input is empty.");
|
|
137
|
-
case "error":
|
|
138
|
-
throw new Error(resolved.message);
|
|
139
|
-
case "command":
|
|
140
|
-
throw buildCommandError(resolved.command);
|
|
141
|
-
case "message":
|
|
142
|
-
return resolved.content;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function extractAssistantText(message: AssistantMessage | null): string {
|
|
147
|
-
if (!message) {
|
|
148
|
-
return "";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return message.content
|
|
152
|
-
.filter(
|
|
153
|
-
(
|
|
154
|
-
block,
|
|
155
|
-
): block is Extract<
|
|
156
|
-
AssistantMessage["content"][number],
|
|
157
|
-
{ type: "text" }
|
|
158
|
-
> => {
|
|
159
|
-
return block.type === "text";
|
|
160
|
-
},
|
|
161
|
-
)
|
|
162
|
-
.map((block) => block.text)
|
|
163
|
-
.join("");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function extractAssistantActivitySnippet(
|
|
167
|
-
message: AssistantMessage,
|
|
168
|
-
): string | null {
|
|
169
|
-
if (!message.content.some((block) => block.type === "toolCall")) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const text = collapseWhitespaceToNull(joinTextBlocks(message.content));
|
|
174
|
-
return text ? truncateText(text, HEADLESS_ACTIVITY_MAX_CHARS) : null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
178
|
-
switch (event.type) {
|
|
179
|
-
case "user_message":
|
|
180
|
-
case "assistant_message":
|
|
181
|
-
case "tool_result":
|
|
182
|
-
case "done":
|
|
183
|
-
case "error":
|
|
184
|
-
case "aborted":
|
|
185
|
-
return true;
|
|
186
|
-
default:
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function createHeadlessOutputController(
|
|
192
|
-
state: AppState,
|
|
193
|
-
stream: HeadlessProcessStream,
|
|
194
|
-
writeImpl: (text: string) => void | Promise<void>,
|
|
195
|
-
options?: {
|
|
196
|
-
attachSigint?: boolean;
|
|
197
|
-
},
|
|
198
|
-
): HeadlessOutputController {
|
|
199
|
-
let brokenPipe = false;
|
|
200
|
-
let outputError: unknown = null;
|
|
201
|
-
let pendingWrite = Promise.resolve();
|
|
202
|
-
const sigintHandler = createSigintHandler(state);
|
|
203
|
-
const attachSigint = options?.attachSigint ?? true;
|
|
204
|
-
|
|
205
|
-
const stopForBrokenPipe = (): void => {
|
|
206
|
-
if (brokenPipe) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
brokenPipe = true;
|
|
210
|
-
state.abortController?.abort();
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const failOutput = (error: unknown): void => {
|
|
214
|
-
if (isBrokenPipeError(error)) {
|
|
215
|
-
stopForBrokenPipe();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
outputError = outputError ?? error;
|
|
220
|
-
state.abortController?.abort();
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const streamErrorHandler = (error: unknown): void => {
|
|
224
|
-
failOutput(error);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
write(text) {
|
|
229
|
-
if (brokenPipe || outputError) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
pendingWrite = pendingWrite.then(async () => {
|
|
234
|
-
if (brokenPipe || outputError) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
await writeImpl(text);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
failOutput(error);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
},
|
|
245
|
-
attach() {
|
|
246
|
-
stream.on("error", streamErrorHandler);
|
|
247
|
-
if (attachSigint) {
|
|
248
|
-
process.on("SIGINT", sigintHandler);
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
detach() {
|
|
252
|
-
stream.off("error", streamErrorHandler);
|
|
253
|
-
if (attachSigint) {
|
|
254
|
-
process.off("SIGINT", sigintHandler);
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
async finalize(stopReason) {
|
|
258
|
-
await pendingWrite;
|
|
259
|
-
if (outputError) {
|
|
260
|
-
throw outputError;
|
|
261
|
-
}
|
|
262
|
-
return brokenPipe ? "stop" : stopReason;
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Run a single headless prompt to completion and stream completed NDJSON events.
|
|
269
|
-
*
|
|
270
|
-
* The raw input is parsed with the same rules as interactive input. Slash
|
|
271
|
-
* commands are rejected in headless mode. Persisted messages and terminal
|
|
272
|
-
* events are written as one JSON object per line; streaming delta/progress
|
|
273
|
-
* events are omitted.
|
|
274
|
-
*
|
|
275
|
-
* @param state - Mutable application state for the run.
|
|
276
|
-
* @param rawInput - Exact raw prompt text supplied by the user.
|
|
277
|
-
* @param options - Optional event-output overrides.
|
|
278
|
-
* @returns The terminal stop reason for the agent loop.
|
|
279
|
-
*/
|
|
280
|
-
export async function runHeadlessPrompt(
|
|
281
|
-
state: AppState,
|
|
282
|
-
rawInput: string,
|
|
283
|
-
options?: HeadlessRunOptions,
|
|
284
|
-
): Promise<HeadlessStopReason> {
|
|
285
|
-
const content = resolveHeadlessContent(state, rawInput);
|
|
286
|
-
const output = createHeadlessOutputController(
|
|
287
|
-
state,
|
|
288
|
-
process.stdout,
|
|
289
|
-
options?.writeLine ?? ((line) => defaultWrite(process.stdout, `${line}\n`)),
|
|
290
|
-
);
|
|
291
|
-
const hooks: SubmitTurnHooks = {
|
|
292
|
-
onEvent: (event) => {
|
|
293
|
-
if (!shouldWriteHeadlessJsonEvent(event)) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
output.write(JSON.stringify(event));
|
|
1
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
2
|
+
import { streamAgent } from "./agent";
|
|
3
|
+
import { buildSystemPrompt, injectEnvReminder, MAIN_PROMPT } from "./prompt";
|
|
4
|
+
import { bash, runBashTool } from "./tool-bash";
|
|
5
|
+
import { edit, runEditTool } from "./tool-edit";
|
|
6
|
+
import { runTaskTool, task } from "./tool-task";
|
|
7
|
+
import type { AgentContex, CliOptions, ToolAndRunner } from "./types";
|
|
8
|
+
|
|
9
|
+
export async function streamHeadless(
|
|
10
|
+
options: CliOptions,
|
|
11
|
+
leave: (s?: string) => void,
|
|
12
|
+
) {
|
|
13
|
+
const tools: ToolAndRunner[] = [
|
|
14
|
+
{ tool: bash, runner: runBashTool },
|
|
15
|
+
{ tool: edit, runner: runEditTool },
|
|
16
|
+
{
|
|
17
|
+
tool: task,
|
|
18
|
+
runner: (args) => runTaskTool(options, args),
|
|
297
19
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
state,
|
|
306
|
-
hooks,
|
|
307
|
-
);
|
|
308
|
-
return await output.finalize(stopReason);
|
|
309
|
-
} finally {
|
|
310
|
-
output.detach();
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Run a single headless prompt to completion and write the final assistant text.
|
|
316
|
-
*
|
|
317
|
-
* The raw input is parsed with the same rules as interactive input. Slash
|
|
318
|
-
* commands are rejected in headless mode. The final assistant text is written
|
|
319
|
-
* to stdout, while lightweight assistant commentary snippets from tool-use
|
|
320
|
-
* turns are written to stderr.
|
|
321
|
-
*
|
|
322
|
-
* @param state - Mutable application state for the run.
|
|
323
|
-
* @param rawInput - Exact raw prompt text supplied by the user.
|
|
324
|
-
* @param options - Optional final-text output overrides.
|
|
325
|
-
* @returns The terminal stop reason for the agent loop.
|
|
326
|
-
*/
|
|
327
|
-
export async function runHeadlessPromptText(
|
|
328
|
-
state: AppState,
|
|
329
|
-
rawInput: string,
|
|
330
|
-
options?: HeadlessTextRunOptions,
|
|
331
|
-
): Promise<HeadlessStopReason> {
|
|
332
|
-
const content = resolveHeadlessContent(state, rawInput);
|
|
333
|
-
const finalOutput = createHeadlessOutputController(
|
|
334
|
-
state,
|
|
335
|
-
process.stdout,
|
|
336
|
-
options?.writeText ?? ((text) => defaultWrite(process.stdout, text)),
|
|
337
|
-
);
|
|
338
|
-
const activityOutput = createHeadlessOutputController(
|
|
339
|
-
state,
|
|
340
|
-
process.stderr,
|
|
341
|
-
options?.writeActivity ?? ((text) => defaultWrite(process.stderr, text)),
|
|
342
|
-
{ attachSigint: false },
|
|
343
|
-
);
|
|
344
|
-
let finalAssistantMessage: AssistantMessage | null = null;
|
|
345
|
-
const hooks: SubmitTurnHooks = {
|
|
346
|
-
onEvent: (event) => {
|
|
347
|
-
switch (event.type) {
|
|
348
|
-
case "assistant_message": {
|
|
349
|
-
const activitySnippet = extractAssistantActivitySnippet(
|
|
350
|
-
event.message,
|
|
351
|
-
);
|
|
352
|
-
if (activitySnippet) {
|
|
353
|
-
activityOutput.write(`${activitySnippet}\n`);
|
|
354
|
-
}
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
case "done":
|
|
358
|
-
case "error":
|
|
359
|
-
case "aborted":
|
|
360
|
-
finalAssistantMessage = event.message;
|
|
361
|
-
return;
|
|
362
|
-
default:
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
20
|
+
];
|
|
21
|
+
const envReminder = await injectEnvReminder();
|
|
22
|
+
const messages: Message[] = [
|
|
23
|
+
{
|
|
24
|
+
role: "user",
|
|
25
|
+
content: `${envReminder}\n\n${options.prompt || ""}`,
|
|
26
|
+
timestamp: Date.now(),
|
|
365
27
|
},
|
|
28
|
+
];
|
|
29
|
+
console.log(JSON.stringify(messages[0]));
|
|
30
|
+
|
|
31
|
+
const systemPrompt = await buildSystemPrompt(MAIN_PROMPT);
|
|
32
|
+
const ctx: AgentContex = {
|
|
33
|
+
systemPrompt,
|
|
34
|
+
tools,
|
|
35
|
+
messages,
|
|
36
|
+
options,
|
|
366
37
|
};
|
|
367
38
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
hooks,
|
|
376
|
-
);
|
|
377
|
-
const finalText = extractAssistantText(finalAssistantMessage);
|
|
378
|
-
if (finalText.length > 0) {
|
|
379
|
-
finalOutput.write(finalText);
|
|
39
|
+
const agent = streamAgent(ctx);
|
|
40
|
+
for await (const ev of agent) {
|
|
41
|
+
switch (ev.type) {
|
|
42
|
+
case "message_end":
|
|
43
|
+
case "tool_message_end":
|
|
44
|
+
console.log(JSON.stringify(ev.message));
|
|
45
|
+
break;
|
|
380
46
|
}
|
|
381
|
-
const [finalStopReason, activityStopReason] = await Promise.all([
|
|
382
|
-
finalOutput.finalize(stopReason),
|
|
383
|
-
activityOutput.finalize(stopReason),
|
|
384
|
-
]);
|
|
385
|
-
return finalStopReason === "stop" || activityStopReason === "stop"
|
|
386
|
-
? "stop"
|
|
387
|
-
: stopReason;
|
|
388
|
-
} finally {
|
|
389
|
-
activityOutput.detach();
|
|
390
|
-
finalOutput.detach();
|
|
391
47
|
}
|
|
48
|
+
|
|
49
|
+
leave();
|
|
392
50
|
}
|