@oh-my-pi/pi-coding-agent 17.1.1 → 17.1.3

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 (105) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/cli.js +6317 -4085
  3. package/dist/types/cli/bench-cli.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +6 -3
  5. package/dist/types/config/settings-schema.d.ts +4 -0
  6. package/dist/types/launch/broker-list-order.test.d.ts +1 -0
  7. package/dist/types/modes/interactive-mode.d.ts +2 -6
  8. package/dist/types/modes/types.d.ts +8 -5
  9. package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
  10. package/dist/types/task/executor.d.ts +3 -1
  11. package/dist/types/task/structured-subagent.d.ts +3 -0
  12. package/dist/types/task/types.d.ts +11 -11
  13. package/dist/types/thinking.d.ts +13 -0
  14. package/dist/types/tools/auto-generated-guard.d.ts +5 -2
  15. package/dist/types/tools/eval-format/index.d.ts +7 -0
  16. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  17. package/dist/types/tools/eval-format/julia.d.ts +2 -0
  18. package/dist/types/tools/eval-format/python.d.ts +2 -0
  19. package/dist/types/tools/eval-format/ruby.d.ts +2 -0
  20. package/dist/types/tools/memory-render.d.ts +1 -4
  21. package/dist/types/tools/resolve.d.ts +7 -0
  22. package/dist/types/tools/todo.d.ts +4 -1
  23. package/dist/types/web/search/providers/base.d.ts +16 -0
  24. package/dist/types/web/search/providers/brave.d.ts +2 -0
  25. package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
  26. package/dist/types/web/search/providers/gemini.d.ts +3 -0
  27. package/dist/types/web/search/providers/jina.d.ts +2 -0
  28. package/dist/types/web/search/providers/kagi.d.ts +2 -0
  29. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  30. package/dist/types/web/search/providers/parallel.d.ts +2 -0
  31. package/dist/types/web/search/providers/perplexity.d.ts +3 -0
  32. package/dist/types/web/search/providers/searxng.d.ts +10 -0
  33. package/dist/types/web/search/providers/tavily.d.ts +8 -0
  34. package/dist/types/web/search/query.d.ts +190 -0
  35. package/package.json +12 -12
  36. package/src/cli/bench-cli.ts +12 -1
  37. package/src/cli/web-search-cli.ts +7 -0
  38. package/src/config/model-resolver.ts +17 -6
  39. package/src/config/settings-schema.ts +5 -0
  40. package/src/debug/raw-sse-buffer.ts +157 -30
  41. package/src/edit/hashline/filesystem.ts +1 -1
  42. package/src/edit/modes/patch.ts +1 -1
  43. package/src/export/share.ts +4 -3
  44. package/src/launch/broker-list-order.test.ts +89 -0
  45. package/src/launch/broker.ts +49 -8
  46. package/src/modes/controllers/input-controller.ts +8 -8
  47. package/src/modes/interactive-mode.ts +60 -5
  48. package/src/modes/types.ts +9 -4
  49. package/src/modes/utils/ui-helpers.ts +7 -8
  50. package/src/prompts/system/resolve-device-reminder.md +1 -1
  51. package/src/prompts/system/workflow-notice.md +1 -1
  52. package/src/prompts/tools/ast-edit.md +2 -1
  53. package/src/prompts/tools/bash.md +1 -0
  54. package/src/prompts/tools/eval.md +2 -2
  55. package/src/prompts/tools/task.md +5 -2
  56. package/src/prompts/tools/web-search.md +2 -0
  57. package/src/sdk.ts +8 -4
  58. package/src/session/agent-session.ts +14 -0
  59. package/src/session/queued-messages.ts +7 -1
  60. package/src/session/stream-guards.ts +1 -1
  61. package/src/task/executor.ts +17 -9
  62. package/src/task/index.ts +14 -34
  63. package/src/task/structured-subagent.ts +4 -0
  64. package/src/task/types.ts +15 -13
  65. package/src/thinking.ts +27 -0
  66. package/src/tools/ast-edit.ts +4 -1
  67. package/src/tools/auto-generated-guard.ts +18 -5
  68. package/src/tools/bash.ts +13 -0
  69. package/src/tools/browser/render.ts +2 -1
  70. package/src/tools/eval-format/index.ts +24 -0
  71. package/src/tools/eval-format/javascript.ts +952 -0
  72. package/src/tools/eval-format/julia.ts +446 -0
  73. package/src/tools/eval-format/python.ts +544 -0
  74. package/src/tools/eval-format/ruby.ts +380 -0
  75. package/src/tools/eval-render.ts +12 -6
  76. package/src/tools/memory-render.ts +11 -2
  77. package/src/tools/render-utils.ts +8 -3
  78. package/src/tools/resolve.ts +10 -1
  79. package/src/tools/todo.ts +58 -6
  80. package/src/tools/write.ts +1 -1
  81. package/src/web/search/index.ts +28 -5
  82. package/src/web/search/providers/anthropic.ts +62 -4
  83. package/src/web/search/providers/base.ts +16 -0
  84. package/src/web/search/providers/brave.ts +30 -3
  85. package/src/web/search/providers/codex.ts +14 -1
  86. package/src/web/search/providers/duckduckgo.ts +23 -1
  87. package/src/web/search/providers/ecosia.ts +5 -1
  88. package/src/web/search/providers/exa.ts +28 -1
  89. package/src/web/search/providers/firecrawl.ts +37 -3
  90. package/src/web/search/providers/gemini.ts +12 -2
  91. package/src/web/search/providers/google.ts +2 -1
  92. package/src/web/search/providers/jina.ts +31 -8
  93. package/src/web/search/providers/kagi.ts +10 -1
  94. package/src/web/search/providers/kimi.ts +16 -1
  95. package/src/web/search/providers/mojeek.ts +14 -1
  96. package/src/web/search/providers/parallel.ts +48 -2
  97. package/src/web/search/providers/perplexity.ts +94 -6
  98. package/src/web/search/providers/searxng.ts +145 -9
  99. package/src/web/search/providers/startpage.ts +8 -2
  100. package/src/web/search/providers/synthetic.ts +7 -1
  101. package/src/web/search/providers/tavily.ts +52 -3
  102. package/src/web/search/providers/tinyfish.ts +6 -1
  103. package/src/web/search/providers/xai.ts +49 -2
  104. package/src/web/search/providers/zai.ts +19 -1
  105. package/src/web/search/query.ts +850 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Structured web-search query parsing.
