@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
  3. package/dist/types/commands/auth-gateway.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +60 -12
  5. package/dist/types/edit/file-snapshot-store.d.ts +9 -6
  6. package/dist/types/edit/hashline/diff.d.ts +4 -5
  7. package/dist/types/edit/streaming.d.ts +2 -1
  8. package/dist/types/eval/py/index.d.ts +1 -0
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/shared-events.d.ts +1 -1
  11. package/dist/types/internal-urls/index.d.ts +1 -0
  12. package/dist/types/internal-urls/vault-protocol.d.ts +93 -0
  13. package/dist/types/lib/xai-http.d.ts +40 -0
  14. package/dist/types/mcp/transports/http.d.ts +9 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +2 -1
  16. package/dist/types/session/agent-session.d.ts +4 -1
  17. package/dist/types/tools/fetch.d.ts +16 -0
  18. package/dist/types/tools/image-gen.d.ts +6 -2
  19. package/dist/types/tools/index.d.ts +1 -0
  20. package/dist/types/tools/match-line-format.d.ts +2 -2
  21. package/dist/types/tools/plan-mode-guard.d.ts +5 -6
  22. package/dist/types/tools/render-utils.d.ts +3 -1
  23. package/dist/types/tools/tts.d.ts +18 -0
  24. package/dist/types/tools/write.d.ts +2 -0
  25. package/dist/types/utils/file-mentions.d.ts +2 -0
  26. package/package.json +8 -8
  27. package/src/cli/args.ts +2 -0
  28. package/src/cli/auth-broker-cli.ts +2 -1
  29. package/src/cli/auth-gateway-cli.ts +210 -9
  30. package/src/commands/auth-gateway.ts +7 -1
  31. package/src/config/model-registry.ts +41 -9
  32. package/src/config/settings-schema.ts +55 -13
  33. package/src/edit/file-snapshot-store.ts +9 -6
  34. package/src/edit/hashline/diff.ts +26 -13
  35. package/src/edit/hashline/execute.ts +13 -9
  36. package/src/edit/renderer.ts +9 -9
  37. package/src/edit/streaming.ts +4 -6
  38. package/src/eval/py/index.ts +1 -1
  39. package/src/extensibility/custom-tools/types.ts +1 -1
  40. package/src/extensibility/shared-events.ts +1 -1
  41. package/src/internal-urls/docs-index.generated.ts +7 -7
  42. package/src/internal-urls/index.ts +1 -0
  43. package/src/internal-urls/router.ts +2 -0
  44. package/src/internal-urls/vault-protocol.ts +936 -0
  45. package/src/lib/xai-http.ts +124 -0
  46. package/src/main.ts +1 -2
  47. package/src/mcp/transports/http.ts +29 -2
  48. package/src/modes/components/tool-execution.ts +6 -4
  49. package/src/modes/controllers/event-controller.ts +10 -3
  50. package/src/modes/controllers/selector-controller.ts +7 -2
  51. package/src/modes/interactive-mode.ts +11 -3
  52. package/src/modes/utils/ui-helpers.ts +2 -1
  53. package/src/prompts/system/system-prompt.md +3 -0
  54. package/src/prompts/tools/ast-edit.md +1 -1
  55. package/src/prompts/tools/ast-grep.md +1 -1
  56. package/src/prompts/tools/read.md +3 -3
  57. package/src/prompts/tools/search.md +1 -1
  58. package/src/sdk.ts +41 -10
  59. package/src/session/agent-session.ts +112 -14
  60. package/src/system-prompt.ts +2 -0
  61. package/src/tools/ast-edit.ts +10 -7
  62. package/src/tools/ast-grep.ts +12 -11
  63. package/src/tools/eval.ts +28 -3
  64. package/src/tools/fetch.ts +52 -24
  65. package/src/tools/image-gen.ts +205 -7
  66. package/src/tools/index.ts +1 -0
  67. package/src/tools/match-line-format.ts +2 -2
  68. package/src/tools/path-utils.ts +2 -0
  69. package/src/tools/plan-mode-guard.ts +20 -7
  70. package/src/tools/read.ts +70 -55
  71. package/src/tools/render-utils.ts +15 -0
  72. package/src/tools/search.ts +14 -14
  73. package/src/tools/tts.ts +133 -0
  74. package/src/tools/write.ts +61 -6
  75. package/src/utils/file-mentions.ts +11 -5
  76. package/src/web/search/providers/codex.ts +2 -1
@@ -1,7 +1,8 @@
1
- import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
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 { computeFileHash, formatHashlineHeader } from "@oh-my-pi/hashline";
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 && err.message.startsWith("regex parse 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; fileHash: 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
- const fullText = await Bun.file(absoluteFilePath).text();
620
- const fileHash = computeFileHash(fullText);
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
- getFileSnapshotStore(this.session).recordSparse(hashContext.absolutePath, cacheEntries, {
675
- fileHash: hashContext.fileHash,
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.fileHash}` : "",
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.fileHash));
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);
@@ -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
+ };
@@ -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
- const lines = content.split("\n");
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 { computeFileHash, formatHashlineHeader, formatNumberedLines } from "@oh-my-pi/hashline";
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
- let { output, lineCount } = buildTextOutput(content);
358
- if (options?.useHashLines) {
359
- output = `${formatHashlineHeader(resolvedPath, computeFileHash(content))}\n${formatNumberedLines(output)}`;
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.4";
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",