mini-coder 0.7.3 → 0.8.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/src/tui.ts DELETED
@@ -1,314 +0,0 @@
1
- import { cel, HStack, ProcessTerminal, VStack } from "@cel-tui/core";
2
- import type {
3
- AssistantMessage,
4
- ToolResultMessage,
5
- } from "@earendil-works/pi-ai";
6
- import { streamAgent } from "./agent";
7
- import { getBranchLabel } from "./git";
8
- import {
9
- buildSystemPrompt,
10
- injectEnvReminder,
11
- insertToolUsageReminder,
12
- MAIN_PROMPT,
13
- } from "./prompt";
14
- import { updateSession } from "./session";
15
- import { estimateTokens, formatTimestamp, secureRandomString } from "./shared";
16
- import { activeTuiTheme, applyTUITheme, getTUITheme } from "./themes";
17
- import { bash, runBashTool } from "./tool-bash";
18
- import { edit, runEditTool } from "./tool-edit";
19
- import { read, runReadTool } from "./tool-read";
20
- import {
21
- ActivityPill,
22
- ContextPill,
23
- GitPill,
24
- ModelPill,
25
- Spinner,
26
- TextPill,
27
- theme,
28
- } from "./tui-components";
29
- import { Conversation, emptyState } from "./tui-conversation";
30
- import { Editor } from "./tui-editor";
31
- import { mainMenu } from "./tui-overlay";
32
- import type { AgentContex, ToolAndRunner, TUIMessage, TUIState } from "./types";
33
- import { getAvailableUpdate } from "./update";
34
-
35
- async function refreshAvailableUpdate(state: TUIState): Promise<void> {
36
- state.availableUpdate = await getAvailableUpdate();
37
- }
38
-
39
- function clearOrAbort(state: TUIState) {
40
- // Are we mid stream? Abort it.
41
- if (state.streaming) {
42
- state.abortController?.abort();
43
- }
44
-
45
- // Is the user clearing a state prompt?
46
- if (state.prompt?.length) {
47
- state.prompt = "";
48
- }
49
- }
50
-
51
- export function initTUI(state: TUIState, leave: (s: string) => void) {
52
- // TODO: Cleanup accumulated sessions for this cwd.
53
- const { spinnerEvery, currentSpinner } = Spinner();
54
- void refreshAvailableUpdate(state);
55
-
56
- // Stable 60fps rendering.
57
- // This ensure Xfps, and excessive calls get coalesced in cel-tui.
58
- const fps = 60;
59
- const baseFramerateIntervalId = setInterval(() => {
60
- if (state.streaming) {
61
- spinnerEvery();
62
- }
63
- cel.setTitle(
64
- `mc ${state.streaming ? currentSpinner() : ">"} ../${state.cwd}`,
65
- );
66
- cel.render();
67
- }, 1000 / fps);
68
-
69
- let menu = mainMenu(state);
70
-
71
- const onWindowKeyPress = (key: string) => {
72
- if (key === "ctrl+q" || key === "ctrl+c" || key === "ctrl+d") {
73
- // Quit
74
- clearInterval(baseFramerateIntervalId);
75
- cel.stop();
76
- leave("Done.");
77
- } else if (key === "escape") {
78
- // Abort or clear prompt
79
- clearOrAbort(state);
80
- } else if (key === "ctrl+p") {
81
- menu = mainMenu(state);
82
- state.overlay = true;
83
- } else if (key === "ctrl+f") {
84
- menu = mainMenu(state, "fork");
85
- state.overlay = true;
86
- }
87
- };
88
-
89
- const onChange = (value: string) => {
90
- state.prompt = value;
91
- };
92
-
93
- const onEditorKeyPress = (key: string) => {
94
- const submit = async () => {
95
- await streamAgentTUI(state);
96
- };
97
- // onKeyPress
98
- if (key === "enter") {
99
- if (state.prompt === ":q") {
100
- clearInterval(baseFramerateIntervalId);
101
- cel.stop();
102
- leave("Done. I like vim too.");
103
- return false;
104
- }
105
- if (state.prompt === ":n" || state.prompt === "/new") {
106
- state.sessionId = undefined;
107
- state.messages = [];
108
- state.tuiMessages = [];
109
- state.prompt = "";
110
- state.contextSize = 0;
111
- state.scrollOffset = 0;
112
- state.stickToBottom = true;
113
- return false;
114
- }
115
- if (state.prompt && !state.streaming) submit();
116
- return false;
117
- }
118
- };
119
-
120
- applyTUITheme(state.options.theme);
121
- cel.init(new ProcessTerminal(), { theme: activeTuiTheme });
122
- cel.viewport(() => {
123
- const activeTheme = getTUITheme(state.options.theme);
124
- const layers = [
125
- VStack(
126
- {
127
- height: "100%",
128
- gap: 1,
129
- padding: { x: 1, y: 1 },
130
- onKeyPress: onWindowKeyPress,
131
- fgColor: activeTheme.rootFgColor,
132
- bgColor: activeTheme.rootBgColor,
133
- italic: state.forceThemeRefresh,
134
- },
135
- [
136
- state.messages.length ? Conversation(state) : emptyState(state),
137
- HStack({ gap: 1 }, [
138
- ModelPill(state),
139
- TextPill(`../${state.cwd}`, theme.bwhite, theme.bblack),
140
- GitPill(state),
141
- VStack({ flex: 1 }, []),
142
- ActivityPill(state, currentSpinner()),
143
- ContextPill(state),
144
- ]),
145
-
146
- Editor(state, onChange, onEditorKeyPress),
147
- ],
148
- ),
149
- ];
150
- if (state.overlay) {
151
- layers.push(menu());
152
- }
153
-
154
- return layers;
155
- });
156
- }
157
-
158
- async function streamAgentTUI(state: TUIState) {
159
- // Just as a defensive thought, callers should set this ahead of time if doing other
160
- // ops before starting the stream.
161
- state.streaming = true;
162
-
163
- const abortController = new AbortController();
164
- state.abortController = abortController;
165
-
166
- const tools: ToolAndRunner[] = [
167
- { tool: bash, runner: runBashTool },
168
- { tool: edit, runner: runEditTool },
169
- { tool: read, runner: runReadTool },
170
- ];
171
-
172
- let userContent = state.prompt;
173
- if (state.messages.length === 0) {
174
- const envReminder = await injectEnvReminder();
175
- userContent = `${envReminder}\n\n${userContent}`;
176
- }
177
- state.messages.push({
178
- role: "user",
179
- content: userContent,
180
- timestamp: Date.now(),
181
- });
182
- state.tuiMessages.push({
183
- timestamp: formatTimestamp(Date.now()),
184
- role: "user",
185
- text: state.prompt,
186
- });
187
- state.prompt = "";
188
-
189
- const systemPrompt = await buildSystemPrompt(MAIN_PROMPT);
190
- const ctx: AgentContex = {
191
- systemPrompt,
192
- tools,
193
- messages: state.messages,
194
- options: state.options,
195
- signal: state.abortController?.signal,
196
- };
197
-
198
- const toTUIMessage = (partial: AssistantMessage) => {
199
- const text = partial.content
200
- .filter((c) => c.type === "text")
201
- .map((c) => c.text)
202
- .join("")
203
- .trim();
204
- const thinking = partial.content
205
- .filter((c) => c.type === "thinking")
206
- .map((c) => c.thinking)
207
- .join("")
208
- .trim();
209
- const toolCalls = partial.content
210
- .filter((c) => c.type === "toolCall")
211
- .map((c) => {
212
- return {
213
- id: c.id,
214
- tool: c.name,
215
- args: c.arguments,
216
- output: "",
217
- };
218
- });
219
-
220
- return {
221
- timestamp: formatTimestamp(partial.timestamp),
222
- role: "assistant" as const,
223
- text,
224
- thinking,
225
- toolCalls,
226
- };
227
- };
228
-
229
- const updateToolCall = (
230
- partial: ToolResultMessage,
231
- tuiMessages: TUIMessage[],
232
- ) => {
233
- tuiMessages.forEach((c) => {
234
- const parentCall = c.toolCalls?.find((t) => t.id === partial.toolCallId);
235
- if (parentCall) {
236
- parentCall.output = partial.content
237
- .filter((c) => c.type === "text")
238
- .map((c) => c.text)
239
- .join("")
240
- .trim();
241
- }
242
- });
243
- };
244
-
245
- const agent = streamAgent(ctx);
246
- try {
247
- for await (const ev of agent) {
248
- switch (ev.type) {
249
- case "message_start":
250
- state.tuiMessages.push(toTUIMessage(ev.partial));
251
- break;
252
- case "message_update":
253
- state.tuiMessages[state.tuiMessages.length - 1] = toTUIMessage(
254
- ev.partial,
255
- );
256
- break;
257
- case "message_end": {
258
- state.tuiMessages[state.tuiMessages.length - 1] = toTUIMessage(
259
- ev.message,
260
- );
261
- const { systemPrompt, tools, messages } = ctx;
262
- state.contextSize = estimateTokens(
263
- JSON.stringify({ systemPrompt, tools, messages }),
264
- );
265
- break;
266
- }
267
-
268
- case "tool_message_start":
269
- updateToolCall(ev.partial, state.tuiMessages);
270
- break;
271
- case "tool_message_update":
272
- updateToolCall(ev.partial, state.tuiMessages);
273
- break;
274
- case "tool_message_end": {
275
- updateToolCall(ev.message, state.tuiMessages);
276
- const withReminder = insertToolUsageReminder(
277
- state.messages,
278
- ev.message,
279
- );
280
-
281
- const idx = state.messages.findIndex(
282
- (m) =>
283
- m.role === "toolResult" &&
284
- m.toolCallId === withReminder.toolCallId,
285
- );
286
- if (idx >= 0) {
287
- state.messages[idx] = withReminder;
288
- }
289
-
290
- const { systemPrompt, tools, messages } = ctx;
291
- state.contextSize = estimateTokens(
292
- JSON.stringify({ systemPrompt, tools, messages }),
293
- );
294
- }
295
- }
296
- }
297
- } catch (error) {
298
- const text = error instanceof Error ? error.message : String(error);
299
- state.tuiMessages.push({
300
- timestamp: formatTimestamp(Date.now()),
301
- role: "assistant",
302
- text,
303
- });
304
- } finally {
305
- state.streaming = false;
306
- if (!state.sessionId) {
307
- const id = secureRandomString(10);
308
- state.sessionId = id;
309
- }
310
- await updateSession(state.sessionId, state.messages);
311
- }
312
-
313
- state.gitBranch = await getBranchLabel();
314
- }
package/src/types.ts DELETED
@@ -1,194 +0,0 @@
1
- import {
2
- type Api,
3
- type AssistantMessage,
4
- type Message,
5
- type Model,
6
- type Static,
7
- type ThinkingLevel,
8
- type Tool,
9
- type ToolResultMessage,
10
- Type,
11
- } from "@earendil-works/pi-ai";
12
- import type { OAuthCredentials } from "@earendil-works/pi-ai/oauth";
13
- import { TUI_THEME_IDS, type TUIThemeId } from "./themes";
14
-
15
- const ThinkingLevelSchema = Type.Unsafe<ThinkingLevel>(
16
- Type.Union([
17
- Type.Literal("minimal"),
18
- Type.Literal("low"),
19
- Type.Literal("medium"),
20
- Type.Literal("high"),
21
- Type.Literal("xhigh"),
22
- ]),
23
- );
24
-
25
- const ModelSchema = Type.Unsafe<Model<Api>>(
26
- Type.Object({
27
- id: Type.String(),
28
- name: Type.String(),
29
- api: Type.String(),
30
- provider: Type.String(),
31
- baseUrl: Type.String(),
32
- reasoning: Type.Boolean(),
33
- input: Type.Array(
34
- Type.Union([Type.Literal("text"), Type.Literal("image")]),
35
- ),
36
- cost: Type.Object({
37
- input: Type.Number(),
38
- output: Type.Number(),
39
- cacheRead: Type.Number(),
40
- cacheWrite: Type.Number(),
41
- }),
42
- contextWindow: Type.Number(),
43
- maxTokens: Type.Number(),
44
- headers: Type.Optional(Type.Record(Type.String(), Type.String())),
45
- compat: Type.Optional(Type.Unknown()),
46
- }),
47
- );
48
-
49
- const TUIThemeIdSchema = Type.Unsafe<TUIThemeId>(
50
- Type.Union(TUI_THEME_IDS.map((id) => Type.Literal(id))),
51
- );
52
-
53
- export const SettingsSchema = Type.Object({
54
- provider: Type.String(),
55
- model: Type.String(),
56
- effort: ThinkingLevelSchema,
57
- customProviders: Type.Optional(Type.Array(ModelSchema)),
58
- theme: Type.Optional(TUIThemeIdSchema),
59
- });
60
- export type Settings = Static<typeof SettingsSchema>;
61
-
62
- export const CliOptionsSchema = Type.Object({
63
- provider: Type.String(),
64
- model: ModelSchema,
65
- effort: ThinkingLevelSchema,
66
- prompt: Type.Optional(Type.String()),
67
- customProviders: Type.Optional(Type.Array(ModelSchema)),
68
- theme: TUIThemeIdSchema,
69
- });
70
- export type CliOptions = Static<typeof CliOptionsSchema>;
71
-
72
- type SavedOAuthAuth = OAuthCredentials & {
73
- type: "oauth";
74
- };
75
- export type SavedOAuthCreds = Record<string, SavedOAuthAuth>;
76
-
77
- export const MessageSchema = Type.Unsafe<Message>({});
78
- export const SessionSchema = Type.Object({
79
- id: Type.String(),
80
- cwd: Type.String(),
81
- messages: Type.Array(MessageSchema),
82
- });
83
- export type Session = Static<typeof SessionSchema>;
84
- export type Sessions = Session[];
85
-
86
- export type TUIToolCall = {
87
- id: string;
88
- tool: string;
89
- args: Record<string, any>;
90
- output: string;
91
- };
92
-
93
- export type TUIMessage = {
94
- timestamp: string;
95
- role: "user" | "assistant";
96
- text: string;
97
- thinking?: string;
98
- toolCalls?: TUIToolCall[];
99
- };
100
-
101
- export type AvailableUpdate = {
102
- currentVersion: string;
103
- latestVersion: string;
104
- };
105
-
106
- export type TUIState = {
107
- options: CliOptions;
108
- prompt: string;
109
- messages: Message[]; // Context messages
110
- tuiMessages: TUIMessage[];
111
- contextSize?: number;
112
- stickToBottom: boolean;
113
- scrollOffset: number;
114
- streaming: boolean;
115
- abortController?: AbortController;
116
- cwd: string;
117
- gitBranch?: string;
118
- availableUpdate?: AvailableUpdate | undefined;
119
- overlay?: boolean | undefined;
120
- forceThemeRefresh?: boolean | undefined;
121
- sessionId?: string | undefined;
122
- };
123
-
124
- export type AgentContex = {
125
- systemPrompt: string;
126
- tools: ToolAndRunner[];
127
- messages: Message[];
128
- options: CliOptions;
129
- signal?: AbortSignal | undefined;
130
- };
131
-
132
- export type AgentEvent =
133
- | {
134
- type: "message_start" | "message_update";
135
- partial: AssistantMessage;
136
- }
137
- | {
138
- type: "message_end";
139
- message: AssistantMessage;
140
- }
141
- | {
142
- type: "tool_message_start" | "tool_message_update";
143
- partial: ToolResultMessage;
144
- }
145
- | {
146
- type: "tool_message_end";
147
- message: ToolResultMessage;
148
- };
149
-
150
- export type AgentToolEvent =
151
- | {
152
- type: "tool_update";
153
- partial: ToolResultMessage;
154
- }
155
- | {
156
- type: "tool_result";
157
- message: ToolResultMessage;
158
- };
159
-
160
- export type ToolRunnerEvent =
161
- | { type: "output"; text: string }
162
- | {
163
- type: "result";
164
- text: string;
165
- image?: { data: string; mimeType: string };
166
- };
167
-
168
- export type ToolAndRunner = {
169
- tool: Tool;
170
- runner: (
171
- args: Record<string, any>,
172
- signal?: AbortSignal,
173
- ) => AsyncGenerator<ToolRunnerEvent>;
174
- };
175
-
176
- export type SelectListItem = {
177
- label: string;
178
- value: string;
179
- };
180
-
181
- export type SelectState = {
182
- value: string;
183
- selected: string;
184
- label: string;
185
- list: SelectListItem[];
186
- };
187
-
188
- export type SelectOptions = {
189
- filter: string;
190
- list: { label: string; value: string }[];
191
- label?: string | undefined;
192
- onSelect: (s: SelectState) => void | Promise<void>;
193
- onCancel: () => void;
194
- };
package/src/update.ts DELETED
@@ -1,171 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- import { type Static, Type } from "@earendil-works/pi-ai";
5
- import { Value } from "typebox/value";
6
-
7
- import { DATA_DIR } from "./shared";
8
- import type { AvailableUpdate } from "./types";
9
-
10
- const PACKAGE_NAME = "mini-coder";
11
- const PACKAGE_MANIFEST_URL = new URL("../package.json", import.meta.url);
12
- const UPDATE_CHECK_CACHE_PATH = join(DATA_DIR, "update-check.json");
13
- const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
14
- const UPDATE_CHECK_TIMEOUT_MS = 5_000;
15
-
16
- const UpdateCheckCacheSchema = Type.Object({
17
- checkedAt: Type.Number(),
18
- currentVersion: Type.String(),
19
- latestVersion: Type.Optional(Type.String()),
20
- });
21
- type UpdateCheckCache = Static<typeof UpdateCheckCacheSchema>;
22
-
23
- function parseLatestVersion(output: string): string | undefined {
24
- const version = output.trim().split(/\s+/)[0];
25
-
26
- if (!version || !isValidVersion(version)) {
27
- return;
28
- }
29
-
30
- return version;
31
- }
32
-
33
- function isValidVersion(version: string): boolean {
34
- try {
35
- Bun.semver.order(version, version);
36
- return true;
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
- function isNewerVersion(
43
- currentVersion: string,
44
- latestVersion: string,
45
- ): boolean {
46
- try {
47
- return Bun.semver.order(latestVersion, currentVersion) === 1;
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- function isFreshUpdateCheck(cache: UpdateCheckCache, now: number): boolean {
54
- return now - cache.checkedAt < UPDATE_CHECK_INTERVAL_MS;
55
- }
56
-
57
- function getAvailableUpdateFromLatest(
58
- currentVersion: string,
59
- latestVersion: string | undefined,
60
- ): AvailableUpdate | undefined {
61
- if (!latestVersion || !isNewerVersion(currentVersion, latestVersion)) {
62
- return;
63
- }
64
-
65
- return { currentVersion, latestVersion };
66
- }
67
-
68
- async function getCurrentVersion(): Promise<string | undefined> {
69
- const manifest = (await Bun.file(PACKAGE_MANIFEST_URL).json()) as {
70
- version?: unknown;
71
- };
72
-
73
- if (typeof manifest.version !== "string") {
74
- return;
75
- }
76
-
77
- return manifest.version;
78
- }
79
-
80
- async function getLatestVersion(): Promise<string | undefined> {
81
- try {
82
- const proc = Bun.spawn(["bun", "pm", "view", PACKAGE_NAME, "version"], {
83
- stdout: "pipe",
84
- stderr: "ignore",
85
- timeout: UPDATE_CHECK_TIMEOUT_MS,
86
- });
87
- const [stdout, exitCode] = await Promise.all([
88
- proc.stdout.text(),
89
- proc.exited,
90
- ]);
91
-
92
- if (exitCode !== 0) {
93
- return;
94
- }
95
-
96
- return parseLatestVersion(stdout);
97
- } catch {
98
- return;
99
- }
100
- }
101
-
102
- async function readUpdateCheckCache(): Promise<UpdateCheckCache | undefined> {
103
- try {
104
- const file = Bun.file(UPDATE_CHECK_CACHE_PATH);
105
-
106
- if (!(await file.exists())) {
107
- return;
108
- }
109
-
110
- const value = (await file.json()) as unknown;
111
-
112
- if (!Value.Check(UpdateCheckCacheSchema, value)) {
113
- return;
114
- }
115
-
116
- return value;
117
- } catch {
118
- return;
119
- }
120
- }
121
-
122
- async function writeUpdateCheckCache(cache: UpdateCheckCache): Promise<void> {
123
- try {
124
- await mkdir(DATA_DIR, { recursive: true });
125
- await Bun.write(UPDATE_CHECK_CACHE_PATH, JSON.stringify(cache, null, 2));
126
- } catch {
127
- // Update checks are best-effort and must never interrupt startup.
128
- }
129
- }
130
-
131
- export async function getAvailableUpdate(): Promise<
132
- AvailableUpdate | undefined
133
- > {
134
- const currentVersion = await getCurrentVersion().catch(() => undefined);
135
-
136
- if (!currentVersion) {
137
- return;
138
- }
139
-
140
- const now = Date.now();
141
- const cache = await readUpdateCheckCache();
142
-
143
- if (cache && isFreshUpdateCheck(cache, now)) {
144
- return getAvailableUpdateFromLatest(currentVersion, cache.latestVersion);
145
- }
146
-
147
- const latestVersion = await getLatestVersion();
148
- await writeUpdateCheckCache({
149
- checkedAt: now,
150
- currentVersion,
151
- latestVersion,
152
- });
153
-
154
- return getAvailableUpdateFromLatest(currentVersion, latestVersion);
155
- }
156
-
157
- export async function updateMiniCoder(): Promise<void> {
158
- console.log("Updating mini-coder...");
159
-
160
- const proc = Bun.spawn(["bun", "add", "-g", "mini-coder@latest"], {
161
- stdout: "inherit",
162
- stderr: "inherit",
163
- });
164
- const exitCode = await proc.exited;
165
-
166
- if (exitCode !== 0) {
167
- throw new Error(`Update failed with exit code ${exitCode}`);
168
- }
169
-
170
- console.log("mini-coder updated.");
171
- }