@mariozechner/pi-coding-agent 0.25.4 → 0.26.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 +18 -0
- package/README.md +47 -5
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +1 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/session-picker.d.ts +2 -2
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +2 -2
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -13
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/custom-tools/loader.d.ts +3 -2
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +5 -4
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/hooks/loader.d.ts +2 -5
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +4 -7
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/core/model-config.d.ts +5 -4
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +12 -19
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/sdk.d.ts +211 -0
- package/dist/core/sdk.d.ts.map +1 -0
- package/dist/core/sdk.js +466 -0
- package/dist/core/sdk.js.map +1 -0
- package/dist/core/session-manager.d.ts +31 -91
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +187 -352
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +12 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +101 -37
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +7 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +7 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +9 -3
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +10 -7
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts +24 -2
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -16
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +6 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +149 -144
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +7 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +105 -102
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +7 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +128 -124
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +11 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +198 -194
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +31 -29
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +44 -16
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +6 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +90 -86
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +6 -1
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +17 -5
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts +7 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +118 -115
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +6 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +63 -59
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +4 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +142 -312
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +3 -12
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +1 -3
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +1 -1
- package/dist/utils/shell.js.map +1 -1
- package/docs/sdk.md +864 -0
- package/examples/README.md +29 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +36 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +93 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +37 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +45 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +46 -0
- package/examples/sdk/12-full-control.ts +99 -0
- package/examples/sdk/README.md +138 -0
- package/package.json +4 -4
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AppMessage } from "@mariozechner/pi-agent-core";
|
|
2
2
|
export interface SessionHeader {
|
|
3
3
|
type: "session";
|
|
4
4
|
id: string;
|
|
5
5
|
timestamp: string;
|
|
6
6
|
cwd: string;
|
|
7
|
-
provider: string;
|
|
8
|
-
modelId: string;
|
|
9
|
-
thinkingLevel: string;
|
|
10
7
|
branchedFrom?: string;
|
|
11
8
|
}
|
|
12
9
|
export interface SessionMessageEntry {
|
|
@@ -32,7 +29,6 @@ export interface CompactionEntry {
|
|
|
32
29
|
firstKeptEntryIndex: number;
|
|
33
30
|
tokensBefore: number;
|
|
34
31
|
}
|
|
35
|
-
/** Union of all session entry types */
|
|
36
32
|
export type SessionEntry = SessionHeader | SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry;
|
|
37
33
|
export interface LoadedSession {
|
|
38
34
|
messages: AppMessage[];
|
|
@@ -42,116 +38,60 @@ export interface LoadedSession {
|
|
|
42
38
|
modelId: string;
|
|
43
39
|
} | null;
|
|
44
40
|
}
|
|
41
|
+
export interface SessionInfo {
|
|
42
|
+
path: string;
|
|
43
|
+
id: string;
|
|
44
|
+
created: Date;
|
|
45
|
+
modified: Date;
|
|
46
|
+
messageCount: number;
|
|
47
|
+
firstMessage: string;
|
|
48
|
+
allMessagesText: string;
|
|
49
|
+
}
|
|
45
50
|
export declare const SUMMARY_PREFIX = "The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
|
|
46
51
|
export declare const SUMMARY_SUFFIX = "\n</summary>";
|
|
47
|
-
/**
|
|
48
|
-
* Create a user message containing the summary with the standard prefix.
|
|
49
|
-
*/
|
|
50
52
|
export declare function createSummaryMessage(summary: string): AppMessage;
|
|
51
|
-
/**
|
|
52
|
-
* Parse session file content into entries.
|
|
53
|
-
*/
|
|
54
53
|
export declare function parseSessionEntries(content: string): SessionEntry[];
|
|
55
|
-
/**
|
|
56
|
-
* Load session from entries, handling compaction events.
|
|
57
|
-
*
|
|
58
|
-
* Algorithm:
|
|
59
|
-
* 1. Find latest compaction event (if any)
|
|
60
|
-
* 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)
|
|
61
|
-
* 3. Prepend summary as user message
|
|
62
|
-
*/
|
|
63
|
-
/**
|
|
64
|
-
* Get the latest compaction entry from session entries, if any.
|
|
65
|
-
*/
|
|
66
54
|
export declare function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null;
|
|
67
55
|
export declare function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession;
|
|
68
56
|
export declare class SessionManager {
|
|
69
57
|
private sessionId;
|
|
70
58
|
private sessionFile;
|
|
71
59
|
private sessionDir;
|
|
72
|
-
private
|
|
73
|
-
private
|
|
74
|
-
private
|
|
60
|
+
private cwd;
|
|
61
|
+
private persist;
|
|
62
|
+
private flushed;
|
|
75
63
|
private inMemoryEntries;
|
|
76
|
-
constructor(
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/** Reset to a fresh session. Clears pending entries and starts a new session file. */
|
|
64
|
+
private constructor();
|
|
65
|
+
/** Switch to a different session file (used for resume and branching) */
|
|
66
|
+
setSessionFile(sessionFile: string): void;
|
|
67
|
+
isPersisted(): boolean;
|
|
68
|
+
getCwd(): string;
|
|
69
|
+
getSessionId(): string;
|
|
70
|
+
getSessionFile(): string;
|
|
84
71
|
reset(): void;
|
|
85
|
-
|
|
86
|
-
private loadSessionId;
|
|
87
|
-
startSession(state: AgentState): void;
|
|
72
|
+
_persist(entry: SessionEntry): void;
|
|
88
73
|
saveMessage(message: any): void;
|
|
89
74
|
saveThinkingLevelChange(thinkingLevel: string): void;
|
|
90
75
|
saveModelChange(provider: string, modelId: string): void;
|
|
91
76
|
saveCompaction(entry: CompactionEntry): void;
|
|
92
|
-
/**
|
|
93
|
-
* Load session data (messages, model, thinking level) with compaction support.
|
|
94
|
-
*/
|
|
95
77
|
loadSession(): LoadedSession;
|
|
96
|
-
/**
|
|
97
|
-
* @deprecated Use loadSession().messages instead
|
|
98
|
-
*/
|
|
99
78
|
loadMessages(): AppMessage[];
|
|
100
|
-
/**
|
|
101
|
-
* @deprecated Use loadSession().thinkingLevel instead
|
|
102
|
-
*/
|
|
103
79
|
loadThinkingLevel(): string;
|
|
104
|
-
/**
|
|
105
|
-
* @deprecated Use loadSession().model instead
|
|
106
|
-
*/
|
|
107
80
|
loadModel(): {
|
|
108
81
|
provider: string;
|
|
109
82
|
modelId: string;
|
|
110
83
|
} | null;
|
|
111
|
-
getSessionId(): string;
|
|
112
|
-
getSessionFile(): string;
|
|
113
|
-
/**
|
|
114
|
-
* Load entries directly from the session file (internal helper).
|
|
115
|
-
*/
|
|
116
|
-
private loadEntriesFromFile;
|
|
117
|
-
/**
|
|
118
|
-
* Load all entries from the session file or in-memory store.
|
|
119
|
-
* When file persistence is enabled, reads from file (source of truth for resumed sessions).
|
|
120
|
-
* When disabled (--no-session), returns in-memory entries.
|
|
121
|
-
*/
|
|
122
84
|
loadEntries(): SessionEntry[];
|
|
123
|
-
/**
|
|
124
|
-
* Load all sessions for the current directory with metadata
|
|
125
|
-
*/
|
|
126
|
-
loadAllSessions(): Array<{
|
|
127
|
-
path: string;
|
|
128
|
-
id: string;
|
|
129
|
-
created: Date;
|
|
130
|
-
modified: Date;
|
|
131
|
-
messageCount: number;
|
|
132
|
-
firstMessage: string;
|
|
133
|
-
allMessagesText: string;
|
|
134
|
-
}>;
|
|
135
|
-
/**
|
|
136
|
-
* Set the session file to an existing session
|
|
137
|
-
*/
|
|
138
|
-
setSessionFile(path: string): void;
|
|
139
|
-
/**
|
|
140
|
-
* Check if we should initialize the session based on message history.
|
|
141
|
-
* Session is initialized when we have at least 1 user message and 1 assistant message.
|
|
142
|
-
*/
|
|
143
|
-
shouldInitializeSession(messages: any[]): boolean;
|
|
144
|
-
/**
|
|
145
|
-
* Create a branched session from a specific message index.
|
|
146
|
-
* If branchFromIndex is -1, creates an empty session.
|
|
147
|
-
* Returns the new session file path.
|
|
148
|
-
*/
|
|
149
|
-
createBranchedSession(state: any, branchFromIndex: number): string;
|
|
150
|
-
/**
|
|
151
|
-
* Create a branched session from session entries up to (but not including) a specific entry index.
|
|
152
|
-
* This preserves compaction events and all entry types.
|
|
153
|
-
* Returns the new session file path, or null if in --no-session mode (in-memory only).
|
|
154
|
-
*/
|
|
155
85
|
createBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null;
|
|
86
|
+
/** Create a new session for the given directory */
|
|
87
|
+
static create(cwd: string, agentDir?: string): SessionManager;
|
|
88
|
+
/** Open a specific session file */
|
|
89
|
+
static open(path: string, agentDir?: string): SessionManager;
|
|
90
|
+
/** Continue the most recent session for the given directory, or create new if none */
|
|
91
|
+
static continueRecent(cwd: string, agentDir?: string): SessionManager;
|
|
92
|
+
/** Create an in-memory session (no file persistence) */
|
|
93
|
+
static inMemory(): SessionManager;
|
|
94
|
+
/** List all sessions for a directory */
|
|
95
|
+
static list(cwd: string, agentDir?: string): SessionInfo[];
|
|
156
96
|
}
|
|
157
97
|
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingEntries: SessionEntry[] = [];\n\t// In-memory entries for --no-session mode (when enabled=false)\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// If file doesn't exist, loadSessionId() won't set sessionId, so generate one\n\t\t\tif (!this.sessionId) {\n\t\t\t\tthis.sessionId = uuidv4();\n\t\t\t}\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t\t// Load entries into memory\n\t\t\tif (this.sessionInitialized) {\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t}\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t\t// Load entries into memory\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\t/** Check if session persistence is enabled */\n\tisEnabled(): boolean {\n\t\treturn this.enabled;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending entries and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t}\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load entries directly from the session file (internal helper).\n\t */\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all entries from the session file or in-memory store.\n\t * When file persistence is enabled, reads from file (source of truth for resumed sessions).\n\t * When disabled (--no-session), returns in-memory entries.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\t// If file persistence is enabled and file exists, read from file\n\t\tif (this.enabled && existsSync(this.sessionFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\n\t\t// Otherwise return in-memory entries (for --no-session mode)\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t\t// Load entries into memory for consistency\n\t\tif (this.sessionInitialized) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t} else {\n\t\t\tthis.inMemoryEntries = [];\n\t\t}\n\t\tthis.pendingEntries = [];\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(messageEntry)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path, or null if in --no-session mode (in-memory only).\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Build new entries list (up to but not including branch point)\n\t\tconst newEntries: SessionEntry[] = [];\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tnewEntries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.enabled ? this.sessionFile : undefined,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tnewEntries.push(entry);\n\t\t\t}\n\t\t}\n\n\t\tif (this.enabled) {\n\t\t\t// Write to file\n\t\t\tfor (const entry of newEntries) {\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t\treturn newSessionFile;\n\t\t} else {\n\t\t\t// In-memory mode: replace inMemoryEntries, no file created\n\t\t\tthis.inMemoryEntries = newEntries;\n\t\t\tthis.sessionId = newSessionId;\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir as getDefaultAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number;\n\ttokensBefore: number;\n}\n\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport interface SessionInfo {\n\tpath: string;\n\tid: string;\n\tcreated: Date;\n\tmodified: Date;\n\tmessageCount: number;\n\tfirstMessage: string;\n\tallMessagesText: string;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\tmodel = { provider: entry.message.provider, modelId: entry.message.model };\n\t\t}\n\t}\n\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nfunction getSessionDirectory(cwd: string, agentDir: string): string {\n\tconst safePath = `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n\tconst sessionDir = join(agentDir, \"sessions\", safePath);\n\tif (!existsSync(sessionDir)) {\n\t\tmkdirSync(sessionDir, { recursive: true });\n\t}\n\treturn sessionDir;\n}\n\nfunction loadEntriesFromFile(filePath: string): SessionEntry[] {\n\tif (!existsSync(filePath)) return [];\n\n\tconst content = readFileSync(filePath, \"utf8\");\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\nfunction findMostRecentSession(sessionDir: string): string | null {\n\ttry {\n\t\tconst files = readdirSync(sessionDir)\n\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t.map((f) => ({\n\t\t\t\tpath: join(sessionDir, f),\n\t\t\t\tmtime: statSync(join(sessionDir, f)).mtime,\n\t\t\t}))\n\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\treturn files[0]?.path || null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport class SessionManager {\n\tprivate sessionId: string = \"\";\n\tprivate sessionFile: string = \"\";\n\tprivate sessionDir: string;\n\tprivate cwd: string;\n\tprivate persist: boolean;\n\tprivate flushed: boolean = false;\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tprivate constructor(cwd: string, agentDir: string, sessionFile: string | null, persist: boolean) {\n\t\tthis.cwd = cwd;\n\t\tthis.sessionDir = getSessionDirectory(cwd, agentDir);\n\t\tthis.persist = persist;\n\n\t\tif (sessionFile) {\n\t\t\tthis.setSessionFile(sessionFile);\n\t\t} else {\n\t\t\tthis.sessionId = uuidv4();\n\t\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\t\tconst sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t\t\tthis.setSessionFile(sessionFile);\n\t\t}\n\t}\n\n\t/** Switch to a different session file (used for resume and branching) */\n\tsetSessionFile(sessionFile: string): void {\n\t\tthis.sessionFile = resolve(sessionFile);\n\t\tif (existsSync(this.sessionFile)) {\n\t\t\tthis.inMemoryEntries = loadEntriesFromFile(this.sessionFile);\n\t\t\tconst header = this.inMemoryEntries.find((e) => e.type === \"session\");\n\t\t\tthis.sessionId = header ? (header as SessionHeader).id : uuidv4();\n\t\t\tthis.flushed = true;\n\t\t} else {\n\t\t\tthis.sessionId = uuidv4();\n\t\t\tthis.inMemoryEntries = [];\n\t\t\tthis.flushed = false;\n\t\t\tconst entry: SessionHeader = {\n\t\t\t\ttype: \"session\",\n\t\t\t\tid: this.sessionId,\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tcwd: this.cwd,\n\t\t\t};\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t}\n\t}\n\n\tisPersisted(): boolean {\n\t\treturn this.persist;\n\t}\n\n\tgetCwd(): string {\n\t\treturn this.cwd;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\treset(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tthis.flushed = false;\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t\tthis.inMemoryEntries = [\n\t\t\t{\n\t\t\t\ttype: \"session\",\n\t\t\t\tid: this.sessionId,\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tcwd: this.cwd,\n\t\t\t},\n\t\t];\n\t}\n\n\t_persist(entry: SessionEntry): void {\n\t\tif (!this.persist) return;\n\n\t\tconst hasAssistant = this.inMemoryEntries.some((e) => e.type === \"message\" && e.message.role === \"assistant\");\n\t\tif (!hasAssistant) return;\n\n\t\tif (!this.flushed) {\n\t\t\tfor (const e of this.inMemoryEntries) {\n\t\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(e)}\\n`);\n\t\t\t}\n\t\t\tthis.flushed = true;\n\t\t} else {\n\t\t\tappendFileSync(this.sessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\tthis.inMemoryEntries.push(entry);\n\t\tthis._persist(entry);\n\t}\n\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tloadEntries(): SessionEntry[] {\n\t\tif (this.inMemoryEntries.length > 0) {\n\t\t\treturn [...this.inMemoryEntries];\n\t\t} else {\n\t\t\treturn loadEntriesFromFile(this.sessionFile);\n\t\t}\n\t}\n\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string | null {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\tconst newEntries: SessionEntry[] = [];\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\tnewEntries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.persist ? this.sessionFile : undefined,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tnewEntries.push(entry);\n\t\t\t}\n\t\t}\n\n\t\tif (this.persist) {\n\t\t\tfor (const entry of newEntries) {\n\t\t\t\tappendFileSync(newSessionFile, `${JSON.stringify(entry)}\\n`);\n\t\t\t}\n\t\t\treturn newSessionFile;\n\t\t}\n\t\tthis.inMemoryEntries = newEntries;\n\t\tthis.sessionId = newSessionId;\n\t\treturn null;\n\t}\n\n\t/** Create a new session for the given directory */\n\tstatic create(cwd: string, agentDir: string = getDefaultAgentDir()): SessionManager {\n\t\treturn new SessionManager(cwd, agentDir, null, true);\n\t}\n\n\t/** Open a specific session file */\n\tstatic open(path: string, agentDir: string = getDefaultAgentDir()): SessionManager {\n\t\t// Extract cwd from session header if possible, otherwise use process.cwd()\n\t\tconst entries = loadEntriesFromFile(path);\n\t\tconst header = entries.find((e) => e.type === \"session\") as SessionHeader | undefined;\n\t\tconst cwd = header?.cwd ?? process.cwd();\n\t\treturn new SessionManager(cwd, agentDir, path, true);\n\t}\n\n\t/** Continue the most recent session for the given directory, or create new if none */\n\tstatic continueRecent(cwd: string, agentDir: string = getDefaultAgentDir()): SessionManager {\n\t\tconst sessionDir = getSessionDirectory(cwd, agentDir);\n\t\tconst mostRecent = findMostRecentSession(sessionDir);\n\t\tif (mostRecent) {\n\t\t\treturn new SessionManager(cwd, agentDir, mostRecent, true);\n\t\t}\n\t\treturn new SessionManager(cwd, agentDir, null, true);\n\t}\n\n\t/** Create an in-memory session (no file persistence) */\n\tstatic inMemory(): SessionManager {\n\t\treturn new SessionManager(process.cwd(), getDefaultAgentDir(), null, false);\n\t}\n\n\t/** List all sessions for a directory */\n\tstatic list(cwd: string, agentDir: string = getDefaultAgentDir()): SessionInfo[] {\n\t\tconst sessionDir = getSessionDirectory(cwd, agentDir);\n\t\tconst sessions: SessionInfo[] = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch {\n\t\t\t// Return empty list on error\n\t\t}\n\n\t\treturn sessions;\n\t}\n}\n"]}
|