@oh-my-pi/pi-coding-agent 15.5.6 → 15.5.8
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 +72 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +60 -12
- package/dist/types/edit/file-snapshot-store.d.ts +9 -6
- package/dist/types/edit/hashline/diff.d.ts +4 -5
- package/dist/types/edit/streaming.d.ts +2 -1
- package/dist/types/eval/py/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/vault-protocol.d.ts +93 -0
- package/dist/types/lib/xai-http.d.ts +40 -0
- package/dist/types/mcp/transports/http.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +4 -1
- package/dist/types/tools/fetch.d.ts +16 -0
- package/dist/types/tools/image-gen.d.ts +6 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/match-line-format.d.ts +2 -2
- package/dist/types/tools/plan-mode-guard.d.ts +5 -6
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/tts.d.ts +18 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +2 -0
- package/package.json +8 -8
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-broker-cli.ts +2 -1
- package/src/cli/auth-gateway-cli.ts +210 -9
- package/src/commands/auth-gateway.ts +7 -1
- package/src/config/model-registry.ts +41 -9
- package/src/config/settings-schema.ts +55 -13
- package/src/edit/file-snapshot-store.ts +9 -6
- package/src/edit/hashline/diff.ts +26 -13
- package/src/edit/hashline/execute.ts +13 -9
- package/src/edit/renderer.ts +9 -9
- package/src/edit/streaming.ts +4 -6
- package/src/eval/py/index.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/shared-events.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +2 -0
- package/src/internal-urls/vault-protocol.ts +936 -0
- package/src/lib/xai-http.ts +124 -0
- package/src/main.ts +1 -2
- package/src/mcp/transports/http.ts +29 -2
- package/src/modes/components/tool-execution.ts +6 -4
- package/src/modes/controllers/event-controller.ts +10 -3
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +11 -3
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +3 -3
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +41 -10
- package/src/session/agent-session.ts +112 -14
- package/src/system-prompt.ts +2 -0
- package/src/tools/ast-edit.ts +10 -7
- package/src/tools/ast-grep.ts +12 -11
- package/src/tools/eval.ts +28 -3
- package/src/tools/fetch.ts +52 -24
- package/src/tools/image-gen.ts +205 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/match-line-format.ts +2 -2
- package/src/tools/path-utils.ts +2 -0
- package/src/tools/plan-mode-guard.ts +20 -7
- package/src/tools/read.ts +70 -55
- package/src/tools/render-utils.ts +15 -0
- package/src/tools/search.ts +14 -14
- package/src/tools/tts.ts +133 -0
- package/src/tools/write.ts +61 -6
- package/src/utils/file-mentions.ts +11 -5
- package/src/web/search/providers/codex.ts +2 -1
package/src/tools/search.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
5
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
7
|
import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi/pi-natives";
|
|
7
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -478,8 +479,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
478
479
|
);
|
|
479
480
|
}
|
|
480
481
|
} catch (err) {
|
|
481
|
-
if (err instanceof Error &&
|
|
482
|
-
throw new ToolError(err.message);
|
|
482
|
+
if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) {
|
|
483
|
+
throw new ToolError(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
|
|
483
484
|
}
|
|
484
485
|
throw err;
|
|
485
486
|
}
|
|
@@ -609,16 +610,16 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
609
610
|
matchesByFile.get(relativePath)!.push(match);
|
|
610
611
|
}
|
|
611
612
|
const displayLines: string[] = [];
|
|
612
|
-
const hashContexts = new Map<string, { absolutePath: string;
|
|
613
|
+
const hashContexts = new Map<string, { absolutePath: string; tag?: string }>();
|
|
614
|
+
const snapshotStore = baseDisplayMode.hashLines ? getFileSnapshotStore(this.session) : undefined;
|
|
613
615
|
if (baseDisplayMode.hashLines) {
|
|
614
616
|
for (const relativePath of fileList) {
|
|
615
617
|
if (archiveDisplaySet.has(relativePath)) continue;
|
|
616
618
|
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
617
619
|
if (immutableSourcePaths.has(absoluteFilePath)) continue;
|
|
618
620
|
try {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
hashContexts.set(relativePath, { absolutePath: absoluteFilePath, fileHash });
|
|
621
|
+
await access(absoluteFilePath, constants.R_OK);
|
|
622
|
+
hashContexts.set(relativePath, { absolutePath: absoluteFilePath });
|
|
622
623
|
} catch {
|
|
623
624
|
// Best-effort: if the file disappeared between grep and render, fall back to plain line output.
|
|
624
625
|
}
|
|
@@ -671,9 +672,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
671
672
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
672
673
|
}
|
|
673
674
|
if (cacheEntries.length > 0 && hashContext) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
});
|
|
675
|
+
const tag = snapshotStore?.recordSparse(hashContext.absolutePath, cacheEntries);
|
|
676
|
+
if (tag) hashContext.tag = tag;
|
|
677
677
|
}
|
|
678
678
|
return { model: modelOut, display: displayOut };
|
|
679
679
|
};
|
|
@@ -684,7 +684,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
684
684
|
return {
|
|
685
685
|
modelLines: rendered.model,
|
|
686
686
|
displayLines: rendered.display,
|
|
687
|
-
headerSuffix: hashContext ? `#${hashContext.
|
|
687
|
+
headerSuffix: hashContext?.tag ? `#${hashContext.tag}` : "",
|
|
688
688
|
skip: rendered.model.length === 0,
|
|
689
689
|
};
|
|
690
690
|
});
|
|
@@ -699,8 +699,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
699
699
|
displayLines.push("");
|
|
700
700
|
}
|
|
701
701
|
const hashContext = hashContexts.get(relativePath);
|
|
702
|
-
if (hashContext) {
|
|
703
|
-
outputLines.push(formatHashlineHeader(relativePath, hashContext.
|
|
702
|
+
if (hashContext?.tag) {
|
|
703
|
+
outputLines.push(formatHashlineHeader(relativePath, hashContext.tag));
|
|
704
704
|
}
|
|
705
705
|
outputLines.push(...rendered.model);
|
|
706
706
|
displayLines.push(...rendered.display);
|
package/src/tools/tts.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Ported from NousResearch/hermes-agent (MIT) — tools/tts_tool.py L167-171, L896-959.
|
|
2
|
+
|
|
3
|
+
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import * as z from "zod/v4";
|
|
5
|
+
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
6
|
+
import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
|
|
7
|
+
import { formatPathRelativeToCwd, resolveToCwd } from "./path-utils";
|
|
8
|
+
|
|
9
|
+
// Hermes tts_tool.py L167-171
|
|
10
|
+
const DEFAULT_XAI_VOICE_ID = "eve" as const;
|
|
11
|
+
const DEFAULT_XAI_LANGUAGE = "en" as const;
|
|
12
|
+
const DEFAULT_XAI_SAMPLE_RATE = 24_000;
|
|
13
|
+
const DEFAULT_XAI_BIT_RATE = 128_000;
|
|
14
|
+
const XAI_MAX_TEXT_LENGTH = 15_000;
|
|
15
|
+
|
|
16
|
+
// Built-in voices per xAI Tier-1 docs (2026-05-16). xAI also accepts custom voice IDs,
|
|
17
|
+
// so the schema does NOT enum-restrict voice_id; this constant only drives the description.
|
|
18
|
+
const XAI_BUILTIN_VOICES = ["ara", "eve", "leo", "rex", "sal"] as const;
|
|
19
|
+
|
|
20
|
+
const formatVoiceList = (): string =>
|
|
21
|
+
XAI_BUILTIN_VOICES.map(v => (v === DEFAULT_XAI_VOICE_ID ? `${v} (default)` : v)).join(", ");
|
|
22
|
+
|
|
23
|
+
type TtsCodec = "mp3" | "wav";
|
|
24
|
+
|
|
25
|
+
const ttsSchema = z.object({
|
|
26
|
+
text: z.string().min(1).max(XAI_MAX_TEXT_LENGTH),
|
|
27
|
+
voice_id: z.string().default(DEFAULT_XAI_VOICE_ID),
|
|
28
|
+
language: z.string().default(DEFAULT_XAI_LANGUAGE),
|
|
29
|
+
output_path: z.string(),
|
|
30
|
+
sample_rate: z.number().int().optional(),
|
|
31
|
+
bit_rate: z.number().int().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
interface TtsToolDetails {
|
|
35
|
+
bytes: number;
|
|
36
|
+
voiceId: string;
|
|
37
|
+
codec: TtsCodec;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ttsTool: CustomTool<typeof ttsSchema, TtsToolDetails> = {
|
|
41
|
+
name: "tts",
|
|
42
|
+
label: "TextToSpeech",
|
|
43
|
+
strict: false,
|
|
44
|
+
approval: "write",
|
|
45
|
+
description:
|
|
46
|
+
`Synthesize speech from text using xAI Grok Voice. Built-in voices: ${formatVoiceList()}. ` +
|
|
47
|
+
"Custom voice IDs also accepted. Output codec inferred from output_path suffix (.wav → wav, else mp3). " +
|
|
48
|
+
`Max ${XAI_MAX_TEXT_LENGTH.toLocaleString("en-US")} characters.`,
|
|
49
|
+
parameters: ttsSchema,
|
|
50
|
+
async execute(
|
|
51
|
+
_toolCallId: string,
|
|
52
|
+
params: z.infer<typeof ttsSchema>,
|
|
53
|
+
_onUpdate,
|
|
54
|
+
ctx: CustomToolContext,
|
|
55
|
+
signal?: AbortSignal,
|
|
56
|
+
): Promise<AgentToolResult<TtsToolDetails, typeof ttsSchema>> {
|
|
57
|
+
const creds = await resolveXAIHttpCredentials(ctx.modelRegistry);
|
|
58
|
+
if (!creds) {
|
|
59
|
+
return {
|
|
60
|
+
isError: true,
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: "No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok Subscription) or set XAI_API_KEY.",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const cwd = ctx.sessionManager.getCwd();
|
|
71
|
+
const outputPath = resolveToCwd(params.output_path, cwd);
|
|
72
|
+
const displayPath = formatPathRelativeToCwd(outputPath, cwd);
|
|
73
|
+
const codec: TtsCodec = outputPath.toLowerCase().endsWith(".wav") ? "wav" : "mp3";
|
|
74
|
+
const voiceId = params.voice_id;
|
|
75
|
+
const language = params.language;
|
|
76
|
+
const sampleRate = params.sample_rate ?? DEFAULT_XAI_SAMPLE_RATE;
|
|
77
|
+
const bitRate = params.bit_rate ?? DEFAULT_XAI_BIT_RATE;
|
|
78
|
+
|
|
79
|
+
const payload: Record<string, unknown> = {
|
|
80
|
+
text: params.text,
|
|
81
|
+
voice_id: voiceId,
|
|
82
|
+
language,
|
|
83
|
+
};
|
|
84
|
+
// Hermes tts_tool.py L926-940 — only send output_format when caller overrides a default.
|
|
85
|
+
const codecOverridden = codec !== "mp3";
|
|
86
|
+
const sampleRateOverridden = sampleRate !== DEFAULT_XAI_SAMPLE_RATE;
|
|
87
|
+
const bitRateOverridden = codec === "mp3" && bitRate !== DEFAULT_XAI_BIT_RATE;
|
|
88
|
+
if (codecOverridden || sampleRateOverridden || bitRateOverridden) {
|
|
89
|
+
const fmt: Record<string, unknown> = { codec };
|
|
90
|
+
if (sampleRate) fmt.sample_rate = sampleRate;
|
|
91
|
+
if (codec === "mp3" && bitRate) fmt.bit_rate = bitRate;
|
|
92
|
+
payload.output_format = fmt;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Compose the caller signal with a 60 s timeout fence.
|
|
96
|
+
const timeoutSignal = AbortSignal.timeout(60_000);
|
|
97
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
98
|
+
|
|
99
|
+
const response = await fetch(`${creds.baseURL}/tts`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${creds.apiKey}`,
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
"User-Agent": ohMyPiXAIUserAgent(),
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify(payload),
|
|
107
|
+
signal: combinedSignal,
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
const detail = await response.text();
|
|
111
|
+
return {
|
|
112
|
+
isError: true,
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `xAI TTS failed (${response.status}): ${detail.slice(0, 300)}`,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
122
|
+
await Bun.write(outputPath, bytes);
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `Saved ${bytes.length} bytes to ${displayPath} (voice=${voiceId}, codec=${codec}).`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
details: { bytes: bytes.length, voiceId, codec },
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
package/src/tools/write.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
|
|
4
5
|
import { stripHashlinePrefixes } from "@oh-my-pi/hashline";
|
|
5
6
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
6
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
8
9
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import * as z from "zod/v4";
|
|
11
|
+
|
|
10
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
13
|
import { InternalUrlRouter } from "../internal-urls";
|
|
12
14
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
@@ -53,6 +55,8 @@ import {
|
|
|
53
55
|
import { ToolError } from "./tool-errors";
|
|
54
56
|
import { toolResult } from "./tool-result";
|
|
55
57
|
|
|
58
|
+
const LOOSE_HASHLINE_HEADER_RE = /^\s*¶\S+#[^ \t\r\n]*\s*$/;
|
|
59
|
+
|
|
56
60
|
let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
|
|
57
61
|
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
58
62
|
if (!fflateModulePromise) fflateModulePromise = import("fflate");
|
|
@@ -70,6 +74,33 @@ export type WriteToolInput = z.infer<typeof writeSchema>;
|
|
|
70
74
|
export interface WriteToolDetails {
|
|
71
75
|
diagnostics?: FileDiagnosticsResult;
|
|
72
76
|
meta?: OutputMeta;
|
|
77
|
+
/** Set when the file was auto-chmod'd because content begins with a `#!` shebang. */
|
|
78
|
+
madeExecutable?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Strip hashline display prefixes from write content.
|
|
83
|
+
*
|
|
84
|
+
* Includes a fallback for loosely-formed section headers that still carry
|
|
85
|
+
* line-number prefixes (for example legacy or malformed hashline echoes).
|
|
86
|
+
*/
|
|
87
|
+
function stripWriteContentWithPotentialLooseHeader(lines: string[]): { text: string; stripped: boolean } {
|
|
88
|
+
const cleaned = stripHashlinePrefixes(lines);
|
|
89
|
+
if (cleaned !== lines) {
|
|
90
|
+
return { text: cleaned.join("\n"), stripped: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const headerIndex = lines.findIndex(line => line.trim().length > 0);
|
|
94
|
+
if (headerIndex === -1 || !LOOSE_HASHLINE_HEADER_RE.test(lines[headerIndex])) {
|
|
95
|
+
return { text: lines.join("\n"), stripped: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const linesWithoutHeader = lines.slice(0, headerIndex).concat(lines.slice(headerIndex + 1));
|
|
99
|
+
const cleanedWithoutHeader = stripHashlinePrefixes(linesWithoutHeader);
|
|
100
|
+
if (cleanedWithoutHeader === linesWithoutHeader) {
|
|
101
|
+
return { text: lines.join("\n"), stripped: false };
|
|
102
|
+
}
|
|
103
|
+
return { text: cleanedWithoutHeader.join("\n"), stripped: true };
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
/**
|
|
@@ -82,10 +113,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
82
113
|
if (!resolveFileDisplayMode(session).hashLines) {
|
|
83
114
|
return { text: content, stripped: false };
|
|
84
115
|
}
|
|
85
|
-
|
|
86
|
-
const cleaned = stripHashlinePrefixes(lines);
|
|
87
|
-
if (cleaned === lines) return { text: content, stripped: false };
|
|
88
|
-
return { text: cleaned.join("\n"), stripped: true };
|
|
116
|
+
return stripWriteContentWithPotentialLooseHeader(content.split("\n"));
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
/**
|
|
@@ -103,6 +131,28 @@ function appendNoteToResult(result: AgentToolResult<WriteToolDetails>, note: str
|
|
|
103
131
|
}
|
|
104
132
|
}
|
|
105
133
|
|
|
134
|
+
/**
|
|
135
|
+
* If `content` begins with a `#!` shebang, ensure the file is executable.
|
|
136
|
+
*
|
|
137
|
+
* Mirrors `chmod a+x` (adds user/group/other execute bits to existing mode).
|
|
138
|
+
* Errors are swallowed: chmod failure (e.g. Windows ACL, read-only mount)
|
|
139
|
+
* MUST NOT fail an otherwise successful write. Returns whether the mode
|
|
140
|
+
* actually changed so the caller can surface a note.
|
|
141
|
+
*/
|
|
142
|
+
async function maybeMarkExecutableForShebang(absolutePath: string, content: string): Promise<boolean> {
|
|
143
|
+
if (!content.startsWith("#!")) return false;
|
|
144
|
+
try {
|
|
145
|
+
const stat = await fs.stat(absolutePath);
|
|
146
|
+
const currentMode = stat.mode & 0o7777;
|
|
147
|
+
const newMode = currentMode | 0o111;
|
|
148
|
+
if (newMode === currentMode) return false;
|
|
149
|
+
await fs.chmod(absolutePath, newMode);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
106
156
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
107
157
|
// Tool Class
|
|
108
158
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -772,6 +822,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
772
822
|
|
|
773
823
|
const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
|
|
774
824
|
invalidateFsScanAfterWrite(absolutePath);
|
|
825
|
+
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
775
826
|
|
|
776
827
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
777
828
|
let resultText = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
|
|
@@ -781,7 +832,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
781
832
|
if (!diagnostics) {
|
|
782
833
|
return {
|
|
783
834
|
content: [{ type: "text", text: resultText }],
|
|
784
|
-
details: {},
|
|
835
|
+
details: { madeExecutable: madeExecutable || undefined },
|
|
785
836
|
};
|
|
786
837
|
}
|
|
787
838
|
|
|
@@ -789,6 +840,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
789
840
|
content: [{ type: "text", text: resultText }],
|
|
790
841
|
details: {
|
|
791
842
|
diagnostics,
|
|
843
|
+
madeExecutable: madeExecutable || undefined,
|
|
792
844
|
meta: outputMeta()
|
|
793
845
|
.diagnostics(diagnostics.summary, diagnostics.messages ?? [])
|
|
794
846
|
.get(),
|
|
@@ -915,13 +967,16 @@ export const writeToolRenderer = {
|
|
|
915
967
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
916
968
|
const lineCount = countLines(fileContent);
|
|
917
969
|
const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
|
|
970
|
+
const execSuffix = result.details?.madeExecutable
|
|
971
|
+
? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
|
|
972
|
+
: "";
|
|
918
973
|
|
|
919
974
|
// Build header with status icon
|
|
920
975
|
const header = renderStatusLine(
|
|
921
976
|
{
|
|
922
977
|
icon: "success",
|
|
923
978
|
title: "Write",
|
|
924
|
-
description: `${langIcon} ${pathDisplay}${lineSuffix}`,
|
|
979
|
+
description: `${langIcon} ${pathDisplay}${lineSuffix}${execSuffix}`,
|
|
925
980
|
},
|
|
926
981
|
uiTheme,
|
|
927
982
|
);
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { formatHashlineHeader, formatNumberedLines, type SnapshotStore } from "@oh-my-pi/hashline";
|
|
11
11
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
13
13
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
14
14
|
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
15
15
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { normalizeToLF } from "../edit/normalize";
|
|
16
17
|
import type { FileMentionMessage } from "../session/messages";
|
|
17
18
|
import {
|
|
18
19
|
DEFAULT_MAX_BYTES,
|
|
@@ -277,7 +278,7 @@ export function extractFileMentions(text: string): string[] {
|
|
|
277
278
|
export async function generateFileMentionMessages(
|
|
278
279
|
filePaths: string[],
|
|
279
280
|
cwd: string,
|
|
280
|
-
options?: { autoResizeImages?: boolean; useHashLines?: boolean },
|
|
281
|
+
options?: { autoResizeImages?: boolean; useHashLines?: boolean; snapshotStore?: SnapshotStore },
|
|
281
282
|
): Promise<AgentMessage[]> {
|
|
282
283
|
if (filePaths.length === 0) return [];
|
|
283
284
|
|
|
@@ -354,9 +355,14 @@ export async function generateFileMentionMessages(
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
const content = await Bun.file(absolutePath).text();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
const snapshotStore = options?.useHashLines ? options.snapshotStore : undefined;
|
|
359
|
+
const normalized = snapshotStore ? normalizeToLF(content) : content;
|
|
360
|
+
let { output, lineCount } = buildTextOutput(normalized);
|
|
361
|
+
if (snapshotStore) {
|
|
362
|
+
const tag = snapshotStore.recordContiguous(absolutePath, 1, normalized.split("\n"), {
|
|
363
|
+
fullText: normalized,
|
|
364
|
+
});
|
|
365
|
+
output = `${formatHashlineHeader(resolvedPath, tag)}\n${formatNumberedLines(output)}`;
|
|
360
366
|
}
|
|
361
367
|
files.push({ path: resolvedPath, content: output, lineCount });
|
|
362
368
|
} catch {
|
|
@@ -19,8 +19,9 @@ import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
|
19
19
|
|
|
20
20
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
21
21
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
22
|
-
const FALLBACK_MODEL = "gpt-5.
|
|
22
|
+
const FALLBACK_MODEL = "gpt-5.5";
|
|
23
23
|
const DEFAULT_MODEL_PREFERENCES = [
|
|
24
|
+
"gpt-5.5",
|
|
24
25
|
"gpt-5.4",
|
|
25
26
|
"gpt-5-codex",
|
|
26
27
|
"gpt-5",
|