@oh-my-pi/pi-coding-agent 14.0.4 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/package.json +11 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/support.ts +5 -0
  5. package/src/cli/list-models.ts +96 -57
  6. package/src/commit/model-selection.ts +16 -13
  7. package/src/config/model-equivalence.ts +674 -0
  8. package/src/config/model-registry.ts +182 -13
  9. package/src/config/model-resolver.ts +203 -74
  10. package/src/config/settings-schema.ts +23 -0
  11. package/src/config/settings.ts +9 -2
  12. package/src/dap/session.ts +31 -39
  13. package/src/debug/log-formatting.ts +2 -2
  14. package/src/edit/modes/chunk.ts +8 -3
  15. package/src/export/html/template.css +82 -0
  16. package/src/export/html/template.generated.ts +1 -1
  17. package/src/export/html/template.js +612 -97
  18. package/src/internal-urls/docs-index.generated.ts +1 -1
  19. package/src/internal-urls/jobs-protocol.ts +2 -1
  20. package/src/lsp/client.ts +5 -3
  21. package/src/lsp/index.ts +4 -9
  22. package/src/lsp/utils.ts +26 -0
  23. package/src/main.ts +6 -1
  24. package/src/memories/index.ts +7 -6
  25. package/src/modes/components/diff.ts +1 -1
  26. package/src/modes/components/model-selector.ts +221 -64
  27. package/src/modes/controllers/command-controller.ts +18 -0
  28. package/src/modes/controllers/event-controller.ts +438 -426
  29. package/src/modes/controllers/selector-controller.ts +13 -5
  30. package/src/modes/theme/mermaid-cache.ts +5 -7
  31. package/src/priority.json +8 -0
  32. package/src/prompts/agents/designer.md +1 -2
  33. package/src/prompts/system/system-prompt.md +5 -1
  34. package/src/prompts/tools/bash.md +15 -0
  35. package/src/prompts/tools/cancel-job.md +1 -1
  36. package/src/prompts/tools/chunk-edit.md +39 -40
  37. package/src/prompts/tools/read-chunk.md +13 -1
  38. package/src/prompts/tools/read.md +9 -0
  39. package/src/prompts/tools/write.md +1 -0
  40. package/src/sdk.ts +7 -4
  41. package/src/session/agent-session.ts +33 -6
  42. package/src/session/compaction/compaction.ts +1 -1
  43. package/src/task/executor.ts +5 -1
  44. package/src/tools/await-tool.ts +2 -1
  45. package/src/tools/bash.ts +221 -56
  46. package/src/tools/browser.ts +84 -21
  47. package/src/tools/cancel-job.ts +2 -1
  48. package/src/tools/fetch.ts +1 -1
  49. package/src/tools/find.ts +40 -94
  50. package/src/tools/gemini-image.ts +1 -0
  51. package/src/tools/inspect-image.ts +1 -1
  52. package/src/tools/read.ts +218 -1
  53. package/src/tools/render-utils.ts +1 -1
  54. package/src/tools/sqlite-reader.ts +623 -0
  55. package/src/tools/write.ts +187 -1
  56. package/src/utils/commit-message-generator.ts +1 -0
  57. package/src/utils/git.ts +24 -1
  58. package/src/utils/image-resize.ts +73 -37
  59. package/src/utils/title-generator.ts +1 -1
  60. package/src/web/scrapers/types.ts +50 -32
  61. package/src/web/search/providers/codex.ts +21 -2
package/src/tools/find.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
- import { FileType, type GlobMatch, glob } from "@oh-my-pi/pi-natives";
4
+ import * as natives from "@oh-my-pi/pi-natives";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
7
7
  import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
@@ -124,46 +124,15 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
124
124
  throw new ToolError("Limit must be a positive number");
125
125
  }
126
126
  const includeHidden = hidden ?? true;
