@oh-my-pi/pi-coding-agent 3.34.0 → 3.36.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 +43 -1
- package/README.md +7 -2
- package/package.json +5 -5
- package/src/core/agent-session.ts +202 -33
- package/src/core/auth-storage.ts +293 -28
- package/src/core/model-registry.ts +7 -8
- package/src/core/sdk.ts +12 -6
- package/src/core/session-manager.ts +15 -0
- package/src/core/settings-manager.ts +20 -6
- package/src/core/system-prompt.ts +1 -0
- package/src/core/title-generator.ts +3 -1
- package/src/core/tools/bash.ts +11 -3
- package/src/core/tools/calculator.ts +500 -0
- package/src/core/tools/edit.ts +1 -0
- package/src/core/tools/grep.ts +1 -1
- package/src/core/tools/index.test.ts +2 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/renderers.ts +3 -0
- package/src/core/tools/task/index.ts +10 -1
- package/src/core/tools/task/model-resolver.ts +5 -4
- package/src/core/tools/web-search/auth.ts +13 -5
- package/src/core/tools/web-search/types.ts +11 -7
- package/src/main.ts +3 -0
- package/src/modes/interactive/components/oauth-selector.ts +1 -2
- package/src/modes/interactive/components/tool-execution.ts +15 -12
- package/src/modes/interactive/interactive-mode.ts +49 -13
- package/src/prompts/tools/ask.md +11 -5
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/calculator.md +8 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { getConfigDirPaths } from "../../../config";
|
|
14
|
-
import type { AnthropicAuthConfig, AuthJson, ModelsJson } from "./types";
|
|
14
|
+
import type { AnthropicAuthConfig, AnthropicOAuthCredential, AuthJson, ModelsJson } from "./types";
|
|
15
15
|
|
|
16
16
|
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
17
17
|
|
|
@@ -76,6 +76,11 @@ export function isOAuthToken(apiKey: string): boolean {
|
|
|
76
76
|
return apiKey.includes("sk-ant-oat");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function normalizeAnthropicOAuthCredentials(entry: AuthJson["anthropic"] | undefined): AnthropicOAuthCredential[] {
|
|
80
|
+
if (!entry) return [];
|
|
81
|
+
return Array.isArray(entry) ? entry : [entry];
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
/**
|
|
80
85
|
* Find Anthropic auth config using 4-tier priority:
|
|
81
86
|
* 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
|
|
@@ -126,13 +131,16 @@ export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
|
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
// 3. OAuth credentials in auth.json (with 5-minute expiry buffer, check all config dirs)
|
|
134
|
+
const expiryBuffer = 5 * 60 * 1000; // 5 minutes
|
|
135
|
+
const now = Date.now();
|
|
129
136
|
for (const configDir of configDirs) {
|
|
130
137
|
const authJson = await readJson<AuthJson>(path.join(configDir, "auth.json"));
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
138
|
+
const credentials = normalizeAnthropicOAuthCredentials(authJson?.anthropic);
|
|
139
|
+
for (const credential of credentials) {
|
|
140
|
+
if (credential.type !== "oauth" || !credential.access) continue;
|
|
141
|
+
if (credential.expires > now + expiryBuffer) {
|
|
134
142
|
return {
|
|
135
|
-
apiKey:
|
|
143
|
+
apiKey: credential.access,
|
|
136
144
|
baseUrl: DEFAULT_BASE_URL,
|
|
137
145
|
isOAuth: true,
|
|
138
146
|
};
|
|
@@ -90,14 +90,18 @@ export interface ModelsJson {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/** auth.json structure for OAuth credentials */
|
|
93
|
+
export interface AnthropicOAuthCredential {
|
|
94
|
+
type: "oauth";
|
|
95
|
+
access: string;
|
|
96
|
+
refresh?: string;
|
|
97
|
+
/** Expiry timestamp in milliseconds */
|
|
98
|
+
expires: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type AnthropicAuthJsonEntry = AnthropicOAuthCredential | AnthropicOAuthCredential[];
|
|
102
|
+
|
|
93
103
|
export interface AuthJson {
|
|
94
|
-
anthropic?:
|
|
95
|
-
type: "oauth";
|
|
96
|
-
access: string;
|
|
97
|
-
refresh?: string;
|
|
98
|
-
/** Expiry timestamp in milliseconds */
|
|
99
|
-
expires: number;
|
|
100
|
-
};
|
|
104
|
+
anthropic?: AnthropicAuthJsonEntry;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
/** Anthropic API response types */
|
package/src/main.ts
CHANGED
|
@@ -289,6 +289,9 @@ async function buildSessionOptions(
|
|
|
289
289
|
process.exit(1);
|
|
290
290
|
}
|
|
291
291
|
options.model = model;
|
|
292
|
+
settingsManager.applyOverrides({
|
|
293
|
+
modelRoles: { default: `${model.provider}/${model.id}` },
|
|
294
|
+
});
|
|
292
295
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
293
296
|
options.model = scopedModels[0].model;
|
|
294
297
|
}
|
|
@@ -70,8 +70,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
70
70
|
const isAvailable = provider.available;
|
|
71
71
|
|
|
72
72
|
// Check if user is logged in for this provider
|
|
73
|
-
const
|
|
74
|
-
const isLoggedIn = credentials?.type === "oauth";
|
|
73
|
+
const isLoggedIn = this.authStorage.hasOAuth(provider.id);
|
|
75
74
|
const statusIndicator = isLoggedIn ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
|
|
76
75
|
|
|
77
76
|
let line = "";
|
|
@@ -369,20 +369,23 @@ export class ToolExecutionComponent extends Container {
|
|
|
369
369
|
this.contentBox.setBgFn(bgFn);
|
|
370
370
|
this.contentBox.clear();
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
component
|
|
372
|
+
const shouldRenderCall = !this.result || !renderer.mergeCallAndResult;
|
|
373
|
+
if (shouldRenderCall) {
|
|
374
|
+
// Render call component
|
|
375
|
+
try {
|
|
376
|
+
const callComponent = renderer.renderCall(this.args, theme);
|
|
377
|
+
if (callComponent) {
|
|
378
|
+
// Ensure component has invalidate() method for Component interface
|
|
379
|
+
const component = callComponent as any;
|
|
380
|
+
if (!component.invalidate) {
|
|
381
|
+
component.invalidate = () => {};
|
|
382
|
+
}
|
|
383
|
+
this.contentBox.addChild(component);
|
|
380
384
|
}
|
|
381
|
-
|
|
385
|
+
} catch {
|
|
386
|
+
// Fall back to default on error
|
|
387
|
+
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
382
388
|
}
|
|
383
|
-
} catch {
|
|
384
|
-
// Fall back to default on error
|
|
385
|
-
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
386
389
|
}
|
|
387
390
|
|
|
388
391
|
// Render result component if we have a result
|
|
@@ -38,6 +38,7 @@ import { VoiceSupervisor } from "../../core/voice-supervisor";
|
|
|
38
38
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
39
39
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog";
|
|
40
40
|
import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
41
|
+
import { resizeImage } from "../../utils/image-resize";
|
|
41
42
|
import { registerAsyncCleanup } from "../cleanup";
|
|
42
43
|
import { ArminComponent } from "./components/armin";
|
|
43
44
|
import { AssistantMessageComponent } from "./components/assistant-message";
|
|
@@ -1139,7 +1140,9 @@ export class InteractiveMode {
|
|
|
1139
1140
|
if (this.session.isStreaming) {
|
|
1140
1141
|
this.editor.addToHistory(text);
|
|
1141
1142
|
this.editor.setText("");
|
|
1142
|
-
|
|
1143
|
+
const images = this.pendingImages.length > 0 ? [...this.pendingImages] : undefined;
|
|
1144
|
+
this.pendingImages = [];
|
|
1145
|
+
await this.session.prompt(text, { streamingBehavior: "steer", images });
|
|
1143
1146
|
this.updatePendingMessagesDisplay();
|
|
1144
1147
|
this.ui.requestRender();
|
|
1145
1148
|
return;
|
|
@@ -1154,7 +1157,7 @@ export class InteractiveMode {
|
|
|
1154
1157
|
if (!hasUserMessages && !this.sessionManager.getSessionTitle()) {
|
|
1155
1158
|
const registry = this.session.modelRegistry;
|
|
1156
1159
|
const smolModel = this.settingsManager.getModelRole("smol");
|
|
1157
|
-
generateSessionTitle(text, registry, smolModel)
|
|
1160
|
+
generateSessionTitle(text, registry, smolModel, this.session.sessionId)
|
|
1158
1161
|
.then(async (title) => {
|
|
1159
1162
|
if (title) {
|
|
1160
1163
|
await this.sessionManager.setSessionTitle(title);
|
|
@@ -1481,11 +1484,14 @@ export class InteractiveMode {
|
|
|
1481
1484
|
}
|
|
1482
1485
|
|
|
1483
1486
|
private sendCompletionNotification(): void {
|
|
1487
|
+
if (this.isBackgrounded === false) return;
|
|
1484
1488
|
if (isNotificationSuppressed()) return;
|
|
1485
1489
|
const method = this.settingsManager.getNotificationOnComplete();
|
|
1486
1490
|
if (method === "off") return;
|
|
1487
1491
|
const protocol = method === "auto" ? detectNotificationProtocol() : method;
|
|
1488
|
-
|
|
1492
|
+
const title = this.sessionManager.getSessionTitle();
|
|
1493
|
+
const message = title ? `${title}: Complete` : "Complete";
|
|
1494
|
+
sendNotification(protocol, message);
|
|
1489
1495
|
}
|
|
1490
1496
|
|
|
1491
1497
|
/** Extract text content from a user message */
|
|
@@ -1504,22 +1510,24 @@ export class InteractiveMode {
|
|
|
1504
1510
|
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
1505
1511
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
1506
1512
|
*/
|
|
1507
|
-
private showStatus(message: string): void {
|
|
1513
|
+
private showStatus(message: string, options?: { dim?: boolean }): void {
|
|
1508
1514
|
if (this.isBackgrounded) {
|
|
1509
1515
|
return;
|
|
1510
1516
|
}
|
|
1511
1517
|
const children = this.chatContainer.children;
|
|
1512
1518
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
1513
1519
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
1520
|
+
const useDim = options?.dim ?? true;
|
|
1521
|
+
const rendered = useDim ? theme.fg("dim", message) : message;
|
|
1514
1522
|
|
|
1515
1523
|
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
1516
|
-
this.lastStatusText.setText(
|
|
1524
|
+
this.lastStatusText.setText(rendered);
|
|
1517
1525
|
this.ui.requestRender();
|
|
1518
1526
|
return;
|
|
1519
1527
|
}
|
|
1520
1528
|
|
|
1521
1529
|
const spacer = new Spacer(1);
|
|
1522
|
-
const text = new Text(
|
|
1530
|
+
const text = new Text(rendered, 1, 0);
|
|
1523
1531
|
this.chatContainer.addChild(spacer);
|
|
1524
1532
|
this.chatContainer.addChild(text);
|
|
1525
1533
|
this.lastStatusSpacer = spacer;
|
|
@@ -1822,10 +1830,24 @@ export class InteractiveMode {
|
|
|
1822
1830
|
try {
|
|
1823
1831
|
const image = await readImageFromClipboard();
|
|
1824
1832
|
if (image) {
|
|
1833
|
+
let imageData = image;
|
|
1834
|
+
if (this.settingsManager.getImageAutoResize()) {
|
|
1835
|
+
try {
|
|
1836
|
+
const resized = await resizeImage({
|
|
1837
|
+
type: "image",
|
|
1838
|
+
data: image.data,
|
|
1839
|
+
mimeType: image.mimeType,
|
|
1840
|
+
});
|
|
1841
|
+
imageData = { data: resized.data, mimeType: resized.mimeType };
|
|
1842
|
+
} catch {
|
|
1843
|
+
imageData = image;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1825
1847
|
this.pendingImages.push({
|
|
1826
1848
|
type: "image",
|
|
1827
|
-
data:
|
|
1828
|
-
mimeType:
|
|
1849
|
+
data: imageData.data,
|
|
1850
|
+
mimeType: imageData.mimeType,
|
|
1829
1851
|
});
|
|
1830
1852
|
// Insert styled placeholder at cursor like Claude does
|
|
1831
1853
|
const imageNum = this.pendingImages.length;
|
|
@@ -1980,7 +2002,8 @@ export class InteractiveMode {
|
|
|
1980
2002
|
|
|
1981
2003
|
private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
|
1982
2004
|
try {
|
|
1983
|
-
const
|
|
2005
|
+
const roleOrder = ["slow", "default", "smol"];
|
|
2006
|
+
const result = await this.session.cycleRoleModels(roleOrder, options);
|
|
1984
2007
|
if (!result) {
|
|
1985
2008
|
this.showStatus("Only one role model available");
|
|
1986
2009
|
return;
|
|
@@ -1989,10 +2012,24 @@ export class InteractiveMode {
|
|
|
1989
2012
|
this.statusLine.invalidate();
|
|
1990
2013
|
this.updateEditorBorderColor();
|
|
1991
2014
|
const roleLabel = result.role === "default" ? "default" : result.role;
|
|
2015
|
+
const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
|
|
1992
2016
|
const thinkingStr =
|
|
1993
2017
|
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
1994
2018
|
const tempLabel = options?.temporary ? " (temporary)" : "";
|
|
1995
|
-
|
|
2019
|
+
const cycleSeparator = theme.fg("dim", " > ");
|
|
2020
|
+
const cycleLabel = roleOrder
|
|
2021
|
+
.map((role) => {
|
|
2022
|
+
if (role === result.role) {
|
|
2023
|
+
return theme.bold(theme.fg("accent", role));
|
|
2024
|
+
}
|
|
2025
|
+
return theme.fg("muted", role);
|
|
2026
|
+
})
|
|
2027
|
+
.join(cycleSeparator);
|
|
2028
|
+
const orderLabel = ` (cycle: ${cycleLabel})`;
|
|
2029
|
+
this.showStatus(
|
|
2030
|
+
`Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
|
|
2031
|
+
{ dim: false },
|
|
2032
|
+
);
|
|
1996
2033
|
} catch (error) {
|
|
1997
2034
|
this.showError(error instanceof Error ? error.message : String(error));
|
|
1998
2035
|
}
|
|
@@ -2576,9 +2613,7 @@ export class InteractiveMode {
|
|
|
2576
2613
|
private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
|
2577
2614
|
if (mode === "logout") {
|
|
2578
2615
|
const providers = this.session.modelRegistry.authStorage.list();
|
|
2579
|
-
const loggedInProviders = providers.filter(
|
|
2580
|
-
(p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth",
|
|
2581
|
-
);
|
|
2616
|
+
const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.hasOAuth(p));
|
|
2582
2617
|
if (loggedInProviders.length === 0) {
|
|
2583
2618
|
this.showStatus("No OAuth providers logged in. Use /login first.");
|
|
2584
2619
|
return;
|
|
@@ -2599,6 +2634,7 @@ export class InteractiveMode {
|
|
|
2599
2634
|
await this.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
2600
2635
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
2601
2636
|
this.chatContainer.addChild(new Spacer(1));
|
|
2637
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
2602
2638
|
// Use OSC 8 hyperlink escape sequence for clickable link
|
|
2603
2639
|
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
2604
2640
|
this.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -8,11 +8,6 @@ Use this tool to:
|
|
|
8
8
|
- Request user preferences (styling, naming conventions, architecture patterns)
|
|
9
9
|
- Offer meaningful choices about task direction
|
|
10
10
|
|
|
11
|
-
Do NOT use for:
|
|
12
|
-
- Questions resolvable by reading files or docs
|
|
13
|
-
- Permission for normal dev tasks (just proceed)
|
|
14
|
-
- Decisions you should make from codebase context
|
|
15
|
-
|
|
16
11
|
Tips:
|
|
17
12
|
- Place recommended option first with " (Recommended)" suffix
|
|
18
13
|
- 2-5 concise, distinct options
|
|
@@ -22,3 +17,14 @@ Tips:
|
|
|
22
17
|
question: "Which authentication method should this API use?"
|
|
23
18
|
options: [{"label": "JWT (Recommended)"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
|
|
24
19
|
</example>
|
|
20
|
+
|
|
21
|
+
## Critical: Resolve before asking
|
|
22
|
+
|
|
23
|
+
**Exhaust all other options before asking.** Questions interrupt user flow.
|
|
24
|
+
|
|
25
|
+
1. **Unknown file location?** → Search with grep/find first. Only ask if search fails.
|
|
26
|
+
2. **Ambiguous syntax/format?** → Infer from context and codebase conventions. Make a reasonable choice.
|
|
27
|
+
3. **Missing details?** → Check docs, related files, commit history. Fill gaps yourself.
|
|
28
|
+
4. **Implementation approach?** → Choose based on codebase patterns. Ask only for genuinely novel architectural decisions.
|
|
29
|
+
|
|
30
|
+
If you can make a reasonable inference from the user's request, **do it**. Users communicate intent, not specifications—your job is to translate intent into correct implementation.
|
|
@@ -13,6 +13,7 @@ Do NOT use Bash for:
|
|
|
13
13
|
|
|
14
14
|
## Command structure
|
|
15
15
|
|
|
16
|
+
- Use `workdir` parameter to run commands in a specific directory instead of `cd dir && ...`
|
|
16
17
|
- Paths with spaces must use double quotes: `cd "/path/with spaces"`
|
|
17
18
|
- For sequential dependent operations, chain with `&&`: `mkdir foo && cd foo && touch bar`
|
|
18
19
|
- For parallel independent operations, make multiple tool calls in one message
|