@oh-my-pi/pi-coding-agent 18.12.4 → 18.12.5

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.
@@ -30,20 +30,28 @@ import type { ToolSession } from "./index.js";
30
30
  * the read.
31
31
  */
32
32
  export declare function scanConflictLines(lines: readonly string[], firstLineNumber: number): ConflictBlock[];
33
+ /** Result of {@link scanFileForConflicts}. */
34
+ export interface ConflictFileScan {
35
+ blocks: ConflictBlock[];
36
+ /** First line the byte cap left unscanned (possibly partial); unset when the whole file was scanned. */
37
+ truncatedAtLine?: number;
38
+ }
33
39
  /**
34
40
  * Scan a whole file for unresolved conflict blocks.
35
41
  *
36
42
  * Reads at most `maxBytes` (default 10 MB) so this stays cheap on
37
- * pathological files. Files truncated by the cap report
38
- * `scanTruncated: true`; only complete blocks within the scanned prefix
39
- * are returned, so trailing partial markers never invent fake blocks.
43
+ * pathological files. Files truncated by the cap report the line the scan
44
+ * stopped at; only complete blocks within the scanned prefix are returned,
45
+ * so trailing partial markers never invent fake blocks.
40
46
  */
41
47
  export declare function scanFileForConflicts(absolutePath: string, options?: {
42
48
  maxBytes?: number;
43
- }): Promise<{
44
- blocks: ConflictBlock[];
45
- scanTruncated: boolean;
46
- }>;
49
+ }): Promise<ConflictFileScan>;
50
+ /**
51
+ * Note for a scan the byte cap stopped at `line`: `:conflicts` cannot list or
52
+ * register blocks from there on, but a ranged read that shows them registers them.
53
+ */
54
+ export declare function formatScanCapNote(displayPath: string, line: number): string;
47
55
  /** Per-session log of conflict regions surfaced by `read`. */
