@oh-my-pi/pi-coding-agent 13.12.7 → 13.12.9
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 +44 -0
- package/package.json +7 -7
- package/src/capability/mcp.ts +7 -1
- package/src/cli/session-picker.ts +38 -28
- package/src/config/keybindings.ts +1 -4
- package/src/discovery/builtin.ts +18 -2
- package/src/discovery/mcp-json.ts +12 -2
- package/src/mcp/oauth-flow.ts +91 -1
- package/src/mcp/types.ts +3 -0
- package/src/modes/components/session-selector.ts +113 -13
- package/src/modes/components/status-line/segments.ts +13 -5
- package/src/modes/controllers/command-controller.ts +8 -46
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +20 -15
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/prompt-action-autocomplete.ts +19 -3
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/hotkeys-markdown.ts +57 -0
- package/src/sdk.ts +23 -6
- package/src/session/agent-session.ts +116 -26
- package/src/session/agent-storage.ts +48 -8
- package/src/session/history-storage.ts +44 -3
- package/src/session/session-manager.ts +113 -46
- package/src/session/session-storage.ts +27 -0
- package/src/slash-commands/builtin-registry.ts +14 -2
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +13 -3
- package/src/tools/find.ts +2 -2
- package/src/tools/grep.ts +3 -2
- package/src/tools/path-utils.ts +4 -0
|
@@ -142,7 +142,13 @@ import {
|
|
|
142
142
|
type PythonExecutionMessage,
|
|
143
143
|
pythonExecutionToText,
|
|
144
144
|
} from "./messages";
|
|
145
|
-
import type {
|
|
145
|
+
import type {
|
|
146
|
+
BranchSummaryEntry,
|
|
147
|
+
CompactionEntry,
|
|
148
|
+
NewSessionOptions,
|
|
149
|
+
SessionContext,
|
|
150
|
+
SessionManager,
|
|
151
|
+
} from "./session-manager";
|
|
146
152
|
import { getLatestCompactionEntry } from "./session-manager";
|
|
147
153
|
|
|
148
154
|
/** Session-specific events that extend the core AgentEvent */
|
|
@@ -215,8 +221,10 @@ export interface AgentSessionConfig {
|
|
|
215
221
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
|
|
216
222
|
/** Enable hidden-by-default MCP tool discovery for this session. */
|
|
217
223
|
mcpDiscoveryEnabled?: boolean;
|
|
218
|
-
/** MCP tool names
|
|
224
|
+
/** MCP tool names to activate for the current session when discovery mode is enabled. */
|
|
219
225
|
initialSelectedMCPToolNames?: string[];
|
|
226
|
+
/** MCP tool names that should seed brand-new sessions created from this AgentSession. */
|
|
227
|
+
defaultSelectedMCPToolNames?: string[];
|
|
220
228
|
/** TTSR manager for time-traveling stream rules */
|
|
221
229
|
ttsrManager?: TtsrManager;
|
|
222
230
|
/** Secret obfuscator for deobfuscating streaming edit content */
|
|
@@ -415,6 +423,8 @@ export class AgentSession {
|
|
|
415
423
|
#discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
|
|
416
424
|
#discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
|
|
417
425
|
#selectedMCPToolNames = new Set<string>();
|
|
426
|
+
#defaultSelectedMCPToolNames = new Set<string>();
|
|
427
|
+
#sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
|
|
418
428
|
|
|
419
429
|
// TTSR manager for time-traveling stream rules
|
|
420
430
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
@@ -464,7 +474,20 @@ export class AgentSession {
|
|
|
464
474
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
465
475
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
466
476
|
this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
|
|
477
|
+
this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
|
|
467
478
|
this.#pruneSelectedMCPToolNames();
|
|
479
|
+
const persistedSelectedMCPToolNames = this.sessionManager.buildSessionContext().selectedMCPToolNames;
|
|
480
|
+
const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
481
|
+
if (
|
|
482
|
+
this.#mcpDiscoveryEnabled &&
|
|
483
|
+
!this.#selectedMCPToolNamesMatch(persistedSelectedMCPToolNames, currentSelectedMCPToolNames)
|
|
484
|
+
) {
|
|
485
|
+
this.sessionManager.appendMCPToolSelection(currentSelectedMCPToolNames);
|
|
486
|
+
}
|
|
487
|
+
this.#rememberSessionDefaultSelectedMCPToolNames(
|
|
488
|
+
this.sessionManager.getSessionFile(),
|
|
489
|
+
this.#defaultSelectedMCPToolNames,
|
|
490
|
+
);
|
|
468
491
|
this.#ttsrManager = config.ttsrManager;
|
|
469
492
|
this.#obfuscator = config.obfuscator;
|
|
470
493
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
@@ -1632,23 +1655,43 @@ export class AgentSession {
|
|
|
1632
1655
|
this.#discoverableMCPSearchIndex = null;
|
|
1633
1656
|
}
|
|
1634
1657
|
|
|
1658
|
+
#filterSelectableMCPToolNames(toolNames: Iterable<string>): string[] {
|
|
1659
|
+
return Array.from(toolNames).filter(name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name));
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1635
1662
|
#pruneSelectedMCPToolNames(): void {
|
|
1636
|
-
|
|
1637
|
-
if (!this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
|
|
1638
|
-
this.#selectedMCPToolNames.delete(name);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1663
|
+
this.#selectedMCPToolNames = new Set(this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames));
|
|
1641
1664
|
}
|
|
1642
1665
|
|
|
1643
|
-
#
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1666
|
+
#selectedMCPToolNamesMatch(left: string[], right: string[]): boolean {
|
|
1667
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
#rememberSessionDefaultSelectedMCPToolNames(
|
|
1671
|
+
sessionFile: string | null | undefined,
|
|
1672
|
+
toolNames: Iterable<string>,
|
|
1673
|
+
): void {
|
|
1674
|
+
if (!sessionFile) return;
|
|
1675
|
+
this.#sessionDefaultSelectedMCPToolNames.set(
|
|
1676
|
+
path.resolve(sessionFile),
|
|
1677
|
+
this.#filterSelectableMCPToolNames(toolNames),
|
|
1649
1678
|
);
|
|
1650
1679
|
}
|
|
1651
1680
|
|
|
1681
|
+
#getSessionDefaultSelectedMCPToolNames(sessionFile: string | null | undefined): string[] {
|
|
1682
|
+
if (!sessionFile) return [];
|
|
1683
|
+
return this.#sessionDefaultSelectedMCPToolNames.get(path.resolve(sessionFile)) ?? [];
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
#persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames: string[]): void {
|
|
1687
|
+
if (!this.#mcpDiscoveryEnabled) return;
|
|
1688
|
+
const nextSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
1689
|
+
if (this.#selectedMCPToolNamesMatch(previousSelectedMCPToolNames, nextSelectedMCPToolNames)) {
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
this.sessionManager.appendMCPToolSelection(nextSelectedMCPToolNames);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1652
1695
|
#getActiveNonMCPToolNames(): string[] {
|
|
1653
1696
|
return this.getActiveToolNames().filter(name => !isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
1654
1697
|
}
|
|
@@ -1699,35 +1742,35 @@ export class AgentSession {
|
|
|
1699
1742
|
if (!this.#mcpDiscoveryEnabled) {
|
|
1700
1743
|
return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
1701
1744
|
}
|
|
1702
|
-
return
|
|
1703
|
-
name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
|
|
1704
|
-
);
|
|
1745
|
+
return this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames);
|
|
1705
1746
|
}
|
|
1706
1747
|
|
|
1707
1748
|
async activateDiscoveredMCPTools(toolNames: string[]): Promise<string[]> {
|
|
1749
|
+
const nextSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
|
|
1708
1750
|
const activated: string[] = [];
|
|
1709
1751
|
for (const name of toolNames) {
|
|
1710
1752
|
if (!isMCPToolName(name) || !this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
|
|
1711
1753
|
continue;
|
|
1712
1754
|
}
|
|
1713
|
-
|
|
1755
|
+
nextSelectedMCPToolNames.add(name);
|
|
1714
1756
|
activated.push(name);
|
|
1715
1757
|
}
|
|
1716
1758
|
if (activated.length === 0) {
|
|
1717
1759
|
return [];
|
|
1718
1760
|
}
|
|
1719
|
-
const nextActive = [
|
|
1761
|
+
const nextActive = [
|
|
1762
|
+
...this.#getActiveNonMCPToolNames(),
|
|
1763
|
+
...this.#filterSelectableMCPToolNames(nextSelectedMCPToolNames),
|
|
1764
|
+
];
|
|
1720
1765
|
await this.setActiveToolsByName(nextActive);
|
|
1721
1766
|
return [...new Set(activated)];
|
|
1722
1767
|
}
|
|
1723
1768
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
*/
|
|
1730
|
-
async setActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
1769
|
+
async #applyActiveToolsByName(
|
|
1770
|
+
toolNames: string[],
|
|
1771
|
+
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
1772
|
+
): Promise<void> {
|
|
1773
|
+
const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
|
|
1731
1774
|
const tools: AgentTool[] = [];
|
|
1732
1775
|
const validToolNames: string[] = [];
|
|
1733
1776
|
for (const name of toolNames) {
|
|
@@ -1751,8 +1794,36 @@ export class AgentSession {
|
|
|
1751
1794
|
this.#baseSystemPrompt = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
1752
1795
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
1753
1796
|
}
|
|
1797
|
+
if (options?.persistMCPSelection !== false) {
|
|
1798
|
+
this.#persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames);
|
|
1799
|
+
}
|
|
1754
1800
|
}
|
|
1755
1801
|
|
|
1802
|
+
/**
|
|
1803
|
+
* Set active tools by name.
|
|
1804
|
+
* Only tools in the registry can be enabled. Unknown tool names are ignored.
|
|
1805
|
+
* Also rebuilds the system prompt to reflect the new tool set.
|
|
1806
|
+
* Changes take effect before the next model call.
|
|
1807
|
+
*/
|
|
1808
|
+
async setActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
1809
|
+
await this.#applyActiveToolsByName(toolNames);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async #restoreMCPSelectionsForSessionContext(
|
|
1813
|
+
sessionContext: SessionContext,
|
|
1814
|
+
options?: { fallbackSelectedMCPToolNames?: Iterable<string> },
|
|
1815
|
+
): Promise<void> {
|
|
1816
|
+
if (!this.#mcpDiscoveryEnabled) return;
|
|
1817
|
+
const nextActiveNonMCPToolNames = this.#getActiveNonMCPToolNames();
|
|
1818
|
+
const fallbackSelectedMCPToolNames = options?.fallbackSelectedMCPToolNames ?? this.#defaultSelectedMCPToolNames;
|
|
1819
|
+
const restoredMCPToolNames = sessionContext.hasPersistedMCPToolSelection
|
|
1820
|
+
? this.#filterSelectableMCPToolNames(sessionContext.selectedMCPToolNames)
|
|
1821
|
+
: this.#filterSelectableMCPToolNames(fallbackSelectedMCPToolNames);
|
|
1822
|
+
this.#rememberSessionDefaultSelectedMCPToolNames(this.sessionFile, restoredMCPToolNames);
|
|
1823
|
+
await this.#applyActiveToolsByName([...nextActiveNonMCPToolNames, ...restoredMCPToolNames], {
|
|
1824
|
+
persistMCPSelection: false,
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1756
1827
|
/** Rebuild the base system prompt using the current active tool set. */
|
|
1757
1828
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
1758
1829
|
if (!this.#rebuildSystemPrompt) return;
|
|
@@ -1766,6 +1837,7 @@ export class AgentSession {
|
|
|
1766
1837
|
* This allows /mcp add/remove/reauth to take effect without restarting the session.
|
|
1767
1838
|
*/
|
|
1768
1839
|
async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
|
|
1840
|
+
const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
1769
1841
|
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
1770
1842
|
for (const name of existingNames) {
|
|
1771
1843
|
if (isMCPToolName(name)) {
|
|
@@ -1796,7 +1868,7 @@ export class AgentSession {
|
|
|
1796
1868
|
this.#pruneSelectedMCPToolNames();
|
|
1797
1869
|
|
|
1798
1870
|
const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()];
|
|
1799
|
-
await this
|
|
1871
|
+
await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
|
|
1800
1872
|
}
|
|
1801
1873
|
|
|
1802
1874
|
/** Whether auto-compaction is currently running */
|
|
@@ -2747,6 +2819,12 @@ export class AgentSession {
|
|
|
2747
2819
|
*/
|
|
2748
2820
|
async newSession(options?: NewSessionOptions): Promise<boolean> {
|
|
2749
2821
|
const previousSessionFile = this.sessionFile;
|
|
2822
|
+
const nextDiscoverySessionToolNames = this.#mcpDiscoveryEnabled
|
|
2823
|
+
? [
|
|
2824
|
+
...this.#getActiveNonMCPToolNames(),
|
|
2825
|
+
...this.#filterSelectableMCPToolNames(this.#defaultSelectedMCPToolNames),
|
|
2826
|
+
]
|
|
2827
|
+
: undefined;
|
|
2750
2828
|
|
|
2751
2829
|
// Emit session_before_switch event with reason "new" (can be cancelled)
|
|
2752
2830
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -2774,6 +2852,13 @@ export class AgentSession {
|
|
|
2774
2852
|
|
|
2775
2853
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
2776
2854
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
2855
|
+
if (nextDiscoverySessionToolNames) {
|
|
2856
|
+
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
2857
|
+
if (this.getSelectedMCPToolNames().length > 0) {
|
|
2858
|
+
this.sessionManager.appendMCPToolSelection(this.getSelectedMCPToolNames());
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
this.#rememberSessionDefaultSelectedMCPToolNames(this.sessionFile, this.#defaultSelectedMCPToolNames);
|
|
2777
2862
|
|
|
2778
2863
|
this.#todoReminderCount = 0;
|
|
2779
2864
|
this.#planReferenceSent = false;
|
|
@@ -4862,6 +4947,8 @@ export class AgentSession {
|
|
|
4862
4947
|
|
|
4863
4948
|
// Reload messages
|
|
4864
4949
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
4950
|
+
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
4951
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
4865
4952
|
|
|
4866
4953
|
// Emit session_switch event to hooks
|
|
4867
4954
|
if (this.#extensionRunner) {
|
|
@@ -4965,6 +5052,8 @@ export class AgentSession {
|
|
|
4965
5052
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
4966
5053
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
4967
5054
|
|
|
5055
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
5056
|
+
|
|
4968
5057
|
// Emit session_branch event to hooks (after branch completes)
|
|
4969
5058
|
if (this.#extensionRunner) {
|
|
4970
5059
|
await this.#extensionRunner.emit({
|
|
@@ -5128,6 +5217,7 @@ export class AgentSession {
|
|
|
5128
5217
|
|
|
5129
5218
|
// Update agent state
|
|
5130
5219
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
5220
|
+
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
5131
5221
|
this.agent.replaceMessages(sessionContext.messages);
|
|
5132
5222
|
this.#syncTodoPhasesFromBranch();
|
|
5133
5223
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -18,7 +18,8 @@ type ModelUsageRow = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/** Bump when schema changes require migration */
|
|
21
|
-
const SCHEMA_VERSION =
|
|
21
|
+
const SCHEMA_VERSION = 5;
|
|
22
|
+
const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
22
23
|
|
|
23
24
|
/** Singleton instances per database path */
|
|
24
25
|
const instances = new Map<string, AgentStorage>();
|
|
@@ -59,9 +60,8 @@ export class AgentStorage {
|
|
|
59
60
|
this.#authStore = new AuthCredentialStore(this.#db);
|
|
60
61
|
|
|
61
62
|
this.#listSettingsStmt = this.#db.prepare("SELECT key, value FROM settings");
|
|
62
|
-
|
|
63
63
|
this.#upsertModelUsageStmt = this.#db.prepare(
|
|
64
|
-
|
|
64
|
+
`INSERT INTO model_usage (model_key, last_used_at) VALUES (?, ${SQLITE_NOW_EPOCH}) ON CONFLICT(model_key) DO UPDATE SET last_used_at = ${SQLITE_NOW_EPOCH}`,
|
|
65
65
|
);
|
|
66
66
|
this.#listModelUsageStmt = this.#db.prepare(
|
|
67
67
|
"SELECT model_key, last_used_at FROM model_usage ORDER BY last_used_at DESC",
|
|
@@ -80,7 +80,7 @@ PRAGMA busy_timeout=5000;
|
|
|
80
80
|
|
|
81
81
|
CREATE TABLE IF NOT EXISTS model_usage (
|
|
82
82
|
model_key TEXT PRIMARY KEY,
|
|
83
|
-
last_used_at INTEGER NOT NULL DEFAULT (
|
|
83
|
+
last_used_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
|
|
84
84
|
);
|
|
85
85
|
|
|
86
86
|
CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
|
|
@@ -96,7 +96,7 @@ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
|
|
|
96
96
|
CREATE TABLE settings (
|
|
97
97
|
key TEXT PRIMARY KEY,
|
|
98
98
|
value TEXT NOT NULL,
|
|
99
|
-
updated_at INTEGER NOT NULL DEFAULT (
|
|
99
|
+
updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
|
|
100
100
|
);
|
|
101
101
|
`);
|
|
102
102
|
} else if (!hasKey || !hasValue) {
|
|
@@ -122,12 +122,12 @@ CREATE TABLE settings (
|
|
|
122
122
|
CREATE TABLE settings (
|
|
123
123
|
key TEXT PRIMARY KEY,
|
|
124
124
|
value TEXT NOT NULL,
|
|
125
|
-
updated_at INTEGER NOT NULL DEFAULT (
|
|
125
|
+
updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
|
|
126
126
|
);
|
|
127
127
|
`);
|
|
128
128
|
if (settings) {
|
|
129
129
|
const insert = this.#db.prepare(
|
|
130
|
-
|
|
130
|
+
`INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ${SQLITE_NOW_EPOCH})`,
|
|
131
131
|
);
|
|
132
132
|
for (const [key, value] of Object.entries(settings)) {
|
|
133
133
|
if (value === undefined) continue;
|
|
@@ -144,12 +144,15 @@ CREATE TABLE settings (
|
|
|
144
144
|
const versionRow = this.#db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get() as
|
|
145
145
|
| { version?: number }
|
|
146
146
|
| undefined;
|
|
147
|
+
const schemaVersion = typeof versionRow?.version === "number" ? versionRow.version : 0;
|
|
147
148
|
if (versionRow?.version !== undefined && versionRow.version !== SCHEMA_VERSION) {
|
|
148
149
|
logger.warn("AgentStorage schema version mismatch", {
|
|
149
150
|
current: versionRow.version,
|
|
150
151
|
expected: SCHEMA_VERSION,
|
|
151
152
|
});
|
|
152
|
-
|
|
153
|
+
}
|
|
154
|
+
if (schemaVersion < SCHEMA_VERSION) {
|
|
155
|
+
this.#migrateSchema(schemaVersion);
|
|
153
156
|
}
|
|
154
157
|
this.#db.prepare("INSERT OR REPLACE INTO schema_version(version) VALUES (?)").run(SCHEMA_VERSION);
|
|
155
158
|
}
|
|
@@ -159,6 +162,43 @@ CREATE TABLE settings (
|
|
|
159
162
|
// v3 → v4: Add disabled column to auth_credentials (handled by AuthCredentialStore)
|
|
160
163
|
// Nothing to do here - AuthCredentialStore will handle this migration
|
|
161
164
|
}
|
|
165
|
+
if (fromVersion < 5) {
|
|
166
|
+
this.#migrateSchemaV4ToV5();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#migrateSchemaV4ToV5(): void {
|
|
171
|
+
const migrate = this.#db.transaction(() => {
|
|
172
|
+
this.#db.exec("ALTER TABLE settings RENAME TO settings_legacy");
|
|
173
|
+
this.#db.exec(`
|
|
174
|
+
CREATE TABLE settings (
|
|
175
|
+
key TEXT PRIMARY KEY,
|
|
176
|
+
value TEXT NOT NULL,
|
|
177
|
+
updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
|
|
178
|
+
);
|
|
179
|
+
`);
|
|
180
|
+
this.#db.exec(`
|
|
181
|
+
INSERT INTO settings (key, value, updated_at)
|
|
182
|
+
SELECT key, value, updated_at
|
|
183
|
+
FROM settings_legacy
|
|
184
|
+
`);
|
|
185
|
+
this.#db.exec("DROP TABLE settings_legacy");
|
|
186
|
+
|
|
187
|
+
this.#db.exec("ALTER TABLE model_usage RENAME TO model_usage_legacy");
|
|
188
|
+
this.#db.exec(`
|
|
189
|
+
CREATE TABLE model_usage (
|
|
190
|
+
model_key TEXT PRIMARY KEY,
|
|
191
|
+
last_used_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
|
|
192
|
+
);
|
|
193
|
+
`);
|
|
194
|
+
this.#db.exec(`
|
|
195
|
+
INSERT INTO model_usage (model_key, last_used_at)
|
|
196
|
+
SELECT model_key, last_used_at
|
|
197
|
+
FROM model_usage_legacy
|
|
198
|
+
`);
|
|
199
|
+
this.#db.exec("DROP TABLE model_usage_legacy");
|
|
200
|
+
});
|
|
201
|
+
migrate();
|
|
162
202
|
}
|
|
163
203
|
|
|
164
204
|
/**
|
|
@@ -17,6 +17,8 @@ type HistoryRow = {
|
|
|
17
17
|
cwd: string | null;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
21
|
+
|
|
20
22
|
export class HistoryStorage {
|
|
21
23
|
#db: Database;
|
|
22
24
|
static #instance?: HistoryStorage;
|
|
@@ -45,7 +47,7 @@ PRAGMA busy_timeout=5000;
|
|
|
45
47
|
CREATE TABLE IF NOT EXISTS history (
|
|
46
48
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
49
|
prompt TEXT NOT NULL,
|
|
48
|
-
created_at INTEGER NOT NULL DEFAULT (
|
|
50
|
+
created_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH}),
|
|
49
51
|
cwd TEXT
|
|
50
52
|
);
|
|
51
53
|
CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
|
|
@@ -54,8 +56,12 @@ CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(prompt, content='histo
|
|
|
54
56
|
|
|
55
57
|
CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
56
58
|
INSERT INTO history_fts(rowid, prompt) VALUES (new.id, new.prompt);
|
|
57
|
-
END;
|
|
58
|
-
`);
|
|
59
|
+
END;
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
if (this.#historySchemaUsesUnixEpoch()) {
|
|
63
|
+
this.#migrateHistorySchema();
|
|
64
|
+
}
|
|
59
65
|
|
|
60
66
|
if (!hasFts) {
|
|
61
67
|
try {
|
|
@@ -135,6 +141,41 @@ END;
|
|
|
135
141
|
fs.mkdirSync(dir, { recursive: true });
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
#historySchemaUsesUnixEpoch(): boolean {
|
|
145
|
+
const row = this.#db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'history'").get() as
|
|
146
|
+
| { sql?: string | null }
|
|
147
|
+
| undefined;
|
|
148
|
+
return row?.sql?.includes("unixepoch(") ?? false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#migrateHistorySchema(): void {
|
|
152
|
+
const migrate = this.#db.transaction(() => {
|
|
153
|
+
this.#db.exec("ALTER TABLE history RENAME TO history_legacy");
|
|
154
|
+
this.#db.exec("DROP INDEX IF EXISTS idx_history_created_at");
|
|
155
|
+
this.#db.exec("DROP TRIGGER IF EXISTS history_ai");
|
|
156
|
+
this.#db.exec("DROP TABLE IF EXISTS history_fts");
|
|
157
|
+
this.#db.exec(`
|
|
158
|
+
CREATE TABLE history (
|
|
159
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
160
|
+
prompt TEXT NOT NULL,
|
|
161
|
+
created_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH}),
|
|
162
|
+
cwd TEXT
|
|
163
|
+
);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
|
|
165
|
+
INSERT INTO history (id, prompt, created_at, cwd)
|
|
166
|
+
SELECT id, prompt, created_at, cwd
|
|
167
|
+
FROM history_legacy;
|
|
168
|
+
DROP TABLE history_legacy;
|
|
169
|
+
CREATE VIRTUAL TABLE history_fts USING fts5(prompt, content='history', content_rowid='id');
|
|
170
|
+
CREATE TRIGGER history_ai AFTER INSERT ON history BEGIN
|
|
171
|
+
INSERT INTO history_fts(rowid, prompt) VALUES (new.id, new.prompt);
|
|
172
|
+
END;
|
|
173
|
+
`);
|
|
174
|
+
this.#db.run("INSERT INTO history_fts(history_fts) VALUES('rebuild')");
|
|
175
|
+
});
|
|
176
|
+
migrate();
|
|
177
|
+
}
|
|
178
|
+
|
|
138
179
|
#normalizeLimit(limit: number): number {
|
|
139
180
|
if (!Number.isFinite(limit)) return 0;
|
|
140
181
|
const clamped = Math.max(0, Math.floor(limit));
|