@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.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 +40 -0
- package/README.md +2 -1
- package/docs/sdk.md +0 -3
- package/package.json +6 -5
- package/src/config.ts +9 -0
- package/src/core/agent-storage.ts +450 -0
- package/src/core/auth-storage.ts +102 -183
- package/src/core/compaction/branch-summarization.ts +5 -4
- package/src/core/compaction/compaction.ts +7 -6
- package/src/core/compaction/utils.ts +6 -11
- package/src/core/custom-commands/bundled/review/index.ts +22 -94
- package/src/core/custom-share.ts +66 -0
- package/src/core/history-storage.ts +15 -7
- package/src/core/prompt-templates.ts +271 -1
- package/src/core/sdk.ts +14 -3
- package/src/core/settings-manager.ts +100 -34
- package/src/core/slash-commands.ts +4 -1
- package/src/core/storage-migration.ts +215 -0
- package/src/core/system-prompt.ts +87 -289
- package/src/core/title-generator.ts +3 -2
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +2 -1
- package/src/core/tools/calculator.ts +2 -1
- package/src/core/tools/edit.ts +2 -1
- package/src/core/tools/find.ts +2 -1
- package/src/core/tools/gemini-image.ts +2 -1
- package/src/core/tools/git.ts +2 -2
- package/src/core/tools/grep.ts +2 -1
- package/src/core/tools/index.test.ts +0 -28
- package/src/core/tools/index.ts +0 -6
- package/src/core/tools/lsp/index.ts +2 -1
- package/src/core/tools/output.ts +2 -1
- package/src/core/tools/read.ts +4 -1
- package/src/core/tools/ssh.ts +4 -2
- package/src/core/tools/task/agents.ts +56 -30
- package/src/core/tools/task/commands.ts +9 -8
- package/src/core/tools/task/index.ts +7 -15
- package/src/core/tools/web-fetch.ts +2 -1
- package/src/core/tools/web-search/auth.ts +106 -16
- package/src/core/tools/web-search/index.ts +3 -2
- package/src/core/tools/web-search/providers/anthropic.ts +44 -6
- package/src/core/tools/write.ts +2 -1
- package/src/core/voice.ts +3 -1
- package/src/main.ts +1 -1
- package/src/migrations.ts +20 -20
- package/src/modes/interactive/controllers/command-controller.ts +527 -0
- package/src/modes/interactive/controllers/event-controller.ts +340 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
- package/src/modes/interactive/controllers/input-controller.ts +585 -0
- package/src/modes/interactive/controllers/selector-controller.ts +585 -0
- package/src/modes/interactive/interactive-mode.ts +364 -3143
- package/src/modes/interactive/theme/theme.ts +5 -5
- package/src/modes/interactive/types.ts +189 -0
- package/src/modes/interactive/utils/ui-helpers.ts +449 -0
- package/src/modes/interactive/utils/voice-manager.ts +96 -0
- package/src/prompts/{explore.md → agents/explore.md} +7 -5
- package/src/prompts/agents/frontmatter.md +7 -0
- package/src/prompts/{plan.md → agents/plan.md} +3 -3
- package/src/prompts/{task.md → agents/task.md} +1 -1
- package/src/prompts/review-request.md +44 -8
- package/src/prompts/system/custom-system-prompt.md +80 -0
- package/src/prompts/system/file-operations.md +12 -0
- package/src/prompts/system/system-prompt.md +232 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +9 -3
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/title-system.md +0 -8
- /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
- /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
- /package/src/prompts/{implement.md → agents/implement.md} +0 -0
- /package/src/prompts/{init.md → agents/init.md} +0 -0
- /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
- /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
- /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
- /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
- /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
- /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
- /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
package/src/core/auth-storage.ts
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credential storage for API keys and OAuth tokens.
|
|
3
|
-
* Handles loading, saving, and refreshing credentials from
|
|
4
|
-
*
|
|
5
|
-
* Uses file locking to prevent race conditions when multiple pi instances
|
|
6
|
-
* try to refresh tokens simultaneously.
|
|
3
|
+
* Handles loading, saving, and refreshing credentials from agent.db.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
|
-
import {
|
|
10
|
-
chmodSync,
|
|
11
|
-
closeSync,
|
|
12
|
-
existsSync,
|
|
13
|
-
openSync,
|
|
14
|
-
readFileSync,
|
|
15
|
-
renameSync,
|
|
16
|
-
statSync,
|
|
17
|
-
unlinkSync,
|
|
18
|
-
writeFileSync,
|
|
19
|
-
} from "node:fs";
|
|
20
|
-
import { dirname } from "node:path";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
21
7
|
import {
|
|
22
8
|
getEnvApiKey,
|
|
23
9
|
getOAuthApiKey,
|
|
@@ -29,7 +15,10 @@ import {
|
|
|
29
15
|
type OAuthCredentials,
|
|
30
16
|
type OAuthProvider,
|
|
31
17
|
} from "@oh-my-pi/pi-ai";
|
|
18
|
+
import { getAgentDbPath } from "../config";
|
|
19
|
+
import { AgentStorage } from "./agent-storage";
|
|
32
20
|
import { logger } from "./logger";
|
|
21
|
+
import { migrateJsonStorage } from "./storage-migration";
|
|
33
22
|
|
|
34
23
|
export type ApiKeyCredential = {
|
|
35
24
|
type: "api_key";
|
|
@@ -46,6 +35,12 @@ export type AuthCredentialEntry = AuthCredential | AuthCredential[];
|
|
|
46
35
|
|
|
47
36
|
export type AuthStorageData = Record<string, AuthCredentialEntry>;
|
|
48
37
|
|
|
38
|
+
/**
|
|
39
|
+
* In-memory representation pairing DB row ID with credential.
|
|
40
|
+
* The ID is required for update/delete operations against agent.db.
|
|
41
|
+
*/
|
|
42
|
+
type StoredCredential = { id: number; credential: AuthCredential };
|
|
43
|
+
|
|
49
44
|
/** Rate limit window from Codex usage API (primary or secondary quota). */
|
|
50
45
|
type CodexUsageWindow = {
|
|
51
46
|
usedPercent?: number;
|
|
@@ -86,18 +81,18 @@ function toBoolean(value: unknown): boolean | undefined {
|
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
/**
|
|
89
|
-
* Credential storage backed by
|
|
90
|
-
* Reads from
|
|
84
|
+
* Credential storage backed by agent.db.
|
|
85
|
+
* Reads from SQLite and migrates legacy auth.json paths.
|
|
91
86
|
*/
|
|
92
87
|
export class AuthStorage {
|
|
93
|
-
// File locking configuration for concurrent access protection
|
|
94
|
-
private static readonly lockRetryDelayMs = 50; // Polling interval when waiting for lock
|
|
95
|
-
private static readonly lockTimeoutMs = 5000; // Max wait time before failing
|
|
96
|
-
private static readonly lockStaleMs = 30000; // Age threshold for auto-removing orphaned locks
|
|
97
88
|
private static readonly codexUsageCacheTtlMs = 60_000; // Cache usage data for 1 minute
|
|
98
89
|
private static readonly defaultBackoffMs = 60_000; // Default backoff when no reset time available
|
|
99
90
|
|
|
100
|
-
|
|
91
|
+
/** Provider -> credentials cache, populated from agent.db on reload(). */
|
|
92
|
+
private data: Map<string, StoredCredential[]> = new Map();
|
|
93
|
+
private storage: AgentStorage;
|
|
94
|
+
/** Resolved path to agent.db (derived from authPath or used directly if .db). */
|
|
95
|
+
private dbPath: string;
|
|
101
96
|
private runtimeOverrides: Map<string, string> = new Map();
|
|
102
97
|
/** Tracks next credential index per provider:type key for round-robin distribution (non-session use). */
|
|
103
98
|
private providerRoundRobinIndex: Map<string, number> = new Map();
|
|
@@ -110,13 +105,28 @@ export class AuthStorage {
|
|
|
110
105
|
private fallbackResolver?: (provider: string) => string | undefined;
|
|
111
106
|
|
|
112
107
|
/**
|
|
113
|
-
* @param authPath -
|
|
114
|
-
* @param fallbackPaths - Additional paths to
|
|
108
|
+
* @param authPath - Legacy auth.json path used for migration and locating agent.db
|
|
109
|
+
* @param fallbackPaths - Additional auth.json paths to migrate (legacy support)
|
|
115
110
|
*/
|
|
116
111
|
constructor(
|
|
117
112
|
private authPath: string,
|
|
118
113
|
private fallbackPaths: string[] = [],
|
|
119
|
-
) {
|
|
114
|
+
) {
|
|
115
|
+
this.dbPath = AuthStorage.resolveDbPath(authPath);
|
|
116
|
+
this.storage = AgentStorage.open(this.dbPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Converts legacy auth.json path to agent.db path, or returns .db path as-is.
|
|
121
|
+
* @param authPath - Path to auth.json or agent.db
|
|
122
|
+
* @returns Resolved path to agent.db
|
|
123
|
+
*/
|
|
124
|
+
private static resolveDbPath(authPath: string): string {
|
|
125
|
+
if (authPath.endsWith(".db")) {
|
|
126
|
+
return authPath;
|
|
127
|
+
}
|
|
128
|
+
return getAgentDbPath(dirname(authPath));
|
|
129
|
+
}
|
|
120
130
|
|
|
121
131
|
/**
|
|
122
132
|
* Set a runtime API key override (not persisted to disk).
|
|
@@ -134,7 +144,7 @@ export class AuthStorage {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
/**
|
|
137
|
-
* Set a fallback resolver for API keys not found in
|
|
147
|
+
* Set a fallback resolver for API keys not found in agent.db or env vars.
|
|
138
148
|
* Used for custom provider keys from models.json.
|
|
139
149
|
*/
|
|
140
150
|
setFallbackResolver(resolver: (provider: string) => string | undefined): void {
|
|
@@ -142,138 +152,53 @@ export class AuthStorage {
|
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
/**
|
|
145
|
-
* Reload credentials from
|
|
146
|
-
*
|
|
155
|
+
* Reload credentials from agent.db.
|
|
156
|
+
* Migrates legacy auth.json/settings.json on first load.
|
|
147
157
|
*/
|
|
148
158
|
async reload(): Promise<void> {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
logger.debug("AuthStorage.reload path check", { path: authPath, exists });
|
|
156
|
-
|
|
157
|
-
if (exists) {
|
|
158
|
-
try {
|
|
159
|
-
this.data = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
160
|
-
logger.debug("AuthStorage.reload loaded", { path: authPath, providers: Object.keys(this.data) });
|
|
161
|
-
return;
|
|
162
|
-
} catch (e) {
|
|
163
|
-
logger.error("AuthStorage failed to parse auth file", { path: authPath, error: String(e) });
|
|
164
|
-
// Continue to next path on parse error
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
159
|
+
const agentDir = dirname(this.dbPath);
|
|
160
|
+
await migrateJsonStorage({
|
|
161
|
+
agentDir,
|
|
162
|
+
settingsPath: join(agentDir, "settings.json"),
|
|
163
|
+
authPaths: [this.authPath, ...this.fallbackPaths],
|
|
164
|
+
});
|
|
168
165
|
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
const records = this.storage.listAuthCredentials();
|
|
167
|
+
const grouped = new Map<string, StoredCredential[]>();
|
|
168
|
+
for (const record of records) {
|
|
169
|
+
const list = grouped.get(record.provider) ?? [];
|
|
170
|
+
list.push({ id: record.id, credential: record.credential });
|
|
171
|
+
grouped.set(record.provider, list);
|
|
172
|
+
}
|
|
173
|
+
this.data = grouped;
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/**
|
|
174
|
-
*
|
|
177
|
+
* Gets cached credentials for a provider.
|
|
178
|
+
* @param provider - Provider name (e.g., "anthropic", "openai")
|
|
179
|
+
* @returns Array of stored credentials, empty if none exist
|
|
175
180
|
*/
|
|
176
|
-
private
|
|
177
|
-
|
|
178
|
-
const tempPath = this.getTempPath();
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
writeFileSync(tempPath, JSON.stringify(this.data, null, 2), { mode: 0o600 });
|
|
182
|
-
renameSync(tempPath, this.authPath);
|
|
183
|
-
chmodSync(this.authPath, 0o600);
|
|
184
|
-
const dir = dirname(this.authPath);
|
|
185
|
-
chmodSync(dir, 0o700);
|
|
186
|
-
} finally {
|
|
187
|
-
this.safeUnlink(tempPath);
|
|
188
|
-
this.releaseLock(lockFd);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Returns the lock file path (auth.json.lock) */
|
|
193
|
-
private getLockPath(): string {
|
|
194
|
-
return `${this.authPath}.lock`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Returns a unique temp file path using pid and timestamp to avoid collisions */
|
|
198
|
-
private getTempPath(): string {
|
|
199
|
-
return `${this.authPath}.tmp-${process.pid}-${Date.now()}`;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Checks if lock file is older than lockStaleMs (orphaned by crashed process) */
|
|
203
|
-
private isLockStale(lockPath: string): boolean {
|
|
204
|
-
try {
|
|
205
|
-
const stats = statSync(lockPath);
|
|
206
|
-
return Date.now() - stats.mtimeMs > AuthStorage.lockStaleMs;
|
|
207
|
-
} catch {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
181
|
+
private getStoredCredentials(provider: string): StoredCredential[] {
|
|
182
|
+
return this.data.get(provider) ?? [];
|
|
210
183
|
}
|
|
211
184
|
|
|
212
185
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* @
|
|
186
|
+
* Updates in-memory credential cache for a provider.
|
|
187
|
+
* Removes the provider entry entirely if credentials array is empty.
|
|
188
|
+
* @param provider - Provider name (e.g., "anthropic", "openai")
|
|
189
|
+
* @param credentials - Array of stored credentials to cache
|
|
216
190
|
*/
|
|
217
|
-
private
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
while (true) {
|
|
224
|
-
try {
|
|
225
|
-
// O_EXCL fails if file exists, providing atomic lock acquisition
|
|
226
|
-
return openSync(lockPath, "wx", 0o600);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
const err = error as NodeJS.ErrnoException;
|
|
229
|
-
if (err.code !== "EEXIST") {
|
|
230
|
-
throw err;
|
|
231
|
-
}
|
|
232
|
-
if (this.isLockStale(lockPath)) {
|
|
233
|
-
this.safeUnlink(lockPath);
|
|
234
|
-
logger.warn("AuthStorage lock was stale, removing", { path: lockPath });
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (Date.now() - start > timeoutMs) {
|
|
238
|
-
throw new Error(`Timed out waiting for auth lock: ${lockPath}`);
|
|
239
|
-
}
|
|
240
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Releases file lock by closing fd and removing lock file */
|
|
246
|
-
private releaseLock(lockFd: number): void {
|
|
247
|
-
const lockPath = this.getLockPath();
|
|
248
|
-
try {
|
|
249
|
-
closeSync(lockFd);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
logger.warn("AuthStorage failed to close lock file", { error: String(error) });
|
|
252
|
-
}
|
|
253
|
-
this.safeUnlink(lockPath);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Removes file if it exists, ignoring ENOENT errors */
|
|
257
|
-
private safeUnlink(path: string): void {
|
|
258
|
-
try {
|
|
259
|
-
unlinkSync(path);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
const err = error as NodeJS.ErrnoException;
|
|
262
|
-
if (err.code !== "ENOENT") {
|
|
263
|
-
logger.warn("AuthStorage failed to remove file", { path, error: String(error) });
|
|
264
|
-
}
|
|
191
|
+
private setStoredCredentials(provider: string, credentials: StoredCredential[]): void {
|
|
192
|
+
if (credentials.length === 0) {
|
|
193
|
+
this.data.delete(provider);
|
|
194
|
+
} else {
|
|
195
|
+
this.data.set(provider, credentials);
|
|
265
196
|
}
|
|
266
197
|
}
|
|
267
198
|
|
|
268
|
-
/** Normalizes credential storage format: single credential becomes array of one */
|
|
269
|
-
private normalizeCredentialEntry(entry: AuthCredentialEntry | undefined): AuthCredential[] {
|
|
270
|
-
if (!entry) return [];
|
|
271
|
-
return Array.isArray(entry) ? entry : [entry];
|
|
272
|
-
}
|
|
273
|
-
|
|
274
199
|
/** Returns all credentials for a provider as an array */
|
|
275
200
|
private getCredentialsForProvider(provider: string): AuthCredential[] {
|
|
276
|
-
return this.
|
|
201
|
+
return this.getStoredCredentials(provider).map((entry) => entry.credential);
|
|
277
202
|
}
|
|
278
203
|
|
|
279
204
|
/** Composite key for round-robin tracking: "anthropic:oauth" or "openai:api_key" */
|
|
@@ -423,21 +348,13 @@ export class AuthStorage {
|
|
|
423
348
|
|
|
424
349
|
/** Updates credential at index in-place (used for OAuth token refresh) */
|
|
425
350
|
private replaceCredentialAt(provider: string, index: number, credential: AuthCredential): void {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.data[provider] = updated;
|
|
434
|
-
}
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (index === 0) {
|
|
439
|
-
this.data[provider] = credential;
|
|
440
|
-
}
|
|
351
|
+
const entries = this.getStoredCredentials(provider);
|
|
352
|
+
if (index < 0 || index >= entries.length) return;
|
|
353
|
+
const target = entries[index];
|
|
354
|
+
this.storage.updateAuthCredential(target.id, credential);
|
|
355
|
+
const updated = [...entries];
|
|
356
|
+
updated[index] = { id: target.id, credential };
|
|
357
|
+
this.setStoredCredentials(provider, updated);
|
|
441
358
|
}
|
|
442
359
|
|
|
443
360
|
/**
|
|
@@ -445,20 +362,11 @@ export class AuthStorage {
|
|
|
445
362
|
* Cleans up provider entry if last credential removed.
|
|
446
363
|
*/
|
|
447
364
|
private removeCredentialAt(provider: string, index: number): void {
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (updated.length > 0) {
|
|
454
|
-
this.data[provider] = updated;
|
|
455
|
-
} else {
|
|
456
|
-
delete this.data[provider];
|
|
457
|
-
}
|
|
458
|
-
} else {
|
|
459
|
-
delete this.data[provider];
|
|
460
|
-
}
|
|
461
|
-
|
|
365
|
+
const entries = this.getStoredCredentials(provider);
|
|
366
|
+
if (index < 0 || index >= entries.length) return;
|
|
367
|
+
this.storage.deleteAuthCredential(entries[index].id);
|
|
368
|
+
const updated = entries.filter((_value, idx) => idx !== index);
|
|
369
|
+
this.setStoredCredentials(provider, updated);
|
|
462
370
|
this.resetProviderAssignments(provider);
|
|
463
371
|
}
|
|
464
372
|
|
|
@@ -473,29 +381,33 @@ export class AuthStorage {
|
|
|
473
381
|
* Set credential for a provider.
|
|
474
382
|
*/
|
|
475
383
|
async set(provider: string, credential: AuthCredentialEntry): Promise<void> {
|
|
476
|
-
|
|
384
|
+
const normalized = Array.isArray(credential) ? credential : [credential];
|
|
385
|
+
const stored = this.storage.replaceAuthCredentialsForProvider(provider, normalized);
|
|
386
|
+
this.setStoredCredentials(
|
|
387
|
+
provider,
|
|
388
|
+
stored.map((record) => ({ id: record.id, credential: record.credential })),
|
|
389
|
+
);
|
|
477
390
|
this.resetProviderAssignments(provider);
|
|
478
|
-
await this.save();
|
|
479
391
|
}
|
|
480
392
|
|
|
481
393
|
/**
|
|
482
394
|
* Remove credential for a provider.
|
|
483
395
|
*/
|
|
484
396
|
async remove(provider: string): Promise<void> {
|
|
485
|
-
|
|
397
|
+
this.storage.deleteAuthCredentialsForProvider(provider);
|
|
398
|
+
this.data.delete(provider);
|
|
486
399
|
this.resetProviderAssignments(provider);
|
|
487
|
-
await this.save();
|
|
488
400
|
}
|
|
489
401
|
|
|
490
402
|
/**
|
|
491
403
|
* List all providers with credentials.
|
|
492
404
|
*/
|
|
493
405
|
list(): string[] {
|
|
494
|
-
return
|
|
406
|
+
return [...this.data.keys()];
|
|
495
407
|
}
|
|
496
408
|
|
|
497
409
|
/**
|
|
498
|
-
* Check if credentials exist for a provider in
|
|
410
|
+
* Check if credentials exist for a provider in agent.db.
|
|
499
411
|
*/
|
|
500
412
|
has(provider: string): boolean {
|
|
501
413
|
return this.getCredentialsForProvider(provider).length > 0;
|
|
@@ -533,7 +445,16 @@ export class AuthStorage {
|
|
|
533
445
|
* Get all credentials.
|
|
534
446
|
*/
|
|
535
447
|
getAll(): AuthStorageData {
|
|
536
|
-
|
|
448
|
+
const result: AuthStorageData = {};
|
|
449
|
+
for (const [provider, entries] of this.data.entries()) {
|
|
450
|
+
const credentials = entries.map((entry) => entry.credential);
|
|
451
|
+
if (credentials.length === 1) {
|
|
452
|
+
result[provider] = credentials[0];
|
|
453
|
+
} else if (credentials.length > 1) {
|
|
454
|
+
result[provider] = credentials;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return result;
|
|
537
458
|
}
|
|
538
459
|
|
|
539
460
|
/**
|
|
@@ -887,7 +808,6 @@ export class AuthStorage {
|
|
|
887
808
|
|
|
888
809
|
const updated: OAuthCredential = { type: "oauth", ...result.newCredentials };
|
|
889
810
|
this.replaceCredentialAt(provider, selection.index, updated);
|
|
890
|
-
await this.save();
|
|
891
811
|
|
|
892
812
|
if (checkUsage) {
|
|
893
813
|
const usage = await this.getCodexUsage(updated, options?.baseUrl);
|
|
@@ -906,7 +826,6 @@ export class AuthStorage {
|
|
|
906
826
|
return result.apiKey;
|
|
907
827
|
} catch {
|
|
908
828
|
this.removeCredentialAt(provider, selection.index);
|
|
909
|
-
await this.save();
|
|
910
829
|
if (this.getCredentialsForProvider(provider).some((credential) => credential.type === "oauth")) {
|
|
911
830
|
return this.getApiKey(provider, sessionId, options);
|
|
912
831
|
}
|
|
@@ -919,8 +838,8 @@ export class AuthStorage {
|
|
|
919
838
|
* Get API key for a provider.
|
|
920
839
|
* Priority:
|
|
921
840
|
* 1. Runtime override (CLI --api-key)
|
|
922
|
-
* 2. API key from
|
|
923
|
-
* 3. OAuth token from
|
|
841
|
+
* 2. API key from agent.db
|
|
842
|
+
* 3. OAuth token from agent.db (auto-refreshed)
|
|
924
843
|
* 4. Environment variable
|
|
925
844
|
* 5. Fallback resolver (models.json custom providers)
|
|
926
845
|
*/
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
11
|
-
import branchSummaryPrompt from "../../prompts/branch-summary.md" with { type: "text" };
|
|
12
|
-
import branchSummaryPreamble from "../../prompts/branch-summary-preamble.md" with { type: "text" };
|
|
11
|
+
import branchSummaryPrompt from "../../prompts/compaction/branch-summary.md" with { type: "text" };
|
|
12
|
+
import branchSummaryPreamble from "../../prompts/compaction/branch-summary-preamble.md" with { type: "text" };
|
|
13
13
|
import {
|
|
14
14
|
convertToLlm,
|
|
15
15
|
createBranchSummaryMessage,
|
|
16
16
|
createCompactionSummaryMessage,
|
|
17
17
|
createCustomMessage,
|
|
18
18
|
} from "../messages";
|
|
19
|
+
import { renderPromptTemplate } from "../prompt-templates";
|
|
19
20
|
import type { ReadonlySessionManager, SessionEntry } from "../session-manager";
|
|
20
21
|
import { estimateTokens } from "./compaction";
|
|
21
22
|
import {
|
|
@@ -237,9 +238,9 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
|
|
|
237
238
|
// Summary Generation
|
|
238
239
|
// ============================================================================
|
|
239
240
|
|
|
240
|
-
const BRANCH_SUMMARY_PREAMBLE = branchSummaryPreamble;
|
|
241
|
+
const BRANCH_SUMMARY_PREAMBLE = renderPromptTemplate(branchSummaryPreamble);
|
|
241
242
|
|
|
242
|
-
const BRANCH_SUMMARY_PROMPT = branchSummaryPrompt;
|
|
243
|
+
const BRANCH_SUMMARY_PROMPT = renderPromptTemplate(branchSummaryPrompt);
|
|
243
244
|
|
|
244
245
|
/**
|
|
245
246
|
* Generate a summary of abandoned branch entries.
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { complete, completeSimple } from "@oh-my-pi/pi-ai";
|
|
11
|
-
import compactionSummaryPrompt from "../../prompts/compaction-summary.md" with { type: "text" };
|
|
12
|
-
import compactionTurnPrefixPrompt from "../../prompts/compaction-turn-prefix.md" with { type: "text" };
|
|
13
|
-
import compactionUpdateSummaryPrompt from "../../prompts/compaction-update-summary.md" with { type: "text" };
|
|
11
|
+
import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
|
|
12
|
+
import compactionTurnPrefixPrompt from "../../prompts/compaction/compaction-turn-prefix.md" with { type: "text" };
|
|
13
|
+
import compactionUpdateSummaryPrompt from "../../prompts/compaction/compaction-update-summary.md" with { type: "text" };
|
|
14
14
|
import { convertToLlm, createBranchSummaryMessage, createCustomMessage } from "../messages";
|
|
15
|
+
import { renderPromptTemplate } from "../prompt-templates";
|
|
15
16
|
import type { CompactionEntry, SessionEntry } from "../session-manager";
|
|
16
17
|
import {
|
|
17
18
|
computeFileLists,
|
|
@@ -386,9 +387,9 @@ export function findCutPoint(
|
|
|
386
387
|
// Summarization
|
|
387
388
|
// ============================================================================
|
|
388
389
|
|
|
389
|
-
const SUMMARIZATION_PROMPT = compactionSummaryPrompt;
|
|
390
|
+
const SUMMARIZATION_PROMPT = renderPromptTemplate(compactionSummaryPrompt);
|
|
390
391
|
|
|
391
|
-
const UPDATE_SUMMARIZATION_PROMPT = compactionUpdateSummaryPrompt;
|
|
392
|
+
const UPDATE_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionUpdateSummaryPrompt);
|
|
392
393
|
|
|
393
394
|
/**
|
|
394
395
|
* Generate a summary of the conversation using the LLM.
|
|
@@ -552,7 +553,7 @@ export function prepareCompaction(
|
|
|
552
553
|
// Main compaction function
|
|
553
554
|
// ============================================================================
|
|
554
555
|
|
|
555
|
-
const TURN_PREFIX_SUMMARIZATION_PROMPT = compactionTurnPrefixPrompt;
|
|
556
|
+
const TURN_PREFIX_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionTurnPrefixPrompt);
|
|
556
557
|
|
|
557
558
|
/**
|
|
558
559
|
* Generate summaries for compaction using prepared data.
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
6
6
|
import type { Message } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import
|
|
7
|
+
import fileOperationsTemplate from "../../prompts/system/file-operations.md" with { type: "text" };
|
|
8
|
+
import summarizationSystemPrompt from "../../prompts/system/summarization-system.md" with { type: "text" };
|
|
9
|
+
import { renderPromptTemplate } from "../prompt-templates";
|
|
8
10
|
|
|
9
11
|
// ============================================================================
|
|
10
12
|
// File Operation Tracking
|
|
@@ -71,15 +73,8 @@ export function computeFileLists(fileOps: FileOperations): { readFiles: string[]
|
|
|
71
73
|
* Format file operations as XML tags for summary.
|
|
72
74
|
*/
|
|
73
75
|
export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
|
|
77
|
-
}
|
|
78
|
-
if (modifiedFiles.length > 0) {
|
|
79
|
-
sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
|
|
80
|
-
}
|
|
81
|
-
if (sections.length === 0) return "";
|
|
82
|
-
return `\n\n${sections.join("\n\n")}`;
|
|
76
|
+
if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
|
|
77
|
+
return renderPromptTemplate(fileOperationsTemplate, { readFiles, modifiedFiles });
|
|
83
78
|
}
|
|
84
79
|
|
|
85
80
|
// ============================================================================
|
|
@@ -150,4 +145,4 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
150
145
|
// Summarization System Prompt
|
|
151
146
|
// ============================================================================
|
|
152
147
|
|
|
153
|
-
export const SUMMARIZATION_SYSTEM_PROMPT = summarizationSystemPrompt;
|
|
148
|
+
export const SUMMARIZATION_SYSTEM_PROMPT = renderPromptTemplate(summarizationSystemPrompt);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
|
|
16
16
|
import type { HookCommandContext } from "../../../hooks/types";
|
|
17
|
+
import { renderPromptTemplate } from "../../../prompt-templates";
|
|
17
18
|
import type { CustomCommand, CustomCommandAPI } from "../../types";
|
|
18
19
|
|
|
19
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -164,20 +165,6 @@ function getRecommendedAgentCount(stats: DiffStats): number {
|
|
|
164
165
|
return Math.min(16, fileCount);
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
/**
|
|
168
|
-
* Format diff stats as a markdown table for the prompt.
|
|
169
|
-
*/
|
|
170
|
-
function formatFileTable(files: FileDiff[]): string {
|
|
171
|
-
if (files.length === 0) return "_No files to review._";
|
|
172
|
-
|
|
173
|
-
const rows = files.map((f) => {
|
|
174
|
-
const ext = getFileExt(f.path);
|
|
175
|
-
return `| ${f.path} | +${f.linesAdded}/-${f.linesRemoved} | ${ext} |`;
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return `| File | +/- | Type |\n|------|-----|------|\n${rows.join("\n")}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
168
|
/**
|
|
182
169
|
* Extract first N lines of actual diff content (excluding headers) for preview.
|
|
183
170
|
*/
|
|
@@ -203,33 +190,6 @@ function getDiffPreview(hunks: string, maxLines: number): string {
|
|
|
203
190
|
return contentLines.join("\n");
|
|
204
191
|
}
|
|
205
192
|
|
|
206
|
-
/**
|
|
207
|
-
* Format condensed diff previews for large changesets.
|
|
208
|
-
*/
|
|
209
|
-
function formatDiffPreviews(files: FileDiff[], linesPerFile: number): string {
|
|
210
|
-
const parts: string[] = [];
|
|
211
|
-
|
|
212
|
-
for (const f of files) {
|
|
213
|
-
const preview = getDiffPreview(f.hunks, linesPerFile);
|
|
214
|
-
if (preview.trim()) {
|
|
215
|
-
parts.push(`#### ${f.path}\n\`\`\`diff\n${preview}\n\`\`\``);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return parts.join("\n\n");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Format excluded files list for the prompt.
|
|
224
|
-
*/
|
|
225
|
-
function formatExcluded(excluded: DiffStats["excluded"]): string {
|
|
226
|
-
if (excluded.length === 0) return "";
|
|
227
|
-
|
|
228
|
-
const items = excluded.map((e) => `- \`${e.path}\` (+${e.linesAdded}/-${e.linesRemoved}) — ${e.reason}`);
|
|
229
|
-
|
|
230
|
-
return `### Excluded Files (${excluded.length})\n\n${items.join("\n")}`;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
193
|
// Thresholds for diff inclusion
|
|
234
194
|
const MAX_DIFF_CHARS = 50_000; // Don't include diff above this
|
|
235
195
|
const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than this
|
|
@@ -241,59 +201,27 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
|
|
|
241
201
|
const agentCount = getRecommendedAgentCount(stats);
|
|
242
202
|
const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
|
|
243
203
|
const totalLines = stats.totalAdded + stats.totalRemoved;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<diff>
|
|
267
|
-
${rawDiff.trim()}
|
|
268
|
-
</diff>`;
|
|
269
|
-
} else {
|
|
270
|
-
const linesPerFile = Math.max(5, Math.floor(100 / stats.files.length));
|
|
271
|
-
diffSection = `### Diff Previews
|
|
272
|
-
|
|
273
|
-
_Full diff too large (${stats.files.length} files). Showing first ~${linesPerFile} lines per file. Reviewers should fetch full diffs for assigned files._
|
|
274
|
-
|
|
275
|
-
${formatDiffPreviews(stats.files, linesPerFile)}`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Build diff instruction
|
|
279
|
-
const diffInstruction = skipDiff
|
|
280
|
-
? "Run `git diff` or `git show` to get the diff for assigned files"
|
|
281
|
-
: "Use the diff hunks provided below (don't re-run git diff)";
|
|
282
|
-
|
|
283
|
-
// Replace template variables
|
|
284
|
-
return reviewRequestTemplate
|
|
285
|
-
.replace("{MODE}", mode)
|
|
286
|
-
.replace("{FILE_COUNT}", String(stats.files.length))
|
|
287
|
-
.replace("{LINES_ADDED}", String(stats.totalAdded))
|
|
288
|
-
.replace("{LINES_REMOVED}", String(stats.totalRemoved))
|
|
289
|
-
.replace("{FILE_TABLE}", formatFileTable(stats.files))
|
|
290
|
-
.replace("{EXCLUDED_SECTION}", stats.excluded.length > 0 ? formatExcluded(stats.excluded) : "")
|
|
291
|
-
.replace("{DISTRIBUTION_GUIDANCE}", distributionGuidance)
|
|
292
|
-
.replace("{GROUPING_GUIDANCE}", groupingGuidance)
|
|
293
|
-
.replace("{DIFF_INSTRUCTION}", diffInstruction)
|
|
294
|
-
.replace("{DIFF_SECTION}", diffSection)
|
|
295
|
-
.replace(/\n{3,}/g, "\n\n") // Collapse multiple blank lines
|
|
296
|
-
.trim();
|
|
204
|
+
const linesPerFile = skipDiff ? Math.max(5, Math.floor(100 / stats.files.length)) : 0;
|
|
205
|
+
|
|
206
|
+
const filesWithExt = stats.files.map((f) => ({
|
|
207
|
+
...f,
|
|
208
|
+
ext: getFileExt(f.path),
|
|
209
|
+
hunksPreview: skipDiff ? getDiffPreview(f.hunks, linesPerFile) : "",
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
return renderPromptTemplate(reviewRequestTemplate, {
|
|
213
|
+
mode,
|
|
214
|
+
files: filesWithExt,
|
|
215
|
+
excluded: stats.excluded,
|
|
216
|
+
totalAdded: stats.totalAdded,
|
|
217
|
+
totalRemoved: stats.totalRemoved,
|
|
218
|
+
totalLines,
|
|
219
|
+
agentCount,
|
|
220
|
+
multiAgent: agentCount > 1,
|
|
221
|
+
skipDiff,
|
|
222
|
+
rawDiff: rawDiff.trim(),
|
|
223
|
+
linesPerFile,
|
|
224
|
+
});
|
|
297
225
|
}
|
|
298
226
|
|
|
299
227
|
export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
|