127
-
128
- // If custom operations provided with glob, use that instead of fd
129
- if (this.#customOps?.glob) {
130
- if (!(await this.#customOps.exists(searchPath))) {
131
- throw new ToolError(`Path not found: ${scopePath}`);
132
- }
133
-
134
- if (!hasGlob && this.#customOps.stat) {
135
- const stat = await this.#customOps.stat(searchPath);
136
- if (stat.isFile()) {
137
- const files = [scopePath];
138
- const details: FindToolDetails = {
139
- scopePath,
140
- fileCount: 1,
141
- files,
142
- truncated: false,
143
- };
144
- return toolResult(details).text(files.join("\n")).done();
145
- }
146
- }
147
-
148
- const results = await this.#customOps.glob(globPattern, searchPath, {
149
- ignore: ["**/node_modules/**", "**/.git/**"],
150
- limit: effectiveLimit,
151
- });
152
-
153
- if (results.length === 0) {
127
+ const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
128
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
129
+ const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
130
+ if (files.length === 0) {
154
131
  const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
155
132
  return toolResult(details).text("No files found matching pattern").done();
156
133
  }
157
134
 
158
- // Relativize paths
159
- const relativized = results.map(p => {
160
- if (p.startsWith(searchPath)) {
161
- return p.slice(searchPath.length + 1);
162
- }
163
- return path.relative(searchPath, p);
164
- });
165
-
166
- const listLimit = applyListLimit(relativized, { limit: effectiveLimit });
135
+ const listLimit = applyListLimit(files, { limit: effectiveLimit });
167
136
  const limited = listLimit.items;
168
137
  const limitMeta = listLimit.meta;
169
138
  const rawOutput = limited.join("\n");
@@ -186,6 +155,32 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
186
155
  }
187
156
 
188
157
  return resultBuilder.done();
158
+ };
159
+
160
+ if (this.#customOps?.glob) {
161
+ if (!(await this.#customOps.exists(searchPath))) {
162
+ throw new ToolError(`Path not found: ${scopePath}`);
163
+ }
164
+
165
+ if (!hasGlob && this.#customOps.stat) {
166
+ const stat = await this.#customOps.stat(searchPath);
167
+ if (stat.isFile()) {
168
+ return buildResult([scopePath]);
169
+ }
170
+ }
171
+
172
+ const results = await this.#customOps.glob(globPattern, searchPath, {
173
+ ignore: ["**/node_modules/**", "**/.git/**"],
174
+ limit: effectiveLimit,
175
+ });
176
+ const relativized = results.map(p => {
177
+ if (p.startsWith(searchPath)) {
178
+ return p.slice(searchPath.length + 1);
179
+ }
180
+ return path.relative(searchPath, p);
181
+ });
182
+
183
+ return buildResult(relativized);
189
184
  }
190
185
 
191
186
  let searchStat: fs.Stats;
@@ -199,20 +194,13 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
199
194
  }
200
195
 
201
196
  if (!hasGlob && searchStat.isFile()) {
202
- const files = [scopePath];
203
- const details: FindToolDetails = {
204
- scopePath,
205
- fileCount: 1,
206
- files,
207
- truncated: false,
208
- };
209
- return toolResult(details).text(files.join("\n")).done();
197
+ return buildResult([scopePath]);
210
198
  }
211
199
  if (!searchStat.isDirectory()) {
212
200
  throw new ToolError(`Path is not a directory: ${searchPath}`);
213
201
  }
214
202
 
215
- let matches: GlobMatch[];
203
+ let matches: natives.GlobMatch[];
216
204
  const onUpdateMatches: string[] = [];
217
205
  const updateIntervalMs = 200;
218
206
  let lastUpdate = 0;
@@ -233,27 +221,25 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
233
221
  });
234
222
  };
235
223
  const onMatch = onUpdate
