@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.1
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 +66 -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-session.ts +3 -3
- 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/export-html/index.ts +1 -33
- 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 +130 -290
- 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/complete.ts +5 -2
- 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 +5 -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/discovery/builtin.ts +9 -54
- package/src/discovery/claude.ts +16 -69
- package/src/discovery/codex.ts +11 -36
- package/src/discovery/helpers.ts +52 -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 +363 -3139
- 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/agents/planner.md +112 -0
- package/src/prompts/agents/task.md +15 -0
- 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 +237 -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 +34 -22
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/architect-plan.md +0 -10
- package/src/prompts/implement-with-critic.md +0 -11
- package/src/prompts/implement.md +0 -11
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/task.md +0 -14
- package/src/prompts/title-system.md +0 -8
- /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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
|
|
4
|
-
import { getAgentDir } from "../config";
|
|
4
|
+
import { getAgentDbPath, getAgentDir } from "../config";
|
|
5
5
|
import { loadSync } from "../discovery";
|
|
6
6
|
import type { SymbolPreset } from "../modes/interactive/theme/theme";
|
|
7
|
+
import { AgentStorage } from "./agent-storage";
|
|
8
|
+
import { logger } from "./logger";
|
|
7
9
|
|
|
8
10
|
export interface CompactionSettings {
|
|
9
11
|
enabled?: boolean; // default: true
|
|
@@ -337,10 +339,36 @@ function normalizeBashInterceptorSettings(
|
|
|
337
339
|
return { enabled, simpleLs, patterns };
|
|
338
340
|
}
|
|
339
341
|
|
|
342
|
+
let cachedNerdFonts: boolean | null = null;
|
|
343
|
+
|
|
344
|
+
function hasNerdFonts(): boolean {
|
|
345
|
+
if (cachedNerdFonts !== null) {
|
|
346
|
+
return cachedNerdFonts;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const envOverride = process.env.NERD_FONTS;
|
|
350
|
+
if (envOverride === "1") {
|
|
351
|
+
cachedNerdFonts = true;
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (envOverride === "0") {
|
|
355
|
+
cachedNerdFonts = false;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const termProgram = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
360
|
+
const term = (process.env.TERM || "").toLowerCase();
|
|
361
|
+
const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
|
|
362
|
+
cachedNerdFonts = nerdTerms.some((candidate) => termProgram.includes(candidate) || term.includes(candidate));
|
|
363
|
+
return cachedNerdFonts;
|
|
364
|
+
}
|
|
365
|
+
|
|
340
366
|
function normalizeSettings(settings: Settings): Settings {
|
|
341
367
|
const merged = deepMergeSettings(DEFAULT_SETTINGS, settings);
|
|
368
|
+
const symbolPreset = merged.symbolPreset ?? (hasNerdFonts() ? "nerd" : "unicode");
|
|
342
369
|
return {
|
|
343
370
|
...merged,
|
|
371
|
+
symbolPreset,
|
|
344
372
|
bashInterceptor: normalizeBashInterceptorSettings(merged.bashInterceptor),
|
|
345
373
|
};
|
|
346
374
|
}
|
|
@@ -377,15 +405,23 @@ function deepMergeSettings(base: Settings, overrides: Settings): Settings {
|
|
|
377
405
|
}
|
|
378
406
|
|
|
379
407
|
export class SettingsManager {
|
|
380
|
-
|
|
408
|
+
/** SQLite storage for persisted settings (null for in-memory mode) */
|
|
409
|
+
private storage: AgentStorage | null;
|
|
381
410
|
private cwd: string | null;
|
|
382
411
|
private globalSettings: Settings;
|
|
383
412
|
private overrides: Settings;
|
|
384
413
|
private settings!: Settings;
|
|
385
414
|
private persist: boolean;
|
|
386
415
|
|
|
387
|
-
|
|
388
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Private constructor - use static factory methods instead.
|
|
418
|
+
* @param storage - SQLite storage instance for persistence, or null for in-memory mode
|
|
419
|
+
* @param cwd - Current working directory for project settings discovery
|
|
420
|
+
* @param initialSettings - Initial global settings to use
|
|
421
|
+
* @param persist - Whether to persist settings changes to storage
|
|
422
|
+
*/
|
|
423
|
+
private constructor(storage: AgentStorage | null, cwd: string | null, initialSettings: Settings, persist: boolean) {
|
|
424
|
+
this.storage = storage;
|
|
389
425
|
this.cwd = cwd;
|
|
390
426
|
this.persist = persist;
|
|
391
427
|
this.globalSettings = initialSettings;
|
|
@@ -416,9 +452,15 @@ export class SettingsManager {
|
|
|
416
452
|
}
|
|
417
453
|
}
|
|
418
454
|
|
|
419
|
-
/**
|
|
455
|
+
/**
|
|
456
|
+
* Create a SettingsManager that loads from persistent SQLite storage.
|
|
457
|
+
* @param cwd - Current working directory for project settings discovery
|
|
458
|
+
* @param agentDir - Agent directory containing agent.db
|
|
459
|
+
* @returns Configured SettingsManager with merged global and user settings
|
|
460
|
+
*/
|
|
420
461
|
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
|
|
421
|
-
const
|
|
462
|
+
const storage = AgentStorage.open(getAgentDbPath(agentDir));
|
|
463
|
+
SettingsManager.migrateLegacySettingsFile(storage, agentDir);
|
|
422
464
|
|
|
423
465
|
// Use capability API to load user-level settings from all providers
|
|
424
466
|
const result = loadSync(settingsCapability.id, { cwd });
|
|
@@ -431,29 +473,58 @@ export class SettingsManager {
|
|
|
431
473
|
}
|
|
432
474
|
}
|
|
433
475
|
|
|
434
|
-
//
|
|
435
|
-
const
|
|
436
|
-
globalSettings = deepMergeSettings(globalSettings,
|
|
476
|
+
// Load persisted settings from agent.db (legacy settings.json is migrated separately)
|
|
477
|
+
const storedSettings = SettingsManager.loadFromStorage(storage);
|
|
478
|
+
globalSettings = deepMergeSettings(globalSettings, storedSettings);
|
|
437
479
|
|
|
438
|
-
return new SettingsManager(
|
|
480
|
+
return new SettingsManager(storage, cwd, globalSettings, true);
|
|
439
481
|
}
|
|
440
482
|
|
|
441
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Create an in-memory SettingsManager without persistence.
|
|
485
|
+
* @param settings - Initial settings to use
|
|
486
|
+
* @returns SettingsManager that won't persist changes to disk
|
|
487
|
+
*/
|
|
442
488
|
static inMemory(settings: Partial<Settings> = {}): SettingsManager {
|
|
443
489
|
return new SettingsManager(null, null, settings, false);
|
|
444
490
|
}
|
|
445
491
|
|
|
446
|
-
|
|
447
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Load settings from SQLite storage, applying any schema migrations.
|
|
494
|
+
* @param storage - AgentStorage instance, or null for in-memory mode
|
|
495
|
+
* @returns Parsed and migrated settings, or empty object if storage is null/empty
|
|
496
|
+
*/
|
|
497
|
+
private static loadFromStorage(storage: AgentStorage | null): Settings {
|
|
498
|
+
if (!storage) {
|
|
448
499
|
return {};
|
|
449
500
|
}
|
|
501
|
+
const settings = storage.getSettings();
|
|
502
|
+
if (!settings) {
|
|
503
|
+
return {};
|
|
504
|
+
}
|
|
505
|
+
return SettingsManager.migrateSettings(settings as Record<string, unknown>);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private static migrateLegacySettingsFile(storage: AgentStorage, agentDir: string): void {
|
|
509
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
510
|
+
if (!existsSync(settingsPath)) return;
|
|
511
|
+
if (storage.getSettings() !== null) return;
|
|
512
|
+
|
|
450
513
|
try {
|
|
451
|
-
const content = readFileSync(
|
|
452
|
-
const
|
|
453
|
-
|
|
514
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
515
|
+
const parsed = JSON.parse(content);
|
|
516
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const migrated = SettingsManager.migrateSettings(parsed as Record<string, unknown>);
|
|
520
|
+
storage.saveSettings(migrated);
|
|
521
|
+
try {
|
|
522
|
+
renameSync(settingsPath, `${settingsPath}.bak`);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
|
|
525
|
+
}
|
|
454
526
|
} catch (error) {
|
|
455
|
-
|
|
456
|
-
return {};
|
|
527
|
+
logger.warn("SettingsManager failed to migrate settings.json", { error: String(error) });
|
|
457
528
|
}
|
|
458
529
|
}
|
|
459
530
|
|
|
@@ -497,24 +568,19 @@ export class SettingsManager {
|
|
|
497
568
|
this.rebuildSettings();
|
|
498
569
|
}
|
|
499
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Persist current global settings to SQLite storage and rebuild merged settings.
|
|
573
|
+
* Merges with any concurrent changes in storage before saving.
|
|
574
|
+
*/
|
|
500
575
|
private save(): void {
|
|
501
|
-
if (this.persist && this.
|
|
576
|
+
if (this.persist && this.storage) {
|
|
502
577
|
try {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
mkdirSync(dir, { recursive: true });
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Re-read current file to preserve any settings added externally while running
|
|
509
|
-
const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath);
|
|
510
|
-
// Merge: file settings as base, globalSettings (in-memory changes) as overrides
|
|
511
|
-
const mergedSettings = deepMergeSettings(currentFileSettings, this.globalSettings);
|
|
578
|
+
const currentSettings = this.storage.getSettings() ?? {};
|
|
579
|
+
const mergedSettings = deepMergeSettings(currentSettings, this.globalSettings);
|
|
512
580
|
this.globalSettings = mergedSettings;
|
|
513
|
-
|
|
514
|
-
// Save merged settings (project settings are read-only)
|
|
515
|
-
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
|
|
581
|
+
this.storage.saveSettings(this.globalSettings);
|
|
516
582
|
} catch (error) {
|
|
517
|
-
|
|
583
|
+
logger.warn("SettingsManager save failed", { error: String(error) });
|
|
518
584
|
}
|
|
519
585
|
}
|
|
520
586
|
|
|
@@ -2,6 +2,7 @@ import { slashCommandCapability } from "../capability/slash-command";
|
|
|
2
2
|
import type { SlashCommand } from "../discovery";
|
|
3
3
|
import { loadSync } from "../discovery";
|
|
4
4
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
5
|
+
import { renderPromptTemplate } from "./prompt-templates";
|
|
5
6
|
import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -158,7 +159,9 @@ export function expandSlashCommand(text: string, fileCommands: FileSlashCommand[
|
|
|
158
159
|
const fileCommand = fileCommands.find((cmd) => cmd.name === commandName);
|
|
159
160
|
if (fileCommand) {
|
|
160
161
|
const args = parseCommandArgs(argsString);
|
|
161
|
-
|
|
162
|
+
const argsText = args.join(" ");
|
|
163
|
+
const substituted = substituteArgs(fileCommand.content, args);
|
|
164
|
+
return renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
return text;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrates legacy JSON storage (settings.json, auth.json) to SQLite-based agent.db.
|
|
3
|
+
* Settings migrate only when the DB has no settings; auth merges per-provider when missing.
|
|
4
|
+
* Original JSON files are backed up to .bak and removed after successful migration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getAgentDbPath } from "../config";
|
|
8
|
+
import { AgentStorage } from "./agent-storage";
|
|
9
|
+
import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./auth-storage";
|
|
10
|
+
import { logger } from "./logger";
|
|
11
|
+
import type { Settings } from "./settings-manager";
|
|
12
|
+
|
|
13
|
+
/** Paths configuration for the storage migration process. */
|
|
14
|
+
type MigrationPaths = {
|
|
15
|
+
/** Directory containing agent.db */
|
|
16
|
+
agentDir: string;
|
|
17
|
+
/** Path to legacy settings.json file */
|
|
18
|
+
settingsPath: string;
|
|
19
|
+
/** Candidate paths to search for auth.json (checked in order) */
|
|
20
|
+
authPaths: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Result of the JSON-to-SQLite storage migration. */
|
|
24
|
+
export interface StorageMigrationResult {
|
|
25
|
+
/** Whether settings.json was migrated to agent.db */
|
|
26
|
+
migratedSettings: boolean;
|
|
27
|
+
/** Whether auth.json was migrated to agent.db */
|
|
28
|
+
migratedAuth: boolean;
|
|
29
|
+
/** Non-fatal issues encountered during migration */
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Type guard for plain objects.
|
|
35
|
+
* @param value - Value to check
|
|
36
|
+
* @returns True if value is a non-null, non-array object
|
|
37
|
+
*/
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Transforms legacy settings to current schema (e.g., queueMode -> steeringMode).
|
|
44
|
+
* @param settings - Settings object potentially containing deprecated keys
|
|
45
|
+
* @returns Settings with deprecated keys renamed to current equivalents
|
|
46
|
+
*/
|
|
47
|
+
function migrateLegacySettings(settings: Settings): Settings {
|
|
48
|
+
const migrated = { ...settings } as Record<string, unknown>;
|
|
49
|
+
if ("queueMode" in migrated && !("steeringMode" in migrated)) {
|
|
50
|
+
migrated.steeringMode = migrated.queueMode;
|
|
51
|
+
delete migrated.queueMode;
|
|
52
|
+
}
|
|
53
|
+
return migrated as Settings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes credential entries to array format (legacy stored single credentials).
|
|
58
|
+
* @param entry - Single credential or array of credentials
|
|
59
|
+
* @returns Array of credentials (empty if entry is undefined)
|
|
60
|
+
*/
|
|
61
|
+
function normalizeCredentialEntry(entry: AuthCredentialEntry | undefined): AuthCredential[] {
|
|
62
|
+
if (!entry) return [];
|
|
63
|
+
return Array.isArray(entry) ? entry : [entry];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reads and parses a JSON file.
|
|
68
|
+
* @param path - Path to the JSON file
|
|
69
|
+
* @returns Parsed JSON content, or null if file doesn't exist or parsing fails
|
|
70
|
+
*/
|
|
71
|
+
async function readJsonFile<T>(path: string): Promise<T | null> {
|
|
72
|
+
try {
|
|
73
|
+
const file = Bun.file(path);
|
|
74
|
+
if (!(await file.exists())) return null;
|
|
75
|
+
const content = await file.text();
|
|
76
|
+
return JSON.parse(content) as T;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.warn("Storage migration failed to read JSON", { path, error: String(error) });
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Backs up a JSON file to .bak and removes the original.
|
|
85
|
+
* Prevents re-migration on subsequent runs.
|
|
86
|
+
* @param path - Path to the JSON file to backup
|
|
87
|
+
*/
|
|
88
|
+
async function backupJson(path: string): Promise<void> {
|
|
89
|
+
const file = Bun.file(path);
|
|
90
|
+
if (!(await file.exists())) return;
|
|
91
|
+
|
|
92
|
+
const backupPath = `${path}.bak`;
|
|
93
|
+
try {
|
|
94
|
+
const content = await file.arrayBuffer();
|
|
95
|
+
await Bun.write(backupPath, content);
|
|
96
|
+
await file.unlink();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logger.warn("Storage migration failed to backup JSON", { path, error: String(error) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Migrates settings.json to SQLite storage if DB is empty.
|
|
104
|
+
* @param storage - AgentStorage instance to migrate into
|
|
105
|
+
* @param settingsPath - Path to legacy settings.json
|
|
106
|
+
* @param warnings - Array to collect non-fatal warnings
|
|
107
|
+
* @returns True if migration was performed
|
|
108
|
+
*/
|
|
109
|
+
async function migrateSettings(storage: AgentStorage, settingsPath: string, warnings: string[]): Promise<boolean> {
|
|
110
|
+
const settingsFile = Bun.file(settingsPath);
|
|
111
|
+
const settingsExists = await settingsFile.exists();
|
|
112
|
+
const hasDbSettings = storage.getSettings() !== null;
|
|
113
|
+
|
|
114
|
+
if (!settingsExists) return false;
|
|
115
|
+
if (hasDbSettings) {
|
|
116
|
+
warnings.push(`settings.json exists but agent.db is authoritative: ${settingsPath}`);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const settingsJson = await readJsonFile<Settings>(settingsPath);
|
|
121
|
+
if (!settingsJson) return false;
|
|
122
|
+
|
|
123
|
+
storage.saveSettings(migrateLegacySettings(settingsJson));
|
|
124
|
+
await backupJson(settingsPath);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Finds the first valid auth.json from candidate paths (checked in priority order).
|
|
130
|
+
* @param authPaths - Candidate paths to search (e.g., project-local before global)
|
|
131
|
+
* @returns First valid auth file with its path and parsed data, or null if none found
|
|
132
|
+
*/
|
|
133
|
+
async function findFirstAuthJson(authPaths: string[]): Promise<{ path: string; data: AuthStorageData } | null> {
|
|
134
|
+
for (const authPath of authPaths) {
|
|
135
|
+
const data = await readJsonFile<AuthStorageData>(authPath);
|
|
136
|
+
if (data && isRecord(data)) {
|
|
137
|
+
return { path: authPath, data };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validates that a credential has a recognized type.
|
|
145
|
+
* @param entry - Credential to validate
|
|
146
|
+
* @returns True if credential type is api_key or oauth
|
|
147
|
+
*/
|
|
148
|
+
function isValidCredential(entry: AuthCredential): boolean {
|
|
149
|
+
return entry.type === "api_key" || entry.type === "oauth";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Migrates auth.json to SQLite storage for providers missing in agent.db.
|
|
154
|
+
* @param storage - AgentStorage instance to migrate into
|
|
155
|
+
* @param authPaths - Candidate paths to search for auth.json
|
|
156
|
+
* @param warnings - Array to collect non-fatal warnings
|
|
157
|
+
* @returns True if migration was performed
|
|
158
|
+
*/
|
|
159
|
+
async function migrateAuth(storage: AgentStorage, authPaths: string[], warnings: string[]): Promise<boolean> {
|
|
160
|
+
const authJson = await findFirstAuthJson(authPaths);
|
|
161
|
+
if (!authJson) return false;
|
|
162
|
+
|
|
163
|
+
let sawValid = false;
|
|
164
|
+
let migratedAny = false;
|
|
165
|
+
|
|
166
|
+
for (const [provider, entry] of Object.entries(authJson.data)) {
|
|
167
|
+
const credentials = normalizeCredentialEntry(entry)
|
|
168
|
+
.filter(isValidCredential)
|
|
169
|
+
.map((credential) => credential);
|
|
170
|
+
|
|
171
|
+
if (credentials.length === 0) continue;
|
|
172
|
+
sawValid = true;
|
|
173
|
+
|
|
174
|
+
if (storage.listAuthCredentials(provider).length > 0) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
storage.replaceAuthCredentialsForProvider(provider, credentials);
|
|
179
|
+
migratedAny = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (sawValid) {
|
|
183
|
+
await backupJson(authJson.path);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!migratedAny && sawValid) {
|
|
187
|
+
warnings.push(`auth.json entries already present in agent.db: ${authJson.path}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return migratedAny;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Migrates legacy JSON files (settings.json, auth.json) to SQLite-based agent.db.
|
|
195
|
+
* Settings migrate only when the DB has no settings; auth merges per-provider when missing.
|
|
196
|
+
* @param paths - Configuration specifying locations of legacy files and target DB
|
|
197
|
+
* @returns Result indicating what was migrated and any warnings encountered
|
|
198
|
+
*/
|
|
199
|
+
export async function migrateJsonStorage(paths: MigrationPaths): Promise<StorageMigrationResult> {
|
|
200
|
+
const storage = AgentStorage.open(getAgentDbPath(paths.agentDir));
|
|
201
|
+
const warnings: string[] = [];
|
|
202
|
+
|
|
203
|
+
const [migratedSettings, migratedAuth] = await Promise.all([
|
|
204
|
+
migrateSettings(storage, paths.settingsPath, warnings),
|
|
205
|
+
migrateAuth(storage, paths.authPaths, warnings),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
if (warnings.length > 0) {
|
|
209
|
+
for (const warning of warnings) {
|
|
210
|
+
logger.warn("Storage migration warning", { warning });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { migratedSettings, migratedAuth, warnings };
|
|
215
|
+
}
|