@oh-my-pi/pi-coding-agent 12.5.0 → 12.6.0
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 +23 -0
- package/docs/extensions.md +31 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +168 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/extensibility/extensions/index.ts +3 -0
- package/src/extensibility/extensions/loader.ts +11 -0
- package/src/extensibility/extensions/types.ts +112 -1
- package/src/index.ts +2 -0
- package/src/main.ts +19 -8
- package/src/modes/components/welcome.ts +44 -14
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/prompts/system/system-prompt.md +4 -0
- package/src/prompts/tools/hashline.md +63 -72
- package/src/sdk.ts +68 -30
- package/src/session/auth-storage.ts +43 -11
- package/src/system-prompt.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.6.0] - 2026-02-16
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added runtime tests covering extension provider registration and deferred model pattern resolution behavior.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Improved welcome screen responsiveness to dynamically show or hide the right column based on available terminal width
|
|
13
|
+
- Extended extension `registerProvider()` typing with OAuth provider support and source-aware registration metadata.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed welcome screen layout to gracefully handle small terminal widths and prevent rendering errors on narrow displays
|
|
18
|
+
- Fixed welcome screen title truncation to prevent overflow when content exceeds available width
|
|
19
|
+
- Fixed deferred `--model` resolution so extension-provided models are matched before fallback selection and unresolved explicit patterns no longer silently fallback.
|
|
20
|
+
- Fixed CLI `--api-key` handling for deferred model resolution by applying runtime API key overrides after extension model selection.
|
|
21
|
+
- Fixed extension provider registration cleanup to remove stale source-scoped custom API/OAuth providers across extension reloads.
|
|
22
|
+
|
|
23
|
+
## [12.5.1] - 2026-02-15
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added `repeatToolDescriptions` setting to render full tool descriptions in the system prompt instead of a tool name list
|
|
27
|
+
|
|
5
28
|
## [12.5.0] - 2026-02-15
|
|
6
29
|
### Breaking Changes
|
|
7
30
|
|
package/docs/extensions.md
CHANGED
|
@@ -886,6 +886,37 @@ pi.registerCommand("stats", {
|
|
|
886
886
|
},
|
|
887
887
|
});
|
|
888
888
|
```
|
|
889
|
+
### pi.registerProvider(name, config)
|
|
890
|
+
|
|
891
|
+
Register or override providers/models at runtime:
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
pi.registerProvider("my-provider", {
|
|
895
|
+
baseUrl: "https://api.example.com/v1",
|
|
896
|
+
apiKey: "MY_PROVIDER_API_KEY",
|
|
897
|
+
api: "openai-completions",
|
|
898
|
+
models: [
|
|
899
|
+
{
|
|
900
|
+
id: "my-model",
|
|
901
|
+
name: "My Model",
|
|
902
|
+
reasoning: false,
|
|
903
|
+
input: ["text"],
|
|
904
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
905
|
+
contextWindow: 128000,
|
|
906
|
+
maxTokens: 8192,
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
});
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
`registerProvider()` also supports:
|
|
913
|
+
|
|
914
|
+
- `streamSimple` for custom API adapters
|
|
915
|
+
- `headers` / `authHeader` for request customization
|
|
916
|
+
- `oauth` for `/login <provider>` support with extension-defined login/refresh behavior
|
|
917
|
+
|
|
918
|
+
Provider registrations are queued during extension load and applied when the session initializes.
|
|
919
|
+
|
|
889
920
|
|
|
890
921
|
### pi.registerMessageRenderer(customType, renderer)
|
|
891
922
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.6.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@mozilla/readability": "0.6.0",
|
|
87
|
-
"@oh-my-pi/omp-stats": "12.
|
|
88
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
89
|
-
"@oh-my-pi/pi-ai": "12.
|
|
90
|
-
"@oh-my-pi/pi-natives": "12.
|
|
91
|
-
"@oh-my-pi/pi-tui": "12.
|
|
92
|
-
"@oh-my-pi/pi-utils": "12.
|
|
87
|
+
"@oh-my-pi/omp-stats": "12.6.0",
|
|
88
|
+
"@oh-my-pi/pi-agent-core": "12.6.0",
|
|
89
|
+
"@oh-my-pi/pi-ai": "12.6.0",
|
|
90
|
+
"@oh-my-pi/pi-natives": "12.6.0",
|
|
91
|
+
"@oh-my-pi/pi-tui": "12.6.0",
|
|
92
|
+
"@oh-my-pi/pi-utils": "12.6.0",
|
|
93
93
|
"@sinclair/typebox": "^0.34.48",
|
|
94
94
|
"@xterm/headless": "^6.0.0",
|
|
95
95
|
"ajv": "^8.18.0",
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Api,
|
|
3
|
+
type AssistantMessageEventStream,
|
|
4
|
+
type Context,
|
|
3
5
|
getGitHubCopilotBaseUrl,
|
|
4
6
|
getModels,
|
|
5
7
|
getProviders,
|
|
6
8
|
type Model,
|
|
7
9
|
normalizeDomain,
|
|
10
|
+
type OAuthCredentials,
|
|
11
|
+
type OAuthLoginCallbacks,
|
|
12
|
+
registerCustomApi,
|
|
13
|
+
registerOAuthProvider,
|
|
14
|
+
type SimpleStreamOptions,
|
|
15
|
+
unregisterCustomApis,
|
|
16
|
+
unregisterOAuthProviders,
|
|
8
17
|
} from "@oh-my-pi/pi-ai";
|
|
9
18
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
10
19
|
import { type Static, Type } from "@sinclair/typebox";
|
|
@@ -291,6 +300,7 @@ export class ModelRegistry {
|
|
|
291
300
|
#modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
|
292
301
|
#configError: ConfigError | undefined = undefined;
|
|
293
302
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
303
|
+
#registeredProviderSources: Set<string> = new Set();
|
|
294
304
|
|
|
295
305
|
/**
|
|
296
306
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -705,4 +715,162 @@ export class ModelRegistry {
|
|
|
705
715
|
isUsingOAuth(model: Model<Api>): boolean {
|
|
706
716
|
return this.authStorage.hasOAuth(model.provider);
|
|
707
717
|
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Remove custom API/OAuth registrations for a specific extension source.
|
|
721
|
+
*/
|
|
722
|
+
clearSourceRegistrations(sourceId: string): void {
|
|
723
|
+
unregisterCustomApis(sourceId);
|
|
724
|
+
unregisterOAuthProviders(sourceId);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Remove registrations for extension sources that are no longer active.
|
|
729
|
+
*/
|
|
730
|
+
syncExtensionSources(activeSourceIds: string[]): void {
|
|
731
|
+
const activeSources = new Set(activeSourceIds);
|
|
732
|
+
for (const sourceId of this.#registeredProviderSources) {
|
|
733
|
+
if (activeSources.has(sourceId)) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
this.clearSourceRegistrations(sourceId);
|
|
737
|
+
this.#registeredProviderSources.delete(sourceId);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Register a provider dynamically (from extensions).
|
|
743
|
+
*
|
|
744
|
+
* If provider has models: replaces all existing models for this provider.
|
|
745
|
+
* If provider has only baseUrl/headers: overrides existing models' URLs.
|
|
746
|
+
* If provider has streamSimple: registers a custom API streaming function.
|
|
747
|
+
* If provider has oauth: registers OAuth provider for /login support.
|
|
748
|
+
*/
|
|
749
|
+
registerProvider(providerName: string, config: ProviderConfigInput, sourceId?: string): void {
|
|
750
|
+
if (config.streamSimple && !config.api) {
|
|
751
|
+
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (config.models && config.models.length > 0) {
|
|
755
|
+
if (!config.baseUrl) {
|
|
756
|
+
throw new Error(`Provider ${providerName}: "baseUrl" is required when defining models.`);
|
|
757
|
+
}
|
|
758
|
+
if (!config.apiKey && !config.oauth) {
|
|
759
|
+
throw new Error(`Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`);
|
|
760
|
+
}
|
|
761
|
+
for (const modelDef of config.models) {
|
|
762
|
+
const api = modelDef.api || config.api;
|
|
763
|
+
if (!api) {
|
|
764
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (config.streamSimple && config.api) {
|
|
770
|
+
const streamSimple = config.streamSimple;
|
|
771
|
+
registerCustomApi(config.api, streamSimple, sourceId, (model, context, options) =>
|
|
772
|
+
streamSimple(model, context, options as SimpleStreamOptions),
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (config.oauth) {
|
|
777
|
+
registerOAuthProvider({
|
|
778
|
+
...config.oauth,
|
|
779
|
+
id: providerName,
|
|
780
|
+
sourceId,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (sourceId) {
|
|
785
|
+
this.#registeredProviderSources.add(sourceId);
|
|
786
|
+
}
|
|
787
|
+
if (config.apiKey) {
|
|
788
|
+
this.#customProviderApiKeys.set(providerName, config.apiKey);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (config.models && config.models.length > 0) {
|
|
792
|
+
const nextModels = this.#models.filter(m => m.provider !== providerName);
|
|
793
|
+
for (const modelDef of config.models) {
|
|
794
|
+
const api = modelDef.api || config.api;
|
|
795
|
+
if (!api) {
|
|
796
|
+
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
797
|
+
}
|
|
798
|
+
let headers = config.headers || modelDef.headers ? { ...config.headers, ...modelDef.headers } : undefined;
|
|
799
|
+
if (config.authHeader && config.apiKey) {
|
|
800
|
+
const resolvedKey = resolveApiKeyConfig(config.apiKey);
|
|
801
|
+
if (resolvedKey) {
|
|
802
|
+
headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
nextModels.push({
|
|
807
|
+
id: modelDef.id,
|
|
808
|
+
name: modelDef.name,
|
|
809
|
+
api,
|
|
810
|
+
provider: providerName,
|
|
811
|
+
baseUrl: config.baseUrl!,
|
|
812
|
+
reasoning: modelDef.reasoning,
|
|
813
|
+
input: modelDef.input as ("text" | "image")[],
|
|
814
|
+
cost: modelDef.cost,
|
|
815
|
+
contextWindow: modelDef.contextWindow,
|
|
816
|
+
maxTokens: modelDef.maxTokens,
|
|
817
|
+
headers,
|
|
818
|
+
compat: modelDef.compat,
|
|
819
|
+
} as Model<Api>);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (config.oauth?.modifyModels) {
|
|
823
|
+
const credential = this.authStorage.getOAuthCredential(providerName);
|
|
824
|
+
if (credential) {
|
|
825
|
+
this.#models = config.oauth.modifyModels(nextModels, credential);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
this.#models = nextModels;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (config.baseUrl) {
|
|
835
|
+
this.#models = this.#models.map(m => {
|
|
836
|
+
if (m.provider !== providerName) return m;
|
|
837
|
+
return {
|
|
838
|
+
...m,
|
|
839
|
+
baseUrl: config.baseUrl ?? m.baseUrl,
|
|
840
|
+
headers: config.headers ? { ...m.headers, ...config.headers } : m.headers,
|
|
841
|
+
};
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Input type for registerProvider API (from extensions).
|
|
849
|
+
*/
|
|
850
|
+
export interface ProviderConfigInput {
|
|
851
|
+
baseUrl?: string;
|
|
852
|
+
apiKey?: string;
|
|
853
|
+
api?: Api;
|
|
854
|
+
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
855
|
+
headers?: Record<string, string>;
|
|
856
|
+
authHeader?: boolean;
|
|
857
|
+
oauth?: {
|
|
858
|
+
name: string;
|
|
859
|
+
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
|
|
860
|
+
refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
|
|
861
|
+
getApiKey?(credentials: OAuthCredentials): string;
|
|
862
|
+
modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
|
|
863
|
+
};
|
|
864
|
+
models?: Array<{
|
|
865
|
+
id: string;
|
|
866
|
+
name: string;
|
|
867
|
+
api?: Api;
|
|
868
|
+
reasoning: boolean;
|
|
869
|
+
input: ("text" | "image")[];
|
|
870
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
871
|
+
contextWindow: number;
|
|
872
|
+
maxTokens: number;
|
|
873
|
+
headers?: Record<string, string>;
|
|
874
|
+
compat?: Model<Api>["compat"];
|
|
875
|
+
}>;
|
|
708
876
|
}
|
|
@@ -253,6 +253,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
253
253
|
description: "Rewrite tool call arguments to normalized format in session history",
|
|
254
254
|
},
|
|
255
255
|
},
|
|
256
|
+
repeatToolDescriptions: {
|
|
257
|
+
type: "boolean",
|
|
258
|
+
default: false,
|
|
259
|
+
ui: {
|
|
260
|
+
tab: "agent",
|
|
261
|
+
label: "Repeat tool descriptions",
|
|
262
|
+
description: "Render full tool descriptions in the system prompt instead of a tool name list",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
256
265
|
readLineNumbers: {
|
|
257
266
|
type: "boolean",
|
|
258
267
|
default: false,
|
|
@@ -52,6 +52,8 @@ export class ExtensionRuntimeNotInitializedError extends Error {
|
|
|
52
52
|
*/
|
|
53
53
|
export class ExtensionRuntime implements IExtensionRuntime {
|
|
54
54
|
flagValues = new Map<string, boolean | string>();
|
|
55
|
+
pendingProviderRegistrations: Array<{ name: string; config: import("./types").ProviderConfig; sourceId: string }> =
|
|
56
|
+
[];
|
|
55
57
|
|
|
56
58
|
sendMessage(): void {
|
|
57
59
|
throw new ExtensionRuntimeNotInitializedError();
|
|
@@ -108,6 +110,11 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
108
110
|
readonly typebox = TypeBox;
|
|
109
111
|
readonly pi = piCodingAgent;
|
|
110
112
|
readonly flagValues = new Map<string, boolean | string>();
|
|
113
|
+
readonly pendingProviderRegistrations: Array<{
|
|
114
|
+
name: string;
|
|
115
|
+
config: import("./types").ProviderConfig;
|
|
116
|
+
sourceId: string;
|
|
117
|
+
}> = [];
|
|
111
118
|
|
|
112
119
|
constructor(
|
|
113
120
|
private readonly extension: Extension,
|
|
@@ -222,6 +229,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
222
229
|
setThinkingLevel(level: ThinkingLevel, persist?: boolean): void {
|
|
223
230
|
this.runtime.setThinkingLevel(level, persist);
|
|
224
231
|
}
|
|
232
|
+
|
|
233
|
+
registerProvider(name: string, config: import("./types").ProviderConfig): void {
|
|
234
|
+
this.runtime.pendingProviderRegistrations.push({ name, config, sourceId: this.extension.path });
|
|
235
|
+
}
|
|
225
236
|
}
|
|
226
237
|
|
|
227
238
|
/**
|
|
@@ -8,7 +8,18 @@
|
|
|
8
8
|
* - Interact with the user via UI primitives
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
Api,
|
|
13
|
+
AssistantMessageEventStream,
|
|
14
|
+
Context,
|
|
15
|
+
ImageContent,
|
|
16
|
+
Model,
|
|
17
|
+
OAuthCredentials,
|
|
18
|
+
OAuthLoginCallbacks,
|
|
19
|
+
SimpleStreamOptions,
|
|
20
|
+
TextContent,
|
|
21
|
+
ToolResultMessage,
|
|
22
|
+
} from "@oh-my-pi/pi-ai";
|
|
12
23
|
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
|
13
24
|
import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
14
25
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
@@ -976,10 +987,108 @@ export interface ExtensionAPI {
|
|
|
976
987
|
/** Set thinking level (clamped to model capabilities). */
|
|
977
988
|
setThinkingLevel(level: ThinkingLevel): void;
|
|
978
989
|
|
|
990
|
+
// =========================================================================
|
|
991
|
+
// Provider Registration
|
|
992
|
+
// =========================================================================
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Register or override a model provider.
|
|
996
|
+
*
|
|
997
|
+
* If `models` is provided: replaces all existing models for this provider.
|
|
998
|
+
* If only `baseUrl` is provided: overrides the URL for existing models.
|
|
999
|
+
* If `streamSimple` is provided: registers a custom API stream handler.
|
|
1000
|
+
*
|
|
1001
|
+
* @example
|
|
1002
|
+
* // Register a new provider with custom models and streaming
|
|
1003
|
+
* pi.registerProvider("google-vertex-claude", {
|
|
1004
|
+
* baseUrl: "https://us-east5-aiplatform.googleapis.com",
|
|
1005
|
+
* apiKey: "GOOGLE_CLOUD_PROJECT",
|
|
1006
|
+
* api: "vertex-claude-api",
|
|
1007
|
+
* streamSimple: myStreamFunction,
|
|
1008
|
+
* models: [
|
|
1009
|
+
* {
|
|
1010
|
+
* id: "claude-sonnet-4@20250514",
|
|
1011
|
+
* name: "Claude Sonnet 4 (Vertex)",
|
|
1012
|
+
* reasoning: true,
|
|
1013
|
+
* input: ["text", "image"],
|
|
1014
|
+
* cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
1015
|
+
* contextWindow: 200000,
|
|
1016
|
+
* maxTokens: 64000,
|
|
1017
|
+
* }
|
|
1018
|
+
* ]
|
|
1019
|
+
* });
|
|
1020
|
+
*
|
|
1021
|
+
* @example
|
|
1022
|
+
* // Override baseUrl for an existing provider
|
|
1023
|
+
* pi.registerProvider("anthropic", {
|
|
1024
|
+
* baseUrl: "https://proxy.example.com"
|
|
1025
|
+
* });
|
|
1026
|
+
*/
|
|
1027
|
+
registerProvider(name: string, config: ProviderConfig): void;
|
|
1028
|
+
|
|
979
1029
|
/** Shared event bus for extension communication. */
|
|
980
1030
|
events: EventBus;
|
|
981
1031
|
}
|
|
982
1032
|
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
// Provider Registration Types
|
|
1035
|
+
// ============================================================================
|
|
1036
|
+
|
|
1037
|
+
/** Configuration for registering a provider via pi.registerProvider(). */
|
|
1038
|
+
export interface ProviderConfig {
|
|
1039
|
+
/** Base URL for the API endpoint. Required when defining models. */
|
|
1040
|
+
baseUrl?: string;
|
|
1041
|
+
/** API key or environment variable name. Required when defining models unless oauth is provided. */
|
|
1042
|
+
apiKey?: string;
|
|
1043
|
+
/** API type identifier. Required when registering streamSimple or when models don't specify one. */
|
|
1044
|
+
api?: Api;
|
|
1045
|
+
/** Custom streaming function for non-built-in APIs. */
|
|
1046
|
+
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
1047
|
+
/** Custom headers to include in requests. */
|
|
1048
|
+
headers?: Record<string, string>;
|
|
1049
|
+
/** If true, adds Authorization: Bearer header with the resolved API key. */
|
|
1050
|
+
authHeader?: boolean;
|
|
1051
|
+
/** Models to register. If provided, replaces all existing models for this provider. */
|
|
1052
|
+
models?: ProviderModelConfig[];
|
|
1053
|
+
/** OAuth provider for /login support. */
|
|
1054
|
+
oauth?: {
|
|
1055
|
+
/** Display name in login UI. */
|
|
1056
|
+
name: string;
|
|
1057
|
+
/** Run the provider login flow and return credentials (or a plain API key) to persist. */
|
|
1058
|
+
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
|
|
1059
|
+
/** Refresh expired credentials. */
|
|
1060
|
+
refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
|
|
1061
|
+
/** Convert credentials to an API key string for requests. */
|
|
1062
|
+
getApiKey?(credentials: OAuthCredentials): string;
|
|
1063
|
+
/** Optional model rewrite hook for credential-aware routing (e.g., enterprise URLs). */
|
|
1064
|
+
modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** Configuration for a model within a provider. */
|
|
1069
|
+
export interface ProviderModelConfig {
|
|
1070
|
+
/** Model ID (e.g., "claude-sonnet-4@20250514"). */
|
|
1071
|
+
id: string;
|
|
1072
|
+
/** Display name (e.g., "Claude Sonnet 4 (Vertex)"). */
|
|
1073
|
+
name: string;
|
|
1074
|
+
/** API type override for this model. */
|
|
1075
|
+
api?: Api;
|
|
1076
|
+
/** Whether the model supports extended thinking. */
|
|
1077
|
+
reasoning: boolean;
|
|
1078
|
+
/** Supported input types. */
|
|
1079
|
+
input: ("text" | "image")[];
|
|
1080
|
+
/** Cost per million tokens. */
|
|
1081
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
1082
|
+
/** Maximum context window size in tokens. */
|
|
1083
|
+
contextWindow: number;
|
|
1084
|
+
/** Maximum output tokens. */
|
|
1085
|
+
maxTokens: number;
|
|
1086
|
+
/** Custom headers for this model. */
|
|
1087
|
+
headers?: Record<string, string>;
|
|
1088
|
+
/** OpenAI compatibility settings. */
|
|
1089
|
+
compat?: Model<Api>["compat"];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
983
1092
|
/** Extension factory function type. Supports both sync and async initialization. */
|
|
984
1093
|
export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
|
|
985
1094
|
|
|
@@ -1038,6 +1147,8 @@ export type SetThinkingLevelHandler = (level: ThinkingLevel, persist?: boolean)
|
|
|
1038
1147
|
/** Shared state created by loader, used during registration and runtime. */
|
|
1039
1148
|
export interface ExtensionRuntimeState {
|
|
1040
1149
|
flagValues: Map<string, boolean | string>;
|
|
1150
|
+
/** Provider registrations queued during extension loading, processed during session initialization */
|
|
1151
|
+
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; sourceId: string }>;
|
|
1041
1152
|
}
|
|
1042
1153
|
|
|
1043
1154
|
/** Action implementations for ExtensionAPI methods. */
|
package/src/index.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -372,7 +372,7 @@ async function buildSessionOptions(
|
|
|
372
372
|
|
|
373
373
|
// Model from CLI (--model) - uses same fuzzy matching as --models
|
|
374
374
|
if (parsed.model) {
|
|
375
|
-
const available = modelRegistry.
|
|
375
|
+
const available = modelRegistry.getAll();
|
|
376
376
|
const modelMatchPreferences = {
|
|
377
377
|
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
378
378
|
};
|
|
@@ -381,11 +381,13 @@ async function buildSessionOptions(
|
|
|
381
381
|
writeStderr(chalk.yellow(`Warning: ${warning}`));
|
|
382
382
|
}
|
|
383
383
|
if (!model) {
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
// Model not found in built-in registry — defer resolution to after extensions load
|
|
385
|
+
// (extensions may register additional providers/models via registerProvider)
|
|
386
|
+
options.modelPattern = parsed.model;
|
|
387
|
+
} else {
|
|
388
|
+
options.model = model;
|
|
389
|
+
settings.overrideModelRoles({ default: `${model.provider}/${model.id}` });
|
|
386
390
|
}
|
|
387
|
-
options.model = model;
|
|
388
|
-
settings.overrideModelRoles({ default: `${model.provider}/${model.id}` });
|
|
389
391
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
390
392
|
const remembered = settings.getModelRole("default");
|
|
391
393
|
if (remembered) {
|
|
@@ -610,11 +612,13 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
610
612
|
|
|
611
613
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
612
614
|
if (parsedArgs.apiKey) {
|
|
613
|
-
if (!sessionOptions.model) {
|
|
615
|
+
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
614
616
|
writeStderr(chalk.red("--api-key requires a model to be specified via --provider/--model or -m/--models"));
|
|
615
617
|
process.exit(1);
|
|
616
618
|
}
|
|
617
|
-
|
|
619
|
+
if (sessionOptions.model) {
|
|
620
|
+
authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsedArgs.apiKey);
|
|
621
|
+
}
|
|
618
622
|
}
|
|
619
623
|
|
|
620
624
|
time("buildSessionOptions");
|
|
@@ -622,6 +626,9 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
622
626
|
await createAgentSession(sessionOptions);
|
|
623
627
|
debugStartup("main:createAgentSession");
|
|
624
628
|
time("createAgentSession");
|
|
629
|
+
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
|
630
|
+
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
631
|
+
}
|
|
625
632
|
|
|
626
633
|
if (modelFallbackMessage) {
|
|
627
634
|
notifs.push({ kind: "warn", message: modelFallbackMessage });
|
|
@@ -660,7 +667,11 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
660
667
|
debugStartup("main:applyExtensionFlags");
|
|
661
668
|
|
|
662
669
|
if (!isInteractive && !session.model) {
|
|
663
|
-
|
|
670
|
+
if (modelFallbackMessage) {
|
|
671
|
+
writeStderr(chalk.red(modelFallbackMessage));
|
|
672
|
+
} else {
|
|
673
|
+
writeStderr(chalk.red("No models available."));
|
|
674
|
+
}
|
|
664
675
|
writeStderr(chalk.yellow("\nSet an API key environment variable:"));
|
|
665
676
|
writeStderr(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
|
|
666
677
|
writeStderr(chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`));
|
|
@@ -41,12 +41,31 @@ export class WelcomeComponent implements Component {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
render(termWidth: number): string[] {
|
|
44
|
-
// Box dimensions - responsive with
|
|
45
|
-
const minWidth = 80;
|
|
44
|
+
// Box dimensions - responsive with max width and small-terminal support
|
|
46
45
|
const maxWidth = 100;
|
|
47
|
-
const boxWidth = Math.
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const boxWidth = Math.min(maxWidth, Math.max(0, termWidth - 2));
|
|
47
|
+
if (boxWidth < 4) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
|
|
51
|
+
const preferredLeftCol = 26;
|
|
52
|
+
const minLeftCol = 14; // logo width
|
|
53
|
+
const minRightCol = 20;
|
|
54
|
+
const leftMinContentWidth = Math.max(
|
|
55
|
+
minLeftCol,
|
|
56
|
+
visibleWidth("Welcome back!"),
|
|
57
|
+
visibleWidth(this.modelName),
|
|
58
|
+
visibleWidth(this.providerName),
|
|
59
|
+
);
|
|
60
|
+
const desiredLeftCol = Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.35)));
|
|
61
|
+
const dualLeftCol =
|
|
62
|
+
dualContentWidth >= minRightCol + 1
|
|
63
|
+
? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
|
|
64
|
+
: Math.max(1, dualContentWidth - 1);
|
|
65
|
+
const dualRightCol = Math.max(1, dualContentWidth - dualLeftCol);
|
|
66
|
+
const showRightColumn = dualLeftCol >= leftMinContentWidth && dualRightCol >= minRightCol;
|
|
67
|
+
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
68
|
+
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
50
69
|
|
|
51
70
|
// Block-based OMP logo (gradient: magenta → cyan)
|
|
52
71
|
// biome-ignore format: preserve ASCII art layout
|
|
@@ -67,7 +86,7 @@ export class WelcomeComponent implements Component {
|
|
|
67
86
|
];
|
|
68
87
|
|
|
69
88
|
// Right column separator
|
|
70
|
-
const separatorWidth = rightCol - 2; // padding on each side
|
|
89
|
+
const separatorWidth = Math.max(0, rightCol - 2); // padding on each side
|
|
71
90
|
const separator = ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(separatorWidth))}`;
|
|
72
91
|
|
|
73
92
|
// Recent sessions content
|
|
@@ -131,20 +150,31 @@ export class WelcomeComponent implements Component {
|
|
|
131
150
|
const titlePrefixRaw = hChar.repeat(3);
|
|
132
151
|
const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
|
|
133
152
|
const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
const titleSpace = boxWidth - 2;
|
|
154
|
+
if (titleVisLen >= titleSpace) {
|
|
155
|
+
lines.push(tl + truncateToWidth(titleStyled, titleSpace) + tr);
|
|
156
|
+
} else {
|
|
157
|
+
const afterTitle = titleSpace - titleVisLen;
|
|
158
|
+
lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
|
|
159
|
+
}
|
|
137
160
|
|
|
138
161
|
// Content rows
|
|
139
|
-
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
162
|
+
const maxRows = showRightColumn ? Math.max(leftLines.length, rightLines.length) : leftLines.length;
|
|
140
163
|
for (let i = 0; i < maxRows; i++) {
|
|
141
164
|
const left = this.#fitToWidth(leftLines[i] ?? "", leftCol);
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
if (showRightColumn) {
|
|
166
|
+
const right = this.#fitToWidth(rightLines[i] ?? "", rightCol);
|
|
167
|
+
lines.push(v + left + v + right + v);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(v + left + v);
|
|
170
|
+
}
|
|
144
171
|
}
|
|
145
|
-
|
|
146
172
|
// Bottom border
|
|
147
|
-
|
|
173
|
+
if (showRightColumn) {
|
|
174
|
+
lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
|
|
175
|
+
} else {
|
|
176
|
+
lines.push(bl + h.repeat(leftCol) + br);
|
|
177
|
+
}
|
|
148
178
|
|
|
149
179
|
return lines;
|
|
150
180
|
}
|
|
@@ -56,11 +56,15 @@ The question is not "does this work?" but "under what conditions? What happens o
|
|
|
56
56
|
|
|
57
57
|
<tools>
|
|
58
58
|
## Available Tools
|
|
59
|
+
{{#if repeatToolDescriptions}}
|
|
59
60
|
{{#each toolDescriptions}}
|
|
60
61
|
<tool name="{{name}}">
|
|
61
62
|
{{description}}
|
|
62
63
|
</tool>
|
|
63
64
|
{{/each}}
|
|
65
|
+
{{else}}
|
|
66
|
+
{{#list tools join="\n"}}- {{this}}{{/list}}
|
|
67
|
+
{{/if}}
|
|
64
68
|
|
|
65
69
|
{{#ifAny (includes tools "python") (includes tools "bash")}}
|
|
66
70
|
### Precedence: Specialized → Python → Bash
|
|
@@ -1,94 +1,85 @@
|
|
|
1
|
-
# Edit (Hash
|
|
1
|
+
# Edit (Hash Anchored)
|
|
2
2
|
|
|
3
|
-
Line-addressed edits using hash-verified line references. Read
|
|
3
|
+
Line-addressed edits using hash-verified line references. Read files in hashline mode, collect exact `LINE:HASH` references, and submit edits that change only the targeted token or expression.
|
|
4
|
+
**CRITICAL: Copy `LINE:HASH` refs verbatim from read output. Use only the anchor prefix (e.g., `{{hashline 42 "const x = 1"}}`), never the trailing source text after `|`.**
|
|
4
5
|
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- `new_text` must differ from the current line content — sending identical content is rejected as a no-op
|
|
14
|
-
</critical>
|
|
6
|
+
<workflow>
|
|
7
|
+
1. Read the target file (`read`) to obtain `LINE:HASH` references
|
|
8
|
+
2. Collect the exact `LINE:HASH` refs for lines you will change
|
|
9
|
+
3. Direction-lock each mutation: identify the exact current token/expression → the intended replacement
|
|
10
|
+
4. Submit one `edit` call containing all operations for that file
|
|
11
|
+
5. If another edit is needed on the same file: re-read first, then edit (hashes change after every edit)
|
|
12
|
+
6. Respond with tool calls only — no prose
|
|
13
|
+
</workflow>
|
|
15
14
|
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
<operations>
|
|
16
|
+
Four edit variants are available:
|
|
17
|
+
- **`set_line`**: Replace a single line
|
|
18
|
+
`{ set_line: { anchor: "LINE:HASH", new_text: "..." } }`
|
|
19
|
+
`new_text: ""` keeps the line but makes it blank.
|
|
20
|
+
- **`replace_lines`**: Replace a contiguous range (use for deletions with `new_text: ""`)
|
|
21
|
+
`{ replace_lines: { start_anchor: "LINE:HASH", end_anchor: "LINE:HASH", new_text: "..." } }`
|
|
22
|
+
- **`insert_after`**: Add new content after an anchor line
|
|
23
|
+
`{ insert_after: { anchor: "LINE:HASH", text: "..." } }`
|
|
24
|
+
- **`replace`**: Substring-style fuzzy match (when line refs are unavailable)
|
|
25
|
+
`{ replace: { old_text: "...", new_text: "...", all?: boolean } }`
|
|
26
|
+
**Atomicity:** All edits in one call validate against the file as last read. Line numbers and hashes refer to the original state, not post-edit state. The applicator sorts and applies bottom-up automatically.
|
|
27
|
+
</operations>
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
<rules>
|
|
30
|
+
1. **Scope each operation minimally.** One logical change site per operation. Use separate `set_line` ops for non-adjacent lines instead of a wide `replace_lines` that spans unchanged code.
|
|
31
|
+
2. **Preserve original formatting exactly.** Copy each line's whitespace, braces, semicolons, trailing commas, and style — then change only the targeted token/expression. Keep `import { foo }` as-is; keep indentation and line breaks as-is.
|
|
32
|
+
3. **Use `insert_after` for additions.** When adding a field, argument, or import near existing lines, prefer `insert_after` over replacing a neighboring line.
|
|
33
|
+
4. **Ensure `new_text` differs from current content.** Identical content is rejected as a no-op.
|
|
34
|
+
5. **Edit only requested lines.** Leave unrelated code untouched.
|
|
35
|
+
6. **Lock mutation direction.** Replace the exact currently-present token with the intended target. For swaps between two locations, use two `set_line` ops in one call.
|
|
36
|
+
</rules>
|
|
32
37
|
|
|
33
|
-
<
|
|
34
|
-
**
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- Self-check before submitting: if your edit touches lines unrelated to the stated fix, split or narrow it.
|
|
43
|
-
- Do NOT reformat lines you are replacing — preserve exact whitespace, braces (`{ foo }` not `{foo}`), arrow style, and line breaks. Change ONLY the targeted token/expression. Reformatting causes hash verification failure even when the logic is correct.
|
|
44
|
-
- For swaps (exchanging content between two locations), use two `set_line` operations in one call — the applicator handles ordering. Do not try to account for line number shifts between operations.
|
|
45
|
-
</caution>
|
|
46
|
-
<instruction>
|
|
47
|
-
**Recovery:**
|
|
48
|
-
- Hash mismatch (`>>>` error): copy the updated `LINE:HASH` refs from the error verbatim and retry with the same intended mutation. Do NOT re-read unless you need lines not shown in the error.
|
|
49
|
-
- If hash mismatch repeats after applying updated refs, stop blind retries and re-read the relevant region before retrying.
|
|
50
|
-
- After a successful edit, always re-read the file before making another edit to the same file (hashes have changed).
|
|
51
|
-
- No-op error ("identical content"): your replacement text matches what the file already contains. STOP and re-read the file — you are likely targeting the wrong line or your replacement is not actually different. Do NOT retry with the same content. After 2 consecutive no-op errors on the same line, re-read the entire function/block to understand the current file state.
|
|
52
|
-
</instruction>
|
|
53
|
-
|
|
54
|
-
<instruction>
|
|
55
|
-
**Preflight schema and validation (required):**
|
|
56
|
-
- Payload shape is `{"path": string, "edits": [operation, ...]}` with a non-empty `edits` array.
|
|
57
|
-
- Each operation contains exactly one variant key: `set_line`, `replace_lines`, `insert_after`, or `replace`.
|
|
58
|
-
- Required fields by variant:
|
|
59
|
-
- `set_line`: `anchor`, `new_text`
|
|
60
|
-
- `replace_lines`: `start_anchor`, `end_anchor`, `new_text`
|
|
61
|
-
- `insert_after`: `anchor`, `text` (non-empty)
|
|
62
|
-
- `replace`: `old_text`, `new_text` (fuzzy match; `all: true` for replace-all)
|
|
63
|
-
- Each `anchor`/`start_anchor`/`end_anchor` ref matches `^\d+:[A-Za-z0-9]+$` (no spaces, no trailing source text).
|
|
64
|
-
- `new_text`/`text` preserves original formatting and changes only the direction-locked target locus.
|
|
65
|
-
</instruction>
|
|
66
|
-
|
|
67
|
-
<input>
|
|
68
|
-
- `path`: File path
|
|
69
|
-
- `edits`: Array of edit operations (one of the variants above)
|
|
70
|
-
</input>
|
|
38
|
+
<recovery>
|
|
39
|
+
**Hash mismatch (`>>>` error):**
|
|
40
|
+
→ Copy the updated `LINE:HASH` refs from the error output verbatim and retry with the same intended mutation.
|
|
41
|
+
→ Re-read only if you need lines not shown in the error.
|
|
42
|
+
→ If mismatch repeats after applying updated refs, stop and re-read the relevant region.
|
|
43
|
+
**No-op error ("identical content"):**
|
|
44
|
+
→ Stop. Re-read the file — you are targeting the wrong line or your replacement is not different.
|
|
45
|
+
→ After 2 consecutive no-op errors on the same line, re-read the entire function/block.
|
|
46
|
+
</recovery>
|
|
71
47
|
|
|
48
|
+
<examples>
|
|
72
49
|
<example name="replace single line">
|
|
73
|
-
|
|
50
|
+
set_line: { anchor: "{{hashline 2 " x"}}", new_text: " x = 99" }
|
|
74
51
|
</example>
|
|
75
52
|
|
|
76
53
|
<example name="replace range">
|
|
77
|
-
|
|
54
|
+
replace_lines: { start_anchor: "{{hashline 5 "old start line"}}", end_anchor: "{{hashline 8 "old end line"}}", new_text: " combined = True" }
|
|
78
55
|
</example>
|
|
79
56
|
|
|
80
57
|
<example name="delete lines">
|
|
81
|
-
|
|
58
|
+
replace_lines: { start_anchor: "{{hashline 5 "line to delete A"}}", end_anchor: "{{hashline 6 "line to delete B"}}", new_text: "" }
|
|
82
59
|
</example>
|
|
83
60
|
|
|
84
61
|
<example name="insert after">
|
|
85
|
-
|
|
62
|
+
insert_after: { anchor: "{{hashline 3 "anchor line content"}}", text: " # new comment" }
|
|
86
63
|
</example>
|
|
87
64
|
|
|
88
65
|
<example name="multiple edits (bottom-up safe)">
|
|
89
|
-
|
|
66
|
+
set_line: { anchor: "{{hashline 10 "old line 10"}}", new_text: " return False" }
|
|
67
|
+
set_line: { anchor: "{{hashline 3 "old line 3"}}", new_text: " x = 42" }
|
|
90
68
|
</example>
|
|
91
69
|
|
|
92
70
|
<example name="content replace (substr-style, no hashes)">
|
|
93
|
-
|
|
94
|
-
</example>
|
|
71
|
+
replace: { old_text: "x = 42", new_text: "x = 99" }
|
|
72
|
+
</example>
|
|
73
|
+
</examples>
|
|
74
|
+
|
|
75
|
+
<validation>
|
|
76
|
+
Before submitting, verify:
|
|
77
|
+
- [ ] Payload shape: `{"path": string, "edits": [operation, ...]}` with non-empty `edits` array
|
|
78
|
+
- [ ] Each operation has exactly one variant key: `set_line` | `replace_lines` | `insert_after` | `replace`
|
|
79
|
+
- [ ] Each anchor is copied exactly from the `LINE:HASH` prefix (no spaces, no trailing source text)
|
|
80
|
+
- [ ] `new_text`/`text` contains plain replacement lines only — no `LINE:HASH` prefixes, no diff `+` markers
|
|
81
|
+
- [ ] Each replacement differs from the current line content
|
|
82
|
+
- [ ] Each operation targets one logical change site with minimal scope
|
|
83
|
+
- [ ] Formatting of replaced lines matches the original exactly, except for the targeted change
|
|
84
|
+
</validation>
|
|
85
|
+
**REMINDER: Copy `LINE:HASH` refs verbatim. Anchors are `LINE:HASH` only — never `LINE:HASH|content`. Preserve exact formatting. Change only the targeted token.**
|
package/src/sdk.ts
CHANGED
|
@@ -8,7 +8,7 @@ import chalk from "chalk";
|
|
|
8
8
|
import { loadCapability } from "./capability";
|
|
9
9
|
import { type Rule, ruleCapability } from "./capability/rule";
|
|
10
10
|
import { ModelRegistry } from "./config/model-registry";
|
|
11
|
-
import { formatModelString, parseModelString } from "./config/model-resolver";
|
|
11
|
+
import { formatModelString, parseModelPattern, parseModelString } from "./config/model-resolver";
|
|
12
12
|
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
|
|
13
13
|
import { Settings, type SkillsSettings } from "./config/settings";
|
|
14
14
|
import { CursorExecHandlers } from "./cursor";
|
|
@@ -101,6 +101,9 @@ export interface CreateAgentSessionOptions {
|
|
|
101
101
|
|
|
102
102
|
/** Model to use. Default: from settings, else first available */
|
|
103
103
|
model?: Model;
|
|
104
|
+
/** Raw model pattern string (e.g. from --model CLI flag) to resolve after extensions load.
|
|
105
|
+
* Used when model lookup is deferred because extension-provided models aren't registered yet. */
|
|
106
|
+
modelPattern?: string;
|
|
104
107
|
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
|
|
105
108
|
thinkingLevel?: ThinkingLevel;
|
|
106
109
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
|
@@ -323,6 +326,7 @@ export interface BuildSystemPromptOptions {
|
|
|
323
326
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
324
327
|
cwd?: string;
|
|
325
328
|
appendPrompt?: string;
|
|
329
|
+
repeatToolDescriptions?: boolean;
|
|
326
330
|
}
|
|
327
331
|
|
|
328
332
|
/**
|
|
@@ -334,6 +338,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
334
338
|
skills: options.skills,
|
|
335
339
|
contextFiles: options.contextFiles,
|
|
336
340
|
appendSystemPrompt: options.appendPrompt,
|
|
341
|
+
repeatToolDescriptions: options.repeatToolDescriptions,
|
|
337
342
|
});
|
|
338
343
|
}
|
|
339
344
|
|
|
@@ -580,13 +585,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
580
585
|
const hasExistingSession = existingSession.messages.length > 0;
|
|
581
586
|
const hasThinkingEntry = sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
582
587
|
|
|
583
|
-
const hasExplicitModel = options.model !== undefined;
|
|
588
|
+
const hasExplicitModel = options.model !== undefined || options.modelPattern !== undefined;
|
|
584
589
|
let model = options.model;
|
|
585
590
|
let modelFallbackMessage: string | undefined;
|
|
586
|
-
|
|
587
|
-
//
|
|
591
|
+
// If session has data, try to restore model from it.
|
|
592
|
+
// Skip restore when an explicit model was requested.
|
|
588
593
|
const defaultModelStr = existingSession.models.default;
|
|
589
|
-
if (!model && hasExistingSession && defaultModelStr) {
|
|
594
|
+
if (!hasExplicitModel && !model && hasExistingSession && defaultModelStr) {
|
|
590
595
|
const parsedModel = parseModelString(defaultModelStr);
|
|
591
596
|
if (parsedModel) {
|
|
592
597
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
@@ -599,8 +604,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
599
604
|
}
|
|
600
605
|
}
|
|
601
606
|
|
|
602
|
-
// If still no model, try settings default
|
|
603
|
-
|
|
607
|
+
// If still no model, try settings default.
|
|
608
|
+
// Skip settings fallback when an explicit model was requested.
|
|
609
|
+
if (!hasExplicitModel && !model) {
|
|
604
610
|
const settingsDefaultModel = settings.getModelRole("default");
|
|
605
611
|
if (settingsDefaultModel) {
|
|
606
612
|
const parsedModel = parseModelString(settingsDefaultModel);
|
|
@@ -613,29 +619,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
613
619
|
}
|
|
614
620
|
}
|
|
615
621
|
|
|
616
|
-
// Fall back to first available model with a valid API key
|
|
617
|
-
if (!model) {
|
|
618
|
-
const allModels = modelRegistry.getAll();
|
|
619
|
-
for (const candidate of allModels) {
|
|
620
|
-
if (await hasModelApiKey(candidate)) {
|
|
621
|
-
model = candidate;
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
time("findAvailableModel");
|
|
626
|
-
if (model) {
|
|
627
|
-
if (modelFallbackMessage) {
|
|
628
|
-
modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
|
|
629
|
-
}
|
|
630
|
-
} else {
|
|
631
|
-
// No models available - set message so user knows to /login or configure keys
|
|
632
|
-
modelFallbackMessage =
|
|
633
|
-
"No models available. Use /login or set an API key environment variable. Then use /model to select a model.";
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
time("findModel");
|
|
638
|
-
|
|
639
622
|
// For subagent sessions using GitHub Copilot, add X-Initiator header
|
|
640
623
|
// to ensure proper billing (agent-initiated vs user-initiated)
|
|
641
624
|
const taskDepth = options.taskDepth ?? 0;
|
|
@@ -897,6 +880,58 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
897
880
|
}
|
|
898
881
|
}
|
|
899
882
|
|
|
883
|
+
// Process provider registrations queued during extension loading.
|
|
884
|
+
// This must happen before the runner is created so that models registered by
|
|
885
|
+
// extensions are available for model selection on session resume / fallback.
|
|
886
|
+
const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
|
|
887
|
+
modelRegistry.syncExtensionSources(activeExtensionSources);
|
|
888
|
+
for (const sourceId of new Set(activeExtensionSources)) {
|
|
889
|
+
modelRegistry.clearSourceRegistrations(sourceId);
|
|
890
|
+
}
|
|
891
|
+
if (extensionsResult.runtime.pendingProviderRegistrations.length > 0) {
|
|
892
|
+
for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
|
|
893
|
+
modelRegistry.registerProvider(name, config, sourceId);
|
|
894
|
+
}
|
|
895
|
+
extensionsResult.runtime.pendingProviderRegistrations = [];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Resolve deferred --model pattern now that extension models are registered.
|
|
899
|
+
if (!model && options.modelPattern) {
|
|
900
|
+
const availableModels = modelRegistry.getAll();
|
|
901
|
+
const matchPreferences = {
|
|
902
|
+
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
903
|
+
};
|
|
904
|
+
const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences);
|
|
905
|
+
if (resolved) {
|
|
906
|
+
model = resolved;
|
|
907
|
+
modelFallbackMessage = undefined;
|
|
908
|
+
} else {
|
|
909
|
+
modelFallbackMessage = `Model "${options.modelPattern}" not found`;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Fall back to first available model with a valid API key.
|
|
914
|
+
// Skip fallback if the user explicitly requested a model via --model that wasn't found.
|
|
915
|
+
if (!model && !options.modelPattern) {
|
|
916
|
+
const allModels = modelRegistry.getAll();
|
|
917
|
+
for (const candidate of allModels) {
|
|
918
|
+
if (await hasModelApiKey(candidate)) {
|
|
919
|
+
model = candidate;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
time("findAvailableModel");
|
|
924
|
+
if (model) {
|
|
925
|
+
if (modelFallbackMessage) {
|
|
926
|
+
modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
modelFallbackMessage =
|
|
930
|
+
"No models available. Use /login or set an API key environment variable. Then use /model to select a model.";
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
time("findModel");
|
|
900
935
|
// Discover custom commands (TypeScript slash commands)
|
|
901
936
|
const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
|
|
902
937
|
? { commands: [], errors: [] }
|
|
@@ -986,6 +1021,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
986
1021
|
emitEvent: event => cursorEventEmitter?.(event),
|
|
987
1022
|
});
|
|
988
1023
|
|
|
1024
|
+
const repeatToolDescriptions = settings.get("repeatToolDescriptions");
|
|
989
1025
|
const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
|
|
990
1026
|
toolContextStore.setToolNames(toolNames);
|
|
991
1027
|
const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
|
|
@@ -999,6 +1035,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
999
1035
|
rules: rulebookRules,
|
|
1000
1036
|
skillsSettings: settings.getGroup("skills") as SkillsSettings,
|
|
1001
1037
|
appendSystemPrompt: memoryInstructions,
|
|
1038
|
+
repeatToolDescriptions,
|
|
1002
1039
|
});
|
|
1003
1040
|
|
|
1004
1041
|
if (options.systemPrompt === undefined) {
|
|
@@ -1016,6 +1053,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1016
1053
|
skillsSettings: settings.getGroup("skills") as SkillsSettings,
|
|
1017
1054
|
customPrompt: options.systemPrompt,
|
|
1018
1055
|
appendSystemPrompt: memoryInstructions,
|
|
1056
|
+
repeatToolDescriptions,
|
|
1019
1057
|
});
|
|
1020
1058
|
}
|
|
1021
1059
|
return options.systemPrompt(defaultPrompt);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
claudeUsageProvider,
|
|
8
8
|
getEnvApiKey,
|
|
9
9
|
getOAuthApiKey,
|
|
10
|
+
getOAuthProvider,
|
|
10
11
|
githubCopilotUsageProvider,
|
|
11
12
|
googleGeminiCliUsageProvider,
|
|
12
13
|
kimiUsageProvider,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
type OAuthController,
|
|
26
27
|
type OAuthCredentials,
|
|
27
28
|
type OAuthProvider,
|
|
29
|
+
type OAuthProviderId,
|
|
28
30
|
openaiCodexUsageProvider,
|
|
29
31
|
type Provider,
|
|
30
32
|
type UsageCache,
|
|
@@ -634,7 +636,7 @@ export class AuthStorage {
|
|
|
634
636
|
* Login to an OAuth provider.
|
|
635
637
|
*/
|
|
636
638
|
async login(
|
|
637
|
-
provider:
|
|
639
|
+
provider: OAuthProviderId,
|
|
638
640
|
ctrl: OAuthController & {
|
|
639
641
|
/** onAuth is required by auth-storage but optional in OAuthController */
|
|
640
642
|
onAuth: (info: { url: string; instructions?: string }) => void;
|
|
@@ -709,8 +711,26 @@ export class AuthStorage {
|
|
|
709
711
|
await saveApiKeyCredential(apiKey);
|
|
710
712
|
return;
|
|
711
713
|
}
|
|
712
|
-
default:
|
|
713
|
-
|
|
714
|
+
default: {
|
|
715
|
+
const customProvider = getOAuthProvider(provider);
|
|
716
|
+
if (!customProvider) {
|
|
717
|
+
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
718
|
+
}
|
|
719
|
+
const customLoginResult = await customProvider.login({
|
|
720
|
+
onAuth: info => ctrl.onAuth(info),
|
|
721
|
+
onProgress: ctrl.onProgress,
|
|
722
|
+
onPrompt: ctrl.onPrompt,
|
|
723
|
+
onManualCodeInput: async () =>
|
|
724
|
+
ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
|
|
725
|
+
signal: ctrl.signal,
|
|
726
|
+
});
|
|
727
|
+
if (typeof customLoginResult === "string") {
|
|
728
|
+
await saveApiKeyCredential(customLoginResult);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
credentials = customLoginResult;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
714
734
|
}
|
|
715
735
|
const newCredential: OAuthCredential = { type: "oauth", ...credentials };
|
|
716
736
|
const existing = this.#getCredentialsForProvider(provider);
|
|
@@ -1170,14 +1190,28 @@ export class AuthStorage {
|
|
|
1170
1190
|
}
|
|
1171
1191
|
}
|
|
1172
1192
|
|
|
1173
|
-
const oauthCreds: Record<string, OAuthCredentials> = {
|
|
1174
|
-
[provider]: selection.credential,
|
|
1175
|
-
};
|
|
1176
|
-
|
|
1177
1193
|
try {
|
|
1178
|
-
|
|
1194
|
+
let result: { newCredentials: OAuthCredentials; apiKey: string } | null;
|
|
1195
|
+
const customProvider = getOAuthProvider(provider);
|
|
1196
|
+
if (customProvider) {
|
|
1197
|
+
let refreshedCredentials: OAuthCredentials = selection.credential;
|
|
1198
|
+
if (Date.now() >= refreshedCredentials.expires) {
|
|
1199
|
+
if (!customProvider.refreshToken) {
|
|
1200
|
+
throw new Error(`OAuth provider "${provider}" does not support token refresh`);
|
|
1201
|
+
}
|
|
1202
|
+
refreshedCredentials = await customProvider.refreshToken(refreshedCredentials);
|
|
1203
|
+
}
|
|
1204
|
+
const apiKey = customProvider.getApiKey
|
|
1205
|
+
? customProvider.getApiKey(refreshedCredentials)
|
|
1206
|
+
: refreshedCredentials.access;
|
|
1207
|
+
result = { newCredentials: refreshedCredentials, apiKey };
|
|
1208
|
+
} else {
|
|
1209
|
+
const oauthCreds: Record<string, OAuthCredentials> = {
|
|
1210
|
+
[provider]: selection.credential,
|
|
1211
|
+
};
|
|
1212
|
+
result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
|
|
1213
|
+
}
|
|
1179
1214
|
if (!result) return undefined;
|
|
1180
|
-
|
|
1181
1215
|
const updated: OAuthCredential = {
|
|
1182
1216
|
type: "oauth",
|
|
1183
1217
|
access: result.newCredentials.access,
|
|
@@ -1189,7 +1223,6 @@ export class AuthStorage {
|
|
|
1189
1223
|
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
|
1190
1224
|
};
|
|
1191
1225
|
this.#replaceCredentialAt(provider, selection.index, updated);
|
|
1192
|
-
|
|
1193
1226
|
if (checkUsage && !allowBlocked) {
|
|
1194
1227
|
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
1195
1228
|
if (!usageChecked || !sameAccount) {
|
|
@@ -1205,7 +1238,6 @@ export class AuthStorage {
|
|
|
1205
1238
|
return undefined;
|
|
1206
1239
|
}
|
|
1207
1240
|
}
|
|
1208
|
-
|
|
1209
1241
|
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
1210
1242
|
return result.apiKey;
|
|
1211
1243
|
} catch (error) {
|
package/src/system-prompt.ts
CHANGED
|
@@ -430,6 +430,8 @@ export interface BuildSystemPromptOptions {
|
|
|
430
430
|
toolNames?: string[];
|
|
431
431
|
/** Text to append to system prompt. */
|
|
432
432
|
appendSystemPrompt?: string;
|
|
433
|
+
/** Repeat full tool descriptions in system prompt. Default: false */
|
|
434
|
+
repeatToolDescriptions?: boolean;
|
|
433
435
|
/** Skills settings for discovery. */
|
|
434
436
|
skillsSettings?: SkillsSettings;
|
|
435
437
|
/** Working directory. Default: getProjectDir() */
|
|
@@ -454,6 +456,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
454
456
|
customPrompt,
|
|
455
457
|
tools,
|
|
456
458
|
appendSystemPrompt,
|
|
459
|
+
repeatToolDescriptions = false,
|
|
457
460
|
skillsSettings,
|
|
458
461
|
toolNames,
|
|
459
462
|
cwd,
|
|
@@ -553,6 +556,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
553
556
|
return renderPromptTemplate(systemPromptTemplate, {
|
|
554
557
|
tools: toolNamesArray,
|
|
555
558
|
toolDescriptions,
|
|
559
|
+
repeatToolDescriptions,
|
|
556
560
|
environment,
|
|
557
561
|
systemPromptCustomization: systemPromptCustomization ?? "",
|
|
558
562
|
contextFiles,
|