@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,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
|
-
import { getAgentDir } from "../config.js";
|
|
4
|
+
import { getAgentDir as getDefaultAgentDir } from "../config.js";
|
|
5
5
|
function uuidv4() {
|
|
6
6
|
const bytes = randomBytes(16);
|
|
7
7
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
@@ -15,9 +15,6 @@ export const SUMMARY_PREFIX = `The conversation history before this point was co
|
|
|
15
15
|
`;
|
|
16
16
|
export const SUMMARY_SUFFIX = `
|
|
17
17
|
</summary>`;
|
|
18
|
-
/**
|
|
19
|
-
* Create a user message containing the summary with the standard prefix.
|
|
20
|
-
*/
|
|
21
18
|
export function createSummaryMessage(summary) {
|
|
22
19
|
return {
|
|
23
20
|
role: "user",
|
|
@@ -25,9 +22,6 @@ export function createSummaryMessage(summary) {
|
|
|
25
22
|
timestamp: Date.now(),
|
|
26
23
|
};
|
|
27
24
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Parse session file content into entries.
|
|
30
|
-
*/
|
|
31
25
|
export function parseSessionEntries(content) {
|
|
32
26
|
const entries = [];
|
|
33
27
|
const lines = content.trim().split("\n");
|
|
@@ -44,17 +38,6 @@ export function parseSessionEntries(content) {
|
|
|
44
38
|
}
|
|
45
39
|
return entries;
|
|
46
40
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Load session from entries, handling compaction events.
|
|
49
|
-
*
|
|
50
|
-
* Algorithm:
|
|
51
|
-
* 1. Find latest compaction event (if any)
|
|
52
|
-
* 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)
|
|
53
|
-
* 3. Prepend summary as user message
|
|
54
|
-
*/
|
|
55
|
-
/**
|
|
56
|
-
* Get the latest compaction entry from session entries, if any.
|
|
57
|
-
*/
|
|
58
41
|
export function getLatestCompactionEntry(entries) {
|
|
59
42
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
60
43
|
if (entries[i].type === "compaction") {
|
|
@@ -64,22 +47,19 @@ export function getLatestCompactionEntry(entries) {
|
|
|
64
47
|
return null;
|
|
65
48
|
}
|
|
66
49
|
export function loadSessionFromEntries(entries) {
|
|
67
|
-
// Find model and thinking level (always scan all entries)
|
|
68
50
|
let thinkingLevel = "off";
|
|
69
51
|
let model = null;
|
|
70
52
|
for (const entry of entries) {
|
|
71
|
-
if (entry.type === "
|
|
72
|
-
thinkingLevel = entry.thinkingLevel;
|
|
73
|
-
model = { provider: entry.provider, modelId: entry.modelId };
|
|
74
|
-
}
|
|
75
|
-
else if (entry.type === "thinking_level_change") {
|
|
53
|
+
if (entry.type === "thinking_level_change") {
|
|
76
54
|
thinkingLevel = entry.thinkingLevel;
|
|
77
55
|
}
|
|
78
56
|
else if (entry.type === "model_change") {
|
|
79
57
|
model = { provider: entry.provider, modelId: entry.modelId };
|
|
80
58
|
}
|
|
59
|
+
else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
60
|
+
model = { provider: entry.message.provider, modelId: entry.message.model };
|
|
61
|
+
}
|
|
81
62
|
}
|
|
82
|
-
// Find latest compaction event
|
|
83
63
|
let latestCompactionIndex = -1;
|
|
84
64
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
85
65
|
if (entries[i].type === "compaction") {
|
|
@@ -87,7 +67,6 @@ export function loadSessionFromEntries(entries) {
|
|
|
87
67
|
break;
|
|
88
68
|
}
|
|
89
69
|
}
|
|
90
|
-
// No compaction: return all messages
|
|
91
70
|
if (latestCompactionIndex === -1) {
|
|
92
71
|
const messages = [];
|
|
93
72
|
for (const entry of entries) {
|
|
@@ -98,7 +77,6 @@ export function loadSessionFromEntries(entries) {
|
|
|
98
77
|
return { messages, thinkingLevel, model };
|
|
99
78
|
}
|
|
100
79
|
const compactionEvent = entries[latestCompactionIndex];
|
|
101
|
-
// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)
|
|
102
80
|
const keptMessages = [];
|
|
103
81
|
for (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {
|
|
104
82
|
const entry = entries[i];
|
|
@@ -106,146 +84,137 @@ export function loadSessionFromEntries(entries) {
|
|
|
106
84
|
keptMessages.push(entry.message);
|
|
107
85
|
}
|
|
108
86
|
}
|
|
109
|
-
// Build final messages: summary + kept messages
|
|
110
87
|
const messages = [];
|
|
111
88
|
messages.push(createSummaryMessage(compactionEvent.summary));
|
|
112
89
|
messages.push(...keptMessages);
|
|
113
90
|
return { messages, thinkingLevel, model };
|
|
114
91
|
}
|
|
92
|
+
function getSessionDirectory(cwd, agentDir) {
|
|
93
|
+
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
94
|
+
const sessionDir = join(agentDir, "sessions", safePath);
|
|
95
|
+
if (!existsSync(sessionDir)) {
|
|
96
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
return sessionDir;
|
|
99
|
+
}
|
|
100
|
+
function loadEntriesFromFile(filePath) {
|
|
101
|
+
if (!existsSync(filePath))
|
|
102
|
+
return [];
|
|
103
|
+
const content = readFileSync(filePath, "utf8");
|
|
104
|
+
const entries = [];
|
|
105
|
+
const lines = content.trim().split("\n");
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
continue;
|
|
109
|
+
try {
|
|
110
|
+
const entry = JSON.parse(line);
|
|
111
|
+
entries.push(entry);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Skip malformed lines
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return entries;
|
|
118
|
+
}
|
|
119
|
+
function findMostRecentSession(sessionDir) {
|
|
120
|
+
try {
|
|
121
|
+
const files = readdirSync(sessionDir)
|
|
122
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
123
|
+
.map((f) => ({
|
|
124
|
+
path: join(sessionDir, f),
|
|
125
|
+
mtime: statSync(join(sessionDir, f)).mtime,
|
|
126
|
+
}))
|
|
127
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
128
|
+
return files[0]?.path || null;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
115
134
|
export class SessionManager {
|
|
116
|
-
sessionId;
|
|
117
|
-
sessionFile;
|
|
135
|
+
sessionId = "";
|
|
136
|
+
sessionFile = "";
|
|
118
137
|
sessionDir;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// In-memory entries for --no-session mode (when enabled=false)
|
|
138
|
+
cwd;
|
|
139
|
+
persist;
|
|
140
|
+
flushed = false;
|
|
123
141
|
inMemoryEntries = [];
|
|
124
|
-
constructor(
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.
|
|
130
|
-
// If file doesn't exist, loadSessionId() won't set sessionId, so generate one
|
|
131
|
-
if (!this.sessionId) {
|
|
132
|
-
this.sessionId = uuidv4();
|
|
133
|
-
}
|
|
134
|
-
// Mark as initialized since we're loading an existing session
|
|
135
|
-
this.sessionInitialized = existsSync(this.sessionFile);
|
|
136
|
-
// Load entries into memory
|
|
137
|
-
if (this.sessionInitialized) {
|
|
138
|
-
this.inMemoryEntries = this.loadEntriesFromFile();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else if (continueSession) {
|
|
142
|
-
const mostRecent = this.findMostRecentlyModifiedSession();
|
|
143
|
-
if (mostRecent) {
|
|
144
|
-
this.sessionFile = mostRecent;
|
|
145
|
-
this.loadSessionId();
|
|
146
|
-
// Mark as initialized since we're loading an existing session
|
|
147
|
-
this.sessionInitialized = true;
|
|
148
|
-
// Load entries into memory
|
|
149
|
-
this.inMemoryEntries = this.loadEntriesFromFile();
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
this.initNewSession();
|
|
153
|
-
}
|
|
142
|
+
constructor(cwd, agentDir, sessionFile, persist) {
|
|
143
|
+
this.cwd = cwd;
|
|
144
|
+
this.sessionDir = getSessionDirectory(cwd, agentDir);
|
|
145
|
+
this.persist = persist;
|
|
146
|
+
if (sessionFile) {
|
|
147
|
+
this.setSessionFile(sessionFile);
|
|
154
148
|
}
|
|
155
149
|
else {
|
|
156
|
-
this.
|
|
150
|
+
this.sessionId = uuidv4();
|
|
151
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
152
|
+
const sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
|
153
|
+
this.setSessionFile(sessionFile);
|
|
157
154
|
}
|
|
158
155
|
}
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
156
|
+
/** Switch to a different session file (used for resume and branching) */
|
|
157
|
+
setSessionFile(sessionFile) {
|
|
158
|
+
this.sessionFile = resolve(sessionFile);
|
|
159
|
+
if (existsSync(this.sessionFile)) {
|
|
160
|
+
this.inMemoryEntries = loadEntriesFromFile(this.sessionFile);
|
|
161
|
+
const header = this.inMemoryEntries.find((e) => e.type === "session");
|
|
162
|
+
this.sessionId = header ? header.id : uuidv4();
|
|
163
|
+
this.flushed = true;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
this.sessionId = uuidv4();
|
|
167
|
+
this.inMemoryEntries = [];
|
|
168
|
+
this.flushed = false;
|
|
169
|
+
const entry = {
|
|
170
|
+
type: "session",
|
|
171
|
+
id: this.sessionId,
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
cwd: this.cwd,
|
|
174
|
+
};
|
|
175
|
+
this.inMemoryEntries.push(entry);
|
|
175
176
|
}
|
|
176
|
-
return sessionDir;
|
|
177
177
|
}
|
|
178
|
-
|
|
179
|
-
this.
|
|
180
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
181
|
-
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
|
178
|
+
isPersisted() {
|
|
179
|
+
return this.persist;
|
|
182
180
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
this.pendingEntries = [];
|
|
186
|
-
this.inMemoryEntries = [];
|
|
187
|
-
this.sessionInitialized = false;
|
|
188
|
-
this.initNewSession();
|
|
181
|
+
getCwd() {
|
|
182
|
+
return this.cwd;
|
|
189
183
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const files = readdirSync(this.sessionDir)
|
|
193
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
194
|
-
.map((f) => ({
|
|
195
|
-
name: f,
|
|
196
|
-
path: join(this.sessionDir, f),
|
|
197
|
-
mtime: statSync(join(this.sessionDir, f)).mtime,
|
|
198
|
-
}))
|
|
199
|
-
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
200
|
-
return files[0]?.path || null;
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
184
|
+
getSessionId() {
|
|
185
|
+
return this.sessionId;
|
|
205
186
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return;
|
|
209
|
-
const lines = readFileSync(this.sessionFile, "utf8").trim().split("\n");
|
|
210
|
-
for (const line of lines) {
|
|
211
|
-
try {
|
|
212
|
-
const entry = JSON.parse(line);
|
|
213
|
-
if (entry.type === "session") {
|
|
214
|
-
this.sessionId = entry.id;
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
// Skip malformed lines
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.sessionId = uuidv4();
|
|
187
|
+
getSessionFile() {
|
|
188
|
+
return this.sessionFile;
|
|
223
189
|
}
|
|
224
|
-
|
|
225
|
-
|
|
190
|
+
reset() {
|
|
191
|
+
this.sessionId = uuidv4();
|
|
192
|
+
this.flushed = false;
|
|
193
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
194
|
+
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
|
195
|
+
this.inMemoryEntries = [
|
|
196
|
+
{
|
|
197
|
+
type: "session",
|
|
198
|
+
id: this.sessionId,
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
cwd: this.cwd,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
_persist(entry) {
|
|
205
|
+
if (!this.persist)
|
|
226
206
|
return;
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
thinkingLevel: state.thinkingLevel,
|
|
236
|
-
};
|
|
237
|
-
// Always track in memory
|
|
238
|
-
this.inMemoryEntries.push(entry);
|
|
239
|
-
for (const pending of this.pendingEntries) {
|
|
240
|
-
this.inMemoryEntries.push(pending);
|
|
207
|
+
const hasAssistant = this.inMemoryEntries.some((e) => e.type === "message" && e.message.role === "assistant");
|
|
208
|
+
if (!hasAssistant)
|
|
209
|
+
return;
|
|
210
|
+
if (!this.flushed) {
|
|
211
|
+
for (const e of this.inMemoryEntries) {
|
|
212
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(e)}\n`);
|
|
213
|
+
}
|
|
214
|
+
this.flushed = true;
|
|
241
215
|
}
|
|
242
|
-
|
|
243
|
-
// Write to file only if enabled
|
|
244
|
-
if (this.enabled) {
|
|
216
|
+
else {
|
|
245
217
|
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
246
|
-
for (const memEntry of this.inMemoryEntries.slice(1)) {
|
|
247
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(memEntry)}\n`);
|
|
248
|
-
}
|
|
249
218
|
}
|
|
250
219
|
}
|
|
251
220
|
saveMessage(message) {
|
|
@@ -254,17 +223,8 @@ export class SessionManager {
|
|
|
254
223
|
timestamp: new Date().toISOString(),
|
|
255
224
|
message,
|
|
256
225
|
};
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
// Always track in memory
|
|
262
|
-
this.inMemoryEntries.push(entry);
|
|
263
|
-
// Write to file only if enabled
|
|
264
|
-
if (this.enabled) {
|
|
265
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
226
|
+
this.inMemoryEntries.push(entry);
|
|
227
|
+
this._persist(entry);
|
|
268
228
|
}
|
|
269
229
|
saveThinkingLevelChange(thinkingLevel) {
|
|
270
230
|
const entry = {
|
|
@@ -272,17 +232,8 @@ export class SessionManager {
|
|
|
272
232
|
timestamp: new Date().toISOString(),
|
|
273
233
|
thinkingLevel,
|
|
274
234
|
};
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
// Always track in memory
|
|
280
|
-
this.inMemoryEntries.push(entry);
|
|
281
|
-
// Write to file only if enabled
|
|
282
|
-
if (this.enabled) {
|
|
283
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
235
|
+
this.inMemoryEntries.push(entry);
|
|
236
|
+
this._persist(entry);
|
|
286
237
|
}
|
|
287
238
|
saveModelChange(provider, modelId) {
|
|
288
239
|
const entry = {
|
|
@@ -291,101 +242,96 @@ export class SessionManager {
|
|
|
291
242
|
provider,
|
|
292
243
|
modelId,
|
|
293
244
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
// Always track in memory
|
|
299
|
-
this.inMemoryEntries.push(entry);
|
|
300
|
-
// Write to file only if enabled
|
|
301
|
-
if (this.enabled) {
|
|
302
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
245
|
+
this.inMemoryEntries.push(entry);
|
|
246
|
+
this._persist(entry);
|
|
305
247
|
}
|
|
306
248
|
saveCompaction(entry) {
|
|
307
|
-
// Always track in memory
|
|
308
249
|
this.inMemoryEntries.push(entry);
|
|
309
|
-
|
|
310
|
-
if (this.enabled) {
|
|
311
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
312
|
-
}
|
|
250
|
+
this._persist(entry);
|
|
313
251
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Load session data (messages, model, thinking level) with compaction support.
|
|
316
|
-
*/
|
|
317
252
|
loadSession() {
|
|
318
253
|
const entries = this.loadEntries();
|
|
319
254
|
return loadSessionFromEntries(entries);
|
|
320
255
|
}
|
|
321
|
-
/**
|
|
322
|
-
* @deprecated Use loadSession().messages instead
|
|
323
|
-
*/
|
|
324
256
|
loadMessages() {
|
|
325
257
|
return this.loadSession().messages;
|
|
326
258
|
}
|
|
327
|
-
/**
|
|
328
|
-
* @deprecated Use loadSession().thinkingLevel instead
|
|
329
|
-
*/
|
|
330
259
|
loadThinkingLevel() {
|
|
331
260
|
return this.loadSession().thinkingLevel;
|
|
332
261
|
}
|
|
333
|
-
/**
|
|
334
|
-
* @deprecated Use loadSession().model instead
|
|
335
|
-
*/
|
|
336
262
|
loadModel() {
|
|
337
263
|
return this.loadSession().model;
|
|
338
264
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
265
|
+
loadEntries() {
|
|
266
|
+
if (this.inMemoryEntries.length > 0) {
|
|
267
|
+
return [...this.inMemoryEntries];
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
return loadEntriesFromFile(this.sessionFile);
|
|
271
|
+
}
|
|
344
272
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
entries.push(entry);
|
|
273
|
+
createBranchedSessionFromEntries(entries, branchBeforeIndex) {
|
|
274
|
+
const newSessionId = uuidv4();
|
|
275
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
276
|
+
const newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);
|
|
277
|
+
const newEntries = [];
|
|
278
|
+
for (let i = 0; i < branchBeforeIndex; i++) {
|
|
279
|
+
const entry = entries[i];
|
|
280
|
+
if (entry.type === "session") {
|
|
281
|
+
newEntries.push({
|
|
282
|
+
...entry,
|
|
283
|
+
id: newSessionId,
|
|
284
|
+
timestamp: new Date().toISOString(),
|
|
285
|
+
branchedFrom: this.persist ? this.sessionFile : undefined,
|
|
286
|
+
});
|
|
360
287
|
}
|
|
361
|
-
|
|
362
|
-
|
|
288
|
+
else {
|
|
289
|
+
newEntries.push(entry);
|
|
363
290
|
}
|
|
364
291
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
* When disabled (--no-session), returns in-memory entries.
|
|
371
|
-
*/
|
|
372
|
-
loadEntries() {
|
|
373
|
-
// If file persistence is enabled and file exists, read from file
|
|
374
|
-
if (this.enabled && existsSync(this.sessionFile)) {
|
|
375
|
-
return this.loadEntriesFromFile();
|
|
292
|
+
if (this.persist) {
|
|
293
|
+
for (const entry of newEntries) {
|
|
294
|
+
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
295
|
+
}
|
|
296
|
+
return newSessionFile;
|
|
376
297
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
298
|
+
this.inMemoryEntries = newEntries;
|
|
299
|
+
this.sessionId = newSessionId;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
/** Create a new session for the given directory */
|
|
303
|
+
static create(cwd, agentDir = getDefaultAgentDir()) {
|
|
304
|
+
return new SessionManager(cwd, agentDir, null, true);
|
|
305
|
+
}
|
|
306
|
+
/** Open a specific session file */
|
|
307
|
+
static open(path, agentDir = getDefaultAgentDir()) {
|
|
308
|
+
// Extract cwd from session header if possible, otherwise use process.cwd()
|
|
309
|
+
const entries = loadEntriesFromFile(path);
|
|
310
|
+
const header = entries.find((e) => e.type === "session");
|
|
311
|
+
const cwd = header?.cwd ?? process.cwd();
|
|
312
|
+
return new SessionManager(cwd, agentDir, path, true);
|
|
313
|
+
}
|
|
314
|
+
/** Continue the most recent session for the given directory, or create new if none */
|
|
315
|
+
static continueRecent(cwd, agentDir = getDefaultAgentDir()) {
|
|
316
|
+
const sessionDir = getSessionDirectory(cwd, agentDir);
|
|
317
|
+
const mostRecent = findMostRecentSession(sessionDir);
|
|
318
|
+
if (mostRecent) {
|
|
319
|
+
return new SessionManager(cwd, agentDir, mostRecent, true);
|
|
320
|
+
}
|
|
321
|
+
return new SessionManager(cwd, agentDir, null, true);
|
|
322
|
+
}
|
|
323
|
+
/** Create an in-memory session (no file persistence) */
|
|
324
|
+
static inMemory() {
|
|
325
|
+
return new SessionManager(process.cwd(), getDefaultAgentDir(), null, false);
|
|
326
|
+
}
|
|
327
|
+
/** List all sessions for a directory */
|
|
328
|
+
static list(cwd, agentDir = getDefaultAgentDir()) {
|
|
329
|
+
const sessionDir = getSessionDirectory(cwd, agentDir);
|
|
384
330
|
const sessions = [];
|
|
385
331
|
try {
|
|
386
|
-
const files = readdirSync(
|
|
332
|
+
const files = readdirSync(sessionDir)
|
|
387
333
|
.filter((f) => f.endsWith(".jsonl"))
|
|
388
|
-
.map((f) => join(
|
|
334
|
+
.map((f) => join(sessionDir, f));
|
|
389
335
|
for (const file of files) {
|
|
390
336
|
try {
|
|
391
337
|
const stats = statSync(file);
|
|
@@ -399,15 +345,12 @@ export class SessionManager {
|
|
|
399
345
|
for (const line of lines) {
|
|
400
346
|
try {
|
|
401
347
|
const entry = JSON.parse(line);
|
|
402
|
-
// Extract session ID from first session entry
|
|
403
348
|
if (entry.type === "session" && !sessionId) {
|
|
404
349
|
sessionId = entry.id;
|
|
405
350
|
created = new Date(entry.timestamp);
|
|
406
351
|
}
|
|
407
|
-
// Count messages and collect all text
|
|
408
352
|
if (entry.type === "message") {
|
|
409
353
|
messageCount++;
|
|
410
|
-
// Extract text from user and assistant messages
|
|
411
354
|
if (entry.message.role === "user" || entry.message.role === "assistant") {
|
|
412
355
|
const textContent = entry.message.content
|
|
413
356
|
.filter((c) => c.type === "text")
|
|
@@ -415,7 +358,6 @@ export class SessionManager {
|
|
|
415
358
|
.join(" ");
|
|
416
359
|
if (textContent) {
|
|
417
360
|
allMessages.push(textContent);
|
|
418
|
-
// Get first user message for display
|
|
419
361
|
if (!firstMessage && entry.message.role === "user") {
|
|
420
362
|
firstMessage = textContent;
|
|
421
363
|
}
|
|
@@ -437,123 +379,16 @@ export class SessionManager {
|
|
|
437
379
|
allMessagesText: allMessages.join(" "),
|
|
438
380
|
});
|
|
439
381
|
}
|
|
440
|
-
catch
|
|
382
|
+
catch {
|
|
441
383
|
// Skip files that can't be read
|
|
442
|
-
console.error(`Failed to read session file ${file}:`, error);
|
|
443
384
|
}
|
|
444
385
|
}
|
|
445
|
-
// Sort by modified date (most recent first)
|
|
446
386
|
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
447
387
|
}
|
|
448
|
-
catch
|
|
449
|
-
|
|
388
|
+
catch {
|
|
389
|
+
// Return empty list on error
|
|
450
390
|
}
|
|
451
391
|
return sessions;
|
|
452
392
|
}
|
|
453
|
-
/**
|
|
454
|
-
* Set the session file to an existing session
|
|
455
|
-
*/
|
|
456
|
-
setSessionFile(path) {
|
|
457
|
-
this.sessionFile = path;
|
|
458
|
-
this.loadSessionId();
|
|
459
|
-
// Mark as initialized since we're loading an existing session
|
|
460
|
-
this.sessionInitialized = existsSync(path);
|
|
461
|
-
// Load entries into memory for consistency
|
|
462
|
-
if (this.sessionInitialized) {
|
|
463
|
-
this.inMemoryEntries = this.loadEntriesFromFile();
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
this.inMemoryEntries = [];
|
|
467
|
-
}
|
|
468
|
-
this.pendingEntries = [];
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Check if we should initialize the session based on message history.
|
|
472
|
-
* Session is initialized when we have at least 1 user message and 1 assistant message.
|
|
473
|
-
*/
|
|
474
|
-
shouldInitializeSession(messages) {
|
|
475
|
-
if (this.sessionInitialized)
|
|
476
|
-
return false;
|
|
477
|
-
const userMessages = messages.filter((m) => m.role === "user");
|
|
478
|
-
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
|
479
|
-
return userMessages.length >= 1 && assistantMessages.length >= 1;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Create a branched session from a specific message index.
|
|
483
|
-
* If branchFromIndex is -1, creates an empty session.
|
|
484
|
-
* Returns the new session file path.
|
|
485
|
-
*/
|
|
486
|
-
createBranchedSession(state, branchFromIndex) {
|
|
487
|
-
// Create a new session ID for the branch
|
|
488
|
-
const newSessionId = uuidv4();
|
|
489
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
490
|
-
const newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);
|
|
491
|
-
// Write session header
|
|
492
|
-
const entry = {
|
|
493
|
-
type: "session",
|
|
494
|
-
id: newSessionId,
|
|
495
|
-
timestamp: new Date().toISOString(),
|
|
496
|
-
cwd: process.cwd(),
|
|
497
|
-
provider: state.model.provider,
|
|
498
|
-
modelId: state.model.id,
|
|
499
|
-
thinkingLevel: state.thinkingLevel,
|
|
500
|
-
branchedFrom: this.sessionFile,
|
|
501
|
-
};
|
|
502
|
-
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
503
|
-
// Write messages up to and including the branch point (if >= 0)
|
|
504
|
-
if (branchFromIndex >= 0) {
|
|
505
|
-
const messagesToWrite = state.messages.slice(0, branchFromIndex + 1);
|
|
506
|
-
for (const message of messagesToWrite) {
|
|
507
|
-
const messageEntry = {
|
|
508
|
-
type: "message",
|
|
509
|
-
timestamp: new Date().toISOString(),
|
|
510
|
-
message,
|
|
511
|
-
};
|
|
512
|
-
appendFileSync(newSessionFile, `${JSON.stringify(messageEntry)}\n`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
return newSessionFile;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Create a branched session from session entries up to (but not including) a specific entry index.
|
|
519
|
-
* This preserves compaction events and all entry types.
|
|
520
|
-
* Returns the new session file path, or null if in --no-session mode (in-memory only).
|
|
521
|
-
*/
|
|
522
|
-
createBranchedSessionFromEntries(entries, branchBeforeIndex) {
|
|
523
|
-
const newSessionId = uuidv4();
|
|
524
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
525
|
-
const newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);
|
|
526
|
-
// Build new entries list (up to but not including branch point)
|
|
527
|
-
const newEntries = [];
|
|
528
|
-
for (let i = 0; i < branchBeforeIndex; i++) {
|
|
529
|
-
const entry = entries[i];
|
|
530
|
-
if (entry.type === "session") {
|
|
531
|
-
// Rewrite session header with new ID and branchedFrom
|
|
532
|
-
newEntries.push({
|
|
533
|
-
...entry,
|
|
534
|
-
id: newSessionId,
|
|
535
|
-
timestamp: new Date().toISOString(),
|
|
536
|
-
branchedFrom: this.enabled ? this.sessionFile : undefined,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
// Copy other entries as-is
|
|
541
|
-
newEntries.push(entry);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (this.enabled) {
|
|
545
|
-
// Write to file
|
|
546
|
-
for (const entry of newEntries) {
|
|
547
|
-
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
548
|
-
}
|
|
549
|
-
return newSessionFile;
|
|
550
|
-
}
|
|
551
|
-
else {
|
|
552
|
-
// In-memory mode: replace inMemoryEntries, no file created
|
|
553
|
-
this.inMemoryEntries = newEntries;
|
|
554
|
-
this.sessionId = newSessionId;
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
393
|
}
|
|
559
394
|
//# sourceMappingURL=session-manager.js.map
|