236
- ? (err: Error | null, match: GlobMatch | null) => {
224
+ ? (err: Error | null, match: natives.GlobMatch | null) => {
237
225
  if (err || signal?.aborted || !match) return;
238
226
  let relativePath = match.path;
239
227
  if (!relativePath) return;
240
- if (match.fileType === FileType.Dir && !relativePath.endsWith("/")) {
228
+ if (match.fileType === natives.FileType.Dir && !relativePath.endsWith("/")) {
241
229
  relativePath += "/";
242
230
  }
243
231
  onUpdateMatches.push(relativePath);
244
232
  emitUpdate();
245
233
  }
246
234
  : undefined;
247
- const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
248
- const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
249
235
 
250
236
  const doGlob = async (useGitignore: boolean) =>
251
237
  untilAborted(combinedSignal, () =>
252
- glob(
238
+ natives.glob(
253
239
  {
254
240
  pattern: globPattern,
255
241
  path: searchPath,
256
- fileType: FileType.File,
242
+ fileType: natives.FileType.File,
257
243
  hidden: includeHidden,
258
244
  maxResults: effectiveLimit,
259
245
  sortByMtime: true,
@@ -266,7 +252,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
266
252
 
267
253
  try {
268
254
  let result = await doGlob(true);
269
- // If gitignore filtering yielded nothing, retry without it
270
255
  if (result.matches.length === 0) {
271
256
  result = await doGlob(false);
272
257
  }
@@ -282,12 +267,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
282
267
  throw error;
283
268
  }
284
269
 
285
- if (matches.length === 0) {
286
- const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
287
- return toolResult(details).text("No files found matching pattern").done();
288
- }
289
270
  const relativized: string[] = [];
290
-
291
271
  for (const match of matches) {
292
272
  throwIfAborted(signal);
293
273
  const line = match.path;
@@ -297,9 +277,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
297
277
 
298
278
  const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
299
279
  let relativePath = line;
300
-
301
- const isDirectory = match.fileType === FileType.Dir;
302
-
280
+ const isDirectory = match.fileType === natives.FileType.Dir;
303
281
  if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
304
282
  relativePath += "/";
305
283
  }
@@ -307,39 +285,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
307
285
  relativized.push(relativePath);
308
286
  }
309
287
 
310
- if (relativized.length === 0) {
311
- const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
312
- return toolResult(details).text("No files found matching pattern").done();
313
- }
314
-
315
- // Results are already sorted by mtime from native (sortByMtime: true)
316
-
317
- const listLimit = applyListLimit(relativized, { limit: effectiveLimit });
318
- const limited = listLimit.items;
319
- const limitMeta = listLimit.meta;
320
-
321
- // Apply byte truncation (no line limit since we already have result limit)
322
- const rawOutput = limited.join("\n");
323
- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
324
-
325
- const resultOutput = truncation.content;
326
- const details: FindToolDetails = {
327
- scopePath,
328
- fileCount: limited.length,
329
- files: limited,
330
- truncated: Boolean(limitMeta.resultLimit || truncation.truncated),
331
- resultLimitReached: limitMeta.resultLimit?.reached,
332
- truncation: truncation.truncated ? truncation : undefined,
333
- };
334
-
335
- const resultBuilder = toolResult(details)
336
- .text(resultOutput)
337
- .limits({ resultLimit: limitMeta.resultLimit?.reached });
338
- if (truncation.truncated) {
339
- resultBuilder.truncation(truncation, { direction: "head" });
340
- }
341
-
342
- return resultBuilder.done();
288
+ return buildResult(relativized);
343
289
  });
344
290
  }
345
291
  }
@@ -728,6 +728,7 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
728
728
  headers: {
729
729
  "Content-Type": "application/json",
730
730
  Authorization: `Bearer ${apiKey.apiKey}`,
731
+ "X-Title": "Oh-My-Pi",
731
732
  },
732
733
  body: JSON.stringify(requestBody),
733
734
  signal: requestSignal,
@@ -79,7 +79,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
79
79
  const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
80
80
  if (!pattern) return undefined;
81
81
  const expanded = expandRoleAlias(pattern, this.session.settings);
82
- return resolveModelFromString(expanded, availableModels, matchPreferences);
82
+ return resolveModelFromString(expanded, availableModels, matchPreferences, modelRegistry);
83
83
  };