3
+ *
4
+ * Agents habitually embed Google-style directives in search queries —
5
+ * `site:`, `before:`/`after:`, `inurl:`, `filetype:`, quoted phrases, `OR`
6
+ * groups, `-exclusions` — regardless of whether the backing engine parses
7
+ * them. This module turns a raw query into a {@link StructuredQuery} so each
8
+ * provider can:
9
+ *
10
+ * 1. map constraints onto native API parameters where they exist (Perplexity
11
+ * `search_domain_filter`, Tavily `include_domains`, Exa date bounds, …),
12
+ * 2. rebuild a query string containing only the syntax the target engine
13
+ * understands ({@link formatQuery}), and
14
+ * 3. post-filter returned sources leniently ({@link applyQueryConstraints}):
15
+ * a constraint dimension that would eliminate every result is dropped and
16
+ * reported rather than returning nothing.
17
+ */
18
+ import type { SearchSource } from "./types.js";
19
+ /** One free-text token of the query (everything that is not a recognized directive). */
20
+ export interface QueryTerm {
21
+ /** Term text without quotes or operator prefixes. */
22
+ text: string;
23
+ /** Quoted exact phrase (`"like this"`) or verbatim-required (`+term`). */
24
+ phrase?: boolean;
25
+ /** Excluded via `-term` or `NOT term`. */
26
+ negated?: boolean;
27
+ /**
28
+ * OR-group id. Terms sharing an id are alternatives (`a OR b`); terms
29
+ * without a group are implicitly AND-ed. Groups are always contiguous
30
+ * runs in {@link StructuredQuery.terms}.
31
+ */
32
+ group?: number;
33
+ }
34
+ /**
35
+ * A raw query decomposed into free text plus every recognized constraint.
36
+ *
37
+ * All list fields are always present (possibly empty) so consumers can map
38
+ * over them without null checks. Values are stored as typed by the user
39
+ * except for normalization noted per field.
40
+ */
41
+ export interface StructuredQuery {
42
+ /** Original query string, verbatim. */
43
+ raw: string;
44
+ /**
45
+ * Free-text remainder with all recognized directives removed; phrases
46
+ * stay quoted, exclusions keep `-`, OR groups keep `OR`. Empty when the
47
+ * query was directives only — use {@link formatQuery} for a never-empty
48
+ * engine query.
49
+ */
50
+ text: string;
51
+ /** Ordered free-text terms (phrases, exclusions, OR groups). */
52
+ terms: QueryTerm[];
53
+ /** `site:`/`domain:`/`host:` includes — any-of. Lowercased, scheme stripped, may carry a path (`github.com/anthropics`). */
54
+ sites: string[];
55
+ /** `-site:` exclusions, same normalization as {@link sites}. */
56
+ excludedSites: string[];
57
+ /** `inurl:`/`url:`/`allinurl:` substrings — all must appear in the URL. */
58
+ inUrl: string[];
59
+ /** `-inurl:` substrings — none may appear in the URL. */
60
+ excludedInUrl: string[];
61
+ /** `intitle:`/`title:`/`allintitle:` substrings — all must appear in the title. */
62
+ inTitle: string[];
63
+ /** `-intitle:` substrings — none may appear in the title. */
64
+ excludedInTitle: string[];
65
+ /** `intext:`/`inbody:`/`inanchor:`/`allintext:` body substrings. Not post-filterable (snippets are partial); query-building only. */
66
+ inText: string[];
67
+ /** `-intext:` body exclusions. Query-building only. */
68
+ excludedInText: string[];
69
+ /** `filetype:`/`ext:` extensions — any-of. Lowercased, no leading dot. */
70
+ filetypes: string[];
71
+ /** `-filetype:`/`-ext:` extensions — none may match. */
72
+ excludedFiletypes: string[];
73
+ /** Inclusive lower publish-date bound from `after:`/`since:`, ISO `YYYY-MM-DD`. */
74
+ after?: string;
75
+ /** Exclusive upper publish-date bound from `before:`/`until:`, ISO `YYYY-MM-DD`. */
76
+ before?: string;
77
+ /** Language code from `lang:`/`language:`, lowercased (e.g. `en`, `en-us`). */
78
+ lang?: string;
79
+ /** True when any directive or boolean operator was recognized. */
80
+ hasDirectives: boolean;
81
+ /** True when any post-filterable constraint is set (sites, url/title terms, filetypes, date bounds). */
82
+ hasConstraints: boolean;
83
+ }
84
+ /**
85
+ * Query-syntax capabilities of a target engine, used by {@link formatQuery}
86
+ * to decide which parsed features are re-emitted as query text. Everything
87
+ * defaults to `false`: the zero-value produces plain keywords suitable for
88
+ * natural-language APIs.
89
+ */
90
+ export interface QuerySyntax {
91
+ /** Emit `"quoted phrases"`. */
92
+ phrases?: boolean;
93
+ /** Emit `-term` exclusions (negated terms are dropped otherwise). */
94
+ negation?: boolean;
95
+ /** Emit `OR` between alternatives (groups are flattened to keywords otherwise). */
96
+ or?: boolean;
97
+ /** Emit `site:`/`-site:`. */
98
+ site?: boolean;
99
+ /** Emit `inurl:`/`-inurl:`. */
100
+ inUrl?: boolean;
101
+ /** Emit `intitle:`/`-intitle:`. */
102
+ inTitle?: boolean;
103
+ /** Emit `intext:`/`-intext:`. */
104
+ inText?: boolean;
105
+ /** Emit `filetype:`/`-filetype:`. */
106
+ filetype?: boolean;
107
+ /** Emit `before:`/`after:` ISO date bounds. */
108
+ dateRange?: boolean;
109
+ }
110
+ /** Full Google-style syntax: engines that parse the classic operator set (Google, Startpage, Ecosia, Brave, Kagi, Mojeek, SearXNG…). */
111
+ export declare const GOOGLE_QUERY_SYNTAX: QuerySyntax;
112
+ /** Result of {@link applyQueryConstraints}. */
113
+ export interface ConstraintFilterResult {
114
+ /** Sources surviving the lenient filter — never empty when the input was non-empty. */
115
+ sources: SearchSource[];
116
+ /**
117
+ * Directive renderings (`site:arxiv.org`, `before:2024-01-01`, …) of the
118
+ * constraint dimensions that matched zero sources and were therefore
119
+ * relaxed instead of enforced.
120
+ */
121
+ dropped: string[];
122
+ }
123
+ /**
124
+ * Parse a `before:`/`after:` value into ISO `YYYY-MM-DD`.
125
+ * Accepts `YYYY`, `YYYY-MM`, `YYYY-MM-DD` (also `/` and `.` separators) and
126
+ * `MM/DD/YYYY` (day-first assumed when the first field exceeds 12).
127
+ * Bare years/months resolve to the first day of the period, matching
128
+ * Google's `after:2024` ≙ `after:2024-01-01` semantics.
129
+ */
130
+ export declare function parseDateValue(value: string): string | undefined;
131
+ /**
132
+ * Parse a raw query into a {@link StructuredQuery}.
133
+ *
134
+ * Lenient by construction: unknown `name:value` tokens (URLs, `C:\paths`,
135
+ * `TS2345:`, jargon) stay in the free text verbatim, and a directive with an
136
+ * unparseable value (`before:someday`) degrades to a plain term instead of
137
+ * being dropped.
138
+ */
139
+ export declare function parseSearchQuery(raw: string): StructuredQuery;
140
+ /**
141
+ * Rebuild a query string for an engine with the given {@link QuerySyntax}.
142
+ *
143
+ * Constraints whose syntax the engine lacks are omitted (the caller maps
144
+ * them onto API parameters or relies on {@link applyQueryConstraints}).
145
+ * Never returns an empty string for a non-empty input: a directives-only
146
+ * query falls back to the constraint values as keywords, then to `raw` — an
147
+ * engine searching *something* beats an empty-query error.
148
+ */
149
+ export declare function formatQuery(q: StructuredQuery, syntax?: QuerySyntax): string;
150
+ /**
151
+ * Build the engine query for a credential-free HTML engine (Google,
152
+ * Startpage, DuckDuckGo, Ecosia, Mojeek, SearXNG, and the Public Web
153
+ * fan-out over them).
154
+ *
155
+ * Canonicalizes directives via {@link formatQuery} with the engine's
156
+ * {@link QuerySyntax} (default: full Google syntax), after demoting the
157
+ * operators that zero-match across the scraper set: engines only match
158
+ * `site:` against a bare domain (a path yields zero results everywhere),
159
+ * and DuckDuckGo ignores `inurl:` entirely — so either operator silently
160
+ * empties the result set. The raw URL as a plain term matches fine, so
161
+ * bare-domain `site:` filters are kept while path-carrying `site:` and all
162
+ * `inurl:` values become plain keywords; the demotion is structural (before
163
+ * formatting), so OR-grouped and quoted directives are covered. Negated
164
+ * forms (`-site:`, `-inurl:`) pass through untouched — demoting them would
165
+ * invert an exclusion into a search term; the pipeline post-filter
166
+ * ({@link applyQueryConstraints}) enforces every demoted or unsupported
167
+ * constraint on the returned sources. Directive-free queries pass through
168
+ * byte-identical.
169
+ */
170
+ export declare function formatScraperQuery(query: string, parsedQuery?: StructuredQuery, syntax?: QuerySyntax): string;
171
+ /**
172
+ * `site:` matcher: exact host or subdomain of `site`; when `site` carries a
173
+ * path (`github.com/anthropics`), the URL path must start with it.
174
+ */
175
+ export declare function matchesSite(url: string, site: string): boolean;
176
+ /**
177
+ * Strict per-source constraint check: every filterable dimension of `q` must
178
+ * pass. Sources without a resolvable date pass date bounds (a missing date
179
+ * is not proof of violation). For custom provider flows; the standard path
180
+ * is {@link applyQueryConstraints}.
181
+ */
182
+ export declare function matchesQueryConstraints(source: SearchSource, q: StructuredQuery): boolean;
183
+ /**
184
+ * Lenient post-filter: applies each constraint dimension of `q` in turn,
185
+ * skipping (and reporting) any dimension that would eliminate every
186
+ * remaining source. Guarantees a non-empty result for a non-empty input, so
187
+ * a mis-scoped directive degrades to unfiltered results plus a note instead
188
+ * of a dead search.
189
+ */
190
+ export declare function applyQueryConstraints(sources: readonly SearchSource[], q: StructuredQuery): ConstraintFilterResult;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "17.1.1",
4
+ "version": "17.1.3",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -53,17 +53,17 @@
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@babel/traverse": "^7.29.7",
55
55
  "@mozilla/readability": "^0.6.0",
