@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.11

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 (120) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-d9xenpn9.md} +40 -0
  3. package/dist/cli.js +3025 -3016
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/command-help.d.ts +3 -0
  7. package/dist/types/commands/share.d.ts +25 -0
  8. package/dist/types/commit/agentic/index.d.ts +3 -1
  9. package/dist/types/commit/execute.d.ts +32 -0
  10. package/dist/types/commit/index.d.ts +2 -1
  11. package/dist/types/commit/pipeline.d.ts +7 -1
  12. package/dist/types/config/model-registry.d.ts +4 -0
  13. package/dist/types/config/settings-schema.d.ts +1 -34
  14. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  15. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  16. package/dist/types/discovery/contained-path.d.ts +34 -0
  17. package/dist/types/discovery/index.d.ts +1 -0
  18. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  19. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  21. package/dist/types/extensibility/shared-events.d.ts +3 -2
  22. package/dist/types/extensibility/skills.d.ts +5 -0
  23. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  24. package/dist/types/mcp/types.d.ts +15 -0
  25. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  26. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  27. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  28. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  29. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  30. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  31. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  32. package/dist/types/secrets/index.d.ts +23 -1
  33. package/dist/types/session/agent-session-events.d.ts +2 -2
  34. package/dist/types/session/agent-session.d.ts +1 -1
  35. package/dist/types/session/messages.d.ts +1 -0
  36. package/dist/types/session/prewalk.d.ts +3 -1
  37. package/dist/types/session/session-tools.d.ts +8 -0
  38. package/dist/types/session/turn-recovery.d.ts +7 -2
  39. package/dist/types/task/index.d.ts +2 -0
  40. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  41. package/package.json +13 -13
  42. package/scripts/legacy-pi-virtual-module.ts +4 -1
  43. package/src/capability/mcp.ts +11 -0
  44. package/src/capability/skill.ts +6 -0
  45. package/src/cli/command-help.ts +4 -0
  46. package/src/cli/gallery-cli.ts +1 -1
  47. package/src/cli-commands.ts +5 -0
  48. package/src/commands/commit.ts +16 -3
  49. package/src/commands/share.ts +71 -0
  50. package/src/commit/agentic/index.ts +39 -21
  51. package/src/commit/execute.ts +56 -0
  52. package/src/commit/index.ts +2 -1
  53. package/src/commit/pipeline.ts +20 -7
  54. package/src/config/model-discovery.ts +1 -1
  55. package/src/config/model-registry.ts +21 -1
  56. package/src/config/settings-schema.ts +2 -33
  57. package/src/config/settings.ts +46 -0
  58. package/src/discovery/agent-plugin-format.ts +551 -0
  59. package/src/discovery/agent-plugins.ts +341 -0
  60. package/src/discovery/claude-plugins.ts +21 -5
  61. package/src/discovery/contained-path.ts +78 -0
  62. package/src/discovery/helpers.ts +23 -9
  63. package/src/discovery/index.ts +1 -0
  64. package/src/discovery/omp-plugins.ts +20 -7
  65. package/src/exec/non-interactive-env.ts +1 -0
  66. package/src/extensibility/custom-tools/types.ts +2 -2
  67. package/src/extensibility/extensions/loader.ts +78 -14
  68. package/src/extensibility/extensions/runner.ts +6 -0
  69. package/src/extensibility/extensions/types.ts +12 -0
  70. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  71. package/src/extensibility/shared-events.ts +3 -2
  72. package/src/extensibility/skills.ts +9 -0
  73. package/src/internal-urls/skill-protocol.ts +14 -0
  74. package/src/internal-urls/vault-protocol.ts +2 -2
  75. package/src/launch/client.ts +11 -1
  76. package/src/launch/protocol.ts +7 -2
  77. package/src/main.ts +3 -2
  78. package/src/mcp/config.ts +3 -0
  79. package/src/mcp/manager.ts +12 -9
  80. package/src/mcp/transports/header-policy.ts +95 -0
  81. package/src/mcp/transports/http.ts +35 -52
  82. package/src/mcp/transports/sse.ts +20 -27
  83. package/src/mcp/types.ts +15 -0
  84. package/src/modes/acp/acp-agent.ts +15 -4
  85. package/src/modes/components/agent-dashboard.ts +2 -0
  86. package/src/modes/components/agent-hub.ts +5 -0
  87. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  88. package/src/modes/components/chat-transcript-builder.ts +3 -0
  89. package/src/modes/components/custom-editor.ts +0 -9
  90. package/src/modes/components/status-line/component.ts +1 -0
  91. package/src/modes/components/status-line/segments.ts +6 -4
  92. package/src/modes/components/status-line/types.ts +2 -0
  93. package/src/modes/components/tool-execution.ts +13 -16
  94. package/src/modes/components/tree-selector.ts +62 -3
  95. package/src/modes/controllers/command-controller.ts +4 -1
  96. package/src/modes/controllers/event-controller.ts +15 -15
  97. package/src/modes/controllers/input-controller.ts +20 -2
  98. package/src/modes/controllers/selector-controller.ts +1 -0
  99. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  100. package/src/modes/utils/ui-helpers.ts +1 -0
  101. package/src/sdk.ts +6 -49
  102. package/src/secrets/index.ts +52 -1
  103. package/src/session/agent-session-events.ts +2 -2
  104. package/src/session/agent-session.ts +33 -14
  105. package/src/session/messages.ts +1 -0
  106. package/src/session/prewalk.ts +57 -17
  107. package/src/session/session-context.ts +3 -5
  108. package/src/session/session-handoff.ts +5 -1
  109. package/src/session/session-tools.ts +37 -3
  110. package/src/session/todo-tracker.ts +11 -1
  111. package/src/session/turn-recovery.ts +94 -72
  112. package/src/slash-commands/builtin-registry.ts +12 -8
  113. package/src/task/index.ts +21 -6
  114. package/src/tools/bash-skill-urls.ts +15 -0
  115. package/src/tools/bash.ts +10 -11
  116. package/src/tools/debug.ts +1 -1
  117. package/src/tools/jtd-to-json-schema.ts +123 -21
  118. package/src/tools/read.ts +5 -4
  119. package/src/tools/shell-tokenize.ts +98 -0
  120. package/src/web/search/providers/exa.ts +1 -1
