opencode-miniterm 1.0.13 → 1.0.15
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/bun.lock +1 -1
- package/package.json +2 -2
- package/src/ansi.ts +1 -0
- package/src/commands/agents.ts +5 -5
- package/src/commands/debug.ts +3 -4
- package/src/commands/details.ts +2 -4
- package/src/commands/diff.ts +3 -5
- package/src/commands/exit.ts +3 -6
- package/src/commands/init.ts +4 -7
- package/src/commands/log.ts +2 -8
- package/src/commands/models.ts +5 -6
- package/src/commands/new.ts +8 -11
- package/src/commands/page.ts +3 -5
- package/src/commands/quit.ts +3 -15
- package/src/commands/run.ts +2 -4
- package/src/commands/sessions.ts +11 -13
- package/src/commands/undo.ts +5 -7
- package/src/index.ts +19 -975
- package/src/input.ts +450 -0
- package/src/logs.ts +59 -0
- package/src/render.ts +23 -20
- package/src/server.ts +399 -0
- package/src/types.ts +23 -4
- package/src/utils.ts +17 -0
- package/test/render.test.ts +4 -2
package/src/index.ts
CHANGED
|
@@ -1,89 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { mkdir } from "node:fs/promises";
|
|
6
|
-
import { glob } from "node:fs/promises";
|
|
7
|
-
import { stat } from "node:fs/promises";
|
|
8
|
-
import { open } from "node:fs/promises";
|
|
9
|
-
import readline, { type Key } from "node:readline";
|
|
3
|
+
import { createOpencodeServer } from "@opencode-ai/sdk";
|
|
4
|
+
import readline from "node:readline";
|
|
10
5
|
import * as ansi from "./ansi";
|
|
11
|
-
import agentsCommand from "./commands/agents";
|
|
12
|
-
import debugCommand from "./commands/debug";
|
|
13
|
-
import detailsCommand from "./commands/details";
|
|
14
|
-
import diffCommand from "./commands/diff";
|
|
15
|
-
import exitCommand from "./commands/exit";
|
|
16
|
-
import initCommand from "./commands/init";
|
|
17
|
-
import logCommand, { isLoggingEnabled } from "./commands/log";
|
|
18
|
-
import modelsCommand from "./commands/models";
|
|
19
|
-
import newCommand from "./commands/new";
|
|
20
|
-
import pageCommand from "./commands/page";
|
|
21
|
-
import quitCommand from "./commands/quit";
|
|
22
|
-
import runCommand from "./commands/run";
|
|
23
|
-
import sessionsCommand from "./commands/sessions";
|
|
24
|
-
import undoCommand from "./commands/undo";
|
|
25
6
|
import { config, loadConfig, saveConfig } from "./config";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const AUTH_PASSWORD = process.env.OPENCODE_SERVER_PASSWORD || "";
|
|
31
|
-
|
|
32
|
-
const SLASH_COMMANDS = [
|
|
33
|
-
initCommand,
|
|
34
|
-
agentsCommand,
|
|
35
|
-
modelsCommand,
|
|
36
|
-
sessionsCommand,
|
|
37
|
-
newCommand,
|
|
38
|
-
undoCommand,
|
|
39
|
-
detailsCommand,
|
|
40
|
-
diffCommand,
|
|
41
|
-
debugCommand,
|
|
42
|
-
logCommand,
|
|
43
|
-
pageCommand,
|
|
44
|
-
exitCommand,
|
|
45
|
-
quitCommand,
|
|
46
|
-
runCommand,
|
|
47
|
-
];
|
|
7
|
+
import { handleKeyPress, loadSessionHistory } from "./input";
|
|
8
|
+
import { getActiveDisplay, updateSessionTitle, writePrompt } from "./render";
|
|
9
|
+
import { createClient, createSession, startEventListener, validateSession } from "./server";
|
|
10
|
+
import type { State } from "./types";
|
|
48
11
|
|
|
49
12
|
let server: Awaited<ReturnType<typeof createOpencodeServer>> | undefined;
|
|
50
|
-
let client: ReturnType<typeof createOpencodeClient>;
|
|
51
|
-
|
|
52
|
-
let processing = true;
|
|
53
|
-
let retryInterval: ReturnType<typeof setInterval> | null = null;
|
|
54
|
-
let isRequestActive = false;
|
|
55
|
-
|
|
56
|
-
interface AccumulatedPart {
|
|
57
|
-
key: string;
|
|
58
|
-
title: "thinking" | "response" | "tool" | "files" | "todo";
|
|
59
|
-
text: string;
|
|
60
|
-
active?: boolean;
|
|
61
|
-
durationMs?: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface State {
|
|
65
|
-
sessionID: string;
|
|
66
|
-
renderedLines: string[];
|
|
67
|
-
accumulatedResponse: AccumulatedPart[];
|
|
68
|
-
allEvents: Event[];
|
|
69
|
-
write: (text: string) => void;
|
|
70
|
-
lastFileAfter: Map<string, string>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export { updateSessionTitle, setTerminalTitle };
|
|
74
13
|
|
|
75
14
|
let state: State = {
|
|
15
|
+
// @ts-ignore This will get set
|
|
16
|
+
client: null,
|
|
76
17
|
sessionID: "",
|
|
77
18
|
renderedLines: [],
|
|
78
19
|
accumulatedResponse: [],
|
|
79
20
|
allEvents: [],
|
|
80
|
-
write: (text) => process.stdout.write(text),
|
|
81
21
|
lastFileAfter: new Map(),
|
|
22
|
+
write: (text) => process.stdout.write(text),
|
|
23
|
+
shutdown,
|
|
82
24
|
};
|
|
83
25
|
|
|
84
|
-
let logFile: Awaited<ReturnType<typeof open>> | null = null;
|
|
85
|
-
let logFilePath: string | null = null;
|
|
86
|
-
|
|
87
26
|
// ====================
|
|
88
27
|
// MAIN ENTRY POINT
|
|
89
28
|
// ====================
|
|
@@ -101,15 +40,7 @@ async function main() {
|
|
|
101
40
|
}
|
|
102
41
|
|
|
103
42
|
const cwd = process.cwd();
|
|
104
|
-
client =
|
|
105
|
-
baseUrl: SERVER_URL,
|
|
106
|
-
headers: AUTH_PASSWORD
|
|
107
|
-
? {
|
|
108
|
-
Authorization: `Basic ${Buffer.from(`${AUTH_USERNAME}:${AUTH_PASSWORD}`).toString("base64")}`,
|
|
109
|
-
}
|
|
110
|
-
: undefined,
|
|
111
|
-
directory: cwd,
|
|
112
|
-
});
|
|
43
|
+
state.client = createClient(cwd);
|
|
113
44
|
|
|
114
45
|
process.on("SIGINT", () => {
|
|
115
46
|
process.stdout.write("\n");
|
|
@@ -120,8 +51,8 @@ async function main() {
|
|
|
120
51
|
let isNewSession = false;
|
|
121
52
|
|
|
122
53
|
const initialSessionID = config.sessionIDs[cwd];
|
|
123
|
-
if (!initialSessionID || !(await validateSession(initialSessionID))) {
|
|
124
|
-
state.sessionID = await createSession();
|
|
54
|
+
if (!initialSessionID || !(await validateSession(state, initialSessionID))) {
|
|
55
|
+
state.sessionID = await createSession(state);
|
|
125
56
|
isNewSession = true;
|
|
126
57
|
config.sessionIDs[cwd] = state.sessionID;
|
|
127
58
|
saveConfig();
|
|
@@ -129,15 +60,15 @@ async function main() {
|
|
|
129
60
|
state.sessionID = initialSessionID;
|
|
130
61
|
}
|
|
131
62
|
|
|
132
|
-
startEventListener();
|
|
63
|
+
startEventListener(state);
|
|
133
64
|
|
|
134
|
-
await updateSessionTitle();
|
|
65
|
+
await updateSessionTitle(state);
|
|
135
66
|
|
|
136
|
-
|
|
67
|
+
await loadSessionHistory(state);
|
|
137
68
|
|
|
138
69
|
process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
|
|
139
70
|
process.stdout.write(ansi.CURSOR_HOME);
|
|
140
|
-
const activeDisplay = await getActiveDisplay(client);
|
|
71
|
+
const activeDisplay = await getActiveDisplay(state.client);
|
|
141
72
|
console.log(activeDisplay);
|
|
142
73
|
if (!isNewSession) {
|
|
143
74
|
console.log("Resumed last session");
|
|
@@ -145,7 +76,7 @@ async function main() {
|
|
|
145
76
|
console.log();
|
|
146
77
|
console.log(`${ansi.BRIGHT_BLACK}Ask anything...${ansi.RESET}\n`);
|
|
147
78
|
|
|
148
|
-
const
|
|
79
|
+
const _rl = readline.createInterface({
|
|
149
80
|
input: process.stdin,
|
|
150
81
|
output: undefined,
|
|
151
82
|
});
|
|
@@ -157,7 +88,7 @@ async function main() {
|
|
|
157
88
|
process.stdout.write(ansi.DISABLE_LINE_WRAP);
|
|
158
89
|
|
|
159
90
|
process.stdin.on("keypress", async (str, key) => {
|
|
160
|
-
handleKeyPress(str, key);
|
|
91
|
+
handleKeyPress(state, str, key);
|
|
161
92
|
});
|
|
162
93
|
|
|
163
94
|
writePrompt();
|
|
@@ -168,756 +99,6 @@ async function main() {
|
|
|
168
99
|
}
|
|
169
100
|
}
|
|
170
101
|
|
|
171
|
-
// ====================
|
|
172
|
-
// HANDLE INPUT
|
|
173
|
-
// ====================
|
|
174
|
-
|
|
175
|
-
let inputBuffer = "";
|
|
176
|
-
let cursorPosition = 0;
|
|
177
|
-
let completions: string[] = [];
|
|
178
|
-
let history: string[] = [];
|
|
179
|
-
let historyIndex = history.length;
|
|
180
|
-
let selectedCompletion = 0;
|
|
181
|
-
let completionCycling = false;
|
|
182
|
-
let lastSpaceTime = 0;
|
|
183
|
-
let currentInputBuffer: string | null = null;
|
|
184
|
-
|
|
185
|
-
let oldInputBuffer = "";
|
|
186
|
-
let oldWrappedRows = 0;
|
|
187
|
-
let oldCursorRow = 0;
|
|
188
|
-
function renderLine(): void {
|
|
189
|
-
const consoleWidth = process.stdout.columns || 80;
|
|
190
|
-
|
|
191
|
-
// Move to the start of the line (i.e. the prompt position)
|
|
192
|
-
readline.cursorTo(process.stdout, 0);
|
|
193
|
-
if (oldWrappedRows > 0) {
|
|
194
|
-
if (cursorPosition < inputBuffer.length) {
|
|
195
|
-
readline.moveCursor(process.stdout, 0, oldWrappedRows - oldCursorRow);
|
|
196
|
-
}
|
|
197
|
-
readline.moveCursor(process.stdout, 0, -oldWrappedRows);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Find the position where the input has changed (i.e. where the user has
|
|
201
|
-
// typed something)
|
|
202
|
-
let start = 0;
|
|
203
|
-
let currentCol = 2;
|
|
204
|
-
let newWrappedRows = 0;
|
|
205
|
-
for (let i = 0; i < Math.min(oldInputBuffer.length, inputBuffer.length); i++) {
|
|
206
|
-
if (oldInputBuffer[i] !== inputBuffer[i]) {
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
if (currentCol >= consoleWidth) {
|
|
210
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
211
|
-
currentCol = 0;
|
|
212
|
-
newWrappedRows++;
|
|
213
|
-
}
|
|
214
|
-
currentCol++;
|
|
215
|
-
start++;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Clear the old, changed, input
|
|
219
|
-
readline.moveCursor(process.stdout, currentCol, 0);
|
|
220
|
-
readline.clearScreenDown(process.stdout);
|
|
221
|
-
|
|
222
|
-
if (start === 0) {
|
|
223
|
-
writePrompt();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Write the changes from the new input buffer
|
|
227
|
-
let renderExtent = Math.max(cursorPosition + 1, inputBuffer.length);
|
|
228
|
-
for (let i = start; i < renderExtent; i++) {
|
|
229
|
-
if (currentCol >= consoleWidth) {
|
|
230
|
-
process.stdout.write("\n");
|
|
231
|
-
currentCol = 0;
|
|
232
|
-
newWrappedRows++;
|
|
233
|
-
}
|
|
234
|
-
if (i < inputBuffer.length) {
|
|
235
|
-
process.stdout.write(inputBuffer[i]!);
|
|
236
|
-
}
|
|
237
|
-
currentCol++;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Calculate and move to the cursor's position
|
|
241
|
-
let absolutePos = 2 + cursorPosition;
|
|
242
|
-
let newCursorRow = Math.floor(absolutePos / consoleWidth);
|
|
243
|
-
let newCursorCol = absolutePos % consoleWidth;
|
|
244
|
-
readline.cursorTo(process.stdout, 0);
|
|
245
|
-
readline.moveCursor(process.stdout, 0, -1 * (newWrappedRows - newCursorRow));
|
|
246
|
-
readline.cursorTo(process.stdout, newCursorCol);
|
|
247
|
-
|
|
248
|
-
oldInputBuffer = inputBuffer;
|
|
249
|
-
oldWrappedRows = newWrappedRows;
|
|
250
|
-
oldCursorRow = newCursorRow;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function handleKeyPress(str: string, key: Key) {
|
|
254
|
-
if (key.ctrl && key.name === "c") {
|
|
255
|
-
process.stdout.write("\n");
|
|
256
|
-
shutdown();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
for (let command of SLASH_COMMANDS) {
|
|
261
|
-
if (command.running && command.handleKey) {
|
|
262
|
-
await command.handleKey(client, key, str);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
switch (key.name) {
|
|
268
|
-
case "up": {
|
|
269
|
-
if (historyIndex === history.length) {
|
|
270
|
-
currentInputBuffer = inputBuffer;
|
|
271
|
-
}
|
|
272
|
-
if (history.length > 0) {
|
|
273
|
-
if (historyIndex > 0) {
|
|
274
|
-
historyIndex--;
|
|
275
|
-
inputBuffer = history[historyIndex]!;
|
|
276
|
-
} else {
|
|
277
|
-
historyIndex = Math.max(-1, historyIndex - 1);
|
|
278
|
-
inputBuffer = "";
|
|
279
|
-
}
|
|
280
|
-
cursorPosition = inputBuffer.length;
|
|
281
|
-
renderLine();
|
|
282
|
-
}
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
case "down": {
|
|
286
|
-
if (history.length > 0) {
|
|
287
|
-
if (historyIndex < history.length - 1) {
|
|
288
|
-
historyIndex++;
|
|
289
|
-
inputBuffer = history[historyIndex]!;
|
|
290
|
-
} else {
|
|
291
|
-
historyIndex = history.length;
|
|
292
|
-
inputBuffer = currentInputBuffer || "";
|
|
293
|
-
currentInputBuffer = null;
|
|
294
|
-
}
|
|
295
|
-
cursorPosition = inputBuffer.length;
|
|
296
|
-
renderLine();
|
|
297
|
-
}
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
case "tab": {
|
|
301
|
-
if (!completionCycling) {
|
|
302
|
-
await handleTab();
|
|
303
|
-
}
|
|
304
|
-
if (completionCycling && completions.length > 0) {
|
|
305
|
-
await handleTab();
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
case "escape": {
|
|
310
|
-
if (isRequestActive) {
|
|
311
|
-
if (state.sessionID) {
|
|
312
|
-
client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
|
|
313
|
-
}
|
|
314
|
-
stopAnimation();
|
|
315
|
-
process.stdout.write(ansi.CURSOR_SHOW);
|
|
316
|
-
process.stdout.write(`\r ${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
|
|
317
|
-
writePrompt();
|
|
318
|
-
isRequestActive = false;
|
|
319
|
-
} else {
|
|
320
|
-
inputBuffer = "";
|
|
321
|
-
cursorPosition = 0;
|
|
322
|
-
currentInputBuffer = null;
|
|
323
|
-
renderLine();
|
|
324
|
-
}
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
case "return": {
|
|
328
|
-
await acceptInput();
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
case "backspace": {
|
|
332
|
-
if (cursorPosition > 0) {
|
|
333
|
-
inputBuffer = inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
|
|
334
|
-
cursorPosition--;
|
|
335
|
-
currentInputBuffer = null;
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
case "delete": {
|
|
340
|
-
if (cursorPosition < inputBuffer.length) {
|
|
341
|
-
inputBuffer = inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
|
|
342
|
-
currentInputBuffer = null;
|
|
343
|
-
}
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
case "left": {
|
|
347
|
-
if (key.meta) {
|
|
348
|
-
cursorPosition = findPreviousWordBoundary(inputBuffer, cursorPosition);
|
|
349
|
-
} else if (cursorPosition > 0) {
|
|
350
|
-
cursorPosition--;
|
|
351
|
-
}
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
case "right": {
|
|
355
|
-
if (key.meta) {
|
|
356
|
-
cursorPosition = findNextWordBoundary(inputBuffer, cursorPosition);
|
|
357
|
-
} else if (cursorPosition < inputBuffer.length) {
|
|
358
|
-
cursorPosition++;
|
|
359
|
-
}
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
default: {
|
|
363
|
-
if (str === " ") {
|
|
364
|
-
const now = Date.now();
|
|
365
|
-
if (
|
|
366
|
-
now - lastSpaceTime < 500 &&
|
|
367
|
-
cursorPosition > 0 &&
|
|
368
|
-
inputBuffer[cursorPosition - 1] === " "
|
|
369
|
-
) {
|
|
370
|
-
inputBuffer =
|
|
371
|
-
inputBuffer.slice(0, cursorPosition - 1) + ". " + inputBuffer.slice(cursorPosition);
|
|
372
|
-
cursorPosition += 1;
|
|
373
|
-
} else {
|
|
374
|
-
inputBuffer =
|
|
375
|
-
inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
|
|
376
|
-
cursorPosition += str.length;
|
|
377
|
-
}
|
|
378
|
-
lastSpaceTime = now;
|
|
379
|
-
} else if (str) {
|
|
380
|
-
inputBuffer =
|
|
381
|
-
inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
|
|
382
|
-
cursorPosition += str.length;
|
|
383
|
-
}
|
|
384
|
-
currentInputBuffer = null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
completionCycling = false;
|
|
389
|
-
completions = [];
|
|
390
|
-
renderLine();
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function handleTab(): Promise<void> {
|
|
394
|
-
const potentialCompletions = await getCompletions(inputBuffer);
|
|
395
|
-
|
|
396
|
-
if (potentialCompletions.length === 0) {
|
|
397
|
-
completionCycling = false;
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (!completionCycling) {
|
|
402
|
-
completions = potentialCompletions;
|
|
403
|
-
selectedCompletion = 0;
|
|
404
|
-
completionCycling = true;
|
|
405
|
-
inputBuffer = completions[0]!;
|
|
406
|
-
cursorPosition = inputBuffer.length;
|
|
407
|
-
renderLine();
|
|
408
|
-
} else {
|
|
409
|
-
selectedCompletion = (selectedCompletion + 1) % completions.length;
|
|
410
|
-
inputBuffer = completions[selectedCompletion]!;
|
|
411
|
-
cursorPosition = inputBuffer.length;
|
|
412
|
-
renderLine();
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async function getCompletions(text: string): Promise<string[]> {
|
|
417
|
-
if (text.startsWith("/")) {
|
|
418
|
-
return ["/help", ...SLASH_COMMANDS.map((c) => c.name)].filter((cmd) => cmd.startsWith(text));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const atMatch = text.match(/(@[^\s]*)$/);
|
|
422
|
-
if (atMatch) {
|
|
423
|
-
const prefix = atMatch[0]!;
|
|
424
|
-
const searchPattern = prefix.slice(1);
|
|
425
|
-
const pattern = searchPattern.includes("/") ? searchPattern + "*" : "**/" + searchPattern + "*";
|
|
426
|
-
const files = await getFileCompletions(pattern);
|
|
427
|
-
return files.map((file: string) => text.replace(/@[^\s]*$/, "@" + file));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return [];
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async function acceptInput(): Promise<void> {
|
|
434
|
-
process.stdout.write("\n");
|
|
435
|
-
|
|
436
|
-
const input = inputBuffer.trim();
|
|
437
|
-
|
|
438
|
-
inputBuffer = "";
|
|
439
|
-
cursorPosition = 0;
|
|
440
|
-
completionCycling = false;
|
|
441
|
-
completions = [];
|
|
442
|
-
currentInputBuffer = null;
|
|
443
|
-
|
|
444
|
-
if (input) {
|
|
445
|
-
if (history[history.length - 1] !== input) {
|
|
446
|
-
history.push(input);
|
|
447
|
-
}
|
|
448
|
-
historyIndex = history.length;
|
|
449
|
-
try {
|
|
450
|
-
if (input === "/help") {
|
|
451
|
-
process.stdout.write("\n");
|
|
452
|
-
const maxCommandLength = Math.max(...SLASH_COMMANDS.map((c) => c.name.length));
|
|
453
|
-
for (const cmd of SLASH_COMMANDS) {
|
|
454
|
-
const padding = " ".repeat(maxCommandLength - cmd.name.length + 2);
|
|
455
|
-
console.log(
|
|
456
|
-
` ${ansi.BRIGHT_WHITE}${cmd.name}${ansi.RESET}${padding}${ansi.BRIGHT_BLACK}${cmd.description}${ansi.RESET}`,
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
console.log();
|
|
460
|
-
writePrompt();
|
|
461
|
-
return;
|
|
462
|
-
} else if (input.startsWith("/")) {
|
|
463
|
-
const parts = input.match(/(\/[^\s]+)\s*(.*)/)!;
|
|
464
|
-
if (parts) {
|
|
465
|
-
const commandName = parts[1];
|
|
466
|
-
const extra = parts[2]?.trim();
|
|
467
|
-
for (let command of SLASH_COMMANDS) {
|
|
468
|
-
if (command.name === commandName) {
|
|
469
|
-
process.stdout.write("\n");
|
|
470
|
-
await command.run(client, state, extra);
|
|
471
|
-
writePrompt();
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
isRequestActive = true;
|
|
480
|
-
process.stdout.write("\n");
|
|
481
|
-
process.stdout.write(ansi.CURSOR_HIDE);
|
|
482
|
-
startAnimation();
|
|
483
|
-
if (isLoggingEnabled()) {
|
|
484
|
-
console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
|
|
485
|
-
}
|
|
486
|
-
await sendMessage(state.sessionID, input);
|
|
487
|
-
isRequestActive = false;
|
|
488
|
-
} catch (error: any) {
|
|
489
|
-
isRequestActive = false;
|
|
490
|
-
if (error.message !== "Request cancelled") {
|
|
491
|
-
stopAnimation();
|
|
492
|
-
console.error("Error:", error.message);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// ====================
|
|
499
|
-
// SERVER COMMUNICATION
|
|
500
|
-
// ====================
|
|
501
|
-
|
|
502
|
-
async function createSession(): Promise<string> {
|
|
503
|
-
const result = await client.session.create({
|
|
504
|
-
body: {},
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
if (result.error) {
|
|
508
|
-
if (result.response.status === 401 && !AUTH_PASSWORD) {
|
|
509
|
-
throw new Error(
|
|
510
|
-
"Server requires authentication. Set OPENCODE_SERVER_PASSWORD environment variable.",
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
throw new Error(
|
|
514
|
-
`Failed to create session (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return result.data.id;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function validateSession(sessionID: string): Promise<boolean> {
|
|
522
|
-
try {
|
|
523
|
-
const result = await client.session.get({
|
|
524
|
-
path: { id: sessionID },
|
|
525
|
-
});
|
|
526
|
-
return !result.error && result.response.status === 200;
|
|
527
|
-
} catch {
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async function updateSessionTitle(): Promise<void> {
|
|
533
|
-
try {
|
|
534
|
-
const result = await client.session.get({
|
|
535
|
-
path: { id: state.sessionID },
|
|
536
|
-
});
|
|
537
|
-
if (!result.error && result.data?.title) {
|
|
538
|
-
setTerminalTitle(result.data.title);
|
|
539
|
-
} else {
|
|
540
|
-
setTerminalTitle(state.sessionID.substring(0, 8));
|
|
541
|
-
}
|
|
542
|
-
} catch {
|
|
543
|
-
setTerminalTitle(state.sessionID.substring(0, 8));
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
async function loadSessionHistory(): Promise<string[]> {
|
|
548
|
-
try {
|
|
549
|
-
const result = await client.session.messages({
|
|
550
|
-
path: { id: state.sessionID },
|
|
551
|
-
});
|
|
552
|
-
if (result.error || !result.data) {
|
|
553
|
-
return [];
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const history: string[] = [];
|
|
557
|
-
for (const msg of result.data) {
|
|
558
|
-
if (msg.info.role === "user") {
|
|
559
|
-
const textParts = msg.parts
|
|
560
|
-
.filter((p: Part) => p.type === "text")
|
|
561
|
-
.map((p: Part) => (p as any).text || "")
|
|
562
|
-
.filter(Boolean);
|
|
563
|
-
const text = textParts.join("").trim();
|
|
564
|
-
if (text && !text.startsWith("/")) {
|
|
565
|
-
history.push(text);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return history;
|
|
570
|
-
} catch {
|
|
571
|
-
return [];
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function startEventListener(): Promise<void> {
|
|
576
|
-
try {
|
|
577
|
-
const { stream } = await client.event.subscribe({
|
|
578
|
-
onSseError: (error) => {
|
|
579
|
-
console.error(
|
|
580
|
-
`\n${ansi.RED}Connection error:${ansi.RESET}`,
|
|
581
|
-
error instanceof Error ? error.message : String(error),
|
|
582
|
-
);
|
|
583
|
-
},
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
for await (const event of stream) {
|
|
587
|
-
try {
|
|
588
|
-
await processEvent(event);
|
|
589
|
-
} catch (error) {
|
|
590
|
-
console.error(
|
|
591
|
-
`\n${ansi.RED}Event processing error:${ansi.RESET}`,
|
|
592
|
-
error instanceof Error ? error.message : String(error),
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
} catch (error) {
|
|
597
|
-
console.error(
|
|
598
|
-
`\n${ansi.RED}Failed to connect to event stream:${ansi.RESET}`,
|
|
599
|
-
error instanceof Error ? error.message : String(error),
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async function sendMessage(sessionID: string, message: string) {
|
|
605
|
-
processing = false;
|
|
606
|
-
state.accumulatedResponse = [];
|
|
607
|
-
state.allEvents = [];
|
|
608
|
-
state.renderedLines = [];
|
|
609
|
-
|
|
610
|
-
await createLogFile();
|
|
611
|
-
|
|
612
|
-
await writeToLog(`User: ${message}\n\n`);
|
|
613
|
-
|
|
614
|
-
const requestStartTime = Date.now();
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
const result = await client.session.prompt({
|
|
618
|
-
path: { id: sessionID },
|
|
619
|
-
body: {
|
|
620
|
-
model: {
|
|
621
|
-
providerID: config.providerID,
|
|
622
|
-
modelID: config.modelID,
|
|
623
|
-
},
|
|
624
|
-
parts: [{ type: "text", text: message }],
|
|
625
|
-
},
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
if (result.error) {
|
|
629
|
-
throw new Error(
|
|
630
|
-
`Failed to send message (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Play a chime when request is completed
|
|
635
|
-
process.stdout.write("\x07");
|
|
636
|
-
|
|
637
|
-
stopAnimation();
|
|
638
|
-
|
|
639
|
-
const duration = Date.now() - requestStartTime;
|
|
640
|
-
const durationText = formatDuration(duration);
|
|
641
|
-
console.log(` ${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
|
|
642
|
-
|
|
643
|
-
writePrompt();
|
|
644
|
-
} catch (error: any) {
|
|
645
|
-
throw error;
|
|
646
|
-
} finally {
|
|
647
|
-
await closeLogFile();
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// ====================
|
|
652
|
-
// EVENT PROCESSING
|
|
653
|
-
// ====================
|
|
654
|
-
|
|
655
|
-
async function processEvent(event: Event): Promise<void> {
|
|
656
|
-
if (retryInterval && event.type !== "session.status") {
|
|
657
|
-
clearInterval(retryInterval);
|
|
658
|
-
retryInterval = null;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
state.allEvents.push(event);
|
|
662
|
-
|
|
663
|
-
switch (event.type) {
|
|
664
|
-
case "message.part.updated": {
|
|
665
|
-
const part = event.properties.part;
|
|
666
|
-
const delta = event.properties.delta;
|
|
667
|
-
if (part) {
|
|
668
|
-
await processPart(part);
|
|
669
|
-
}
|
|
670
|
-
if (delta !== undefined && part) {
|
|
671
|
-
processDelta(part.id, delta);
|
|
672
|
-
}
|
|
673
|
-
break;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// @ts-ignore this definitely exists
|
|
677
|
-
case "message.part.delta": {
|
|
678
|
-
// @ts-ignore
|
|
679
|
-
const partID = event.properties.partID;
|
|
680
|
-
// @ts-ignore
|
|
681
|
-
const delta = event.properties.delta;
|
|
682
|
-
if (partID !== undefined && delta !== undefined) {
|
|
683
|
-
processDelta(partID, delta);
|
|
684
|
-
}
|
|
685
|
-
break;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
case "session.diff": {
|
|
689
|
-
const diff = event.properties.diff;
|
|
690
|
-
if (diff && diff.length > 0) {
|
|
691
|
-
await processDiff(diff);
|
|
692
|
-
}
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
case "session.idle":
|
|
697
|
-
case "session.status":
|
|
698
|
-
if (event.type === "session.status" && event.properties.status.type === "idle") {
|
|
699
|
-
stopAnimation();
|
|
700
|
-
isRequestActive = false;
|
|
701
|
-
process.stdout.write(ansi.CURSOR_SHOW);
|
|
702
|
-
if (retryInterval) {
|
|
703
|
-
clearInterval(retryInterval);
|
|
704
|
-
retryInterval = null;
|
|
705
|
-
}
|
|
706
|
-
writePrompt();
|
|
707
|
-
}
|
|
708
|
-
if (event.type === "session.status" && event.properties.status.type === "retry") {
|
|
709
|
-
const message = event.properties.status.message;
|
|
710
|
-
const retryTime = event.properties.status.next;
|
|
711
|
-
const sessionID = event.properties.sessionID;
|
|
712
|
-
console.error(`\n\n ${ansi.RED}Error:${ansi.RESET} ${message}`);
|
|
713
|
-
console.error(` ${ansi.BRIGHT_BLACK}Session:${ansi.RESET} ${sessionID}`);
|
|
714
|
-
if (retryTime) {
|
|
715
|
-
if (retryInterval) {
|
|
716
|
-
clearInterval(retryInterval);
|
|
717
|
-
}
|
|
718
|
-
const retryDate = new Date(retryTime);
|
|
719
|
-
|
|
720
|
-
let lastSeconds = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000));
|
|
721
|
-
console.error(` ${ansi.BRIGHT_BLACK}Retrying in ${lastSeconds}s...${ansi.RESET}`);
|
|
722
|
-
|
|
723
|
-
retryInterval = setInterval(() => {
|
|
724
|
-
const remaining = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000));
|
|
725
|
-
if (remaining !== lastSeconds) {
|
|
726
|
-
process.stdout.write(
|
|
727
|
-
`\r ${ansi.BRIGHT_BLACK}Retrying in ${remaining}s...${ansi.RESET}`,
|
|
728
|
-
);
|
|
729
|
-
lastSeconds = remaining;
|
|
730
|
-
}
|
|
731
|
-
if (remaining === 0) {
|
|
732
|
-
if (retryInterval) {
|
|
733
|
-
clearInterval(retryInterval);
|
|
734
|
-
retryInterval = null;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}, 100);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
break;
|
|
741
|
-
|
|
742
|
-
case "session.updated": {
|
|
743
|
-
const session = event.properties.info;
|
|
744
|
-
if (session && session.id === state.sessionID && session.title) {
|
|
745
|
-
setTerminalTitle(session.title);
|
|
746
|
-
}
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
case "todo.updated": {
|
|
751
|
-
const todos = event.properties.todos;
|
|
752
|
-
if (todos) {
|
|
753
|
-
await processTodos(todos);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
default:
|
|
760
|
-
break;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
async function processPart(part: Part): Promise<void> {
|
|
765
|
-
switch (part.type) {
|
|
766
|
-
case "step-start":
|
|
767
|
-
processStepStart();
|
|
768
|
-
break;
|
|
769
|
-
|
|
770
|
-
case "reasoning":
|
|
771
|
-
processReasoning(part);
|
|
772
|
-
break;
|
|
773
|
-
|
|
774
|
-
case "text":
|
|
775
|
-
if (processing) {
|
|
776
|
-
processText(part);
|
|
777
|
-
}
|
|
778
|
-
break;
|
|
779
|
-
|
|
780
|
-
case "step-finish":
|
|
781
|
-
break;
|
|
782
|
-
|
|
783
|
-
case "tool":
|
|
784
|
-
processToolUse(part);
|
|
785
|
-
break;
|
|
786
|
-
|
|
787
|
-
default:
|
|
788
|
-
break;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function processStepStart() {
|
|
793
|
-
processing = true;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
async function processReasoning(part: Part) {
|
|
797
|
-
processing = true;
|
|
798
|
-
let thinkingPart = findLastPart(part.id);
|
|
799
|
-
if (!thinkingPart) {
|
|
800
|
-
thinkingPart = { key: part.id, title: "thinking", text: (part as any).text || "" };
|
|
801
|
-
state.accumulatedResponse.push(thinkingPart);
|
|
802
|
-
} else {
|
|
803
|
-
thinkingPart.text = (part as any).text || "";
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const text = (part as any).text || "";
|
|
807
|
-
const cleanText = ansi.stripAnsiCodes(text.trimStart());
|
|
808
|
-
await writeToLog(`Thinking:\n\n${cleanText}\n\n`);
|
|
809
|
-
|
|
810
|
-
render(state);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function processText(part: Part) {
|
|
814
|
-
let responsePart = findLastPart(part.id);
|
|
815
|
-
if (!responsePart) {
|
|
816
|
-
responsePart = { key: part.id, title: "response", text: (part as any).text || "" };
|
|
817
|
-
state.accumulatedResponse.push(responsePart);
|
|
818
|
-
} else {
|
|
819
|
-
responsePart.text = (part as any).text || "";
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const text = (part as any).text || "";
|
|
823
|
-
const cleanText = ansi.stripAnsiCodes(text.trimStart());
|
|
824
|
-
await writeToLog(`Response:\n\n${cleanText}\n\n`);
|
|
825
|
-
|
|
826
|
-
render(state);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
async function processToolUse(part: Part) {
|
|
830
|
-
const toolPart = part as ToolPart;
|
|
831
|
-
const toolName = toolPart.tool || "unknown";
|
|
832
|
-
const toolInput =
|
|
833
|
-
toolPart.state.input["description"] ||
|
|
834
|
-
toolPart.state.input["filePath"] ||
|
|
835
|
-
toolPart.state.input["path"] ||
|
|
836
|
-
toolPart.state.input["include"] ||
|
|
837
|
-
toolPart.state.input["pattern"] ||
|
|
838
|
-
// TODO: more state.input props?
|
|
839
|
-
"...";
|
|
840
|
-
const toolText = `$ ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
|
|
841
|
-
|
|
842
|
-
if (state.accumulatedResponse[state.accumulatedResponse.length - 1]?.title === "tool") {
|
|
843
|
-
state.accumulatedResponse[state.accumulatedResponse.length - 1]!.text = toolText;
|
|
844
|
-
} else {
|
|
845
|
-
state.accumulatedResponse.push({ key: part.id, title: "tool", text: toolText });
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const cleanToolText = ansi.stripAnsiCodes(toolText);
|
|
849
|
-
await writeToLog(`$ ${cleanToolText}\n\n`);
|
|
850
|
-
|
|
851
|
-
render(state);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function processDelta(partID: string, delta: string) {
|
|
855
|
-
let responsePart = findLastPart(partID);
|
|
856
|
-
if (responsePart) {
|
|
857
|
-
responsePart.text += delta;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
render(state);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
async function processDiff(diff: FileDiff[]) {
|
|
864
|
-
const parts: string[] = [];
|
|
865
|
-
for (const file of diff) {
|
|
866
|
-
const newAfter = file.after ?? "";
|
|
867
|
-
const oldAfter = state.lastFileAfter.get(file.file);
|
|
868
|
-
if (newAfter !== oldAfter) {
|
|
869
|
-
const statusIcon = !file.before ? "A" : !file.after ? "D" : "M";
|
|
870
|
-
const addStr = file.additions > 0 ? `${ansi.GREEN}+${file.additions}${ansi.RESET}` : "";
|
|
871
|
-
const delStr = file.deletions > 0 ? `${ansi.RED}-${file.deletions}${ansi.RESET}` : "";
|
|
872
|
-
const stats = [addStr, delStr].filter(Boolean).join(" ");
|
|
873
|
-
const line = `${ansi.BLUE}${statusIcon}${ansi.RESET} ${file.file} ${stats}`;
|
|
874
|
-
parts.push(line);
|
|
875
|
-
|
|
876
|
-
state.lastFileAfter.set(file.file, newAfter);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (parts.length > 0) {
|
|
881
|
-
state.accumulatedResponse.push({ key: "diff", title: "files", text: parts.join("\n") });
|
|
882
|
-
|
|
883
|
-
const diffText = ansi.stripAnsiCodes(parts.join("\n"));
|
|
884
|
-
await writeToLog(`${diffText}\n\n`);
|
|
885
|
-
|
|
886
|
-
render(state);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
async function processTodos(todos: Todo[]) {
|
|
891
|
-
let todoListText = "Todo:\n";
|
|
892
|
-
|
|
893
|
-
for (let todo of todos) {
|
|
894
|
-
let todoText = "";
|
|
895
|
-
if (todo.status === "completed") {
|
|
896
|
-
todoText += "- [✓] ";
|
|
897
|
-
} else {
|
|
898
|
-
todoText += "- [ ] ";
|
|
899
|
-
}
|
|
900
|
-
todoText += todo.content;
|
|
901
|
-
todoListText += todoText + "\n";
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
state.accumulatedResponse.push({ key: "todo", title: "files", text: todoListText });
|
|
905
|
-
|
|
906
|
-
const cleanTodoText = ansi.stripAnsiCodes(todoListText);
|
|
907
|
-
await writeToLog(`${cleanTodoText}\n`);
|
|
908
|
-
|
|
909
|
-
render(state);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function findLastPart(title: string) {
|
|
913
|
-
for (let i = state.accumulatedResponse.length - 1; i >= 0; i--) {
|
|
914
|
-
const part = state.accumulatedResponse[i];
|
|
915
|
-
if (part?.key === title) {
|
|
916
|
-
return part;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
102
|
function shutdown() {
|
|
922
103
|
if (process.stdin.setRawMode) {
|
|
923
104
|
process.stdin.setRawMode(false);
|
|
@@ -930,141 +111,4 @@ function shutdown() {
|
|
|
930
111
|
process.exit(0);
|
|
931
112
|
}
|
|
932
113
|
|
|
933
|
-
// ====================
|
|
934
|
-
// USER INTERFACE
|
|
935
|
-
// ====================
|
|
936
|
-
|
|
937
|
-
function setTerminalTitle(sessionName: string): void {
|
|
938
|
-
process.stdout.write(`\x1b]0;OC | ${sessionName}\x07`);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function findPreviousWordBoundary(text: string, pos: number): number {
|
|
942
|
-
if (pos <= 0) return 0;
|
|
943
|
-
|
|
944
|
-
let newPos = pos;
|
|
945
|
-
|
|
946
|
-
while (newPos > 0 && /\s/.test(text[newPos - 1]!)) {
|
|
947
|
-
newPos--;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
while (newPos > 0 && !/\s/.test(text[newPos - 1]!)) {
|
|
951
|
-
newPos--;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
return newPos;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
function findNextWordBoundary(text: string, pos: number): number {
|
|
958
|
-
if (pos >= text.length) return text.length;
|
|
959
|
-
|
|
960
|
-
let newPos = pos;
|
|
961
|
-
|
|
962
|
-
while (newPos < text.length && !/\s/.test(text[newPos]!)) {
|
|
963
|
-
newPos++;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
while (newPos < text.length && /\s/.test(text[newPos]!)) {
|
|
967
|
-
newPos++;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
return newPos;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function formatDuration(ms: number): string {
|
|
974
|
-
if (ms < 1000) {
|
|
975
|
-
return `${ms}ms`;
|
|
976
|
-
}
|
|
977
|
-
const seconds = ms / 1000;
|
|
978
|
-
if (seconds < 60) {
|
|
979
|
-
return `${seconds.toFixed(1)}s`;
|
|
980
|
-
}
|
|
981
|
-
const minutes = Math.floor(seconds / 60);
|
|
982
|
-
const remainingSeconds = Math.round(seconds % 60);
|
|
983
|
-
if (minutes < 60) {
|
|
984
|
-
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
985
|
-
}
|
|
986
|
-
const hours = Math.floor(minutes / 60);
|
|
987
|
-
const remainingMinutes = minutes % 60;
|
|
988
|
-
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// ====================
|
|
992
|
-
// LOGGING
|
|
993
|
-
// ====================
|
|
994
|
-
|
|
995
|
-
export function getLogDir(): string {
|
|
996
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
997
|
-
return `${homeDir}/.local/share/opencode-miniterm/log`;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
async function createLogFile(): Promise<void> {
|
|
1001
|
-
if (!isLoggingEnabled()) {
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const logDir = getLogDir();
|
|
1006
|
-
await mkdir(logDir, { recursive: true });
|
|
1007
|
-
|
|
1008
|
-
const now = new Date();
|
|
1009
|
-
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1010
|
-
const filename = `${timestamp}.txt`;
|
|
1011
|
-
logFilePath = `${logDir}/${filename}`;
|
|
1012
|
-
|
|
1013
|
-
try {
|
|
1014
|
-
logFile = await open(logFilePath, "w");
|
|
1015
|
-
} catch (error) {
|
|
1016
|
-
console.error("Failed to create log file:", error);
|
|
1017
|
-
logFile = null;
|
|
1018
|
-
logFilePath = null;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
async function closeLogFile(): Promise<void> {
|
|
1023
|
-
if (logFile) {
|
|
1024
|
-
try {
|
|
1025
|
-
await logFile.close();
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
console.error("Failed to close log file:", error);
|
|
1028
|
-
}
|
|
1029
|
-
logFile = null;
|
|
1030
|
-
logFilePath = null;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
async function writeToLog(text: string): Promise<void> {
|
|
1035
|
-
if (logFile && isLoggingEnabled()) {
|
|
1036
|
-
try {
|
|
1037
|
-
await logFile.write(text);
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
console.error("Failed to write to log file:", error);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ====================
|
|
1045
|
-
// UTILITIES
|
|
1046
|
-
// ====================
|
|
1047
|
-
|
|
1048
|
-
async function getFileCompletions(pattern: string): Promise<string[]> {
|
|
1049
|
-
try {
|
|
1050
|
-
const files: string[] = [];
|
|
1051
|
-
for await (const file of glob(pattern)) {
|
|
1052
|
-
if (
|
|
1053
|
-
!file.startsWith("node_modules/") &&
|
|
1054
|
-
!file.startsWith(".git/") &&
|
|
1055
|
-
!file.startsWith("dist/") &&
|
|
1056
|
-
!file.startsWith("build/")
|
|
1057
|
-
) {
|
|
1058
|
-
const isDir = await stat(file)
|
|
1059
|
-
.then((s) => s.isDirectory())
|
|
1060
|
-
.catch(() => false);
|
|
1061
|
-
files.push(isDir ? file + "/" : file);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return files.sort();
|
|
1065
|
-
} catch {
|
|
1066
|
-
return [];
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
114
|
main().catch(console.error);
|