56
- "@oh-my-pi/hashline": "17.1.1",
57
- "@oh-my-pi/omp-stats": "17.1.1",
58
- "@oh-my-pi/pi-agent-core": "17.1.1",
59
- "@oh-my-pi/pi-ai": "17.1.1",
60
- "@oh-my-pi/pi-catalog": "17.1.1",
61
- "@oh-my-pi/pi-mnemopi": "17.1.1",
62
- "@oh-my-pi/pi-natives": "17.1.1",
63
- "@oh-my-pi/pi-tui": "17.1.1",
64
- "@oh-my-pi/pi-utils": "17.1.1",
65
- "@oh-my-pi/pi-wire": "17.1.1",
66
- "@oh-my-pi/snapcompact": "17.1.1",
56
+ "@oh-my-pi/hashline": "17.1.3",
57
+ "@oh-my-pi/omp-stats": "17.1.3",
58
+ "@oh-my-pi/pi-agent-core": "17.1.3",
59
+ "@oh-my-pi/pi-ai": "17.1.3",
60
+ "@oh-my-pi/pi-catalog": "17.1.3",
61
+ "@oh-my-pi/pi-mnemopi": "17.1.3",
62
+ "@oh-my-pi/pi-natives": "17.1.3",
63
+ "@oh-my-pi/pi-tui": "17.1.3",
64
+ "@oh-my-pi/pi-utils": "17.1.3",
65
+ "@oh-my-pi/pi-wire": "17.1.3",
66
+ "@oh-my-pi/snapcompact": "17.1.3",
67
67
  "@opentelemetry/api": "^1.9.1",