package/src/tools/bash.ts CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  previewWindowRows,
50
50
  replaceTabs,
51
51
  } from "./render-utils";
52
- import { tokenizeShellSegments } from "./shell-tokenize";
52
+ import { extractLeadingCdTarget, tokenizeShellSegments } from "./shell-tokenize";
53
53
  import { ToolAbortError, ToolError } from "./tool-errors";
54
54
  import { toolResult } from "./tool-result";
55
55
  import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
@@ -960,17 +960,16 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
960
960
  let command = rawCommand;
961
961
  const env = normalizeBashEnv(rawEnv);
962
962
 
963
- // Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
964
- // Constrained to a single line so a `&&` that sits on a later line of a multiline
965
- // script can't pull the entire script into the "cwd" capture.
963
+ // Extract a leading `cd <path> && ...` into cwd when the model ignores the
964
+ // cwd parameter. The scanner captures only a single path token and defers
965
+ // to the shell for anything else (redirects, extra args, shell expansion),
966
+ // so it never absorbs shell syntax like `cd /tmp 2>/dev/null && ...` into
967
+ // the structured cwd. Constrained to a top-level `&&` on the first line.
966
968
  if (!cwd) {
967
- const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/);
968
- // Skip extraction when the path needs shell expansion ($VAR, $(...),
969
- // backticks) — resolveToCwd only expands `~`, so routing those through
970
- // cwd would reject commands the shell itself handles fine.
971
- if (cdMatch && !/[$`(]/.test(cdMatch[1])) {
972
- cwd = cdMatch[1].trim().replace(/^["']|["']$/g, "");
973
- command = command.slice(cdMatch[0].length);
969
+ const cd = extractLeadingCdTarget(command);
970
+ if (cd) {
971
+ cwd = cd.path;
972
+ command = cd.rest;
974
973
  }
975
974
  }
976
975
  if (asyncRequested && !this.#asyncEnabled) {
@@ -505,7 +505,7 @@ const ADAPTER_UNAVAILABLE_MESSAGES: Readonly<Record<string, string>> = {
505
505
  dlv: "adapter 'dlv' is not available: install with 'go install github.com/go-delve/delve/cmd/dlv@latest'",
506
506
  rdbg: "adapter 'rdbg' is not available: install with 'gem install debug'",
507
507
  "js-debug-adapter":
508
- "adapter 'js-debug-adapter' is not available: install vscode-js-debug with Mason or set JS_DEBUG_DAP_SERVER to dapDebugServer.js",
508
+ "adapter 'js-debug-adapter' is not available: download it from https://github.com/microsoft/vscode-js-debug",
509
509
  };
510
510
 
511
511
  const ADAPTER_CANONICAL_COMMANDS: Readonly<Record<string, string>> = {
@@ -8,6 +8,7 @@
8
8
  * @see https://datatracker.ietf.org/doc/html/rfc8927
9
9
  */
10
10
 
11
+ import { isRecord } from "@oh-my-pi/pi-utils";
11
12
  import type { JTDPrimitive } from "./jtd-utils.js";
12
13
  import {
13
14
  isJTDDiscriminator,
@@ -134,6 +135,18 @@ function convertSchema(schema: unknown): unknown {
134
135
  return {};
135
136
  }
136
137
 
138
+ const jtdOnlyPrimitiveTypes: Record<string, true> = {
139
+ timestamp: true,
140
+ float32: true,
141
+ float64: true,
142
+ int8: true,
143
+ uint8: true,
144
+ int16: true,
145
+ uint16: true,
146
+ int32: true,
147
+ uint32: true,
148
+ };
149
+
137
150
  /**
138
151
  * Detect if a schema is JTD format (vs JSON Schema).
139
152
  *
@@ -155,11 +168,8 @@ export function isJTDSchema(schema: unknown): boolean {
155
168
  if ("ref" in obj) return true;
156
169
 
157
170
  // JTD type primitives (JSON Schema doesn't have int32, float64, etc.)
158
- if ("type" in obj) {
159
- const jtdPrimitives = ["timestamp", "float32", "float64", "int8", "uint8", "int16", "uint16", "int32", "uint32"];
160
- if (jtdPrimitives.includes(obj.type as string)) {
161
- return true;
162
- }
171
+ if (typeof obj.type === "string" && Object.hasOwn(jtdOnlyPrimitiveTypes, obj.type)) {
172
+ return true;
163
173
  }
164
174
 
165
175
  // JTD properties form without type: "object" (JSON Schema requires it)
@@ -170,36 +180,128 @@ export function isJTDSchema(schema: unknown): boolean {
170
180
  return false;
171
181
  }
172
182
 
173
- function normalizeMixedSchemaNode(schema: unknown): unknown {
174
- if (schema === null || typeof schema !== "object") {
175
- return schema;
183
+ function isUnambiguousJTDSchema(schema: unknown): boolean {
184
+ if (!isRecord(schema)) return false;
185
+
186
+ if (isRecord(schema.elements) || isRecord(schema.values) || isRecord(schema.optionalProperties)) {
187
+ return true;
188
+ }
189
+ if (typeof schema.ref === "string") return true;
190
+ if (typeof schema.type === "string" && Object.hasOwn(jtdOnlyPrimitiveTypes, schema.type)) {
191
+ return true;
192
+ }
193
+ if (typeof schema.discriminator !== "string" || !isRecord(schema.mapping)) {
194
+ return false;
176
195
  }
177
196
 
178
- if (Array.isArray(schema)) {
179
- return schema.map(item => normalizeMixedSchemaNode(item));
197
+ for (const key in schema.mapping) {
198
+ if (!Object.hasOwn(schema.mapping, key)) continue;
199
+ const mapping = schema.mapping[key];
200
+ if (!isRecord(mapping)) return false;
201
+
202
+ let hasSchemaProperties = false;
203
+ if (Object.hasOwn(mapping, "properties")) {
204
+ if (!isRecord(mapping.properties)) return false;
205
+ hasSchemaProperties = true;
206
+ }
207
+ if (Object.hasOwn(mapping, "optionalProperties")) {
208
+ if (!isRecord(mapping.optionalProperties)) return false;
209
+ hasSchemaProperties = true;
210
+ }
211
+ if (!hasSchemaProperties) return false;
180
212
  }
181
213
 
182
- if (isJTDSchema(schema)) {
183
- // `convertSchema` is itself fully recursive and emits pure JSON Schema, so
184
- // re-walking the result with `normalizeMixedSchemaNode` is unnecessary and
185
- // unsafe: it would treat user-named properties whose keys happen to be JTD
186
- // keywords (e.g. `ref`, `elements`) as nested JTD forms (#1345).
187
- return convertSchema(schema);
214
+ return true;
215
+ }
216
+
217
+ function normalizeJsonSchemaArray(value: unknown): unknown {
218
+ if (!Array.isArray(value)) return value;
219
+
220
+ let normalized: unknown[] | undefined;
221
+ for (let index = 0; index < value.length; index++) {
222
+ const item = value[index];
223
+ const converted = normalizeJsonSchemaNode(item);
224
+ if (converted === item) continue;
225
+ normalized ??= value.slice();
226
+ normalized[index] = converted;
227
+ }
228
+ return normalized ?? value;
229
+ }
230
+
231
+ function normalizeJsonSchemaMap(value: unknown): unknown {
232
+ if (!isRecord(value)) return value;
233
+
234
+ let normalized: Record<string, unknown> | undefined;
235
+ for (const key in value) {
236
+ if (!Object.hasOwn(value, key)) continue;
237
+ const item = value[key];
238
+ const converted = normalizeJsonSchemaNode(item);
239
+ if (converted === item) continue;
240
+ normalized ??= { ...value };
241
+ normalized[key] = converted;
188
242
  }
243
+ return normalized ?? value;
244
+ }
189
245
 
190
- const normalized: Record<string, unknown> = {};
191
- for (const [key, value] of Object.entries(schema)) {
192
- normalized[key] = normalizeMixedSchemaNode(value);
246
+ function normalizeJsonSchemaNode(schema: unknown): unknown {
247
+ if (!isRecord(schema)) return schema;
248
+ if (isUnambiguousJTDSchema(schema)) return convertSchema(schema);
249
+
250
+ let normalized: Record<string, unknown> | undefined;
251
+ for (const key in schema) {
252
+ if (!Object.hasOwn(schema, key)) continue;
253
+
254
+ const value = schema[key];
255
+ let converted: unknown;
256
+ switch (key) {
257
+ case "not":
258
+ case "if":
259
+ case "then":
260
+ case "else":
261
+ case "items":
262
+ case "contains":
263
+ case "propertyNames":
264
+ case "additionalProperties":
265
+ case "unevaluatedProperties":
266
+ case "unevaluatedItems":
267
+ case "contentSchema":
268
+ converted = normalizeJsonSchemaNode(value);
269
+ break;
270
+ case "allOf":
271
+ case "anyOf":
272
+ case "oneOf":
273
+ case "prefixItems":
274
+ converted = normalizeJsonSchemaArray(value);
275
+ break;
276
+ case "properties":
277
+ case "patternProperties":
278
+ case "$defs":
279
+ case "definitions":
280
+ case "dependentSchemas":
281
+ converted = normalizeJsonSchemaMap(value);
282
+ break;
283
+ default:
284
+ continue;
285
+ }
286
+
287
+ if (converted === value) continue;
288
+ normalized ??= { ...schema };
289
+ normalized[key] = converted;
193
290
  }
194
291
 
195
- return normalized;
292
+ return normalized ?? schema;
196
293
  }
294
+
197
295
  /**
198
296
  * Convert JTD schema to JSON Schema.
199
297
  * If already JSON Schema, returns as-is.
200
298
  */
201
299
  export function jtdToJsonSchema(schema: unknown): unknown {
202
- return normalizeMixedSchemaNode(schema);
300
+ if (isJTDSchema(schema)) {
301
+ // convertSchema is recursive; re-walking its JSON Schema output caused #1345.
302
+ return convertSchema(schema);
303
+ }
304
+ return normalizeJsonSchemaNode(schema);
203
305
  }
204
306
 
205
307
  /**
package/src/tools/read.ts CHANGED
@@ -2497,6 +2497,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2497
2497
  const imageMetadata = await readImageMetadata(absolutePath);
2498
2498
  const mimeType = imageMetadata?.mimeType;
2499
2499
  const ext = path.extname(absolutePath).toLowerCase();
2500
+ const resolvedDisplayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
2500
2501
  const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
2501
2502
 
2502
2503
  // Profiler reports (macOS `sample` call trees, V8 `.cpuprofile` JSON):
@@ -2541,7 +2542,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2541
2542
  fileSize,
2542
2543
  }));
2543
2544
  } else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
2544
- const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
2545
+ const notebookText = await readEditableNotebookText(absolutePath, resolvedDisplayPath);
2545
2546
  if (isMultiRange(parsed) && parsed.kind === "lines") {
2546
2547
  return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
2547
2548
  details: { resolvedPath: absolutePath },
@@ -2560,7 +2561,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2560
2561
  const result = await convertFileWithMarkit(absolutePath, signal);
2561
2562
  if (result.ok) {
2562
2563
  const renderedContent =
2563
- ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, localReadPath) : result.content;
2564
+ ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, resolvedDisplayPath) : result.content;
2564
2565
  // Route the converted markdown through the in-memory text builder
2565
2566
  // so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
2566
2567
  // raw mode apply against the converted output. Without this,
@@ -2603,7 +2604,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2603
2604
  return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
2604
2605
  .text(
2605
2606
  prependSuffixResolutionNotice(
2606
- `[Cannot read binary file '${formatPathRelativeToCwd(absolutePath, this.session.cwd)}' (${formatBytes(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`,
2607
+ `[Cannot read binary file '${resolvedDisplayPath}' (${formatBytes(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`,
2607
2608
  suffixResolution,
2608
2609
  ),
2609
2610
  )
@@ -2620,7 +2621,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2620
2621
  if (summary?.parsed && summary.elided) {
2621
2622
  const renderedSummary = this.#renderSummary(summary);
2622
2623
  const footer = formatSummaryElisionFooter(
2623
- localReadPath,
2624
+ resolvedDisplayPath,
2624
2625
  renderedSummary.elidedRanges,
2625
2626
  renderedSummary.elidedLines,
2626
2627
  );
@@ -211,3 +211,101 @@ export function extractFlatShellCommandSegments(command: string): FlatShellComma
211
211
  pushSegment(command.length);
212
212
  return segments;
213
213
  }
214
+
215
+ /**
216
+ * Shell metacharacters that end an unquoted `cd` target token. A redirect,
217
+ * extra argument, or any operator in this set means the leading construct is
218
+ * more than a bare `cd <path>`, so extraction must bail.
219
+ */
220
+ const CD_TARGET_TERMINATORS: Record<string, true> = {
221
+ " ": true,
222
+ "\t": true,
223
+ "\n": true,
224
+ "\r": true,
225
+ "&": true,
226
+ "|": true,
227
+ ";": true,
228
+ "<": true,
229
+ ">": true,
230
+ "(": true,
231
+ ")": true,
232
+ };
233
+
234
+ /**
235
+ * Parses a leading `cd <path> && ...` prefix so the bash tool can route the
236
+ * target through its structured `cwd` parameter when the model omits it.
237
+ *
238
+ * Returns the single path token (quotes and backslash escapes resolved to their
239
+ * literal value) and the command remainder after the top-level `&&`, or `null`
240
+ * when the command does not begin with exactly `cd`, one path token, and a
241
+ * top-level `&&`. The scanner deliberately bails on anything else in the prefix
242
+ * — redirects (`cd /tmp 2>/dev/null && ...`), extra arguments, or paths needing
243
+ * shell expansion (`$`, backticks, `(`) — leaving the whole command for the
244
+ * shell instead of absorbing shell syntax into `cwd`.
245
+ */
246
+ export function extractLeadingCdTarget(command: string): { path: string; rest: string } | null {
247
+ const prefix = /^cd[ \t]+/.exec(command);
248
+ if (!prefix) return null;
249
+ let i = prefix[0].length;
250
+ let path = "";
251
+ let inSingle = false;
252
+ let inDouble = false;
253
+ for (; i < command.length; i++) {
254
+ const ch = command[i];
255
+ if (inSingle) {
256
+ if (ch === "'") {
257
+ inSingle = false;
258
+ continue;
259
+ }
260
+ path += ch;
261
+ continue;
262
+ }
263
+ if (inDouble) {
264
+ if (ch === "\\" && i + 1 < command.length) {
265
+ const next = command[i + 1];
266
+ // A line continuation crosses the first physical line. Leave it to
267
+ // the shell rather than turning the escaped newline into cwd text.
268
+ if (next === "\n" || next === "\r") return null;
269
+ if (next === '"' || next === "\\" || next === "$" || next === "`") {
270
+ path += next;
271
+ i++;
272
+ continue;
273
+ }
274
+ }
275
+ if (ch === '"') {
276
+ inDouble = false;
277
+ continue;
278
+ }
279
+ path += ch;
280
+ continue;
281
+ }
282
+ if (ch === "'") {
283
+ inSingle = true;
284
+ continue;
285
+ }
286
+ if (ch === '"') {
287
+ inDouble = true;
288
+ continue;
289
+ }
290
+ if (ch === "\\" && i + 1 < command.length) {
291
+ // Preserve shell line-continuation semantics by declining extraction.
292
+ if (command[i + 1] === "\n" || command[i + 1] === "\r") return null;
293
+ path += command[i + 1];
294
+ i++;
295
+ continue;
296
+ }
297
+ if (CD_TARGET_TERMINATORS[ch]) break;
298
+ path += ch;
299
+ }
300
+ // Unterminated quote or empty target: leave the command for the shell.
301
+ if (inSingle || inDouble || path.length === 0) return null;
302
+ // A path needing shell expansion can't be resolved literally through cwd.
303
+ if (/[$`(]/.test(path)) return null;
304
+ // Skip inter-token whitespace, then require a top-level `&&` (a single `&`,
305
+ // `||`, `;`, `|`, or a redirect all mean this is not a bare `cd <path>`).
306
+ while (command[i] === " " || command[i] === "\t") i++;
307
+ if (command[i] !== "&" || command[i + 1] !== "&") return null;
308
+ i += 2;
309
+ while (command[i] === " " || command[i] === "\t") i++;
310
+ return { path, rest: command.slice(i) };
311
+ }
@@ -464,7 +464,7 @@ export class ExaProvider extends SearchProvider {
464
464
 
465
465
  #settingsAllowSearch(): boolean {
466
466
  try {
467
- if (settings.get("exa.enabled") === false || settings.get("exa.enableSearch") === false) {
467
+ if (settings.get("exa.enabled") === false) {
468
468
  return false;
469
469
  }
470
470
  } catch {