48
56
  export declare class ConflictHistory {
49
57
  #private;
@@ -180,8 +188,8 @@ export interface FormatConflictWarningOptions {
180
188
  totalInFile?: number;
181
189
  /** Display path used inside the `:conflicts` hint. */
182
190
  displayPath?: string;
183
- /** Whether the underlying file scan hit its byte cap. */
184
- scanTruncated?: boolean;
191
+ /** Line where the underlying file scan hit its byte cap, if it did. */
192
+ truncatedAtLine?: number;
185
193
  }
186
194
  export declare function formatConflictWarning(entries: readonly ConflictEntry[], options?: FormatConflictWarningOptions): string;
187
195
  /**
@@ -191,5 +199,5 @@ export declare function formatConflictWarning(entries: readonly ConflictEntry[],
191
199
  */
192
200
  export declare function formatConflictSummary(entries: readonly ConflictEntry[], options?: {
193
201
  displayPath: string;
194
- scanTruncated?: boolean;
202
+ truncatedAtLine?: number;
195
203
  }): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "18.12.4",
3
+ "version": "18.12.5",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "keywords": [
6
6
  "agent",
@@ -543,17 +543,17 @@
543
543
  },
544
544
  "dependencies": {
545
545
  "@babel/parser": "^7.29.7",
546
- "@oh-my-pi/omp-stats": "18.12.4",
547
- "@oh-my-pi/omptype": "18.12.4",
548
- "@oh-my-pi/pi-agent-core": "18.12.4",
549
- "@oh-my-pi/pi-ai": "18.12.4",
550
- "@oh-my-pi/pi-catalog": "18.12.4",
551
- "@oh-my-pi/pi-mnemopi": "18.12.4",
552
- "@oh-my-pi/pi-natives": "18.12.4",
553
- "@oh-my-pi/pi-tui": "18.12.4",
554
- "@oh-my-pi/pi-utils": "18.12.4",
555
- "@oh-my-pi/pi-wire": "18.12.4",
556
- "@oh-my-pi/snapcompact": "18.12.4",
546
+ "@oh-my-pi/omp-stats": "18.12.5",
547
+ "@oh-my-pi/omptype": "18.12.5",
548
+ "@oh-my-pi/pi-agent-core": "18.12.5",
549
+ "@oh-my-pi/pi-ai": "18.12.5",
550
+ "@oh-my-pi/pi-catalog": "18.12.5",
551
+ "@oh-my-pi/pi-mnemopi": "18.12.5",
552
+ "@oh-my-pi/pi-natives": "18.12.5",
553
+ "@oh-my-pi/pi-tui": "18.12.5",
554
+ "@oh-my-pi/pi-utils": "18.12.5",
555
+ "@oh-my-pi/pi-wire": "18.12.5",
556
+ "@oh-my-pi/snapcompact": "18.12.5",
557
557
  "@opentelemetry/api": "^1.9.1",
558
558
  "@opentelemetry/api-logs": "^0.220.0",
559
559
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -739,7 +739,6 @@ async function fetchLatestManifest(
739
739
  }
740
740
  return manifest;
741
741
  };
742
- const isPackument = (body: unknown): boolean => isRecord(body) && isRecord(body["dist-tags"]);
743
742
 
744
743
  let response = await get(npmRegistryPackageUrl(registry, pkg, tag));
745
744
  let data: unknown;
@@ -752,10 +751,16 @@ async function fetchLatestManifest(
752
751
  if (!origin || !(err instanceof SyntaxError)) throw err;
753
752
  useFullPackument = true;
754
753
  }
755
- if (isPackument(data)) {
756
- // Some registries (e.g. Sonatype Nexus) answer the dist-tag shortcut
757
- // with the full packument instead of the tagged version manifest.
758
- data = fromPackument(data);
754
+ const distTags = isRecord(data) ? data["dist-tags"] : undefined;
755
+ if (isRecord(distTags)) {
756
+ if (typeof distTags[tag] === "string" || !origin) {
757
+ // Some registries answer the dist-tag shortcut with a full packument.
758
+ data = fromPackument(data);
759
+ } else {
760
+ // Artifactory can add empty packument fields to a tagged manifest;
761
+ // its full packument may select a different, installable version.
762
+ useFullPackument = true;
763
+ }
759
764
  } else if (origin && !(isRecord(data) && typeof data.version === "string")) {
760
765
  useFullPackument = true;
761
766
  }
@@ -2,7 +2,7 @@
2
2
  description: "Use `import type`, not `import('pkg').Type` in type positions"
3
3
  condition:
4
4
  - "\\btypeof\\s+import\\("
5
- - "\\bimport\\((?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"[^\"\\n]*\"|'[^'\\n]*'|\\x60[^\\x60]*\\x60)(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:,[^()]*)?\\)\\s*(?:\\[\\s*[\"']|\\.\\s*(?!(?:then|catch|finally)\\b)[A-Za-z_$][\\w$]*(?=[^\\w$]))"
5
+ - "\\bimport\\((?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"[^\"\\n]*\"|'[^'\\n]*'|\\x60[^\\x60]*\\x60)(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:,[^()]*)?\\)\\s*(?:\\[\\s*[\"']|\\.\\s*(?!(?:then|catch|finally)(?![\\w$\\u0080-\\uffff]))[A-Za-z_$\\u0080-\\uffff][\\w$\\u0080-\\uffff]*(?=[^\\w$\\u0080-\\uffff]))"
6
6
  scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
7
7
  interruptMode: never
8
8
  ---
@@ -2,7 +2,7 @@
2
2
  description: Prefer Record<K, V> for small static literals; use Set/Map for anything dynamic
3
3
  condition:
4
4
  - "\\bnew\\s+Set\\s*(?:<(?:[^<>;()=]|=>?|<[^<>;]*>|\\([^()]*\\))*>)?\\s*\\((?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60$\\\\]|\\\\[\\s\\S]|\\$(?!\\{))*\\x60)(?:(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60$\\\\]|\\\\[\\s\\S]|\\$(?!\\{))*\\x60))*(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,?(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\]"
5
- - "\\bnew\\s+Map\\s*(?:<(?:[^<>;()=]|=>?|<[^<>;]*>|\\([^()]*\\))*>)?\\s*\\((?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60$\\\\]|\\\\[\\s\\S]|\\$(?!\\{))*\\x60)(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:[^\\[\\]\"'\\x60]|\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60\\\\]|\\\\[\\s\\S])*\\x60|\\[[^\\[\\]]*\\])*\\](?:(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60$\\\\]|\\\\[\\s\\S]|\\$(?!\\{))*\\x60)(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:[^\\[\\]\"'\\x60]|\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60\\\\]|\\\\[\\s\\S])*\\x60|\\[[^\\[\\]]*\\])*\\])*(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,?(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\]"
5
+ - "\\bnew\\s+Map(?![^\\n]{1000})\\s*(?:<(?:[^<>;()=]|=>?|<[^<>;]*>|\\([^()]*\\))*>)?\\s*\\((?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60$\\\\]|\\\\[\\s\\S]|\\$(?!\\{))*\\x60)(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:[^\\[\\]\"'\\x60]|\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60\\\\]|\\\\[\\s\\S])*\\x60|\\[[^\\[\\]]*\\])*\\](?:\\s*as\\s+const\\b)?(?:(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\[(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60$\\\\]|\\\\[\\s\\S]|\\$(?!\\{))*\\x60)(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,(?:[^\\[\\]\"'\\x60]|\"(?:[^\"\\\\\\n]|\\\\.)*\"|'(?:[^'\\\\\\n]|\\\\.)*'|\\x60(?:[^\\x60\\\\]|\\\\[\\s\\S])*\\x60|\\[[^\\[\\]]*\\])*\\](?:\\s*as\\s+const\\b)?)*(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*,?(?:\\s|//.*|/\\*(?:[^*]|\\*(?!/))*\\*/)*\\]"
6
6
  scope: "tool:edit(**/*.{ts,tsx}), tool:write(**/*.{ts,tsx})"
7
7
  interruptMode: never
8
8
  ---
@@ -1167,8 +1167,9 @@ export const cfgProvidersCacheRetention = register({
1167
1167
  },
1168
1168
  {
1169
1169
  value: "long",
1170
- label: "Long (1h)",
1171
- description: "1h TTL where the provider supports it; pricier writes, only warmed during active runs",
1170
+ label: "Long (1h+)",
1171
+ description:
1172
+ "Longer TTL where supported (Anthropic/Bedrock 1h, OpenAI GPT-5.6+ Responses 12h); pricier writes, only warmed during active runs",
1172
1173
  },
1173
1174
  { value: "none", label: "Off", description: "Disable prompt caching and cache-affinity routing" },
1174
1175
  ],
package/src/task/index.ts CHANGED
@@ -792,6 +792,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
792
792
  };
793
793
  }
794
794
 
795
+ /**
796
+ * Thinking override for a spawn. `task.enableEffort` gates the caller's
797
+ * `effort` at runtime, not only in the advertised schema: stale transcripts
798
+ * and lenient argument fallbacks still deliver the field, and honoring it
799
+ * would override the agent's configured thinking selector.
800
+ */
801
+ #effortParams(params: TaskParams): Pick<StructuredSubagentRequest, "effort"> {
802
+ if (params.effort === undefined || !cfgTaskEnableEffort.get(this.session.settings)) return {};
803
+ return { effort: params.effort };
804
+ }
805
+
795
806
  #seanceSchemaFields() {
796
807
  const spawnPolicy = resolveSpawnPolicy(this.session.getSessionSpawns());
797
808
  const planMode = this.session.getPlanModeState?.()?.enabled === true;
@@ -911,7 +922,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
911
922
  sourceSession: params.sourceSession,
912
923
  model: params.model,
913
924
  ...this.#resultParams(params),
914
- ...(params.effort !== undefined ? { effort: params.effort } : {}),
925
+ ...this.#effortParams(params),
915
926
  ...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
916
927
  blockedAgent: this.#blockedAgent,
917
928
  enableLsp: (this.session.enableLsp ?? true) && cfgTaskEnableLsp.get(this.session.settings),
@@ -1738,7 +1749,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1738
1749
  sourceSession: params.sourceSession,
1739
1750
  model: params.model,
1740
1751
  ...this.#resultParams(params),
1741
- ...(params.effort !== undefined ? { effort: params.effort } : {}),
1752
+ ...this.#effortParams(params),
1742
1753
  solutionSpace: params.solutionSpace,
1743
1754
  ...(params.tools?.length
1744
1755
  ? {
@@ -123,18 +123,25 @@ export function scanConflictLines(lines: readonly string[], firstLineNumber: num
123
123
 
124
124
  const SCAN_FILE_DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
125
125
 
126
+ /** Result of {@link scanFileForConflicts}. */
127
+ export interface ConflictFileScan {
128
+ blocks: ConflictBlock[];
129
+ /** First line the byte cap left unscanned (possibly partial); unset when the whole file was scanned. */
130
+ truncatedAtLine?: number;
131
+ }
132
+
126
133
  /**
127
134
  * Scan a whole file for unresolved conflict blocks.
128
135
  *
129
136
  * Reads at most `maxBytes` (default 10 MB) so this stays cheap on
130
- * pathological files. Files truncated by the cap report
131
- * `scanTruncated: true`; only complete blocks within the scanned prefix
132
- * are returned, so trailing partial markers never invent fake blocks.
137
+ * pathological files. Files truncated by the cap report the line the scan
138
+ * stopped at; only complete blocks within the scanned prefix are returned,
139
+ * so trailing partial markers never invent fake blocks.
133
140
  */
134
141
  export async function scanFileForConflicts(
135
142
  absolutePath: string,
136
143
  options: { maxBytes?: number } = {},
137
- ): Promise<{ blocks: ConflictBlock[]; scanTruncated: boolean }> {
144
+ ): Promise<ConflictFileScan> {
138
145
  const maxBytes = options.maxBytes ?? SCAN_FILE_DEFAULT_MAX_BYTES;
139
146
  const file = Bun.file(absolutePath);
140
147
  const size = file.size;
@@ -144,7 +151,16 @@ export async function scanFileForConflicts(
144
151
  // `split("\n")` over a truncated read may leave a partial last line; the
145
152
  // scanner already tolerates an unclosed opener, so no extra trimming.
146
153
  const lines = text.split("\n");
147
- return { blocks: scanConflictLines(lines, 1), scanTruncated: truncated };
154
+ const blocks = scanConflictLines(lines, 1);
155
+ return truncated ? { blocks, truncatedAtLine: lines.length } : { blocks };
156
+ }
157
+
158
+ /**
159
+ * Note for a scan the byte cap stopped at `line`: `:conflicts` cannot list or
160
+ * register blocks from there on, but a ranged read that shows them registers them.
161
+ */
162
+ export function formatScanCapNote(displayPath: string, line: number): string {
163
+ return `- note: the conflict scan stopped at the byte cap at line ${line}; blocks from there on are not counted or registered here — range-read \`${displayPath}:${line}-\` to show and register them.`;
148
164
  }
149
165
 
150
166
  /**
@@ -593,8 +609,8 @@ export interface FormatConflictWarningOptions {
593
609
  totalInFile?: number;
594
610
  /** Display path used inside the `:conflicts` hint. */
595
611
  displayPath?: string;
596
- /** Whether the underlying file scan hit its byte cap. */
597
- scanTruncated?: boolean;
612
+ /** Line where the underlying file scan hit its byte cap, if it did. */
613
+ truncatedAtLine?: number;
598
614
  }
599
615
 
600
616
  export function formatConflictWarning(
@@ -615,8 +631,8 @@ export function formatConflictWarning(
615
631
  } else {
616
632
  out.push(`⚠ ${total} unresolved ${word} detected`);
617
633
  }
618
- if (options.scanTruncated) {
619
- out.push("- note: file scan hit the byte cap; additional conflicts may exist beyond the scanned prefix.");
634
+ if (options.truncatedAtLine !== undefined) {
635
+ out.push(formatScanCapNote(options.displayPath ?? "<file>", options.truncatedAtLine));
620
636
  }
621
637
 
622
638
  const oursLabel = pickLabel(entries, e => e.oursLabel);
@@ -679,14 +695,14 @@ export function formatConflictWarning(
679
695
  */
680
696
  export function formatConflictSummary(
681
697
  entries: readonly ConflictEntry[],
682
- options: { displayPath: string; scanTruncated?: boolean } = { displayPath: "" },
698
+ options: { displayPath: string; truncatedAtLine?: number } = { displayPath: "" },
683
699
  ): string {
684
700
  const lines: string[] = [];
685
701
  const total = entries.length;
686
702
  const word = total === 1 ? "conflict" : "conflicts";
687
703
  lines.push(`⚠ ${total} unresolved ${word} in ${options.displayPath || "<file>"}`);
688
- if (options.scanTruncated) {
689
- lines.push("- note: file scan hit the byte cap; additional conflicts may exist beyond the scanned prefix.");
704
+ if (options.truncatedAtLine !== undefined) {
705
+ lines.push(formatScanCapNote(options.displayPath || "<file>", options.truncatedAtLine));
690
706
  }
691
707
  const oursLabel = pickLabel(entries, e => e.oursLabel);
692
708
  const theirsLabel = pickLabel(entries, e => e.theirsLabel);
package/src/tools/read.ts CHANGED
@@ -77,6 +77,7 @@ import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
77
77
  import {
78
78
  formatConflictSummary,
79
79
  formatConflictWarning,
80
+ formatScanCapNote,
80
81
  getConflictHistory,
81
82
  recoverConflictUriPrefix,
82
83
  scanConflictLines,
@@ -2760,18 +2761,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2760
2761
  // Cheap full-file scan only when the window already showed
2761
2762
  // at least one conflict — otherwise pay nothing on clean files.
2762
2763
  let totalInFile = entries.length;
2763
- let scanTruncated = false;
2764
+ let truncatedAtLine: number | undefined;
2764
2765
  try {
2765
2766
  const fileScan = await scanFileForConflicts(absolutePath);
2766
2767
  totalInFile = Math.max(entries.length, fileScan.blocks.length);
2767
- scanTruncated = fileScan.scanTruncated;
2768
+ truncatedAtLine = fileScan.truncatedAtLine;
2768
2769
  } catch {
2769
2770
  // Best-effort enrichment; fall back to window-only count.
2770
2771
  }
2771
2772
  outputText += formatConflictWarning(entries, {
2772
2773
  totalInFile,
2773
2774
  displayPath: displayPathForWarning,
2774
- scanTruncated,
2775
+ truncatedAtLine,
2775
2776
  });
2776
2777
  details.conflictCount = entries.length;
2777
2778
  }
@@ -2853,10 +2854,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2853
2854
  }),
2854
2855
  );
2855
2856
 
2856
- const summary =
2857
- entries.length === 0
2858
- ? `No unresolved git merge conflicts in ${displayPath}.`
2859
- : formatConflictSummary(entries, { displayPath, scanTruncated: scan.scanTruncated });
2857
+ let summary: string;
2858
+ if (entries.length > 0) {
2859
+ summary = formatConflictSummary(entries, { displayPath, truncatedAtLine: scan.truncatedAtLine });
2860
+ } else if (scan.truncatedAtLine !== undefined) {
2861
+ summary = `No unresolved git merge conflicts before line ${scan.truncatedAtLine} of ${displayPath}.\n${formatScanCapNote(displayPath, scan.truncatedAtLine)}`;
2862
+ } else {
2863
+ summary = `No unresolved git merge conflicts in ${displayPath}.`;
2864
+ }
2860
2865
 
2861
2866
  const details: ReadToolDetails = {
2862
2867
  resolvedPath: absolutePath,