84
84
 
85
85
  const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
package/src/tools/read.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
@@ -38,7 +39,6 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
38
39
  import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
39
40
  import { convertFileWithMarkit } from "../utils/markit";
40
41
  import { type ArchiveReader, openArchive, parseArchivePathCandidates } from "./archive-reader";
41
-
42
42
  import {
43
43
  executeReadUrl,
44
44
  isReadableUrlPath,
@@ -52,6 +52,22 @@ import { applyListLimit } from "./list-limit";
52
52
  import { formatFullOutputReference, formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
53
53
  import { expandPath, resolveReadPath } from "./path-utils";
54
54
  import { formatAge, formatBytes, shortenPath, wrapBrackets } from "./render-utils";
55
+ import {
56
+ executeReadQuery,
57
+ getRowByKey,
58
+ getRowByRowId,
59
+ getTableSchema,
60
+ isSqliteFile,
61
+ listTables,
62
+ parseSqlitePathCandidates,
63
+ parseSqliteSelector,
64
+ queryRows,
65
+ renderRow,
66
+ renderSchema,
67
+ renderTable,
68
+ renderTableList,
69
+ resolveTableRowLookup,
70
+ } from "./sqlite-reader";
55
71
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
56
72
  import { toolResult } from "./tool-result";
57
73
 
@@ -420,6 +436,29 @@ interface ResolvedArchiveReadPath {
420
436
  suffixResolution?: { from: string; to: string };
421
437
  }
422
438
 
439
+ interface ResolvedSqliteReadPath {
440
+ absolutePath: string;
441
+ sqliteSubPath: string;
442
+ queryString: string;
443
+ suffixResolution?: { from: string; to: string };
444
+ }
445
+
446
+ function parseSqliteSelectorInput(selector: string | undefined): { subPath: string; queryString: string } {
447
+ if (!selector) {
448
+ return { subPath: "", queryString: "" };
449
+ }
450
+
451
+ const queryIndex = selector.indexOf("?");
452
+ if (queryIndex === -1) {
453
+ return { subPath: selector.replace(/^:+/, ""), queryString: "" };
454
+ }
455
+
456
+ return {
457
+ subPath: selector.slice(0, queryIndex).replace(/^:+/, ""),
458
+ queryString: selector.slice(queryIndex + 1),
459
+ };
460
+ }
461
+
423
462
  /**
424
463
  * Read tool implementation.
425
464
  *
@@ -502,6 +541,53 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
502
541
  return null;
503
542
  }
504
543
 
544
+ async #resolveSqliteReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedSqliteReadPath | null> {
545
+ const candidates = parseSqlitePathCandidates(readPath);
546
+ for (const candidate of candidates) {
547
+ let absolutePath = resolveReadPath(candidate.sqlitePath, this.session.cwd);
548
+ let suffixResolution: { from: string; to: string } | undefined;
549
+
550
+ try {
551
+ const stat = await Bun.file(absolutePath).stat();
552
+ if (stat.isDirectory()) continue;
553
+ if (!(await isSqliteFile(absolutePath))) continue;
554
+
555
+ return {
556
+ absolutePath,
557
+ sqliteSubPath: candidate.subPath,
558
+ queryString: candidate.queryString,
559
+ suffixResolution,
560
+ };
561
+ } catch (error) {
562
+ if (!isNotFoundError(error) || isRemoteMountPath(absolutePath)) continue;
563
+
564
+ const suffixMatch = await findUniqueSuffixMatch(candidate.sqlitePath, this.session.cwd, signal);
565
+ if (!suffixMatch) continue;
566
+
567
+ try {
568
+ const retryStat = await Bun.file(suffixMatch.absolutePath).stat();
569
+ if (retryStat.isDirectory()) continue;
570
+ if (!(await isSqliteFile(suffixMatch.absolutePath))) continue;
571
+
572
+ absolutePath = suffixMatch.absolutePath;
573
+ suffixResolution = { from: candidate.sqlitePath, to: suffixMatch.displayPath };
574
+ return {
575
+ absolutePath,
576
+ sqliteSubPath: candidate.subPath,
577
+ queryString: candidate.queryString,
578
+ suffixResolution,
579
+ };
580
+ } catch (retryError) {
581
+ if (!isNotFoundError(retryError)) {
582
+ throw retryError;
583
+ }
584
+ }
585
+ }
586
+ }
587
+
588
+ return null;
589
+ }
590
+
505
591
  #buildInMemoryTextResult(
506
592
  text: string,
507
593
  offset: number | undefined,
@@ -709,6 +795,132 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
709
795
  return result;
710
796
  }
711
797
 
798
+ async #readSqlite(
799
+ sel: string | undefined,
800
+ resolvedSqlitePath: ResolvedSqliteReadPath,
801
+ signal?: AbortSignal,
802
+ ): Promise<AgentToolResult<ReadToolDetails>> {
803
+ throwIfAborted(signal);
804
+
805
+ const selectorInput = sel
806
+ ? parseSqliteSelectorInput(sel)
807
+ : { subPath: resolvedSqlitePath.sqliteSubPath, queryString: resolvedSqlitePath.queryString };
808
+ const selector = parseSqliteSelector(selectorInput.subPath, selectorInput.queryString);
809
+ const details: ReadToolDetails = {
810
+ resolvedPath: resolvedSqlitePath.absolutePath,
811
+ suffixResolution: resolvedSqlitePath.suffixResolution,
812
+ };
813
+
814
+ let db: Database | null = null;
815
+ try {
816
+ db = new Database(resolvedSqlitePath.absolutePath, { readonly: true, strict: true });
817
+ db.run("PRAGMA busy_timeout = 3000");
818
+ throwIfAborted(signal);
819
+
820
+ switch (selector.kind) {
821
+ case "list": {
822
+ const listLimit = applyListLimit(listTables(db), { limit: 500 });
823
+ const output = prependSuffixResolutionNotice(
824
+ renderTableList(listLimit.items),
825
+ resolvedSqlitePath.suffixResolution,
826
+ );
827
+ const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
828
+ details.truncation = truncation.truncated ? truncation : undefined;
829
+ const resultBuilder = toolResult<ReadToolDetails>(details)
830
+ .text(truncation.content)
831
+ .sourcePath(resolvedSqlitePath.absolutePath)
832
+ .limits({ resultLimit: listLimit.meta.resultLimit?.reached });
833
+ if (truncation.truncated) {
834
+ resultBuilder.truncation(truncation, { direction: "head" });
835
+ }
836
+ return resultBuilder.done();
837
+ }
838
+ case "schema": {
839
+ const sampleRows = queryRows(db, selector.table, { limit: selector.sampleLimit, offset: 0 });
840
+ let output = renderSchema(getTableSchema(db, selector.table), {
841
+ columns: sampleRows.columns,
842
+ rows: sampleRows.rows,
843
+ });
844
+ if (sampleRows.rows.length < sampleRows.totalCount) {
845
+ const remaining = sampleRows.totalCount - sampleRows.rows.length;
846
+ output += `\n[${remaining} more rows; use sel="${selector.table}?limit=20&offset=${sampleRows.rows.length}" to continue]`;
847
+ }
848
+ return toolResult<ReadToolDetails>(details)
849
+ .text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution))
850
+ .sourcePath(resolvedSqlitePath.absolutePath)
851
+ .done();
852
+ }
853
+ case "row": {
854
+ const lookup = resolveTableRowLookup(db, selector.table);
855
+ const row =
856
+ lookup.kind === "pk"
857
+ ? getRowByKey(db, selector.table, lookup, selector.key)
858
+ : getRowByRowId(db, selector.table, selector.key);
859
+ if (!row) {
860
+ return toolResult<ReadToolDetails>(details)
861
+ .text(
862
+ prependSuffixResolutionNotice(
863
+ `No row found in table '${selector.table}' for key '${selector.key}'.`,
864
+ resolvedSqlitePath.suffixResolution,
865
+ ),
866
+ )
867
+ .sourcePath(resolvedSqlitePath.absolutePath)
868
+ .done();
869
+ }
870
+ return toolResult<ReadToolDetails>(details)
871
+ .text(prependSuffixResolutionNotice(renderRow(row), resolvedSqlitePath.suffixResolution))
872
+ .sourcePath(resolvedSqlitePath.absolutePath)
873
+ .done();
874
+ }
875
+ case "query": {
876
+ const page = queryRows(db, selector.table, selector);
877
+ return toolResult<ReadToolDetails>(details)
878
+ .text(
879
+ prependSuffixResolutionNotice(
880
+ renderTable(page.columns, page.rows, {
881
+ totalCount: page.totalCount,
882
+ offset: selector.offset,
883
+ limit: selector.limit,
884
+ table: selector.table,
885
+ dbPath: resolvedSqlitePath.absolutePath,
886
+ }),
887
+ resolvedSqlitePath.suffixResolution,
888
+ ),
889
+ )
890
+ .sourcePath(resolvedSqlitePath.absolutePath)
891
+ .done();
892
+ }
893
+ case "raw": {
894
+ const result = executeReadQuery(db, selector.sql);
895
+ return toolResult<ReadToolDetails>(details)
896
+ .text(
897
+ prependSuffixResolutionNotice(
898
+ renderTable(result.columns, result.rows, {
899
+ totalCount: result.rows.length,
900
+ offset: 0,
901
+ limit: result.rows.length || DEFAULT_MAX_LINES,
902
+ table: "query",
903
+ dbPath: resolvedSqlitePath.absolutePath,
904
+ }),
905
+ resolvedSqlitePath.suffixResolution,
906
+ ),
907
+ )
908
+ .sourcePath(resolvedSqlitePath.absolutePath)
909
+ .done();
910
+ }
911
+ }
912
+
913
+ throw new ToolError("Unsupported SQLite selector");
914
+ } catch (error) {
915
+ if (error instanceof ToolError) {
916
+ throw error;
917
+ }
918
+ throw new ToolError(error instanceof Error ? error.message : String(error));
919
+ } finally {
920
+ db?.close();
921
+ }
922
+ }
923
+
712
924
  async execute(
713
925
  _toolCallId: string,
714
926
  params: ReadParams,
@@ -769,6 +981,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
769
981
  return this.#readArchive(readPath, offset, limit, archivePath, signal);
770
982
  }
771
983
 
984
+ const sqlitePath = await this.#resolveSqliteReadPath(readPath, signal);
985
+ if (sqlitePath) {
986
+ return this.#readSqlite(sel, sqlitePath, signal);
987
+ }
988
+
772
989
  let absolutePath = resolveReadPath(localReadPath, this.session.cwd);
773
990
  let suffixResolution: { from: string; to: string } | undefined;
774
991
 
@@ -14,7 +14,7 @@ import type { Theme } from "../modes/theme/theme";
14
14
  import { formatDimensionNote, type ResizedImage } from "../utils/image-resize";
15
15
 
16
16
  export { Ellipsis } from "@oh-my-pi/pi-natives";
17
- export { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
17
+ export { replaceTabs, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
18
18
 
19
19
  // =============================================================================
20
20
  // Standardized Display Constants