@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.12
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.
- package/CHANGELOG.md +49 -0
- package/dist/cli.js +3119 -3028
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/model-registry.ts +9 -0
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +11 -0
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.ts +6 -35
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +54 -10
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +128 -14
- package/src/system-prompt.ts +3 -2
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +53 -14
- package/src/tools/index.ts +11 -0
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +108 -50
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
|
@@ -6,14 +6,22 @@ The shell invokes **real binaries** with simple args. It is NOT full GNU Bash.
|
|
|
6
6
|
|
|
7
7
|
Use bash ONLY for: a single binary call, or one short pipeline that COMPUTES a fact and does not depend on shell-specific regex/quoting (`wc -l`, `sort | uniq -c`, `comm`, `diff`, a checksum, `git status`).
|
|
8
8
|
|
|
9
|
-
Anything below → `eval` cell, not bash:
|
|
9
|
+
{{#if hasEval}}Anything below → `eval` cell, not bash:
|
|
10
10
|
- Inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists for that language
|
|
11
11
|
- Heredocs (`<<EOF`), `while`/`for`/`if`/`case` shell control flow
|
|
12
12
|
- `$(…)` command substitution nested inside another command
|
|
13
13
|
- Pipelines with more than two stages, or stages that need control flow or quote/JSON escaping
|
|
14
14
|
- Multiline commands, `&&`-chains mixing control flow
|
|
15
15
|
- Quote/JSON escaping that fights the shell
|
|
16
|
-
|
|
16
|
+
{{else}}Anything below means you are writing a shell program, not invoking one. Prefer a purpose-built tool, a checked-in script, or a single repo command instead:
|
|
17
|
+
- Inline interpreter scripts (`-e`/`-c`/`--eval`)
|
|
18
|
+
- Heredocs (`<<EOF`), `while`/`for`/`if`/`case` shell control flow
|
|
19
|
+
- `$(…)` command substitution nested inside another command
|
|
20
|
+
- Pipelines with more than two stages, or stages that need control flow or quote/JSON escaping
|
|
21
|
+
- Multiline commands, `&&`-chains mixing control flow
|
|
22
|
+
- Quote/JSON escaping that fights the shell
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if hasGrep}}- GNU grep BRE extensions are not guaranteed in the embedded shell: use `grep -E 'json|tool'` for alternation instead of `grep 'json\|tool'`; use the built-in `grep` tool with `pattern: "json|tool"` (Rust regex, so `\bword\b` works there){{#if hasEval}}, or `eval` for exact text processing{{/if}}.{{else}}- GNU grep BRE extensions are not guaranteed in the embedded shell: use `grep -E 'json|tool'` for alternation instead of `grep 'json\|tool'`{{#if hasEval}}, or use `eval` for exact text processing{{/if}}.{{/if}}
|
|
17
25
|
|
|
18
26
|
<instruction>
|
|
19
27
|
- `cwd` sets the working dir, not `cd dir && …`
|
|
@@ -23,14 +31,17 @@ Anything below → `eval` cell, not bash:
|
|
|
23
31
|
- `;` only when later commands should run despite earlier failures
|
|
24
32
|
- Multiple bash calls per message run concurrently. NEVER split order-dependent commands across parallel calls — chain with `&&` in one call.
|
|
25
33
|
- Internal URIs (`skill://`, `agent://`, …) auto-resolve to FS paths
|
|
26
|
-
- Need exact pipeline semantics (`cmd | head`, multi-stage filtering) or output truncation? Prefer `eval` and process the stream directly.
|
|
34
|
+
{{#if hasEval}}- Need exact pipeline semantics (`cmd | head`, multi-stage filtering) or output truncation? Prefer `eval` and process the stream directly.{{else}}- Need exact pipeline semantics (`cmd | head`, multi-stage filtering) or output truncation? Use a checked-in script, purpose-built tool, or single command that owns the output shape.{{/if}}
|
|
27
35
|
{{#if asyncEnabled}}
|
|
28
36
|
- `async: true` for long-running commands when you don't need immediate output: returns a background job ID; result delivered as a follow-up.
|
|
29
37
|
{{/if}}
|
|
30
38
|
</instruction>
|
|
31
39
|
|
|
32
40
|
<critical>
|
|
33
|
-
- The embedded shell invokes real binaries with simple args; it is NOT full GNU Bash. Loops, conditionals, heredocs, inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists, several piped stages, exact pipeline semantics, or quote/JSON escaping mean you're writing a program → use `eval` cells: restartable, stateful, and free of shell-quoting traps.
|
|
41
|
+
{{#if hasEval}}- The embedded shell invokes real binaries with simple args; it is NOT full GNU Bash and NOT a scripting surface. Loops, conditionals, heredocs, inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists, several piped stages, exact pipeline semantics, or quote/JSON escaping mean you're writing a program → use `eval` cells: restartable, stateful, and free of shell-quoting traps.{{else}}- The embedded shell invokes real binaries with simple args; it is NOT full GNU Bash and NOT a scripting surface. Loops, conditionals, heredocs, inline interpreter scripts, several piped stages, exact pipeline semantics, or quote/JSON escaping mean you're writing a shell program; use a purpose-built tool or checked-in script instead.{{/if}}
|
|
42
|
+
{{#if hasGrep}}- NEVER shell out to search content or files: `grep/rg` → `grep`.{{else}}- Avoid shelling out for broad content search; use an active search/read tool when one is available.{{/if}}
|
|
43
|
+
{{#if hasRead}}{{#if hasGlob}}- NEVER use `ls` or `find` to list or locate files — `ls` → `read` (a directory path lists entries), `find` → the `glob` tool (globbing). This is non-negotiable, even for a single quick listing.{{else}}- Prefer `read` for known file and directory reads. Only use shell listing when no file-listing tool is active.{{/if}}{{else}}{{#if hasGlob}}- Prefer `glob` for file discovery; avoid `find` when `glob` is active.{{else}}- If no file read/listing tool is active, keep shell inspection narrow and state that limitation.{{/if}}{{/if}}
|
|
44
|
+
- Avoid head/tail/redirections: stderr already merged; long output auto-truncated, FULL capture kept at `artifact://<id>`.
|
|
34
45
|
</critical>
|
|
35
46
|
|
|
36
47
|
<output>
|
|
@@ -41,9 +52,9 @@ Anything below → `eval` cell, not bash:
|
|
|
41
52
|
{{#if asyncEnabled}}
|
|
42
53
|
# Timeout and async
|
|
43
54
|
|
|
44
|
-
- `timeout` is seconds
|
|
45
|
-
- `async: true` defers only reporting — it does NOT extend
|
|
46
|
-
- Need >3600s?
|
|
55
|
+
- `timeout` is seconds; nonzero values are clamped to `1..3600` and the process is killed on elapse. Set `timeout: 0` only for commands that must run until completion or explicit cancellation.
|
|
56
|
+
- `async: true` defers only reporting — it does NOT extend a nonzero timeout; use `timeout: 0` when a daemon or watcher must be cancellation-owned.
|
|
57
|
+
- Need a daemon or >3600s run? Use `async: true` with `timeout: 0` when the harness should keep it alive until cancellation, or detach/manage lifecycle yourself (`cmd &`, supervisor, self-restarting script). The shell session persists across calls.
|
|
47
58
|
{{/if}}
|
|
48
59
|
{{#if autoBackgroundEnabled}}
|
|
49
60
|
|
|
@@ -2,7 +2,7 @@ Greps files using regex.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Rust regex (RE2-style): alternation is `foo|bar`, not GNU BRE-style `foo\|bar`; Rust word boundaries like `\bword\b` are supported. Use line anchors or post-filters instead of lookaround/backreferences.
|
|
5
|
-
- `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`).
|
|
5
|
+
- `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`). Literal colon filename + line range? Use `selector` (e.g. `{"path":"test:1-2","selector":"1-2"}`), not recursive `path:"test:1-2:1-2"`.
|
|
6
6
|
- Cross-line patterns detected from literal `\n` or `\\n` in `pattern`.
|
|
7
7
|
</instruction>
|
|
8
8
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via
|
|
1
|
+
Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via `path` plus optional `selector`.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- SHOULD parallelize independent reads.
|
|
@@ -7,7 +7,8 @@ Read files, directories, archives, SQLite, images, documents, internal resources
|
|
|
7
7
|
|
|
8
8
|
## Parameters
|
|
9
9
|
|
|
10
|
-
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`, `ssh://`), or URL.
|
|
10
|
+
- `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`, `ssh://`), or URL. Inline `:<sel>` still works for ranges/modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
|
|
11
|
+
- `selector` — optional selector without leading `:` (e.g. `"50-200"`, `"raw"`, `"raw:50-100"`, `"conflicts"`). Use when `path` contains literal colons: `{"path":"test:1-2","selector":"1-2"}`.
|
|
11
12
|
|
|
12
13
|
## Selectors
|
|
13
14
|
|
|
@@ -72,6 +73,6 @@ All URI schemes take the same line selectors. `artifact://<id>` recovers spilled
|
|
|
72
73
|
`ssh://host/<absolute-path>` reads a remote text file (UTF-8, ≤1 MiB) or lists a directory one level deep, on a pre-configured SSH host or `~/.ssh/config` alias; `ssh://host/` lists the remote root and bare `ssh://` lists the configured hosts. Files are also writable via `write` and searchable via `search`; a directory only lists (`search` refuses a directory, `write` refuses to overwrite one). A literal `:`, `?`, or `#` in the remote path must be percent-encoded (`%3A`/`%3F`/`%23`) — a trailing `:sel` is read as a line selector, and `?`/`#` start a URL query/fragment. Requires a POSIX login shell (`sh`/`bash`/`zsh`); a Windows host or a non-POSIX shell (fish, csh/tcsh) is rejected — use the `ssh` tool there.
|
|
73
74
|
|
|
74
75
|
<critical>
|
|
75
|
-
-
|
|
76
|
+
- Literal colon filename + selector? Use `selector`, not recursive `path:"file:sel:sel"`.
|
|
76
77
|
- Summary footer names elided ranges? Re-issue ONLY those ranges. NEVER guess `..`/`…` content.
|
|
77
78
|
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -1525,10 +1525,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1525
1525
|
// entries capture it at fetch time and are dropped at injection if a newer
|
|
1526
1526
|
// mutation (any tool) bumped it in the meantime.
|
|
1527
1527
|
const fileMutationVersions = new Map<string, number>();
|
|
1528
|
+
const activeToolNames = new Set<string>();
|
|
1529
|
+
const setActiveToolNames = (names: Iterable<string>): void => {
|
|
1530
|
+
activeToolNames.clear();
|
|
1531
|
+
for (const name of names) {
|
|
1532
|
+
activeToolNames.add(name);
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1528
1535
|
const toolSession: ToolSession = {
|
|
1529
1536
|
get cwd() {
|
|
1530
1537
|
return sessionManager.getCwd();
|
|
1531
1538
|
},
|
|
1539
|
+
isToolActive: name => activeToolNames.has(name),
|
|
1540
|
+
setActiveToolNames,
|
|
1532
1541
|
hasUI: options.hasUI ?? false,
|
|
1533
1542
|
enableLsp,
|
|
1534
1543
|
get hasEditTool() {
|
|
@@ -2558,6 +2567,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2558
2567
|
});
|
|
2559
2568
|
hasRegistered = true;
|
|
2560
2569
|
|
|
2570
|
+
setActiveToolNames(initialToolNames);
|
|
2561
2571
|
const { systemPrompt } = await logger.time(
|
|
2562
2572
|
"buildSystemPrompt",
|
|
2563
2573
|
rebuildSystemPrompt,
|
|
@@ -2852,6 +2862,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2852
2862
|
rebuildSystemPrompt,
|
|
2853
2863
|
reloadSshTool,
|
|
2854
2864
|
requestedToolNames: requestedToolNameSet,
|
|
2865
|
+
setActiveToolNames,
|
|
2855
2866
|
getMcpServerInstructions: mcpManager
|
|
2856
2867
|
? () => {
|
|
2857
2868
|
const raw = mcpManager.getServerInstructions();
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type AsideMessage,
|
|
36
36
|
type CompactionSummaryMessage,
|
|
37
37
|
countTokens,
|
|
38
|
+
createToolScopedAbortReason,
|
|
38
39
|
resolveTelemetry,
|
|
39
40
|
type StreamFn,
|
|
40
41
|
ThinkingLevel,
|
|
@@ -108,6 +109,7 @@ import {
|
|
|
108
109
|
clearAnthropicFastModeFallback,
|
|
109
110
|
deriveClaudeDeviceId,
|
|
110
111
|
Effort,
|
|
112
|
+
isUsageLimitOutcome,
|
|
111
113
|
parseRateLimitReason,
|
|
112
114
|
realizesPriorityServiceTier,
|
|
113
115
|
resolveModelServiceTier,
|
|
@@ -124,6 +126,7 @@ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
|
124
126
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
125
127
|
import {
|
|
126
128
|
escapeXmlText,
|
|
129
|
+
extractHttpStatusFromError,
|
|
127
130
|
extractRetryHint,
|
|
128
131
|
formatDuration,
|
|
129
132
|
getAgentDbPath,
|
|
@@ -179,7 +182,12 @@ import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
|
179
182
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
180
183
|
import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
181
184
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
182
|
-
import {
|
|
185
|
+
import {
|
|
186
|
+
getDefault,
|
|
187
|
+
onAppendOnlyModeChanged,
|
|
188
|
+
onModelRolesChanged,
|
|
189
|
+
validateProviderMaxInFlightRequests,
|
|
190
|
+
} from "../config/settings";
|
|
183
191
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
184
192
|
import { loadCapability } from "../discovery";
|
|
185
193
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
@@ -237,7 +245,7 @@ import { theme } from "../modes/theme/theme";
|
|
|
237
245
|
import { parseTurnBudget } from "../modes/turn-budget";
|
|
238
246
|
import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
|
|
239
247
|
import { computeNonMessageBreakdown, computeNonMessageTokens } from "../modes/utils/context-usage";
|
|
240
|
-
import { containsWorkflow,
|
|
248
|
+
import { containsWorkflow, renderWorkflowNotice } from "../modes/workflow";
|
|
241
249
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
242
250
|
import type { PlanModeState } from "../plan-mode/state";
|
|
243
251
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
@@ -314,6 +322,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
|
314
322
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
315
323
|
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
316
324
|
import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
|
|
325
|
+
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
317
326
|
import { generateSessionTitle } from "../utils/title-generator";
|
|
318
327
|
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
319
328
|
import type { AuthStorage } from "./auth-storage";
|
|
@@ -686,6 +695,8 @@ export interface AgentSessionConfig {
|
|
|
686
695
|
toolRegistry?: Map<string, AgentTool>;
|
|
687
696
|
/** Tool names whose current registry entry is still the built-in implementation. */
|
|
688
697
|
builtInToolNames?: Iterable<string>;
|
|
698
|
+
/** Update tool-session predicates that render guidance from the live active tool set. */
|
|
699
|
+
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
689
700
|
/** Current session pre-LLM message transform pipeline */
|
|
690
701
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
691
702
|
/**
|
|
@@ -724,6 +735,8 @@ export interface AgentSessionConfig {
|
|
|
724
735
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
725
736
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
726
737
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
738
|
+
/** Local calendar date provider used by prompt-cache invalidation. Defaults to the host local date. */
|
|
739
|
+
getLocalCalendarDate?: () => string;
|
|
727
740
|
/** Rebuild the SSH tool from current capability discovery results. */
|
|
728
741
|
reloadSshTool?: () => Promise<AgentTool | null>;
|
|
729
742
|
requestedToolNames?: ReadonlySet<string>;
|
|
@@ -870,6 +883,7 @@ export interface HandoffResult {
|
|
|
870
883
|
export interface SessionHandoffOptions {
|
|
871
884
|
autoTriggered?: boolean;
|
|
872
885
|
signal?: AbortSignal;
|
|
886
|
+
onSwitchCancelled?: () => void;
|
|
873
887
|
}
|
|
874
888
|
|
|
875
889
|
/** Result from cycleModel() */
|
|
@@ -1555,6 +1569,7 @@ export class AgentSession {
|
|
|
1555
1569
|
#cancelExitRecorder?: () => void;
|
|
1556
1570
|
#exitRecorded = false;
|
|
1557
1571
|
#unsubscribeAppendOnly?: () => void;
|
|
1572
|
+
#unsubscribeModelRoles?: () => void;
|
|
1558
1573
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
1559
1574
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
1560
1575
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
@@ -1716,8 +1731,10 @@ export class AgentSession {
|
|
|
1716
1731
|
#rebuildSystemPrompt:
|
|
1717
1732
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
1718
1733
|
| undefined;
|
|
1734
|
+
#getLocalCalendarDate: () => string;
|
|
1719
1735
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1720
1736
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1737
|
+
#setActiveToolNames: ((names: Iterable<string>) => void) | undefined;
|
|
1721
1738
|
#disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
|
|
1722
1739
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1723
1740
|
#baseSystemPrompt: string[];
|
|
@@ -2161,8 +2178,10 @@ export class AgentSession {
|
|
|
2161
2178
|
});
|
|
2162
2179
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
2163
2180
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
2181
|
+
this.#getLocalCalendarDate = config.getLocalCalendarDate ?? formatLocalCalendarDate;
|
|
2164
2182
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
2165
2183
|
this.#reloadSshTool = config.reloadSshTool;
|
|
2184
|
+
this.#setActiveToolNames = config.setActiveToolNames;
|
|
2166
2185
|
this.#disconnectOwnedMcpManager = config.disconnectOwnedMcpManager;
|
|
2167
2186
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
2168
2187
|
this.#promptModelKey = this.#currentPromptModelKey();
|
|
@@ -2259,6 +2278,11 @@ export class AgentSession {
|
|
|
2259
2278
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
2260
2279
|
// Re-evaluate append-only context mode when the setting changes at runtime.
|
|
2261
2280
|
this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
|
|
2281
|
+
this.#unsubscribeModelRoles = onModelRolesChanged(() => {
|
|
2282
|
+
if (!this.#advisorEnabled || this.#isDisposed) return;
|
|
2283
|
+
if (this.#advisors.length > 0 && !this.#advisorRuntimeMatchesCurrentConfig()) this.#stopAdvisorRuntime();
|
|
2284
|
+
this.#buildAdvisorRuntime(true);
|
|
2285
|
+
});
|
|
2262
2286
|
}
|
|
2263
2287
|
// -------------------------------------------------------------------------
|
|
2264
2288
|
// Advisor runtime lifecycle
|
|
@@ -2391,7 +2415,9 @@ export class AgentSession {
|
|
|
2391
2415
|
#advisorRuntimeSignature(config: AdvisorConfig, slug: string, model: Model, thinkingLevel: ThinkingLevel): string {
|
|
2392
2416
|
const tools = config.tools?.length ? config.tools.join("\u001e") : "";
|
|
2393
2417
|
const instructions = config.instructions?.trim() ?? "";
|
|
2394
|
-
return [config.name, slug, model
|
|
2418
|
+
return [config.name, slug, formatModelStringWithRouting(model), thinkingLevel, tools, instructions].join(
|
|
2419
|
+
"\u001f",
|
|
2420
|
+
);
|
|
2395
2421
|
}
|
|
2396
2422
|
|
|
2397
2423
|
#advisorRuntimeMatchesCurrentConfig(): boolean {
|
|
@@ -2539,6 +2565,21 @@ export class AgentSession {
|
|
|
2539
2565
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
|
|
2540
2566
|
obfuscator: this.#obfuscator,
|
|
2541
2567
|
beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
|
|
2568
|
+
onTurnError: async error => {
|
|
2569
|
+
// Mirror the auth-gateway's usage-limit remedy: the in-stream a/b/c
|
|
2570
|
+
// auth retry rotates through siblings within one request but never
|
|
2571
|
+
// blocks the LAST failing credential, so without this the advisor
|
|
2572
|
+
// re-picks the same exhausted account every retry. Usage limits
|
|
2573
|
+
// only — other failures keep the plain retry/notify path (never
|
|
2574
|
+
// suspect-mark a credential on a transient advisor error).
|
|
2575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2576
|
+
if (!isUsageLimitOutcome(extractHttpStatusFromError(error), message)) return;
|
|
2577
|
+
await this.#modelRegistry.authStorage.markUsageLimitReached(advisorModel.provider, advisorSessionId, {
|
|
2578
|
+
retryAfterMs: extractRetryHint(undefined, message),
|
|
2579
|
+
baseUrl: advisorModel.baseUrl,
|
|
2580
|
+
modelId: advisorModel.id,
|
|
2581
|
+
});
|
|
2582
|
+
},
|
|
2542
2583
|
notifyFailure: error => {
|
|
2543
2584
|
const message = error instanceof Error ? error.message : String(error);
|
|
2544
2585
|
this.emitNotice(
|
|
@@ -4666,7 +4707,8 @@ export class AgentSession {
|
|
|
4666
4707
|
// Decide first: a non-interrupting tool-source match attaches to the
|
|
4667
4708
|
// specific tool call's result instead of driving a loop-wide follow-up.
|
|
4668
4709
|
const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
|
|
4669
|
-
const
|
|
4710
|
+
const matchedToolId = this.#extractTtsrToolCallId(matchContext);
|
|
4711
|
+
const perToolId = shouldInterrupt ? undefined : matchedToolId;
|
|
4670
4712
|
if (perToolId) {
|
|
4671
4713
|
this.#addPerToolTtsrInjections(perToolId, matches);
|
|
4672
4714
|
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
@@ -4682,7 +4724,16 @@ export class AgentSession {
|
|
|
4682
4724
|
// Abort the stream immediately — do not gate on extension callbacks
|
|
4683
4725
|
this.#ttsrAbortPending = true;
|
|
4684
4726
|
this.#ensureTtsrResumePromise();
|
|
4685
|
-
this
|
|
4727
|
+
const abortReason = this.#formatTtsrAbortReason(matches);
|
|
4728
|
+
this.agent.abort(
|
|
4729
|
+
matchedToolId
|
|
4730
|
+
? createToolScopedAbortReason(
|
|
4731
|
+
abortReason,
|
|
4732
|
+
{ [matchedToolId]: abortReason },
|
|
4733
|
+
"TTSR interrupt on another tool call",
|
|
4734
|
+
)
|
|
4735
|
+
: abortReason,
|
|
4736
|
+
);
|
|
4686
4737
|
// Notify extensions (fire-and-forget, does not block abort)
|
|
4687
4738
|
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
4688
4739
|
// Schedule retry after a short delay
|
|
@@ -5755,6 +5806,10 @@ export class AgentSession {
|
|
|
5755
5806
|
this.#unsubscribeAppendOnly();
|
|
5756
5807
|
this.#unsubscribeAppendOnly = undefined;
|
|
5757
5808
|
}
|
|
5809
|
+
if (this.#unsubscribeModelRoles) {
|
|
5810
|
+
this.#unsubscribeModelRoles();
|
|
5811
|
+
this.#unsubscribeModelRoles = undefined;
|
|
5812
|
+
}
|
|
5758
5813
|
this.#eventListeners = [];
|
|
5759
5814
|
}
|
|
5760
5815
|
|
|
@@ -6298,6 +6353,7 @@ export class AgentSession {
|
|
|
6298
6353
|
),
|
|
6299
6354
|
);
|
|
6300
6355
|
}
|
|
6356
|
+
this.#setActiveToolNames?.(validToolNames);
|
|
6301
6357
|
const activeNameSet = new Set(validToolNames);
|
|
6302
6358
|
for (const name of Array.from(this.#selectedDiscoveredToolNames)) {
|
|
6303
6359
|
if (!activeNameSet.has(name) || isMCPToolName(name) || !this.#toolRegistry.has(name)) {
|
|
@@ -6403,6 +6459,7 @@ export class AgentSession {
|
|
|
6403
6459
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
6404
6460
|
if (!this.#rebuildSystemPrompt) return;
|
|
6405
6461
|
const activeToolNames = this.getActiveToolNames();
|
|
6462
|
+
this.#setActiveToolNames?.(activeToolNames);
|
|
6406
6463
|
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
6407
6464
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
6408
6465
|
this.#baseSystemPromptBeforeMemoryPromotion = undefined;
|
|
@@ -6525,7 +6582,7 @@ export class AgentSession {
|
|
|
6525
6582
|
entries.sort();
|
|
6526
6583
|
instructionsSegment = entries.join("\u0006");
|
|
6527
6584
|
}
|
|
6528
|
-
const date =
|
|
6585
|
+
const date = this.#getLocalCalendarDate();
|
|
6529
6586
|
return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
|
|
6530
6587
|
}
|
|
6531
6588
|
|
|
@@ -7342,11 +7399,15 @@ export class AgentSession {
|
|
|
7342
7399
|
timestamp,
|
|
7343
7400
|
});
|
|
7344
7401
|
}
|
|
7345
|
-
if (
|
|
7402
|
+
if (
|
|
7403
|
+
this.#magicKeywordEnabled("workflow") &&
|
|
7404
|
+
containsWorkflow(text) &&
|
|
7405
|
+
this.getActiveToolNames().includes("task")
|
|
7406
|
+
) {
|
|
7346
7407
|
keywordNotices.push({
|
|
7347
7408
|
role: "custom",
|
|
7348
7409
|
customType: "workflow-notice",
|
|
7349
|
-
content:
|
|
7410
|
+
content: renderWorkflowNotice({ taskBatch: this.settings.get("task.batch") }),
|
|
7350
7411
|
display: false,
|
|
7351
7412
|
attribution: "user",
|
|
7352
7413
|
timestamp,
|
|
@@ -8787,6 +8848,7 @@ export class AgentSession {
|
|
|
8787
8848
|
|
|
8788
8849
|
const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
|
|
8789
8850
|
|
|
8851
|
+
this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
|
|
8790
8852
|
this.#clearActiveRetryFallback();
|
|
8791
8853
|
this.#setModelWithProviderSessionReset(targetModel);
|
|
8792
8854
|
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
@@ -8824,6 +8886,7 @@ export class AgentSession {
|
|
|
8824
8886
|
|
|
8825
8887
|
const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
|
|
8826
8888
|
|
|
8889
|
+
this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
|
|
8827
8890
|
this.#clearActiveRetryFallback();
|
|
8828
8891
|
this.#setModelWithProviderSessionReset(targetModel);
|
|
8829
8892
|
this.sessionManager.appendModelChange(
|
|
@@ -8983,6 +9046,7 @@ export class AgentSession {
|
|
|
8983
9046
|
const next = scopedModels[nextIndex];
|
|
8984
9047
|
|
|
8985
9048
|
// Apply model
|
|
9049
|
+
this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(next.model));
|
|
8986
9050
|
this.#clearActiveRetryFallback();
|
|
8987
9051
|
this.#setModelWithProviderSessionReset(next.model);
|
|
8988
9052
|
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
@@ -9013,6 +9077,7 @@ export class AgentSession {
|
|
|
9013
9077
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
9014
9078
|
}
|
|
9015
9079
|
|
|
9080
|
+
this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(nextModel));
|
|
9016
9081
|
this.#clearActiveRetryFallback();
|
|
9017
9082
|
this.#setModelWithProviderSessionReset(nextModel);
|
|
9018
9083
|
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
@@ -10041,6 +10106,17 @@ export class AgentSession {
|
|
|
10041
10106
|
|
|
10042
10107
|
// Start a new session
|
|
10043
10108
|
const previousSessionFile = this.sessionFile;
|
|
10109
|
+
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
10110
|
+
const result = (await this.#extensionRunner.emit({
|
|
10111
|
+
type: "session_before_switch",
|
|
10112
|
+
reason: "handoff",
|
|
10113
|
+
})) as SessionBeforeSwitchResult | undefined;
|
|
10114
|
+
|
|
10115
|
+
if (result?.cancel) {
|
|
10116
|
+
options?.onSwitchCancelled?.();
|
|
10117
|
+
return undefined;
|
|
10118
|
+
}
|
|
10119
|
+
}
|
|
10044
10120
|
await this.sessionManager.flush();
|
|
10045
10121
|
this.#cancelOwnAsyncJobs();
|
|
10046
10122
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
@@ -10097,6 +10173,13 @@ export class AgentSession {
|
|
|
10097
10173
|
this.agent.replaceMessages(sessionContext.messages);
|
|
10098
10174
|
this.#resetAllAdvisorRuntimes();
|
|
10099
10175
|
this.#syncTodoPhasesFromBranch();
|
|
10176
|
+
if (this.#extensionRunner) {
|
|
10177
|
+
await this.#extensionRunner.emit({
|
|
10178
|
+
type: "session_switch",
|
|
10179
|
+
reason: "handoff",
|
|
10180
|
+
previousSessionFile,
|
|
10181
|
+
});
|
|
10182
|
+
}
|
|
10100
10183
|
|
|
10101
10184
|
return { document: handoffText, savedPath };
|
|
10102
10185
|
} catch (error) {
|
|
@@ -12313,13 +12396,17 @@ export class AgentSession {
|
|
|
12313
12396
|
// queue, not the core steering queue (which handoff's agent.reset() would wipe).
|
|
12314
12397
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
12315
12398
|
if (action === "handoff") {
|
|
12399
|
+
let handoffSwitchCancelled = false;
|
|
12316
12400
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
12317
12401
|
const handoffResult = await this.handoff(handoffFocus, {
|
|
12318
12402
|
autoTriggered: true,
|
|
12319
12403
|
signal: autoCompactionSignal,
|
|
12404
|
+
onSwitchCancelled: () => {
|
|
12405
|
+
handoffSwitchCancelled = true;
|
|
12406
|
+
},
|
|
12320
12407
|
});
|
|
12321
12408
|
if (!handoffResult) {
|
|
12322
|
-
const aborted = autoCompactionSignal.aborted;
|
|
12409
|
+
const aborted = autoCompactionSignal.aborted || handoffSwitchCancelled;
|
|
12323
12410
|
if (aborted) {
|
|
12324
12411
|
await this.#emitSessionEvent({
|
|
12325
12412
|
type: "auto_compaction_end",
|
|
@@ -13220,6 +13307,7 @@ export class AgentSession {
|
|
|
13220
13307
|
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
13221
13308
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13222
13309
|
if (!parsedCurrent) return undefined;
|
|
13310
|
+
const chains = this.#getRetryFallbackChains();
|
|
13223
13311
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
13224
13312
|
const currentPlainSelector = this.model
|
|
13225
13313
|
? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
|
|
@@ -13229,11 +13317,11 @@ export class AgentSession {
|
|
|
13229
13317
|
? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
|
|
13230
13318
|
: undefined;
|
|
13231
13319
|
|
|
13232
|
-
for (const role of Object.keys(
|
|
13320
|
+
for (const role of Object.keys(chains)) {
|
|
13233
13321
|
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
13234
13322
|
if (primarySelector?.raw === currentSelector) return role;
|
|
13235
13323
|
}
|
|
13236
|
-
for (const role of Object.keys(
|
|
13324
|
+
for (const role of Object.keys(chains)) {
|
|
13237
13325
|
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
13238
13326
|
if (!primarySelector) continue;
|
|
13239
13327
|
if (currentPlainSelector && primarySelector.raw === currentPlainSelector) return role;
|
|
@@ -13241,6 +13329,14 @@ export class AgentSession {
|
|
|
13241
13329
|
if (primaryBaseSelector === currentBaseSelector) return role;
|
|
13242
13330
|
if (currentPlainBaseSelector && primaryBaseSelector === currentPlainBaseSelector) return role;
|
|
13243
13331
|
}
|
|
13332
|
+
const defaultChain = chains.default;
|
|
13333
|
+
if (
|
|
13334
|
+
Array.isArray(defaultChain) &&
|
|
13335
|
+
defaultChain.length > 0 &&
|
|
13336
|
+
this.#getRetryFallbackPrimarySelector("default") === undefined
|
|
13337
|
+
) {
|
|
13338
|
+
return "default";
|
|
13339
|
+
}
|
|
13244
13340
|
return undefined;
|
|
13245
13341
|
}
|
|
13246
13342
|
|
|
@@ -13259,9 +13355,27 @@ export class AgentSession {
|
|
|
13259
13355
|
}
|
|
13260
13356
|
|
|
13261
13357
|
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
13262
|
-
|
|
13263
|
-
if (chain.length <= 1) return [];
|
|
13358
|
+
let chain = this.#getRetryFallbackEffectiveChain(role);
|
|
13264
13359
|
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
13360
|
+
if (chain.length === 0 && role === "default" && parsedCurrent) {
|
|
13361
|
+
const chains = this.#getRetryFallbackChains();
|
|
13362
|
+
const defaultChain = chains.default;
|
|
13363
|
+
if (
|
|
13364
|
+
Array.isArray(defaultChain) &&
|
|
13365
|
+
defaultChain.length > 0 &&
|
|
13366
|
+
this.#getRetryFallbackPrimarySelector("default") === undefined
|
|
13367
|
+
) {
|
|
13368
|
+
const seen = new Set<string>([parsedCurrent.raw]);
|
|
13369
|
+
chain = [parsedCurrent];
|
|
13370
|
+
for (const selector of defaultChain) {
|
|
13371
|
+
const parsed = parseRetryFallbackSelector(selector, this.#modelRegistry);
|
|
13372
|
+
if (!parsed || seen.has(parsed.raw)) continue;
|
|
13373
|
+
seen.add(parsed.raw);
|
|
13374
|
+
chain.push(parsed);
|
|
13375
|
+
}
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
if (chain.length <= 1) return [];
|
|
13265
13379
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
13266
13380
|
const currentPlainSelector =
|
|
13267
13381
|
this.model && parsedCurrent
|
|
@@ -15535,7 +15649,7 @@ export class AgentSession {
|
|
|
15535
15649
|
lastAttemptAtByAccount: coordinator.lastAttemptAtByAccount,
|
|
15536
15650
|
});
|
|
15537
15651
|
if (!decision.redeem) {
|
|
15538
|
-
logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
|
|
15652
|
+
logger.debug("codex-auto-reset: skipped", { reason: decision.reason, account: accountKey });
|
|
15539
15653
|
return false;
|
|
15540
15654
|
}
|
|
15541
15655
|
if (shouldPromptCodexAutoRedeem(cfg.autoRedeem) && !(await this.#confirmCodexAutoRedeem(decision))) {
|
package/src/system-prompt.ts
CHANGED
|
@@ -24,6 +24,7 @@ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { ty
|
|
|
24
24
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
25
25
|
import { shortenPath } from "./tools/render-utils";
|
|
26
26
|
import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
27
|
+
import { formatLocalCalendarDate } from "./utils/local-date";
|
|
27
28
|
import { normalizePromptPath } from "./utils/prompt-path";
|
|
28
29
|
import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
29
30
|
|
|
@@ -400,7 +401,7 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
|
|
|
400
401
|
return userLevel?.content ?? null;
|
|
401
402
|
}
|
|
402
403
|
|
|
403
|
-
export const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES = ["read", "bash", "
|
|
404
|
+
export const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
|
|
404
405
|
|
|
405
406
|
export interface SystemPromptToolMetadata {
|
|
406
407
|
label: string;
|
|
@@ -693,7 +694,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
693
694
|
}
|
|
694
695
|
}
|
|
695
696
|
|
|
696
|
-
const date =
|
|
697
|
+
const date = formatLocalCalendarDate();
|
|
697
698
|
const dateTime = date;
|
|
698
699
|
const promptCwd = shortenPath(normalizePromptPath(resolvedCwd));
|
|
699
700
|
const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
|
|
@@ -140,6 +140,30 @@ function unquoteToken(token: string): string {
|
|
|
140
140
|
return token;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function isInsideShellQuote(command: string, index: number): boolean {
|
|
144
|
+
let quote: "'" | '"' | undefined;
|
|
145
|
+
for (let i = 0; i < index; i++) {
|
|
146
|
+
const char = command[i];
|
|
147
|
+
if (char === "\\" && quote !== "'") {
|
|
148
|
+
i++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (char === "'" && quote !== '"') {
|
|
152
|
+
quote = quote === "'" ? undefined : "'";
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (char === '"' && quote !== "'") {
|
|
156
|
+
quote = quote === '"' ? undefined : '"';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return quote !== undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isEmbeddedInQuotedText(command: string, token: string, index: number): boolean {
|
|
163
|
+
if (token.startsWith("'") || token.startsWith('"')) return false;
|
|
164
|
+
return isInsideShellQuote(command, index);
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
/** Shell-escape a path using single quotes. */
|
|
144
168
|
function shellEscape(p: string): string {
|
|
145
169
|
return `'${p.replace(/'/g, "'\\''")}'`;
|
|
@@ -216,6 +240,7 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
|
|
|
216
240
|
|
|
217
241
|
/**
|
|
218
242
|
* Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
|
|
243
|
+
* Unresolvable URLs and literal mentions inside larger quoted text are left unchanged.
|
|
219
244
|
* Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
|
|
220
245
|
*/
|
|
221
246
|
export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
|
|
@@ -231,15 +256,22 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
|
|
|
231
256
|
const index = match.index;
|
|
232
257
|
if (index === undefined) continue;
|
|
233
258
|
|
|
259
|
+
if (isEmbeddedInQuotedText(command, token, index)) continue;
|
|
260
|
+
|
|
234
261
|
const rawUrl = unquoteToken(token);
|
|
235
262
|
const url = normalizeLocalScheme(rawUrl);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
263
|
+
let resolvedPath: string;
|
|
264
|
+
try {
|
|
265
|
+
resolvedPath = await resolveInternalUrlToPath(
|
|
266
|
+
url,
|
|
267
|
+
options.skills,
|
|
268
|
+
options.internalRouter,
|
|
269
|
+
options.localOptions,
|
|
270
|
+
options.ensureLocalParentDirs,
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
243
275
|
const replacement = options.noEscape ? resolvedPath : shellEscape(resolvedPath);
|
|
244
276
|
expanded = `${expanded.slice(0, index)}${replacement}${expanded.slice(index + token.length)}`;
|
|
245
277
|
}
|