@oh-my-pi/pi-coding-agent 6.8.2 → 6.8.4
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/CHANGELOG.md +30 -0
- package/package.json +6 -6
- package/src/core/agent-session.ts +7 -18
- package/src/core/bash-executor.ts +7 -64
- package/src/core/custom-commands/loader.ts +0 -8
- package/src/core/extensions/types.ts +0 -3
- package/src/core/index.ts +1 -1
- package/src/core/keybindings.ts +10 -2
- package/src/core/python-executor.ts +15 -25
- package/src/core/ssh/ssh-executor.ts +8 -13
- package/src/core/streaming-output.ts +64 -150
- package/src/core/tools/bash.ts +10 -44
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/output.ts +7 -8
- package/src/core/tools/python.ts +6 -15
- package/src/core/tools/ssh.ts +6 -14
- package/src/core/tools/task/executor.ts +7 -16
- package/src/core/tools/task/index.ts +11 -12
- package/src/core/tools/task/types.ts +2 -1
- package/src/core/tools/todo-write.ts +2 -18
- package/src/core/tools/truncate.ts +92 -0
- package/src/core/tools/web-fetch.ts +6 -4
- package/src/core/tools/web-scrapers/utils.ts +7 -30
- package/src/core/tools/web-search/providers/anthropic.ts +0 -1
- package/src/index.ts +0 -1
- package/src/modes/interactive/components/todo-display.ts +1 -8
- package/src/modes/interactive/components/tree-selector.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +1 -15
- package/src/utils/clipboard.ts +27 -20
- package/src/core/tools/task/artifacts.ts +0 -112
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
4
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
@@ -13,7 +12,6 @@ import todoWriteDescription from "../../prompts/tools/todo-write.md" with { type
|
|
|
13
12
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
14
13
|
import { renderPromptTemplate } from "../prompt-templates";
|
|
15
14
|
import type { ToolSession } from "../sdk";
|
|
16
|
-
import { ensureArtifactsDir, getArtifactsDir } from "./task/artifacts";
|
|
17
15
|
|
|
18
16
|
const todoWriteSchema = Type.Object({
|
|
19
17
|
todos: Type.Array(
|
|
@@ -135,10 +133,6 @@ async function loadTodoFile(filePath: string): Promise<TodoFile | null> {
|
|
|
135
133
|
}
|
|
136
134
|
}
|
|
137
135
|
|
|
138
|
-
async function saveTodoFile(filePath: string, data: TodoFile): Promise<void> {
|
|
139
|
-
await Bun.write(filePath, JSON.stringify(data, null, 2));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
136
|
function formatTodoSummary(todos: TodoItem[]): string {
|
|
143
137
|
if (todos.length === 0) return "Todo list cleared.";
|
|
144
138
|
const completed = todos.filter((t) => t.status === "completed").length;
|
|
@@ -200,24 +194,14 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
200
194
|
};
|
|
201
195
|
}
|
|
202
196
|
|
|
203
|
-
const
|
|
204
|
-
if (!artifactsDir) {
|
|
205
|
-
return {
|
|
206
|
-
content: [{ type: "text", text: formatTodoSummary(todos) }],
|
|
207
|
-
details: { todos, updatedAt, storage: "memory" },
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
ensureArtifactsDir(artifactsDir);
|
|
212
|
-
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
|
|
197
|
+
const todoPath = path.join(sessionFile.slice(0, -6), TODO_FILE_NAME);
|
|
213
198
|
const existing = await loadTodoFile(todoPath);
|
|
214
199
|
const storedTodos = existing?.todos ?? [];
|
|
215
200
|
const merged = todos.length > 0 ? todos : [];
|
|
216
201
|
const fileData: TodoFile = { updatedAt, todos: merged };
|
|
217
202
|
|
|
218
203
|
try {
|
|
219
|
-
|
|
220
|
-
await saveTodoFile(todoPath, fileData);
|
|
204
|
+
await Bun.write(todoPath, JSON.stringify(fileData, null, 2));
|
|
221
205
|
} catch (error) {
|
|
222
206
|
logger.error("Failed to write todo file", { path: todoPath, error: String(error) });
|
|
223
207
|
return {
|
|
@@ -289,3 +289,95 @@ export function truncateLine(
|
|
|
289
289
|
}
|
|
290
290
|
return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
|
|
291
291
|
}
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// Truncation notice formatting
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
export interface TailTruncationNoticeOptions {
|
|
298
|
+
/** Path to full output file (e.g., from bash/python executor) */
|
|
299
|
+
fullOutputPath?: string;
|
|
300
|
+
/** Original content for computing last line size when lastLinePartial */
|
|
301
|
+
originalContent?: string;
|
|
302
|
+
/** Additional suffix to append inside the brackets */
|
|
303
|
+
suffix?: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Format a truncation notice for tail-truncated output (bash, python, ssh).
|
|
308
|
+
* Returns empty string if not truncated.
|
|
309
|
+
*
|
|
310
|
+
* Examples:
|
|
311
|
+
* - "[Showing last 50KB of line 1000 (line is 2.1MB). Full output: /tmp/out.txt]"
|
|
312
|
+
* - "[Showing lines 500-1000 of 1000. Full output: /tmp/out.txt]"
|
|
313
|
+
* - "[Showing lines 500-1000 of 1000 (50KB limit). Full output: /tmp/out.txt]"
|
|
314
|
+
*/
|
|
315
|
+
export function formatTailTruncationNotice(
|
|
316
|
+
truncation: TruncationResult,
|
|
317
|
+
options: TailTruncationNoticeOptions = {},
|
|
318
|
+
): string {
|
|
319
|
+
if (!truncation.truncated) {
|
|
320
|
+
return "";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { fullOutputPath, originalContent, suffix = "" } = options;
|
|
324
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
325
|
+
const endLine = truncation.totalLines;
|
|
326
|
+
const fullOutputPart = fullOutputPath ? `. Full output: ${fullOutputPath}` : "";
|
|
327
|
+
|
|
328
|
+
let notice: string;
|
|
329
|
+
|
|
330
|
+
if (truncation.lastLinePartial) {
|
|
331
|
+
let lastLineSizePart = "";
|
|
332
|
+
if (originalContent) {
|
|
333
|
+
const lastLine = originalContent.split("\n").pop() || "";
|
|
334
|
+
lastLineSizePart = ` (line is ${formatSize(Buffer.byteLength(lastLine, "utf-8"))})`;
|
|
335
|
+
}
|
|
336
|
+
notice = `[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine}${lastLineSizePart}${fullOutputPart}${suffix}]`;
|
|
337
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
338
|
+
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputPart}${suffix}]`;
|
|
339
|
+
} else {
|
|
340
|
+
notice = `[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(truncation.maxBytes)} limit)${fullOutputPart}${suffix}]`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return `\n\n${notice}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface HeadTruncationNoticeOptions {
|
|
347
|
+
/** 1-indexed start line number (default: 1) */
|
|
348
|
+
startLine?: number;
|
|
349
|
+
/** Total lines in the original file (for "of N" display) */
|
|
350
|
+
totalFileLines?: number;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Format a truncation notice for head-truncated output (read tool).
|
|
355
|
+
* Returns empty string if not truncated.
|
|
356
|
+
*
|
|
357
|
+
* Examples:
|
|
358
|
+
* - "[Showing lines 1-2000 of 5000. Use offset=2001 to continue]"
|
|
359
|
+
* - "[Showing lines 100-2099 of 5000 (50KB limit). Use offset=2100 to continue]"
|
|
360
|
+
*/
|
|
361
|
+
export function formatHeadTruncationNotice(
|
|
362
|
+
truncation: TruncationResult,
|
|
363
|
+
options: HeadTruncationNoticeOptions = {},
|
|
364
|
+
): string {
|
|
365
|
+
if (!truncation.truncated) {
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const startLineDisplay = options.startLine ?? 1;
|
|
370
|
+
const totalFileLines = options.totalFileLines ?? truncation.totalLines;
|
|
371
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
372
|
+
const nextOffset = endLineDisplay + 1;
|
|
373
|
+
|
|
374
|
+
let notice: string;
|
|
375
|
+
|
|
376
|
+
if (truncation.truncatedBy === "lines") {
|
|
377
|
+
notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
378
|
+
} else {
|
|
379
|
+
notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(truncation.maxBytes)} limit). Use offset=${nextOffset} to continue]`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return `\n\n${notice}`;
|
|
383
|
+
}
|
|
@@ -24,7 +24,9 @@ import { convertWithMarkitdown, fetchBinary } from "./web-scrapers/utils";
|
|
|
24
24
|
// Types and Constants
|
|
25
25
|
// =============================================================================
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const MIN_TIMEOUT = 1_000;
|
|
28
|
+
const DEFAULT_TIMEOUT = 20_000;
|
|
29
|
+
const MAX_TIMEOUT = 45_000;
|
|
28
30
|
|
|
29
31
|
// Convertible document types (markitdown supported)
|
|
30
32
|
const CONVERTIBLE_MIMES = new Set([
|
|
@@ -109,7 +111,7 @@ async function exec(
|
|
|
109
111
|
): Promise<{ stdout: string; stderr: string; ok: boolean }> {
|
|
110
112
|
const proc = ptree.cspawn([cmd, ...args], {
|
|
111
113
|
stdin: options?.input ? "pipe" : null,
|
|
112
|
-
timeout: options?.timeout,
|
|
114
|
+
timeout: options?.timeout ? options.timeout * 1000 : undefined,
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
if (options?.input) {
|
|
@@ -244,7 +246,7 @@ async function tryMdSuffix(url: string, timeout: number, signal?: AbortSignal):
|
|
|
244
246
|
if (signal?.aborted) {
|
|
245
247
|
return null;
|
|
246
248
|
}
|
|
247
|
-
const result = await loadPage(candidate, { timeout: Math.min(timeout,
|
|
249
|
+
const result = await loadPage(candidate, { timeout: Math.min(timeout, MAX_TIMEOUT), signal });
|
|
248
250
|
if (result.ok && result.content.trim().length > 100 && !looksLikeHtml(result.content)) {
|
|
249
251
|
return result.content;
|
|
250
252
|
}
|
|
@@ -910,7 +912,7 @@ export class WebFetchTool implements AgentTool<typeof webFetchSchema, WebFetchTo
|
|
|
910
912
|
}
|
|
911
913
|
|
|
912
914
|
// Clamp timeout
|
|
913
|
-
const effectiveTimeout = Math.min(Math.max(timeout,
|
|
915
|
+
const effectiveTimeout = Math.min(Math.max(timeout, MIN_TIMEOUT), MAX_TIMEOUT);
|
|
914
916
|
|
|
915
917
|
const result = await renderUrl(url, effectiveTimeout, raw, signal);
|
|
916
918
|
|
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { ptree } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { ensureTool } from "../../../utils/tools-manager";
|
|
7
7
|
import { createRequestSignal } from "./types";
|
|
8
8
|
|
|
9
9
|
const MAX_BYTES = 50 * 1024 * 1024; // 50MB for binary files
|
|
10
10
|
|
|
11
|
-
interface ExecResult {
|
|
12
|
-
stdout: string;
|
|
13
|
-
stderr: string;
|
|
14
|
-
ok: boolean;
|
|
15
|
-
exitCode: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function exec(
|
|
19
|
-
cmd: string,
|
|
20
|
-
args: string[],
|
|
21
|
-
options?: { timeout?: number; input?: string | Buffer },
|
|
22
|
-
): Promise<ExecResult> {
|
|
23
|
-
void options;
|
|
24
|
-
const result = await $`${cmd} ${args}`.quiet().nothrow();
|
|
25
|
-
const decoder = new TextDecoder();
|
|
26
|
-
return {
|
|
27
|
-
stdout: result.stdout ? decoder.decode(result.stdout) : "",
|
|
28
|
-
stderr: result.stderr ? decoder.decode(result.stderr) : "",
|
|
29
|
-
ok: result.exitCode === 0,
|
|
30
|
-
exitCode: result.exitCode ?? -1,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
11
|
export interface ConvertResult {
|
|
35
12
|
content: string;
|
|
36
13
|
ok: boolean;
|
|
@@ -72,16 +49,16 @@ export async function convertWithMarkitdown(
|
|
|
72
49
|
|
|
73
50
|
try {
|
|
74
51
|
await Bun.write(tmpFile, content);
|
|
75
|
-
const result = await
|
|
76
|
-
|
|
77
|
-
|
|
52
|
+
const result = await ptree.cspawn([markitdown, tmpFile], { timeout });
|
|
53
|
+
const [stdout, stderr, exitCode] = await Promise.all([result.stdout.text(), result.stderr.text(), result.exited]);
|
|
54
|
+
if (exitCode !== 0) {
|
|
78
55
|
return {
|
|
79
|
-
content:
|
|
56
|
+
content: stdout,
|
|
80
57
|
ok: false,
|
|
81
|
-
error: stderr.length > 0 ? stderr : `markitdown failed (exit ${
|
|
58
|
+
error: stderr.length > 0 ? stderr : `markitdown failed (exit ${exitCode})`,
|
|
82
59
|
};
|
|
83
60
|
}
|
|
84
|
-
return { content:
|
|
61
|
+
return { content: stdout, ok: true };
|
|
85
62
|
} finally {
|
|
86
63
|
try {
|
|
87
64
|
await rm(tmpFile, { force: true });
|
package/src/index.ts
CHANGED
|
@@ -194,7 +194,6 @@ export {
|
|
|
194
194
|
export { type FileSlashCommand, loadSlashCommands as discoverSlashCommands } from "./core/slash-commands";
|
|
195
195
|
// Tools (detail types and utilities)
|
|
196
196
|
export {
|
|
197
|
-
type BashOperations,
|
|
198
197
|
type BashToolDetails,
|
|
199
198
|
DEFAULT_MAX_BYTES,
|
|
200
199
|
DEFAULT_MAX_LINES,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { getArtifactsDir } from "../../../core/tools/task/artifacts";
|
|
5
4
|
import { theme } from "../theme/theme";
|
|
6
5
|
import type { TodoItem } from "../types";
|
|
7
6
|
|
|
@@ -40,13 +39,7 @@ export class TodoDisplayComponent {
|
|
|
40
39
|
return;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
const artifactsDir =
|
|
44
|
-
if (!artifactsDir) {
|
|
45
|
-
this.todos = [];
|
|
46
|
-
this.visible = false;
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
42
|
+
const artifactsDir = this.sessionFile.slice(0, -6); // strip .jsonl extension
|
|
50
43
|
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
|
|
51
44
|
const data = await loadTodoFile(todoPath);
|
|
52
45
|
this.todos = data?.todos ?? [];
|
|
@@ -707,7 +707,7 @@ class TreeList implements Component {
|
|
|
707
707
|
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
708
708
|
this.applyFilter();
|
|
709
709
|
}
|
|
710
|
-
} else if (keyData
|
|
710
|
+
} else if (matchesKey(keyData, "shift+l") && !this.searchQuery) {
|
|
711
711
|
const selected = this.filteredNodes[this.selectedIndex];
|
|
712
712
|
if (selected && this.onLabelEdit) {
|
|
713
713
|
this.onLabelEdit(selected.node.entry.id, selected.node.label);
|
|
@@ -821,7 +821,7 @@ export class TreeSelectorComponent extends Container {
|
|
|
821
821
|
new TruncatedText(
|
|
822
822
|
theme.fg(
|
|
823
823
|
"muted",
|
|
824
|
-
" Up/Down: move. Left/Right: page.
|
|
824
|
+
" Up/Down: move. Left/Right: page. Shift+L: label. Ctrl+O/Shift+Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
|
|
825
825
|
),
|
|
826
826
|
0,
|
|
827
827
|
0,
|
|
@@ -28,7 +28,6 @@ import { getRecentSessions } from "../../core/session-manager";
|
|
|
28
28
|
import type { SettingsManager } from "../../core/settings-manager";
|
|
29
29
|
import { loadSlashCommands } from "../../core/slash-commands";
|
|
30
30
|
import { setTerminalTitle } from "../../core/title-generator";
|
|
31
|
-
import { getArtifactsDir } from "../../core/tools/task/artifacts";
|
|
32
31
|
import { VoiceSupervisor } from "../../core/voice-supervisor";
|
|
33
32
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
34
33
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
@@ -384,15 +383,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
384
383
|
|
|
385
384
|
// Initial top border update
|
|
386
385
|
this.updateEditorTopBorder();
|
|
387
|
-
|
|
388
|
-
if (!startupQuiet) {
|
|
389
|
-
const templateNames = this.session.promptTemplates.map((template) => template.name).sort();
|
|
390
|
-
if (templateNames.length > 0) {
|
|
391
|
-
const preview = templateNames.slice(0, 3).join(", ");
|
|
392
|
-
const suffix = templateNames.length > 3 ? ` +${templateNames.length - 3} more` : "";
|
|
393
|
-
this.showStatus(`Loaded prompt templates: ${preview}${suffix}`);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
386
|
}
|
|
397
387
|
|
|
398
388
|
async getUserInput(): Promise<{ text: string; images?: ImageContent[] }> {
|
|
@@ -481,11 +471,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
481
471
|
this.renderTodoList();
|
|
482
472
|
return;
|
|
483
473
|
}
|
|
484
|
-
const artifactsDir =
|
|
485
|
-
if (!artifactsDir) {
|
|
486
|
-
this.renderTodoList();
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
474
|
+
const artifactsDir = sessionFile.slice(0, -6);
|
|
489
475
|
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
|
|
490
476
|
const file = Bun.file(todoPath);
|
|
491
477
|
if (!(await file.exists())) {
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -32,33 +32,40 @@ function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function copyToClipboard(text: string): Promise<void> {
|
|
35
|
-
|
|
35
|
+
const p = platform();
|
|
36
|
+
const timeout = 5000;
|
|
37
|
+
|
|
36
38
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
39
|
+
if (p === "darwin") {
|
|
40
|
+
await Bun.spawn(["pbcopy"], { stdin: Buffer.from(text), timeout }).exited;
|
|
41
|
+
} else if (p === "win32") {
|
|
42
|
+
await Bun.spawn(["clip"], { stdin: Buffer.from(text), timeout }).exited;
|
|
43
|
+
} else {
|
|
44
|
+
const wayland = isWaylandSession();
|
|
45
|
+
if (wayland) {
|
|
46
|
+
const wlCopyPath = Bun.which("wl-copy");
|
|
47
|
+
if (wlCopyPath) {
|
|
48
|
+
// Fire-and-forget: wl-copy may not exit promptly, so we unref to avoid blocking
|
|
49
|
+
void Bun.spawn([wlCopyPath], { stdin: Buffer.from(text), timeout }).unref();
|
|
47
50
|
return;
|
|
48
|
-
} else {
|
|
49
|
-
promise = $`xclip -selection clipboard -t text/plain -i ${text}`.quiet().then(() => void 0);
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Linux - try xclip first, fall back to xsel
|
|
55
|
+
try {
|
|
56
|
+
await Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: Buffer.from(text), timeout }).exited;
|
|
57
|
+
} catch {
|
|
58
|
+
await Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: Buffer.from(text), timeout }).exited;
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
} catch (error) {
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
63
|
+
if (p === "linux") {
|
|
64
|
+
const tools = isWaylandSession() ? "wl-copy, xclip, or xsel" : "xclip or xsel";
|
|
65
|
+
throw new Error(`Failed to copy to clipboard. Install ${tools}: ${msg}`);
|
|
58
66
|
}
|
|
59
|
-
throw new Error(`Failed to copy to clipboard: ${
|
|
67
|
+
throw new Error(`Failed to copy to clipboard: ${msg}`);
|
|
60
68
|
}
|
|
61
|
-
await Promise.race([promise, Bun.sleep(3000)]);
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
export interface ClipboardImage {
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session artifacts for subagent outputs.
|
|
3
|
-
*
|
|
4
|
-
* When a session exists, writes agent outputs to a sibling directory.
|
|
5
|
-
* Otherwise uses temp files that are cleaned up after execution.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as os from "node:os";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import { nanoid } from "nanoid";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Derive artifacts directory from session file path.
|
|
15
|
-
*
|
|
16
|
-
* /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid.jsonl
|
|
17
|
-
* → /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid/
|
|
18
|
-
*/
|
|
19
|
-
export function getArtifactsDir(sessionFile: string | null): string | null {
|
|
20
|
-
if (!sessionFile) return null;
|
|
21
|
-
// Strip .jsonl extension to get directory path
|
|
22
|
-
if (sessionFile.endsWith(".jsonl")) {
|
|
23
|
-
return sessionFile.slice(0, -6);
|
|
24
|
-
}
|
|
25
|
-
return sessionFile;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Ensure artifacts directory exists.
|
|
30
|
-
*/
|
|
31
|
-
export function ensureArtifactsDir(dir: string): void {
|
|
32
|
-
if (!fs.existsSync(dir)) {
|
|
33
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Generate artifact file paths for an agent run.
|
|
39
|
-
*/
|
|
40
|
-
export function getArtifactPaths(
|
|
41
|
-
dir: string,
|
|
42
|
-
taskId: string,
|
|
43
|
-
): { inputPath: string; outputPath: string; jsonlPath: string } {
|
|
44
|
-
return {
|
|
45
|
-
inputPath: path.join(dir, `${taskId}.in.md`),
|
|
46
|
-
outputPath: path.join(dir, `${taskId}.out.md`),
|
|
47
|
-
jsonlPath: path.join(dir, `${taskId}.jsonl`),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Write artifacts for an agent run.
|
|
53
|
-
*/
|
|
54
|
-
export async function writeArtifacts(
|
|
55
|
-
dir: string,
|
|
56
|
-
taskId: string,
|
|
57
|
-
input: string,
|
|
58
|
-
output: string,
|
|
59
|
-
jsonlEvents?: string[],
|
|
60
|
-
): Promise<{ inputPath: string; outputPath: string; jsonlPath?: string }> {
|
|
61
|
-
ensureArtifactsDir(dir);
|
|
62
|
-
|
|
63
|
-
const paths = getArtifactPaths(dir, taskId);
|
|
64
|
-
|
|
65
|
-
// Write input
|
|
66
|
-
await Bun.write(paths.inputPath, input);
|
|
67
|
-
|
|
68
|
-
// Write output
|
|
69
|
-
await Bun.write(paths.outputPath, output);
|
|
70
|
-
|
|
71
|
-
// Write JSONL if events provided
|
|
72
|
-
if (jsonlEvents && jsonlEvents.length > 0) {
|
|
73
|
-
await Bun.write(paths.jsonlPath, jsonlEvents.join("\n"));
|
|
74
|
-
return paths;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { inputPath: paths.inputPath, outputPath: paths.outputPath };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Create a temporary artifacts directory.
|
|
82
|
-
*/
|
|
83
|
-
export function createTempArtifactsDir(runId?: string): string {
|
|
84
|
-
const id = runId || nanoid();
|
|
85
|
-
const dir = path.join(os.tmpdir(), `omp-task-${id}`);
|
|
86
|
-
ensureArtifactsDir(dir);
|
|
87
|
-
return dir;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Clean up temporary artifacts.
|
|
92
|
-
*/
|
|
93
|
-
export async function cleanupTempArtifacts(paths: string[]): Promise<void> {
|
|
94
|
-
for (const p of paths) {
|
|
95
|
-
try {
|
|
96
|
-
await fs.promises.unlink(p);
|
|
97
|
-
} catch {
|
|
98
|
-
// Ignore cleanup errors
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Clean up a temporary directory and its contents.
|
|
105
|
-
*/
|
|
106
|
-
export async function cleanupTempDir(dir: string): Promise<void> {
|
|
107
|
-
try {
|
|
108
|
-
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
109
|
-
} catch {
|
|
110
|
-
// Ignore cleanup errors
|
|
111
|
-
}
|
|
112
|
-
}
|