@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -35
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +18 -111
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-loader.ts +31 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/stt/recorder.ts +68 -55
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/types.ts +11 -8
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
|
@@ -857,6 +857,18 @@ export class ModelRegistry {
|
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
859
|
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
|
860
|
+
// #reloadStaticModels above may have rebuilt #models from static sources,
|
|
861
|
+
// dropping models previously discovered by OTHER runtime providers (their
|
|
862
|
+
// fetchDynamicModels results live only in #models + the SQLite cache, not
|
|
863
|
+
// in #loadModels' static inputs). Restore them from cache with the default
|
|
864
|
+
// online-if-uncached strategy: no network while their cached row is
|
|
865
|
+
// fresh, so the scoped refresh above stays the only forced fetch.
|
|
866
|
+
const otherRuntimeProviderIds = new Set(
|
|
867
|
+
[...this.#runtimeModelManagers.keys()].filter(runtimeId => runtimeId !== providerId),
|
|
868
|
+
);
|
|
869
|
+
if (otherRuntimeProviderIds.size > 0) {
|
|
870
|
+
await this.#refreshRuntimeDiscoveries("online-if-uncached", otherRuntimeProviderIds);
|
|
871
|
+
}
|
|
860
872
|
}
|
|
861
873
|
|
|
862
874
|
/**
|
|
@@ -1105,9 +1117,33 @@ export class ModelRegistry {
|
|
|
1105
1117
|
if (cache.fresh && cache.authoritative) {
|
|
1106
1118
|
authoritativeFreshProviders.add(providerId);
|
|
1107
1119
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1120
|
+
// The v10 model cache never persists request headers (#5780): restore
|
|
1121
|
+
// them from the bundled static catalog, and drop cached rows whose
|
|
1122
|
+
// headers cannot be rebuilt so the bundled fallback (which still
|
|
1123
|
+
// carries its headers) wins the startup merge instead of a cached
|
|
1124
|
+
// model with required transport headers missing.
|
|
1125
|
+
const omittedHeaderIds = new Set(cache.headerOmittedModelIds);
|
|
1126
|
+
const unrestorableHeaderIds = new Set(cache.unrestorableHeaderModelIds);
|
|
1127
|
+
const bundledById =
|
|
1128
|
+
omittedHeaderIds.size > 0
|
|
1129
|
+
? new Map(
|
|
1130
|
+
(getBundledModels(providerId as Parameters<typeof getBundledModels>[0]) as Model<Api>[]).map(
|
|
1131
|
+
bundledModel => [bundledModel.id, bundledModel],
|
|
1132
|
+
),
|
|
1133
|
+
)
|
|
1134
|
+
: undefined;
|
|
1135
|
+
const models: ModelSpec<Api>[] = [];
|
|
1136
|
+
for (const cachedModel of cache.models) {
|
|
1137
|
+
const spec = cachedModel.provider === providerId ? cachedModel : { ...cachedModel, provider: providerId };
|
|
1138
|
+
if (!omittedHeaderIds.has(spec.id)) {
|
|
1139
|
+
models.push(spec);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (unrestorableHeaderIds.has(spec.id)) continue;
|
|
1143
|
+
const bundledHeaders = bundledById?.get(spec.id)?.headers;
|
|
1144
|
+
if (!bundledHeaders) continue;
|
|
1145
|
+
models.push({ ...spec, headers: bundledHeaders });
|
|
1146
|
+
}
|
|
1111
1147
|
const providerOverride = this.#providerOverrides.get(providerId);
|
|
1112
1148
|
const withTransport = providerOverride
|
|
1113
1149
|
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
@@ -1145,13 +1181,20 @@ export class ModelRegistry {
|
|
|
1145
1181
|
continue;
|
|
1146
1182
|
}
|
|
1147
1183
|
const configStale = this.#isDiscoveryCacheOlderThanModelsConfig(cache.updatedAt);
|
|
1184
|
+
// Cached rows never persist headers (#5780); models that had live
|
|
1185
|
+
// headers cannot be rebuilt here, so exclude them and mark the
|
|
1186
|
+
// discovery stale to force a refetch instead of returning models
|
|
1187
|
+
// missing required transport headers.
|
|
1188
|
+
const omittedHeaderIds = new Set(cache.headerOmittedModelIds);
|
|
1189
|
+
const usableCacheModels =
|
|
1190
|
+
omittedHeaderIds.size > 0 ? cache.models.filter(model => !omittedHeaderIds.has(model.id)) : cache.models;
|
|
1148
1191
|
const models = this.#applyProviderModelOverrides(
|
|
1149
1192
|
providerConfig.provider,
|
|
1150
1193
|
this.#normalizeDiscoverableModels(
|
|
1151
1194
|
providerConfig,
|
|
1152
1195
|
this.#applyProviderCompat(
|
|
1153
1196
|
providerConfig.compat,
|
|
1154
|
-
|
|
1197
|
+
usableCacheModels.map(model => buildModel(model)),
|
|
1155
1198
|
),
|
|
1156
1199
|
),
|
|
1157
1200
|
);
|
|
@@ -1160,7 +1203,12 @@ export class ModelRegistry {
|
|
|
1160
1203
|
provider: providerConfig.provider,
|
|
1161
1204
|
status: "cached",
|
|
1162
1205
|
optional: providerConfig.optional ?? false,
|
|
1163
|
-
stale:
|
|
1206
|
+
stale:
|
|
1207
|
+
providerConfig.discovery.type === "llama.cpp" ||
|
|
1208
|
+
!cache.fresh ||
|
|
1209
|
+
!cache.authoritative ||
|
|
1210
|
+
configStale ||
|
|
1211
|
+
omittedHeaderIds.size > 0,
|
|
1164
1212
|
fetchedAt: cache.updatedAt,
|
|
1165
1213
|
models: models.map(model => model.id),
|
|
1166
1214
|
});
|
|
@@ -1593,6 +1641,7 @@ export class ModelRegistry {
|
|
|
1593
1641
|
providerId: string,
|
|
1594
1642
|
strategy: ModelRefreshStrategy,
|
|
1595
1643
|
cacheProviderId: string,
|
|
1644
|
+
authoritative: boolean,
|
|
1596
1645
|
): Promise<string | undefined> {
|
|
1597
1646
|
const peekedKey = await this.#peekApiKeyForProvider(providerId);
|
|
1598
1647
|
if (isAuthenticated(peekedKey) || strategy === "offline") {
|
|
@@ -1602,7 +1651,13 @@ export class ModelRegistry {
|
|
|
1602
1651
|
if (oauthCredentials.length === 0) {
|
|
1603
1652
|
return peekedKey;
|
|
1604
1653
|
}
|
|
1605
|
-
|
|
1654
|
+
// Authoritative providers prune bundled models only when their manager is
|
|
1655
|
+
// actually constructed, which needs an authenticated key. A fresh cache does
|
|
1656
|
+
// not let us skip the refresh here: with an expired OAuth token peekedKey is
|
|
1657
|
+
// undefined, the manager is never added, and stale bundled models survive the
|
|
1658
|
+
// full cache TTL. So only take the no-refresh shortcut for non-authoritative
|
|
1659
|
+
// providers, whose bundled models stay visible regardless.
|
|
1660
|
+
if (strategy === "online-if-uncached" && !authoritative) {
|
|
1606
1661
|
// Mirror shouldFetchRemoteSources: built-in managers use the catalog's
|
|
1607
1662
|
// default TTL, so only refresh when the manager will actually fetch.
|
|
1608
1663
|
const cache = readModelCache<Api>(
|
|
@@ -1634,11 +1689,13 @@ export class ModelRegistry {
|
|
|
1634
1689
|
): Promise<ModelManagerOptions<Api>[]> {
|
|
1635
1690
|
const specialProviderDescriptors: Array<{
|
|
1636
1691
|
providerId: string;
|
|
1692
|
+
authoritative: boolean;
|
|
1637
1693
|
resolveKey: (value: string | undefined) => string | undefined;
|
|
1638
1694
|
createOptions: (key: string) => ModelManagerOptions<Api>;
|
|
1639
1695
|
}> = [
|
|
1640
1696
|
{
|
|
1641
1697
|
providerId: "google-antigravity",
|
|
1698
|
+
authoritative: false,
|
|
1642
1699
|
resolveKey: extractGoogleOAuthToken,
|
|
1643
1700
|
createOptions: oauthToken =>
|
|
1644
1701
|
googleAntigravityModelManagerOptions({
|
|
@@ -1649,6 +1706,7 @@ export class ModelRegistry {
|
|
|
1649
1706
|
},
|
|
1650
1707
|
{
|
|
1651
1708
|
providerId: "google-gemini-cli",
|
|
1709
|
+
authoritative: false,
|
|
1652
1710
|
resolveKey: extractGoogleOAuthToken,
|
|
1653
1711
|
createOptions: oauthToken =>
|
|
1654
1712
|
googleGeminiCliModelManagerOptions({
|
|
@@ -1659,12 +1717,14 @@ export class ModelRegistry {
|
|
|
1659
1717
|
},
|
|
1660
1718
|
{
|
|
1661
1719
|
providerId: "openai-codex",
|
|
1720
|
+
authoritative: true,
|
|
1662
1721
|
resolveKey: value => value,
|
|
1663
1722
|
createOptions: accessToken => {
|
|
1664
1723
|
const accountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", accessToken);
|
|
1665
1724
|
return openaiCodexModelManagerOptions({
|
|
1666
1725
|
accessToken,
|
|
1667
1726
|
accountId,
|
|
1727
|
+
fetch: this.#fetch,
|
|
1668
1728
|
});
|
|
1669
1729
|
},
|
|
1670
1730
|
},
|
|
@@ -1689,12 +1749,22 @@ export class ModelRegistry {
|
|
|
1689
1749
|
const cacheProviderId =
|
|
1690
1750
|
descriptor.createModelManagerOptions({ baseUrl: discoveryBaseUrl, fetch: this.#fetch })
|
|
1691
1751
|
.cacheProviderId ?? descriptor.providerId;
|
|
1692
|
-
return this.#resolveBuiltInDiscoveryApiKey(
|
|
1752
|
+
return this.#resolveBuiltInDiscoveryApiKey(
|
|
1753
|
+
descriptor.providerId,
|
|
1754
|
+
strategy,
|
|
1755
|
+
cacheProviderId,
|
|
1756
|
+
descriptor.dynamicModelsAuthoritative ?? false,
|
|
1757
|
+
);
|
|
1693
1758
|
}),
|
|
1694
1759
|
);
|
|
1695
1760
|
const specialKeys = await Promise.all(
|
|
1696
1761
|
enabledSpecialProviderDescriptors.map(descriptor =>
|
|
1697
|
-
this.#resolveBuiltInDiscoveryApiKey(
|
|
1762
|
+
this.#resolveBuiltInDiscoveryApiKey(
|
|
1763
|
+
descriptor.providerId,
|
|
1764
|
+
strategy,
|
|
1765
|
+
descriptor.providerId,
|
|
1766
|
+
descriptor.authoritative,
|
|
1767
|
+
),
|
|
1698
1768
|
),
|
|
1699
1769
|
);
|
|
1700
1770
|
const options: ModelManagerOptions<Api>[] = [];
|
|
@@ -1986,6 +2056,17 @@ export class ModelRegistry {
|
|
|
1986
2056
|
);
|
|
1987
2057
|
}
|
|
1988
2058
|
|
|
2059
|
+
/**
|
|
2060
|
+
* Whether the provider's configured API key is resolved from a command.
|
|
2061
|
+
*
|
|
2062
|
+
* Callers use this to distinguish the registry's command-first resolver
|
|
2063
|
+
* path from lower-priority credentials in {@link authStorage}.
|
|
2064
|
+
*/
|
|
2065
|
+
hasCommandBackedApiKey(provider: string): boolean {
|
|
2066
|
+
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
2067
|
+
return isCommandConfigValue(keyConfig);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1989
2070
|
getDiscoverableProviders(): string[] {
|
|
1990
2071
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1991
2072
|
return this.#discoverableProviders
|
|
@@ -1138,6 +1138,8 @@ export function resolveAgentPrewalkPattern(options: AgentPrewalkResolutionOption
|
|
|
1138
1138
|
export interface ResolvedModelRoleValue {
|
|
1139
1139
|
model: Model<Api> | undefined;
|
|
1140
1140
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
1141
|
+
/** matchedPatternIndex identifies the first configured pattern that matched an available model. */
|
|
1142
|
+
matchedPatternIndex?: number;
|
|
1141
1143
|
explicitThinkingLevel: boolean;
|
|
1142
1144
|
warning: string | undefined;
|
|
1143
1145
|
}
|
|
@@ -1167,11 +1169,12 @@ export function resolveModelRoleValue(
|
|
|
1167
1169
|
// models) once and reuse it across every fallback pattern instead of
|
|
1168
1170
|
// rebuilding it per pattern inside parseModelPattern.
|
|
1169
1171
|
const preferenceContext = buildPreferenceContext(availableModels, matchPreferences);
|
|
1170
|
-
for (const effectivePattern of effectivePatterns) {
|
|
1172
|
+
for (const [patternIndex, effectivePattern] of effectivePatterns.entries()) {
|
|
1171
1173
|
const resolved = matchPatternWithContext(effectivePattern, availableModels, preferenceContext);
|
|
1172
1174
|
if (resolved.model) {
|
|
1173
1175
|
return {
|
|
1174
1176
|
model: resolved.model,
|
|
1177
|
+
matchedPatternIndex: patternIndex,
|
|
1175
1178
|
thinkingLevel: resolved.explicitThinkingLevel
|
|
1176
1179
|
? resolved.thinkingLevel === AUTO_THINKING
|
|
1177
1180
|
? AUTO_THINKING
|
|
@@ -1598,6 +1601,10 @@ export function filterAvailableModelsByEnabledPatterns(
|
|
|
1598
1601
|
|
|
1599
1602
|
export interface ResolveCliModelResult {
|
|
1600
1603
|
model: Model<Api> | undefined;
|
|
1604
|
+
/** configuredPatterns is the full configured fallback chain when the selector resolves through a role. */
|
|
1605
|
+
configuredPatterns?: string[];
|
|
1606
|
+
/** configuredPatternIndex identifies the configured role pattern that matched an available model. */
|
|
1607
|
+
configuredPatternIndex?: number;
|
|
1601
1608
|
selector?: string;
|
|
1602
1609
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
1603
1610
|
warning: string | undefined;
|
|
@@ -1606,6 +1613,8 @@ export interface ResolveCliModelResult {
|
|
|
1606
1613
|
|
|
1607
1614
|
/**
|
|
1608
1615
|
* Resolve a single model from CLI flags.
|
|
1616
|
+
*
|
|
1617
|
+
* Exact model names take precedence over configured role names.
|
|
1609
1618
|
*/
|
|
1610
1619
|
export function resolveCliModel(options: {
|
|
1611
1620
|
cliProvider?: string;
|
|
@@ -1630,19 +1639,6 @@ export function resolveCliModel(options: {
|
|
|
1630
1639
|
};
|
|
1631
1640
|
}
|
|
1632
1641
|
|
|
1633
|
-
if (!cliProvider && modelRoleAliasPrefixLength(cliModel) !== undefined) {
|
|
1634
|
-
const resolved = resolveModelRoleValue(cliModel, availableModels, { settings, matchPreferences: preferences });
|
|
1635
|
-
if (resolved.model) {
|
|
1636
|
-
return {
|
|
1637
|
-
model: resolved.model,
|
|
1638
|
-
selector: formatModelString(resolved.model),
|
|
1639
|
-
thinkingLevel: resolved.thinkingLevel,
|
|
1640
|
-
warning: resolved.warning,
|
|
1641
|
-
error: undefined,
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
1642
|
const providerMap = new Map<string, string>();
|
|
1647
1643
|
for (const model of availableModels) {
|
|
1648
1644
|
providerMap.set(model.provider.toLowerCase(), model.provider);
|
|
@@ -1682,6 +1678,73 @@ export function resolveCliModel(options: {
|
|
|
1682
1678
|
error: undefined,
|
|
1683
1679
|
};
|
|
1684
1680
|
}
|
|
1681
|
+
const { base: exactBase, level: exactThinkingLevel } = splitThinkingSuffix(
|
|
1682
|
+
trimmedModel,
|
|
1683
|
+
-1,
|
|
1684
|
+
MAX_THINKING_SUFFIX_OPTIONS,
|
|
1685
|
+
);
|
|
1686
|
+
if (exactThinkingLevel) {
|
|
1687
|
+
let exactSuffixed = findExactModelReferenceMatch(exactBase, availableModels);
|
|
1688
|
+
if (!exactSuffixed) {
|
|
1689
|
+
const lowerExactBase = exactBase.toLowerCase();
|
|
1690
|
+
exactSuffixed = availableModels.find(
|
|
1691
|
+
model =>
|
|
1692
|
+
model.id.toLowerCase() === lowerExactBase ||
|
|
1693
|
+
`${model.provider}/${model.id}`.toLowerCase() === lowerExactBase,
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
if (exactSuffixed) {
|
|
1697
|
+
return {
|
|
1698
|
+
model: exactSuffixed,
|
|
1699
|
+
selector: formatModelString(exactSuffixed),
|
|
1700
|
+
warning: undefined,
|
|
1701
|
+
thinkingLevel: exactThinkingLevel,
|
|
1702
|
+
error: undefined,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
let configuredPatterns: string[] | undefined;
|
|
1708
|
+
if (!cliProvider) {
|
|
1709
|
+
const { base: bareRoleName, level: bareRoleThinkingLevel } = splitThinkingSuffix(
|
|
1710
|
+
trimmedModel,
|
|
1711
|
+
-1,
|
|
1712
|
+
MAX_THINKING_SUFFIX_OPTIONS,
|
|
1713
|
+
);
|
|
1714
|
+
const roleSelector =
|
|
1715
|
+
modelRoleAliasPrefixLength(trimmedModel) !== undefined
|
|
1716
|
+
? trimmedModel
|
|
1717
|
+
: settings?.getModelRole(bareRoleName) !== undefined
|
|
1718
|
+
? `${formatModelRoleAlias(bareRoleName)}${bareRoleThinkingLevel ? `:${bareRoleThinkingLevel}` : ""}`
|
|
1719
|
+
: undefined;
|
|
1720
|
+
if (roleSelector) {
|
|
1721
|
+
configuredPatterns = resolveConfiguredModelPatterns([roleSelector], settings);
|
|
1722
|
+
const resolved = resolveModelRoleValue(roleSelector, availableModels, {
|
|
1723
|
+
settings,
|
|
1724
|
+
matchPreferences: preferences,
|
|
1725
|
+
});
|
|
1726
|
+
if (resolved.model) {
|
|
1727
|
+
return {
|
|
1728
|
+
model: resolved.model,
|
|
1729
|
+
selector: formatModelString(resolved.model),
|
|
1730
|
+
configuredPatterns,
|
|
1731
|
+
configuredPatternIndex: resolved.matchedPatternIndex,
|
|
1732
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
1733
|
+
warning: resolved.warning,
|
|
1734
|
+
error: undefined,
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
if (configuredPatterns && configuredPatterns.length > 0) {
|
|
1738
|
+
return {
|
|
1739
|
+
model: undefined,
|
|
1740
|
+
configuredPatterns,
|
|
1741
|
+
selector: undefined,
|
|
1742
|
+
thinkingLevel: undefined,
|
|
1743
|
+
warning: resolved.warning,
|
|
1744
|
+
error: `Model "${trimmedModel}" not found. Run "omp models" to see available models.`,
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1685
1748
|
}
|
|
1686
1749
|
|
|
1687
1750
|
let pattern = trimmedModel;
|
|
@@ -1725,6 +1788,7 @@ export function resolveCliModel(options: {
|
|
|
1725
1788
|
const display = provider ? `${provider}/${pattern}` : cliModel;
|
|
1726
1789
|
return {
|
|
1727
1790
|
model: undefined,
|
|
1791
|
+
configuredPatterns,
|
|
1728
1792
|
selector: undefined,
|
|
1729
1793
|
thinkingLevel: undefined,
|
|
1730
1794
|
warning,
|
|
@@ -61,6 +61,7 @@ const OpenAICompatFields = {
|
|
|
61
61
|
"supportsImageDetailOriginal?": "boolean",
|
|
62
62
|
// anthropic-messages compat flags (same `compat` slot, per-api interpretation)
|
|
63
63
|
"supportsEagerToolInputStreaming?": "boolean",
|
|
64
|
+
"allowAnthropicHeaderOverrides?": "boolean",
|
|
64
65
|
"requiresToolResultId?": "boolean",
|
|
65
66
|
"replayUnsignedThinking?": "boolean",
|
|
66
67
|
} as const;
|
package/src/config/settings.ts
CHANGED
|
@@ -355,7 +355,9 @@ export class Settings {
|
|
|
355
355
|
this.#cwd = path.normalize(options.cwd ?? getProjectDir());
|
|
356
356
|
this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
|
|
357
357
|
this.#configPath = options.inMemory ? null : path.join(this.#agentDir, MAIN_CONFIG_FILENAMES[0]);
|
|
358
|
-
|
|
358
|
+
const configFiles = process.env.PI_CONFIG_FILES?.split(path.delimiter).filter(Boolean) ?? [];
|
|
359
|
+
if (options.configFiles) configFiles.push(...options.configFiles);
|
|
360
|
+
this.#configFiles = configFiles.map(file => path.resolve(this.#cwd, expandTilde(file)));
|
|
359
361
|
this.#persist = !options.inMemory && options.readOnly !== true;
|
|
360
362
|
|
|
361
363
|
if (options.overrides) {
|
package/src/dap/client.ts
CHANGED
|
@@ -55,6 +55,8 @@ export class DapClient {
|
|
|
55
55
|
readonly adapter: DapResolvedAdapter;
|
|
56
56
|
readonly cwd: string;
|
|
57
57
|
readonly proc: DapClientState["proc"];
|
|
58
|
+
/** TCP server port reused by child DAP sessions. */
|
|
59
|
+
readonly port?: number;
|
|
58
60
|
/** ReadableStream of DAP bytes — from proc.stdout (stdio) or a socket (socket mode). */
|
|
59
61
|
readonly #readable: ReadableStream<Uint8Array>;
|
|
60
62
|
/** Write sink — proc.stdin (stdio) or a socket (socket mode). */
|
|
@@ -78,7 +80,12 @@ export class DapClient {
|
|
|
78
80
|
adapter: DapResolvedAdapter,
|
|
79
81
|
cwd: string,
|
|
80
82
|
proc: DapClientState["proc"],
|
|
81
|
-
options?: {
|
|
83
|
+
options?: {
|
|
84
|
+
readable?: ReadableStream<Uint8Array>;
|
|
85
|
+
writeSink?: DapWriteSink;
|
|
86
|
+
socket?: { end(): void };
|
|
87
|
+
port?: number;
|
|
88
|
+
},
|
|
82
89
|
) {
|
|
83
90
|
this.adapter = adapter;
|
|
84
91
|
this.cwd = cwd;
|
|
@@ -86,6 +93,7 @@ export class DapClient {
|
|
|
86
93
|
this.#readable = options?.readable ?? (proc.stdout as ReadableStream<Uint8Array>);
|
|
87
94
|
this.#writeSink = options?.writeSink ?? proc.stdin;
|
|
88
95
|
this.#socket = options?.socket;
|
|
96
|
+
this.port = options?.port;
|
|
89
97
|
this.proc.exited.then(
|
|
90
98
|
() => this.#rejectPendingWritesForExit(),
|
|
91
99
|
() => this.#rejectPendingWritesForExit(),
|
|
@@ -96,6 +104,9 @@ export class DapClient {
|
|
|
96
104
|
if (adapter.connectMode === "socket") {
|
|
97
105
|
return DapClient.#spawnSocket({ adapter, cwd, socketReadyTimeoutMs });
|
|
98
106
|
}
|
|
107
|
+
if (adapter.connectMode === "tcp") {
|
|
108
|
+
return DapClient.#spawnTcp({ adapter, cwd, socketReadyTimeoutMs });
|
|
109
|
+
}
|
|
99
110
|
// Merge non-interactive env and start in a new session (detached → setsid)
|
|
100
111
|
// so the adapter process tree has no controlling terminal. Without this,
|
|
101
112
|
// debuggee children can reach /dev/tty and trigger SIGTTIN, suspending
|
|
@@ -118,6 +129,85 @@ export class DapClient {
|
|
|
118
129
|
return client;
|
|
119
130
|
}
|
|
120
131
|
|
|
132
|
+
/** Connect to another session on an existing TCP DAP server. */
|
|
133
|
+
static async connect({
|
|
134
|
+
adapter,
|
|
135
|
+
cwd,
|
|
136
|
+
host,
|
|
137
|
+
port,
|
|
138
|
+
}: {
|
|
139
|
+
adapter: DapResolvedAdapter;
|
|
140
|
+
cwd: string;
|
|
141
|
+
host: string;
|
|
142
|
+
port: number;
|
|
143
|
+
}): Promise<DapClient> {
|
|
144
|
+
const exited = Promise.withResolvers<void>();
|
|
145
|
+
const { readable, writeSink, socket } = await connectTcpSocket(host, port, () => exited.resolve());
|
|
146
|
+
const proc = {
|
|
147
|
+
exited: exited.promise,
|
|
148
|
+
exitCode: null,
|
|
149
|
+
stdin: { write: () => 0, flush: () => undefined },
|
|
150
|
+
stdout: new ReadableStream<Uint8Array>(),
|
|
151
|
+
stderr: new ReadableStream<Uint8Array>(),
|
|
152
|
+
peekStderr: () => "",
|
|
153
|
+
kill: () => {
|
|
154
|
+
exited.resolve();
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
} as unknown as DapClientState["proc"];
|
|
158
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket, port });
|
|
159
|
+
exited.promise.then(() => client.#handleProcessExit());
|
|
160
|
+
void client.#startMessageReader();
|
|
161
|
+
return client;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Spawn an adapter that listens on a caller-selected TCP port. */
|
|
165
|
+
static async #spawnTcp({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise<DapClient> {
|
|
166
|
+
const host = "127.0.0.1";
|
|
167
|
+
const reservation = Bun.listen({
|
|
168
|
+
hostname: host,
|
|
169
|
+
port: 0,
|
|
170
|
+
socket: {
|
|
171
|
+
open() {},
|
|
172
|
+
data() {},
|
|
173
|
+
close() {},
|
|
174
|
+
error() {},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const port = reservation.port;
|
|
178
|
+
reservation.stop(true);
|
|
179
|
+
const args = adapter.args.map(arg => arg.replaceAll("$" + "{port}", String(port)));
|
|
180
|
+
const proc = ptree.spawn([adapter.resolvedCommand, ...args], {
|
|
181
|
+
cwd,
|
|
182
|
+
stdin: "pipe",
|
|
183
|
+
env: {
|
|
184
|
+
...Bun.env,
|
|
185
|
+
...NON_INTERACTIVE_ENV,
|
|
186
|
+
},
|
|
187
|
+
detached: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { readable, writeSink, socket } = await waitForTcpTransport(
|
|
192
|
+
host,
|
|
193
|
+
port,
|
|
194
|
+
socketReadyTimeoutMs ?? SOCKET_READY_TIMEOUT_MS,
|
|
195
|
+
proc,
|
|
196
|
+
);
|
|
197
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket, port });
|
|
198
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
199
|
+
void client.#startMessageReader();
|
|
200
|
+
return client;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
try {
|
|
203
|
+
proc.kill();
|
|
204
|
+
} catch {
|
|
205
|
+
/* proc may already be dead */
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
121
211
|
/**
|
|
122
212
|
* Spawn a socket-mode adapter (e.g. dlv).
|
|
123
213
|
* Linux: connect to a unix domain socket via --listen=unix:<path>
|
|
@@ -659,6 +749,83 @@ async function waitForCondition(
|
|
|
659
749
|
throw new Error(`Socket not ready after ${timeoutMs}ms`);
|
|
660
750
|
}
|
|
661
751
|
|
|
752
|
+
/** Connect once to a TCP DAP server. */
|
|
753
|
+
async function connectTcpSocket(host: string, port: number, onClose?: () => void): Promise<SocketTransport> {
|
|
754
|
+
const { promise, resolve, reject } = Promise.withResolvers<SocketTransport>();
|
|
755
|
+
let streamController: ReadableStreamDefaultController<Uint8Array>;
|
|
756
|
+
let opened = false;
|
|
757
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
758
|
+
start(controller) {
|
|
759
|
+
streamController = controller;
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
void Bun.connect({
|
|
764
|
+
hostname: host,
|
|
765
|
+
port,
|
|
766
|
+
socket: {
|
|
767
|
+
open(socket) {
|
|
768
|
+
opened = true;
|
|
769
|
+
resolve({
|
|
770
|
+
readable,
|
|
771
|
+
writeSink: socketToSink(socket),
|
|
772
|
+
socket,
|
|
773
|
+
});
|
|
774
|
+
},
|
|
775
|
+
data(_socket, data) {
|
|
776
|
+
streamController.enqueue(new Uint8Array(data));
|
|
777
|
+
},
|
|
778
|
+
close() {
|
|
779
|
+
onClose?.();
|
|
780
|
+
if (!opened) {
|
|
781
|
+
reject(new Error(`Connection to TCP port ${host}:${port} closed before opening`));
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
streamController.close();
|
|
785
|
+
} catch {
|
|
786
|
+
/* already closed */
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
error(_socket, error) {
|
|
790
|
+
onClose?.();
|
|
791
|
+
if (!opened) {
|
|
792
|
+
reject(error);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
streamController.error(error);
|
|
796
|
+
} catch {
|
|
797
|
+
/* already closed */
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
}).catch(error => {
|
|
802
|
+
onClose?.();
|
|
803
|
+
reject(error);
|
|
804
|
+
});
|
|
805
|
+
return promise;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Wait for a TCP DAP server and retain the first successful connection. */
|
|
809
|
+
async function waitForTcpTransport(
|
|
810
|
+
host: string,
|
|
811
|
+
port: number,
|
|
812
|
+
timeoutMs: number,
|
|
813
|
+
proc: { exitCode: number | null },
|
|
814
|
+
): Promise<SocketTransport> {
|
|
815
|
+
const deadline = Date.now() + timeoutMs;
|
|
816
|
+
while (Date.now() < deadline) {
|
|
817
|
+
if (proc.exitCode !== null) {
|
|
818
|
+
throw new Error(`Adapter process exited before TCP port ${host}:${port} was ready`);
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
return await connectTcpSocket(host, port);
|
|
822
|
+
} catch {
|
|
823
|
+
await Bun.sleep(50);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
throw new Error(`TCP port ${host}:${port} was not ready after ${timeoutMs}ms`);
|
|
827
|
+
}
|
|
828
|
+
|
|
662
829
|
interface SocketTransport {
|
|
663
830
|
readable: ReadableStream<Uint8Array>;
|
|
664
831
|
writeSink: DapWriteSink;
|
package/src/dap/config.ts
CHANGED
|
@@ -10,6 +10,8 @@ import DEFAULTS from "./defaults.json" with { type: "json" };
|
|
|
10
10
|
import type { DapAdapterConfig, DapResolvedAdapter } from "./types";
|
|
11
11
|
|
|
12
12
|
const EXTENSIONLESS_DEBUGGER_ORDER: readonly string[] = ["gdb", "lldb-dap"];
|
|
13
|
+
const JS_DEBUG_SERVER_ENV = "JS_DEBUG_DAP_SERVER";
|
|
14
|
+
const DAP_PORT_ARGUMENT = "$" + "{port}";
|
|
13
15
|
|
|
14
16
|
interface NormalizedConfig {
|
|
15
17
|
adapters: Record<string, unknown>;
|
|
@@ -45,7 +47,7 @@ function normalizeObject(value: unknown): Record<string, unknown> {
|
|
|
45
47
|
function normalizeAdapterConfig(config: unknown): DapAdapterConfig | null {
|
|
46
48
|
if (!isRecord(config)) return null;
|
|
47
49
|
if (typeof config.command !== "string" || config.command.length === 0) return null;
|
|
48
|
-
const connectMode = config.connectMode === "socket"
|
|
50
|
+
const connectMode = config.connectMode === "socket" || config.connectMode === "tcp" ? config.connectMode : undefined;
|
|
49
51
|
return {
|
|
50
52
|
command: config.command,
|
|
51
53
|
args: normalizeStringArray(config.args),
|
|
@@ -184,6 +186,52 @@ function normalizeCommandForCwd(command: string, cwd: string): string {
|
|
|
184
186
|
return command;
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
function resolveJsDebugServerPath(cwd: string): string | null {
|
|
190
|
+
const configured = process.env[JS_DEBUG_SERVER_ENV];
|
|
191
|
+
const dataHome = process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
|
|
192
|
+
const candidates = [
|
|
193
|
+
...(configured ? [path.resolve(cwd, configured)] : []),
|
|
194
|
+
path.join(dataHome, "nvim", "mason", "packages", "js-debug-adapter", "js-debug", "src", "dapDebugServer.js"),
|
|
195
|
+
path.join(os.homedir(), ".local", "opt", "js-debug", "src", "dapDebugServer.js"),
|
|
196
|
+
];
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveDefaultJsDebugAdapter(
|
|
204
|
+
adapterName: string,
|
|
205
|
+
config: DapAdapterConfig,
|
|
206
|
+
cwd: string,
|
|
207
|
+
localRoots?: readonly string[],
|
|
208
|
+
): DapResolvedAdapter | null | undefined {
|
|
209
|
+
if (adapterName !== "js-debug-adapter" || config.command !== "js-debug-adapter") {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const serverPath = resolveJsDebugServerPath(cwd);
|
|
213
|
+
if (!serverPath) return null;
|
|
214
|
+
const nodeCommand = resolveCommand("node", cwd, {
|
|
215
|
+
cache: WhichCachePolicy.Fresh,
|
|
216
|
+
PATH: process.env.PATH,
|
|
217
|
+
localRoots,
|
|
218
|
+
});
|
|
219
|
+
const resolvedCommand = nodeCommand ?? process.execPath;
|
|
220
|
+
return {
|
|
221
|
+
name: adapterName,
|
|
222
|
+
command: nodeCommand ? "node" : "bun",
|
|
223
|
+
args: [serverPath, DAP_PORT_ARGUMENT, "127.0.0.1"],
|
|
224
|
+
resolvedCommand,
|
|
225
|
+
languages: config.languages ?? [],
|
|
226
|
+
fileTypes: config.fileTypes ?? [],
|
|
227
|
+
rootMarkers: config.rootMarkers ?? [],
|
|
228
|
+
launchDefaults: config.launchDefaults ?? {},
|
|
229
|
+
attachDefaults: config.attachDefaults ?? {},
|
|
230
|
+
connectMode: "tcp",
|
|
231
|
+
acceptsDirectoryProgram: config.acceptsDirectoryProgram === true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
187
235
|
function resolveAdapterFromConfig(
|
|
188
236
|
adapterName: string,
|
|
189
237
|
configs: Record<string, DapAdapterConfig>,
|
|
@@ -192,6 +240,8 @@ function resolveAdapterFromConfig(
|
|
|
192
240
|
): DapResolvedAdapter | null {
|
|
193
241
|
const config = configs[adapterName];
|
|
194
242
|
if (!config) return null;
|
|
243
|
+
const jsDebugAdapter = resolveDefaultJsDebugAdapter(adapterName, config, cwd, localRoots);
|
|
244
|
+
if (jsDebugAdapter !== undefined) return jsDebugAdapter;
|
|
195
245
|
const normalizedCommand = normalizeCommandForCwd(config.command, cwd);
|
|
196
246
|
const commandIsBare =
|
|
197
247
|
!path.isAbsolute(config.command) && !config.command.includes("/") && !config.command.includes("\\");
|