68
68
  "@opentelemetry/api-logs": "^0.220.0",
69
69
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -76,6 +76,7 @@ export interface BenchCommandArgs {
76
76
 
77
77
  export interface BenchModelRegistry {
78
78
  getAll(): Model<Api>[];
79
+ getAvailable(): Model<Api>[];
79
80
  getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined>;
80
81
  resolver(model: ApiKeyResolverModel, sessionId?: string): ApiKeyResolver;
81
82
  hasConfiguredAuth?(model: Model<Api>): boolean;
@@ -693,7 +694,17 @@ function resolveBenchModels(
693
694
  const resolved: BenchTarget[] = [];
694
695
  const errors: string[] = [];
695
696
  for (const selector of selectors) {
696
- const result = resolveCliModel({ cliModel: selector, modelRegistry, settings, preferences });
697
+ // Bench intentionally resolves against the full catalog first, then applies
698
+ // its own exact-id credential fallback below. Using the CLI resolver's
699
+ // authenticated default here would silently redirect non-equivalent bare
700
+ // ids and suppress the warning for equivalent cross-provider models.
701
+ const result = resolveCliModel({
702
+ cliModel: selector,
703
+ modelRegistry,
704
+ availableModels: modelRegistry.getAll(),
705
+ settings,
706
+ preferences,
707
+ });
697
708
  if (result.error) {
698
709
  errors.push(`${selector}: ${result.error}`);
699
710
  continue;
@@ -130,8 +130,15 @@ ${chalk.bold("Options:")}
130
130
  --compact Render condensed output
131
131
  -h, --help Show this help
132
132
 
133
+ ${chalk.bold("Query directives:")}
134
+ site:/-site: after:/before: (YYYY-MM-DD) inurl: intitle: filetype:
135
+ "exact phrase" -term OR
136
+ Mapped to native provider filters where available, otherwise applied as a
137
+ lenient post-filter (a constraint matching nothing is relaxed, not fatal).
138
+
133
139
  ${chalk.bold("Examples:")}
134
140
  ${APP_NAME} q --provider=exa "what's the color of the sky"
135
141
  ${APP_NAME} q --provider=brave --recency=week "latest TypeScript 5.7 changes"
142
+ ${APP_NAME} q 'transformer scaling site:arxiv.org after:2024 -site:reddit.com'
136
143
  `);
137
144
  }
@@ -466,7 +466,7 @@ export interface ModelMatchPreferences {
466
466
  }
467
467
 
468
468
  export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable">;
469
- type CliModelRegistry = Pick<ModelRegistry, "getAll">;
469
+ type CliModelRegistry = Pick<ModelRegistry, "getAll" | "getAvailable">;
470
470
  type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
471
471
  type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
472
472
 
@@ -1601,6 +1601,7 @@ function findExactCliModel(
1601
1601
  selector: string,
1602
1602
  allModels: Model<Api>[],
1603
1603
  availableModels: Model<Api>[],
1604
+ options?: { catalogFallback?: boolean },
1604
1605
  ): Model<Api> | undefined {
1605
1606
  // Explicit provider/id references stay authoritative against the full catalog.
1606
1607
  const referenced = findExactModelReferenceMatch(selector, allModels);
@@ -1615,6 +1616,13 @@ function findExactCliModel(
1615
1616
  model.id.toLowerCase() === lower || formatModelString(model).toLowerCase() === lower;
1616
1617
  const preferred = availableModels.find(isFlatMatch);
1617
1618
  if (preferred) return preferred;
1619
+ // The unauthenticated catalog fallback is a weak match: a bare id like
1620
+ // `default` collides with the bundled `cursor/default` model, which must not
1621
+ // shadow a configured `modelRoles.default` role the user can actually run.
1622
+ // Callers resolving a possible role name pass `catalogFallback: false` so the
1623
+ // role gets a chance first; the deferred fuzzy fallback below still recovers
1624
+ // the catalog id when no role matches.
1625
+ if (options?.catalogFallback === false) return undefined;
1618
1626
  return availableModels === allModels ? undefined : allModels.find(isFlatMatch);
1619
1627
  }
1620
1628
 
@@ -1635,13 +1643,16 @@ export interface ResolveCliModelResult {
1635
1643
  /**
1636
1644
  * Resolve a single model from CLI flags.
1637
1645
  *
1638
- * Exact model names take precedence over configured role names.
1646
+ * Explicit `provider/id` references and authenticated bare ids take precedence
1647
+ * over configured role names, which in turn take precedence over an
1648
+ * unauthenticated catalog-only id (so a bundled `cursor/default` never shadows a
1649
+ * configured `modelRoles.default`).
1639
1650
  */
1640
1651
  export function resolveCliModel(options: {
1641
1652
  cliProvider?: string;
1642
1653
  cliModel?: string;
1643
1654
  modelRegistry: CliModelRegistry;
1644
- /** Authenticated models to prefer for unqualified selectors; omit to preserve catalog-order behavior. */
1655
+ /** Authenticated models to prefer for unqualified selectors; defaults to the registry's authenticated set. */
1645
1656
  availableModels?: Model<Api>[];
1646
1657
  settings?: Settings;
1647
1658
  preferences?: ModelMatchPreferences;
@@ -1662,7 +1673,7 @@ export function resolveCliModel(options: {
1662
1673
  };
1663
1674
  }
1664
1675
 
1665
- const availableModels = preferredModels ?? allModels;
1676
+ const availableModels = preferredModels ?? modelRegistry.getAvailable();
1666
1677
  const providerMap = new Map<string, string>();
1667
1678
  for (const model of allModels) {
1668
1679
  providerMap.set(model.provider.toLowerCase(), model.provider);
@@ -1680,7 +1691,7 @@ export function resolveCliModel(options: {
1680
1691
 
1681
1692
  const trimmedModel = cliModel.trim();
1682
1693
  if (!provider) {
1683
- const exact = findExactCliModel(trimmedModel, allModels, availableModels);
1694
+ const exact = findExactCliModel(trimmedModel, allModels, availableModels, { catalogFallback: false });
1684
1695
  if (exact) {
1685
1696
  return {
1686
1697
  model: exact,
@@ -1696,7 +1707,7 @@ export function resolveCliModel(options: {
1696
1707
  MAX_THINKING_SUFFIX_OPTIONS,
1697
1708
  );
1698
1709
  if (exactThinkingLevel) {
1699
- const exactSuffixed = findExactCliModel(exactBase, allModels, availableModels);
1710
+ const exactSuffixed = findExactCliModel(exactBase, allModels, availableModels, { catalogFallback: false });
1700
1711
  if (exactSuffixed) {
1701
1712
  return {
1702
1713
  model: exactSuffixed,
@@ -5266,6 +5266,11 @@ export const SETTINGS_SCHEMA = {
5266
5266
  default: undefined,
5267
5267
  },
5268
5268
 
5269
+ "searxng.engines": {
5270
+ type: "string",
5271
+ default: undefined,
5272
+ },
5273
+
5269
5274
  "searxng.language": {
5270
5275
  type: "string",
5271
5276
  default: undefined,
@@ -3,6 +3,13 @@ import type { Model, ProviderResponseMetadata, RawSseEvent } from "@oh-my-pi/pi-
3
3
  const MAX_RAW_SSE_EVENTS = 1_000;
4
4
  const MAX_RAW_SSE_CHARS = 512_000;
5
5
  const MAX_RAW_SSE_EVENT_CHARS = 64_000;
6
+ // Reserve room for the `: omp-debug-truncated` / `: omp-debug-elided` marker
7
+ // lines so a trimmed event stays within MAX_RAW_SSE_EVENT_CHARS overall.
8
+ const TRIM_MARKER_RESERVE = 200;
9
+ // Caps applied to individual tool entries when compacting a `tools` array
10
+ // inside an oversized `data:` payload.
11
+ const MAX_TOOL_SCHEMA_CHARS = 200;
12
+ const MAX_TOOL_DESCRIPTION_CHARS = 200;
6
13
 
7
14
  export type RawSseDebugRecord =
8
15
  | {
@@ -45,42 +52,162 @@ export interface RawSseDebugSnapshot {
45
52
  // path. The parallel array keeps records as plain monomorphic objects.
46
53
  type TrimResult = { raw: string[]; truncated: boolean; originalChars: number; chars: number };
47
54
 
48
- // Single-pass trim. Returns the final `chars` count using the historical
49
- // formula `reduce(line.length + 1, init = 1)` so the new accounting matches
50
- // the previous `countRecordChars` byte-for-byte (the trailing +1 covers the
51
- // record-level newline that `rawRecordText` appends in `toRawText`).
52
- //
53
- // When the event fits within budget the input `raw` array is returned
54
- // **by reference** see the ownership contract documented at
55
- // `RawSseDebugBuffer.recordEvent` below.
56
- function trimRawLines(raw: string[]): TrimResult {
57
- let originalChars = 0;
58
- for (let i = 0; i < raw.length; i++) originalChars += raw[i].length + 1;
55
+ // `chars` uses the historical formula `reduce(line.length + 1, init = 1)` so
56
+ // the accounting matches the previous `countRecordChars` byte-for-byte (the
57
+ // trailing +1 covers the record-level newline that `rawRecordText` appends in
58
+ // `toRawText`).
59
+ function countLines(lines: readonly string[]): number {
60
+ let chars = 0;
61
+ for (let i = 0; i < lines.length; i++) chars += lines[i].length + 1;
62
+ return chars;
63
+ }
64
+
65
+ function elideText(text: string, max: number): string {
66
+ if (text.length <= max) return text;
67
+ return `${text.slice(0, max)}… (+${text.length - max} chars)`;
68
+ }
69
+
70
+ // Shrinks one tool definition in place: schemas (`parameters` for OpenAI
71
+ // shapes, `input_schema` for Anthropic) become elided JSON strings and long
72
+ // descriptions are cut, while `name`/`type` survive untouched. Chat-completions
73
+ // nests the payload under `function`.
74
+ function compactToolEntry(tool: unknown): boolean {
75
+ if (typeof tool !== "object" || tool === null) return false;
76
+ const obj = tool as Record<string, unknown>;
77
+ let changed = false;
78
+ if (typeof obj.function === "object" && obj.function !== null) {
79
+ changed = compactToolEntry(obj.function);
80
+ }
81
+ for (const key of ["parameters", "input_schema"]) {
82
+ const schema = obj[key];
83
+ if (schema === undefined || schema === null) continue;
84
+ const text = typeof schema === "string" ? schema : JSON.stringify(schema);
85
+ if (text.length <= MAX_TOOL_SCHEMA_CHARS) continue;
86
+ obj[key] = elideText(text, MAX_TOOL_SCHEMA_CHARS);
87
+ changed = true;
88
+ }
89
+ if (typeof obj.description === "string" && obj.description.length > MAX_TOOL_DESCRIPTION_CHARS) {
90
+ obj.description = elideText(obj.description, MAX_TOOL_DESCRIPTION_CHARS);
91
+ changed = true;
92
+ }
93
+ return changed;
94
+ }
95
+
96
+ // Walks a parsed SSE payload and compacts every `tools` array it finds
97
+ // (e.g. `response.tools` echoed back by the Responses API). Mutates `node`.
98
+ function compactToolsDeep(node: unknown): boolean {
99
+ if (Array.isArray(node)) {
100
+ let changed = false;
101
+ for (const item of node) changed = compactToolsDeep(item) || changed;
102
+ return changed;
103
+ }
104
+ if (typeof node !== "object" || node === null) return false;
105
+ let changed = false;
106
+ const obj = node as Record<string, unknown>;
107
+ for (const key in obj) {
108
+ const value = obj[key];
109
+ if (key === "tools" && Array.isArray(value)) {
110
+ for (const tool of value) changed = compactToolEntry(tool) || changed;
111
+ } else {
112
+ changed = compactToolsDeep(value) || changed;
113
+ }
114
+ }
115
+ return changed;
116
+ }
59
117
 
118
+ // Rewrites oversized `data:` lines with tool schemas compacted. Returns null
119
+ // when nothing changed (unparseable payloads or no tools to shrink). Only
120
+ // invoked on events that already blew the budget, so the JSON round-trip is
121
+ // off the streaming hot path.
122
+ function compactToolLines(raw: readonly string[]): string[] | null {
123
+ let changed = false;
124
+ const out = raw.map(line => {
125
+ if (!line.startsWith("data:") || line.length <= MAX_TOOL_SCHEMA_CHARS) return line;
126
+ const start = line.charCodeAt(5) === 32 ? 6 : 5;
127
+ try {
128
+ const parsed = JSON.parse(line.slice(start));
129
+ if (!compactToolsDeep(parsed)) return line;
130
+ changed = true;
131
+ return `data: ${JSON.stringify(parsed)}`;
132
+ } catch {
133
+ return line;
134
+ }
135
+ });
136
+ return changed ? out : null;
137
+ }
138
+
139
+ // Keeps the first and last portions of an over-budget event and drops the
140
+ // middle, so leading fields (id/model/status) AND trailing fields
141
+ // (usage/finish_reason) both stay visible. A `: omp-debug-elided` comment
142
+ // marks the cut; split lines carry `…` at the cut edge.
143
+ function headTailTrim(lines: string[], budget: number, elidedTotal: number): string[] {
144
+ const headBudget = budget >> 1;
145
+ const tailBudget = budget - headBudget;
146
+
147
+ let i = 0;
148
+ let headRemaining = headBudget;
149
+ const out: string[] = [];
150
+ while (i < lines.length && lines[i].length + 1 <= headRemaining) {
151
+ headRemaining -= lines[i].length + 1;
152
+ out.push(lines[i]);
153
+ i++;
154
+ }
155
+
156
+ let j = lines.length - 1;
157
+ let tailRemaining = tailBudget;
158
+ const tail: string[] = [];
159
+ while (j >= i && lines[j].length + 1 <= tailRemaining) {
160
+ tailRemaining -= lines[j].length + 1;
161
+ tail.push(lines[j]);
162
+ j--;
163
+ }
164
+ tail.reverse();
165
+
166
+ let elided = elidedTotal - countLines(out) - countLines(tail);
167
+ if (i <= j) {
168
+ // lines[i..j] straddle the cut: keep a head slice of the first and a
169
+ // tail slice of the last (the same line when i === j).
170
+ const headSlice = lines[i].slice(0, Math.max(0, headRemaining - 2));
171
+ const tailStart =
172
+ i === j
173
+ ? Math.max(headSlice.length, lines[j].length - tailRemaining + 2)
174
+ : Math.max(0, lines[j].length - tailRemaining + 2);
175
+ const tailSlice = lines[j].slice(tailStart);
176
+ elided -= headSlice.length + tailSlice.length;
177
+ if (headSlice.length > 0) out.push(`${headSlice}…`);
178
+ out.push(`: omp-debug-elided chars=${Math.max(0, elided)}`);
179
+ if (tailSlice.length > 0) out.push(`…${tailSlice}`);
180
+ } else if (elided > 0) {
181
+ out.push(`: omp-debug-elided chars=${elided}`);
182
+ }
183
+ out.push(...tail);
184
+ return out;
185
+ }
186
+
187
+ // Trim pipeline for one SSE event:
188
+ // 1. fits → return `raw` **by reference** (ownership contract at
189
+ // `RawSseDebugBuffer.recordEvent` below).
190
+ // 2. over budget → compact tool schemas inside `data:` JSON payloads;
191
+ // if that alone fits, the payload stays parseable JSON.
192
+ // 3. still over → head+tail trim (middle elided).
193
+ // Any trimmed result ends with the `: omp-debug-truncated` marker carrying
194
+ // the original size.
195
+ function trimRawLines(raw: string[]): TrimResult {
196
+ const originalChars = countLines(raw);
60
197
  if (originalChars <= MAX_RAW_SSE_EVENT_CHARS) {
61
198
  return { raw, truncated: false, originalChars, chars: originalChars + 1 };
62
199
  }
63
200
 
64
- const trimmed: string[] = [];
65
- let remaining = MAX_RAW_SSE_EVENT_CHARS;
66
- let chars = 1; // matches reduce(.., init = 1)
67
- for (const line of raw) {
68
- if (remaining <= 0) break;
69
- if (line.length + 1 <= remaining) {
70
- trimmed.push(line);
71
- chars += line.length + 1;
72
- remaining -= line.length + 1;
73
- continue;
74
- }
75
- const slice = line.slice(0, Math.max(0, remaining));
76
- trimmed.push(slice);
77
- chars += slice.length + 1;
78
- remaining = 0;
201
+ const budget = MAX_RAW_SSE_EVENT_CHARS - TRIM_MARKER_RESERVE;
202
+ let lines = compactToolLines(raw) ?? raw;
203
+ const compactedChars = lines === raw ? originalChars : countLines(lines);
204
+ if (compactedChars > budget) {
205
+ lines = headTailTrim(lines, budget, compactedChars);
206
+ } else if (lines === raw) {
207
+ lines = raw.slice();
79
208
  }
80
- const tail = `: omp-debug-truncated originalChars=${originalChars}`;
81
- trimmed.push(tail);
82
- chars += tail.length + 1;
83
- return { raw: trimmed, truncated: true, originalChars, chars };
209
+ lines.push(`: omp-debug-truncated originalChars=${originalChars}`);
210
+ return { raw: lines, truncated: true, originalChars, chars: countLines(lines) + 1 };
84
211
  }
85
212
 
86
213
  export function formatRawSseIsoTime(timestamp: number): string {
@@ -121,7 +121,7 @@ export class HashlineFilesystem extends Filesystem {
121
121
  throw error;
122
122
  }
123
123
  // Refuse edits against generated files (lockfiles, models.json, …).
124
- assertEditableFileContent(content, relativePath);
124
+ assertEditableFileContent(content, relativePath, this.session.settings);
125
125
  return content;
126
126
  }
127
127
 
@@ -1816,7 +1816,7 @@ export async function executePatchSingle(
1816
1816
  const resolvedPath = resolvePlanPath(session, path);
1817
1817
  const resolvedRename = rename ? resolvePlanPath(session, rename) : undefined;
1818
1818
 
1819
- await assertEditableFile(resolvedPath, path);
1819
+ await assertEditableFile(resolvedPath, path, session.settings);
1820
1820
 
1821
1821
  // Capture pre-edit content so we can verify the write actually hit disk.
1822
1822
  // `LspFileSystem.writeFile` delegates to a writethrough callback that, in
@@ -400,8 +400,9 @@ function redactShareMessage(
400
400
  };
401
401
  case "assistant":
402
402
  // Drop opaque provider-replay state (encrypted reasoning / native history) the viewer
403
- // never reads and we cannot redact field-by-field: `providerPayload` and any
404
- // `redactedThinking` blocks.
403
+ // never reads and we cannot redact field-by-field: `providerPayload`, any
404
+ // `redactedThinking` blocks, and native Anthropic server-tool blocks
405
+ // (`server_tool_use` input / `web_search_tool_result` encrypted_content).
405
406
  return {
406
407
  ...message,
407
408
  providerPayload: undefined,
@@ -410,7 +411,7 @@ function redactShareMessage(
410
411
  ? undefined
411
412
  : o.obfuscate(message.errorMessage, sharedRegexSecretValues),
412
413
  content: message.content.flatMap((block): AssistantMessage["content"] => {
413
- if (block.type === "redactedThinking") return [];
414
+ if (block.type === "redactedThinking" || block.type === "anthropicServerTool") return [];
414
415
  if (block.type === "text") return [{ ...block, text: o.obfuscate(block.text, sharedRegexSecretValues) }];
415
416
  if (block.type === "thinking") {
416
417
  return [{ ...block, thinking: o.obfuscate(block.thinking, sharedRegexSecretValues) }];