@oh-my-pi/pi-coding-agent 12.6.0 → 12.7.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 +7 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +14 -9
- package/src/cli/web-search-cli.ts +2 -0
- package/src/commands/web-search.ts +1 -0
- package/src/config/model-resolver.ts +102 -3
- package/src/config/settings-schema.ts +1 -1
- package/src/extensibility/extensions/index.ts +9 -0
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +68 -5
- package/src/extensibility/plugins/git-url.ts +18 -7
- package/src/main.ts +45 -20
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/settings-defs.ts +7 -1
- package/src/modes/controllers/extension-ui-controller.ts +22 -0
- package/src/modes/controllers/selector-controller.ts +3 -1
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/rpc/rpc-mode.ts +5 -0
- package/src/session/agent-session.ts +84 -14
- package/src/task/render.ts +1 -1
- package/src/web/search/index.ts +15 -5
- package/src/web/search/provider.ts +3 -1
- package/src/web/search/providers/zai.ts +352 -0
- package/src/web/search/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.7.0] - 2026-02-16
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added Z.AI web search provider support via remote MCP endpoint (webSearchPrime)
|
|
9
|
+
- Added `zai` as a selectable web search provider option in settings
|
|
10
|
+
- Added Z.AI to automatic provider fallback chain for web search
|
|
11
|
+
|
|
5
12
|
## [12.6.0] - 2026-02-16
|
|
6
13
|
### Added
|
|
7
14
|
|
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.7.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.7.0",
|
|
88
|
+
"@oh-my-pi/pi-agent-core": "12.7.0",
|
|
89
|
+
"@oh-my-pi/pi-ai": "12.7.0",
|
|
90
|
+
"@oh-my-pi/pi-natives": "12.7.0",
|
|
91
|
+
"@oh-my-pi/pi-tui": "12.7.0",
|
|
92
|
+
"@oh-my-pi/pi-utils": "12.7.0",
|
|
93
93
|
"@sinclair/typebox": "^0.34.48",
|
|
94
94
|
"@xterm/headless": "^6.0.0",
|
|
95
95
|
"ajv": "^8.18.0",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -59,23 +59,28 @@ function hasBun(): boolean {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Get the latest release info from
|
|
62
|
+
* Get the latest release info from the npm registry.
|
|
63
|
+
* Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
|
|
63
64
|
*/
|
|
64
65
|
async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
65
|
-
const response = await fetch(`https://
|
|
66
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE}/latest`);
|
|
66
67
|
if (!response.ok) {
|
|
67
68
|
throw new Error(`Failed to fetch release info: ${response.statusText}`);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
const data = (await response.json()) as {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
71
|
+
const data = (await response.json()) as { version: string };
|
|
72
|
+
const version = data.version;
|
|
73
|
+
const tag = `v${version}`;
|
|
74
74
|
|
|
75
|
+
// Construct deterministic GitHub release download URLs for the current platform
|
|
76
|
+
const makeAsset = (name: string) => ({
|
|
77
|
+
name,
|
|
78
|
+
url: `https://github.com/${REPO}/releases/download/${tag}/${name}`,
|
|
79
|
+
});
|
|
75
80
|
return {
|
|
76
|
-
tag
|
|
77
|
-
version
|
|
78
|
-
assets:
|
|
81
|
+
tag,
|
|
82
|
+
version,
|
|
83
|
+
assets: [makeAsset(getBinaryName()), makeAsset(getNativeAddonName())],
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -25,6 +25,7 @@ const PROVIDERS: Array<SearchProviderId | "auto"> = [
|
|
|
25
25
|
"perplexity",
|
|
26
26
|
"exa",
|
|
27
27
|
"jina",
|
|
28
|
+
"zai",
|
|
28
29
|
"gemini",
|
|
29
30
|
"codex",
|
|
30
31
|
];
|
|
@@ -99,6 +100,7 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
|
99
100
|
provider: cmd.provider,
|
|
100
101
|
recency: cmd.recency,
|
|
101
102
|
limit: cmd.limit,
|
|
103
|
+
no_fallback: cmd.provider !== undefined && cmd.provider !== "auto",
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
const result = await runSearchQuery(params);
|
|
@@ -258,6 +258,7 @@ function parseModelPatternWithContext(
|
|
|
258
258
|
pattern: string,
|
|
259
259
|
availableModels: Model<Api>[],
|
|
260
260
|
context: ModelPreferenceContext,
|
|
261
|
+
options?: { allowInvalidThinkingLevelFallback?: boolean },
|
|
261
262
|
): ParsedModelResult {
|
|
262
263
|
// Try exact match first
|
|
263
264
|
const exactMatch = tryMatchModel(pattern, availableModels, context);
|
|
@@ -277,7 +278,7 @@ function parseModelPatternWithContext(
|
|
|
277
278
|
|
|
278
279
|
if (isValidThinkingLevel(suffix)) {
|
|
279
280
|
// Valid thinking level - recurse on prefix and use this level
|
|
280
|
-
const result = parseModelPatternWithContext(prefix, availableModels, context);
|
|
281
|
+
const result = parseModelPatternWithContext(prefix, availableModels, context, options);
|
|
281
282
|
if (result.model) {
|
|
282
283
|
// Only use this thinking level if no warning from inner recursion
|
|
283
284
|
const explicitThinkingLevel = !result.warning;
|
|
@@ -291,8 +292,13 @@ function parseModelPatternWithContext(
|
|
|
291
292
|
return result;
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;
|
|
296
|
+
if (!allowFallback) {
|
|
297
|
+
return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
298
|
+
}
|
|
299
|
+
|
|
294
300
|
// Invalid suffix - recurse on prefix and warn
|
|
295
|
-
const result = parseModelPatternWithContext(prefix, availableModels, context);
|
|
301
|
+
const result = parseModelPatternWithContext(prefix, availableModels, context, options);
|
|
296
302
|
if (result.model) {
|
|
297
303
|
return {
|
|
298
304
|
model: result.model,
|
|
@@ -308,9 +314,10 @@ export function parseModelPattern(
|
|
|
308
314
|
pattern: string,
|
|
309
315
|
availableModels: Model<Api>[],
|
|
310
316
|
preferences?: ModelMatchPreferences,
|
|
317
|
+
options?: { allowInvalidThinkingLevelFallback?: boolean },
|
|
311
318
|
): ParsedModelResult {
|
|
312
319
|
const context = buildPreferenceContext(availableModels, preferences);
|
|
313
|
-
return parseModelPatternWithContext(pattern, availableModels, context);
|
|
320
|
+
return parseModelPatternWithContext(pattern, availableModels, context, options);
|
|
314
321
|
}
|
|
315
322
|
|
|
316
323
|
const PREFIX_MODEL_ROLE = "pi/";
|
|
@@ -486,6 +493,98 @@ export async function resolveModelScope(
|
|
|
486
493
|
return scopedModels;
|
|
487
494
|
}
|
|
488
495
|
|
|
496
|
+
export interface ResolveCliModelResult {
|
|
497
|
+
model: Model<Api> | undefined;
|
|
498
|
+
thinkingLevel?: ThinkingLevel;
|
|
499
|
+
warning: string | undefined;
|
|
500
|
+
error: string | undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Resolve a single model from CLI flags.
|
|
505
|
+
*/
|
|
506
|
+
export function resolveCliModel(options: {
|
|
507
|
+
cliProvider?: string;
|
|
508
|
+
cliModel?: string;
|
|
509
|
+
modelRegistry: ModelRegistry;
|
|
510
|
+
preferences?: ModelMatchPreferences;
|
|
511
|
+
}): ResolveCliModelResult {
|
|
512
|
+
const { cliProvider, cliModel, modelRegistry, preferences } = options;
|
|
513
|
+
|
|
514
|
+
if (!cliModel) {
|
|
515
|
+
return { model: undefined, warning: undefined, error: undefined };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const availableModels = modelRegistry.getAll();
|
|
519
|
+
if (availableModels.length === 0) {
|
|
520
|
+
return {
|
|
521
|
+
model: undefined,
|
|
522
|
+
warning: undefined,
|
|
523
|
+
error: "No models available. Check your installation or add models to models.json.",
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const providerMap = new Map<string, string>();
|
|
528
|
+
for (const model of availableModels) {
|
|
529
|
+
providerMap.set(model.provider.toLowerCase(), model.provider);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined;
|
|
533
|
+
if (cliProvider && !provider) {
|
|
534
|
+
return {
|
|
535
|
+
model: undefined,
|
|
536
|
+
warning: undefined,
|
|
537
|
+
error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!provider) {
|
|
542
|
+
const lower = cliModel.toLowerCase();
|
|
543
|
+
const exact = availableModels.find(
|
|
544
|
+
model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
|
|
545
|
+
);
|
|
546
|
+
if (exact) {
|
|
547
|
+
return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let pattern = cliModel;
|
|
552
|
+
|
|
553
|
+
if (!provider) {
|
|
554
|
+
const slashIndex = cliModel.indexOf("/");
|
|
555
|
+
if (slashIndex !== -1) {
|
|
556
|
+
const maybeProvider = cliModel.substring(0, slashIndex);
|
|
557
|
+
const canonical = providerMap.get(maybeProvider.toLowerCase());
|
|
558
|
+
if (canonical) {
|
|
559
|
+
provider = canonical;
|
|
560
|
+
pattern = cliModel.substring(slashIndex + 1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
const prefix = `${provider}/`;
|
|
565
|
+
if (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
566
|
+
pattern = cliModel.substring(prefix.length);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
|
|
571
|
+
const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
|
|
572
|
+
allowInvalidThinkingLevelFallback: false,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
if (!model) {
|
|
576
|
+
const display = provider ? `${provider}/${pattern}` : cliModel;
|
|
577
|
+
return {
|
|
578
|
+
model: undefined,
|
|
579
|
+
thinkingLevel: undefined,
|
|
580
|
+
warning,
|
|
581
|
+
error: `Model "${display}" not found. Use --list-models to see available models.`,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return { model, thinkingLevel, warning, error: undefined };
|
|
586
|
+
}
|
|
587
|
+
|
|
489
588
|
export interface InitialModelResult {
|
|
490
589
|
model: Model<Api> | undefined;
|
|
491
590
|
thinkingLevel: ThinkingLevel;
|
|
@@ -617,7 +617,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
617
617
|
// ─────────────────────────────────────────────────────────────────────────
|
|
618
618
|
"providers.webSearch": {
|
|
619
619
|
type: "enum",
|
|
620
|
-
values: ["auto", "exa", "jina", "perplexity", "anthropic"] as const,
|
|
620
|
+
values: ["auto", "exa", "jina", "zai", "perplexity", "anthropic"] as const,
|
|
621
621
|
default: "auto",
|
|
622
622
|
ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true },
|
|
623
623
|
},
|
|
@@ -67,9 +67,13 @@ export type {
|
|
|
67
67
|
InputEventResult,
|
|
68
68
|
KeybindingsManager,
|
|
69
69
|
LoadExtensionsResult,
|
|
70
|
+
// Events - Message
|
|
71
|
+
MessageEndEvent,
|
|
70
72
|
// Message Rendering
|
|
71
73
|
MessageRenderer,
|
|
72
74
|
MessageRenderOptions,
|
|
75
|
+
MessageStartEvent,
|
|
76
|
+
MessageUpdateEvent,
|
|
73
77
|
// Provider Registration
|
|
74
78
|
ProviderConfig,
|
|
75
79
|
ProviderModelConfig,
|
|
@@ -104,11 +108,16 @@ export type {
|
|
|
104
108
|
SetActiveToolsHandler,
|
|
105
109
|
SetModelHandler,
|
|
106
110
|
SetThinkingLevelHandler,
|
|
111
|
+
TerminalInputHandler,
|
|
107
112
|
// Events - Tool
|
|
108
113
|
ToolCallEvent,
|
|
109
114
|
ToolCallEventResult,
|
|
110
115
|
// Tools
|
|
111
116
|
ToolDefinition,
|
|
117
|
+
// Events - Tool Execution
|
|
118
|
+
ToolExecutionEndEvent,
|
|
119
|
+
ToolExecutionStartEvent,
|
|
120
|
+
ToolExecutionUpdateEvent,
|
|
112
121
|
ToolRenderResultOptions,
|
|
113
122
|
ToolResultEvent,
|
|
114
123
|
ToolResultEventResult,
|
|
@@ -130,6 +130,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
130
130
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
131
131
|
input: async (_title, _placeholder, _dialogOptions) => undefined,
|
|
132
132
|
notify: () => {},
|
|
133
|
+
onTerminalInput: () => () => {},
|
|
133
134
|
setStatus: () => {},
|
|
134
135
|
setWorkingMessage: () => {},
|
|
135
136
|
setWidget: () => {},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import type {
|
|
12
12
|
Api,
|
|
13
|
+
AssistantMessageEvent,
|
|
13
14
|
AssistantMessageEventStream,
|
|
14
15
|
Context,
|
|
15
16
|
ImageContent,
|
|
@@ -75,6 +76,9 @@ export interface ExtensionUIDialogOptions {
|
|
|
75
76
|
outline?: boolean;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/** Raw terminal input listener for extensions. */
|
|
80
|
+
export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string } | undefined;
|
|
81
|
+
|
|
78
82
|
/**
|
|
79
83
|
* UI context for extensions to request interactive UI.
|
|
80
84
|
* Each mode (interactive, RPC, print) provides its own implementation.
|
|
@@ -92,6 +96,9 @@ export interface ExtensionUIContext {
|
|
|
92
96
|
/** Show a notification to the user. */
|
|
93
97
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
94
98
|
|
|
99
|
+
/** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
|
|
100
|
+
onTerminalInput(handler: TerminalInputHandler): () => void;
|
|
101
|
+
|
|
95
102
|
/** Set status text in the footer/status bar. Pass undefined to clear. */
|
|
96
103
|
setStatus(key: string, text: string | undefined): void;
|
|
97
104
|
|
|
@@ -168,12 +175,11 @@ export interface ExtensionUIContext {
|
|
|
168
175
|
// ============================================================================
|
|
169
176
|
|
|
170
177
|
export interface ContextUsage {
|
|
171
|
-
tokens
|
|
178
|
+
/** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
|
|
179
|
+
tokens: number | null;
|
|
172
180
|
contextWindow: number;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
trailingTokens: number;
|
|
176
|
-
lastUsageIndex: number | null;
|
|
181
|
+
/** Context usage as percentage of context window, or null if tokens is unknown. */
|
|
182
|
+
percent: number | null;
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
export interface CompactOptions {
|
|
@@ -472,6 +478,51 @@ export interface TurnEndEvent {
|
|
|
472
478
|
toolResults: ToolResultMessage[];
|
|
473
479
|
}
|
|
474
480
|
|
|
481
|
+
/** Fired when a message starts (user, assistant, or toolResult) */
|
|
482
|
+
export interface MessageStartEvent {
|
|
483
|
+
type: "message_start";
|
|
484
|
+
message: AgentMessage;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Fired during assistant message streaming with token-by-token updates */
|
|
488
|
+
export interface MessageUpdateEvent {
|
|
489
|
+
type: "message_update";
|
|
490
|
+
message: AgentMessage;
|
|
491
|
+
assistantMessageEvent: AssistantMessageEvent;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Fired when a message ends */
|
|
495
|
+
export interface MessageEndEvent {
|
|
496
|
+
type: "message_end";
|
|
497
|
+
message: AgentMessage;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Fired when a tool starts executing */
|
|
501
|
+
export interface ToolExecutionStartEvent {
|
|
502
|
+
type: "tool_execution_start";
|
|
503
|
+
toolCallId: string;
|
|
504
|
+
toolName: string;
|
|
505
|
+
args: unknown;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Fired during tool execution with partial/streaming output */
|
|
509
|
+
export interface ToolExecutionUpdateEvent {
|
|
510
|
+
type: "tool_execution_update";
|
|
511
|
+
toolCallId: string;
|
|
512
|
+
toolName: string;
|
|
513
|
+
args: unknown;
|
|
514
|
+
partialResult: unknown;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Fired when a tool finishes executing */
|
|
518
|
+
export interface ToolExecutionEndEvent {
|
|
519
|
+
type: "tool_execution_end";
|
|
520
|
+
toolCallId: string;
|
|
521
|
+
toolName: string;
|
|
522
|
+
result: unknown;
|
|
523
|
+
isError: boolean;
|
|
524
|
+
}
|
|
525
|
+
|
|
475
526
|
/** Fired when auto-compaction starts */
|
|
476
527
|
export interface AutoCompactionStartEvent {
|
|
477
528
|
type: "auto_compaction_start";
|
|
@@ -711,6 +762,12 @@ export type ExtensionEvent =
|
|
|
711
762
|
| AgentEndEvent
|
|
712
763
|
| TurnStartEvent
|
|
713
764
|
| TurnEndEvent
|
|
765
|
+
| MessageStartEvent
|
|
766
|
+
| MessageUpdateEvent
|
|
767
|
+
| MessageEndEvent
|
|
768
|
+
| ToolExecutionStartEvent
|
|
769
|
+
| ToolExecutionUpdateEvent
|
|
770
|
+
| ToolExecutionEndEvent
|
|
714
771
|
| AutoCompactionStartEvent
|
|
715
772
|
| AutoCompactionEndEvent
|
|
716
773
|
| AutoRetryStartEvent
|
|
@@ -879,6 +936,12 @@ export interface ExtensionAPI {
|
|
|
879
936
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
|
880
937
|
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
881
938
|
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
|
939
|
+
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
|
|
940
|
+
on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): void;
|
|
941
|
+
on(event: "message_end", handler: ExtensionHandler<MessageEndEvent>): void;
|
|
942
|
+
on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
|
|
943
|
+
on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
|
|
944
|
+
on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;
|
|
882
945
|
on(event: "auto_compaction_start", handler: ExtensionHandler<AutoCompactionStartEvent>): void;
|
|
883
946
|
on(event: "auto_compaction_end", handler: ExtensionHandler<AutoCompactionEndEvent>): void;
|
|
884
947
|
on(event: "auto_retry_start", handler: ExtensionHandler<AutoRetryStartEvent>): void;
|
|
@@ -172,7 +172,7 @@ function parseGenericGitUrl(url: string): GitSource | null {
|
|
|
172
172
|
if (scpLikeMatch) {
|
|
173
173
|
host = scpLikeMatch[1] ?? "";
|
|
174
174
|
repoPath = scpLikeMatch[2] ?? "";
|
|
175
|
-
} else if (/^https?:\/\/|^ssh:\/\//.test(repoWithoutRef)) {
|
|
175
|
+
} else if (/^https?:\/\/|^ssh:\/\/|^git:\/\//.test(repoWithoutRef)) {
|
|
176
176
|
try {
|
|
177
177
|
const parsed = new URL(repoWithoutRef);
|
|
178
178
|
if (parsed.hash) {
|
|
@@ -210,20 +210,30 @@ function parseGenericGitUrl(url: string): GitSource | null {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
* Parse
|
|
213
|
+
* Parse git source into a GitSource.
|
|
214
|
+
*
|
|
215
|
+
* Rules:
|
|
216
|
+
* - With `git:` prefix, accept shorthand forms.
|
|
217
|
+
* - Without `git:` prefix, only accept explicit protocol URLs.
|
|
214
218
|
*
|
|
215
219
|
* Handles:
|
|
216
220
|
* - `git:` prefixed URLs (`git:github.com/user/repo`)
|
|
217
|
-
* - SSH SCP-like URLs (`git@github.com:user/repo`)
|
|
218
|
-
* - HTTPS/HTTP/SSH protocol URLs
|
|
219
|
-
* - Bare `host/user/repo` shorthand
|
|
221
|
+
* - SSH SCP-like URLs (`git:git@github.com:user/repo`)
|
|
222
|
+
* - HTTPS/HTTP/SSH/git protocol URLs
|
|
220
223
|
* - Ref pinning via `@ref` suffix
|
|
221
224
|
*
|
|
222
225
|
* Recognizes GitHub, GitLab, Bitbucket, Sourcehut, and Codeberg natively.
|
|
223
226
|
* Falls back to generic URL parsing for other hosts.
|
|
224
227
|
*/
|
|
225
228
|
export function parseGitUrl(source: string): GitSource | null {
|
|
226
|
-
const
|
|
229
|
+
const trimmed = source.trim();
|
|
230
|
+
const hasGitPrefix = /^git:(?!\/\/)/i.test(trimmed);
|
|
231
|
+
const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed;
|
|
232
|
+
|
|
233
|
+
if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
227
237
|
const hashIndex = url.indexOf("#");
|
|
228
238
|
if (hashIndex >= 0) {
|
|
229
239
|
const hash = url.slice(hashIndex + 1);
|
|
@@ -244,7 +254,7 @@ export function parseGitUrl(source: string): GitSource | null {
|
|
|
244
254
|
const directCandidates: string[] = [];
|
|
245
255
|
if (scpMatch) {
|
|
246
256
|
directCandidates.push(`https://${scpMatch[1]}/${scpMatch[2]}`);
|
|
247
|
-
} else if (/^https?:\/\/|^ssh:\/\//.test(split.repo)) {
|
|
257
|
+
} else if (/^https?:\/\/|^ssh:\/\/|^git:\/\//.test(split.repo)) {
|
|
248
258
|
directCandidates.push(split.repo);
|
|
249
259
|
}
|
|
250
260
|
|
|
@@ -254,6 +264,7 @@ export function parseGitUrl(source: string): GitSource | null {
|
|
|
254
264
|
!split.repo.startsWith("http://") &&
|
|
255
265
|
!split.repo.startsWith("https://") &&
|
|
256
266
|
!split.repo.startsWith("ssh://") &&
|
|
267
|
+
!split.repo.startsWith("git://") &&
|
|
257
268
|
!split.repo.startsWith("git@");
|
|
258
269
|
const result = tryKnownHostSource(split, withRef, needsHttps ? `https://${split.repo}` : split.repo);
|
|
259
270
|
if (result) return result;
|
package/src/main.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { listModels } from "./cli/list-models";
|
|
|
20
20
|
import { selectSession } from "./cli/session-picker";
|
|
21
21
|
import { findConfigFile } from "./config";
|
|
22
22
|
import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
|
|
23
|
-
import {
|
|
23
|
+
import { parseModelString, resolveCliModel, resolveModelScope, type ScopedModel } from "./config/model-resolver";
|
|
24
24
|
import { Settings, settings } from "./config/settings";
|
|
25
25
|
import { initializeWithSettings } from "./discovery";
|
|
26
26
|
import { exportFromFile } from "./export/html";
|
|
@@ -355,10 +355,11 @@ async function buildSessionOptions(
|
|
|
355
355
|
scopedModels: ScopedModel[],
|
|
356
356
|
sessionManager: SessionManager | undefined,
|
|
357
357
|
modelRegistry: ModelRegistry,
|
|
358
|
-
): Promise<CreateAgentSessionOptions> {
|
|
358
|
+
): Promise<{ options: CreateAgentSessionOptions; cliThinkingFromModel: boolean }> {
|
|
359
359
|
const options: CreateAgentSessionOptions = {
|
|
360
360
|
cwd: parsed.cwd ?? getProjectDir(),
|
|
361
361
|
};
|
|
362
|
+
let cliThinkingFromModel = false;
|
|
362
363
|
|
|
363
364
|
// Auto-discover SYSTEM.md if no CLI system prompt provided
|
|
364
365
|
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
|
@@ -370,23 +371,38 @@ async function buildSessionOptions(
|
|
|
370
371
|
options.sessionManager = sessionManager;
|
|
371
372
|
}
|
|
372
373
|
|
|
373
|
-
// Model from CLI
|
|
374
|
+
// Model from CLI
|
|
375
|
+
// - supports --provider <name> --model <pattern>
|
|
376
|
+
// - supports --model <provider>/<pattern>
|
|
374
377
|
if (parsed.model) {
|
|
375
|
-
const available = modelRegistry.getAll();
|
|
376
378
|
const modelMatchPreferences = {
|
|
377
379
|
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
378
380
|
};
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
381
|
+
const resolved = resolveCliModel({
|
|
382
|
+
cliProvider: parsed.provider,
|
|
383
|
+
cliModel: parsed.model,
|
|
384
|
+
modelRegistry,
|
|
385
|
+
preferences: modelMatchPreferences,
|
|
386
|
+
});
|
|
387
|
+
if (resolved.warning) {
|
|
388
|
+
writeStderr(chalk.yellow(`Warning: ${resolved.warning}`));
|
|
382
389
|
}
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
+
if (resolved.error) {
|
|
391
|
+
if (!parsed.provider && !parsed.model.includes(":")) {
|
|
392
|
+
// Model not found in built-in registry — defer resolution to after extensions load
|
|
393
|
+
// (extensions may register additional providers/models via registerProvider)
|
|
394
|
+
options.modelPattern = parsed.model;
|
|
395
|
+
} else {
|
|
396
|
+
writeStderr(chalk.red(resolved.error));
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
} else if (resolved.model) {
|
|
400
|
+
options.model = resolved.model;
|
|
401
|
+
settings.overrideModelRoles({ default: `${resolved.model.provider}/${resolved.model.id}` });
|
|
402
|
+
if (!parsed.thinking && resolved.thinkingLevel) {
|
|
403
|
+
options.thinkingLevel = resolved.thinkingLevel;
|
|
404
|
+
cliThinkingFromModel = true;
|
|
405
|
+
}
|
|
390
406
|
}
|
|
391
407
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
392
408
|
const remembered = settings.getModelRole("default");
|
|
@@ -472,7 +488,7 @@ async function buildSessionOptions(
|
|
|
472
488
|
options.additionalExtensionPaths = [];
|
|
473
489
|
}
|
|
474
490
|
|
|
475
|
-
return options;
|
|
491
|
+
return { options, cliThinkingFromModel };
|
|
476
492
|
}
|
|
477
493
|
|
|
478
494
|
export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
|
|
@@ -604,7 +620,12 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
604
620
|
sessionManager = await SessionManager.open(selectedPath);
|
|
605
621
|
}
|
|
606
622
|
|
|
607
|
-
const sessionOptions = await buildSessionOptions(
|
|
623
|
+
const { options: sessionOptions, cliThinkingFromModel } = await buildSessionOptions(
|
|
624
|
+
parsedArgs,
|
|
625
|
+
scopedModels,
|
|
626
|
+
sessionManager,
|
|
627
|
+
modelRegistry,
|
|
628
|
+
);
|
|
608
629
|
debugStartup("main:buildSessionOptions");
|
|
609
630
|
sessionOptions.authStorage = authStorage;
|
|
610
631
|
sessionOptions.modelRegistry = modelRegistry;
|
|
@@ -613,7 +634,9 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
613
634
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
614
635
|
if (parsedArgs.apiKey) {
|
|
615
636
|
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
616
|
-
writeStderr(
|
|
637
|
+
writeStderr(
|
|
638
|
+
chalk.red("--api-key requires a model to be specified via --model, --provider/--model, or --models"),
|
|
639
|
+
);
|
|
617
640
|
process.exit(1);
|
|
618
641
|
}
|
|
619
642
|
if (sessionOptions.model) {
|
|
@@ -678,9 +701,11 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
678
701
|
process.exit(1);
|
|
679
702
|
}
|
|
680
703
|
|
|
681
|
-
// Clamp thinking level to model capabilities
|
|
682
|
-
|
|
683
|
-
|
|
704
|
+
// Clamp thinking level to model capabilities for CLI-provided thinking levels.
|
|
705
|
+
// This covers both --thinking <level> and --model <pattern>:<thinking>.
|
|
706
|
+
const cliThinkingOverride = parsedArgs.thinking !== undefined || cliThinkingFromModel;
|
|
707
|
+
if (session.model && cliThinkingOverride) {
|
|
708
|
+
let effectiveThinking = session.thinkingLevel;
|
|
684
709
|
if (!session.model.reasoning) {
|
|
685
710
|
effectiveThinking = "off";
|
|
686
711
|
} else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
3
|
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
4
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
5
|
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
@@ -175,22 +174,12 @@ export class FooterComponent implements Component {
|
|
|
175
174
|
}
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)
|
|
185
|
-
const contextTokens = lastAssistantMessage
|
|
186
|
-
? lastAssistantMessage.usage.input +
|
|
187
|
-
lastAssistantMessage.usage.output +
|
|
188
|
-
lastAssistantMessage.usage.cacheRead +
|
|
189
|
-
lastAssistantMessage.usage.cacheWrite
|
|
190
|
-
: 0;
|
|
191
|
-
const contextWindow = state.model?.contextWindow || 0;
|
|
192
|
-
const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
193
|
-
const contextPercent = contextPercentValue.toFixed(1);
|
|
177
|
+
// Calculate context usage from session (handles compaction correctly).
|
|
178
|
+
// After compaction, tokens are unknown until the next LLM response.
|
|
179
|
+
const contextUsage = this.session.getContextUsage();
|
|
180
|
+
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
181
|
+
const contextPercentValue = contextUsage?.percent ?? 0;
|
|
182
|
+
const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
194
183
|
|
|
195
184
|
// Format token counts (similar to web-ui)
|
|
196
185
|
const formatTokens = (count: number): string => {
|
|
@@ -239,7 +228,10 @@ export class FooterComponent implements Component {
|
|
|
239
228
|
// Colorize context percentage based on usage
|
|
240
229
|
let contextPercentStr: string;
|
|
241
230
|
const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
|
|
242
|
-
const contextPercentDisplay =
|
|
231
|
+
const contextPercentDisplay =
|
|
232
|
+
contextPercent === "?"
|
|
233
|
+
? `?/${formatTokens(contextWindow)}${autoIndicator}`
|
|
234
|
+
: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
243
235
|
if (contextPercentValue > 90) {
|
|
244
236
|
contextPercentStr = theme.fg("error", contextPercentDisplay);
|
|
245
237
|
} else if (contextPercentValue > 70) {
|
|
@@ -146,10 +146,16 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
146
146
|
],
|
|
147
147
|
// Provider options
|
|
148
148
|
"providers.webSearch": [
|
|
149
|
-
{
|
|
149
|
+
{
|
|
150
|
+
value: "auto",
|
|
151
|
+
label: "Auto",
|
|
152
|
+
description: "Priority: Exa > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI",
|
|
153
|
+
},
|
|
150
154
|
{ value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
|
|
155
|
+
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
151
156
|
{ value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
|
|
152
157
|
{ value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
|
|
158
|
+
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
153
159
|
],
|
|
154
160
|
"providers.image": [
|
|
155
161
|
{ value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
|