@mariozechner/pi-coding-agent 0.25.4 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +47 -5
  3. package/dist/cli/session-picker.d.ts +2 -2
  4. package/dist/cli/session-picker.d.ts.map +1 -1
  5. package/dist/cli/session-picker.js +2 -2
  6. package/dist/cli/session-picker.js.map +1 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +1 -13
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/custom-tools/loader.d.ts +3 -2
  11. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  12. package/dist/core/custom-tools/loader.js +5 -4
  13. package/dist/core/custom-tools/loader.js.map +1 -1
  14. package/dist/core/hooks/loader.d.ts +2 -5
  15. package/dist/core/hooks/loader.d.ts.map +1 -1
  16. package/dist/core/hooks/loader.js +4 -7
  17. package/dist/core/hooks/loader.js.map +1 -1
  18. package/dist/core/model-config.d.ts +5 -4
  19. package/dist/core/model-config.d.ts.map +1 -1
  20. package/dist/core/model-config.js +12 -19
  21. package/dist/core/model-config.js.map +1 -1
  22. package/dist/core/sdk.d.ts +211 -0
  23. package/dist/core/sdk.d.ts.map +1 -0
  24. package/dist/core/sdk.js +462 -0
  25. package/dist/core/sdk.js.map +1 -0
  26. package/dist/core/session-manager.d.ts +31 -91
  27. package/dist/core/session-manager.d.ts.map +1 -1
  28. package/dist/core/session-manager.js +187 -352
  29. package/dist/core/session-manager.js.map +1 -1
  30. package/dist/core/settings-manager.d.ts +12 -2
  31. package/dist/core/settings-manager.d.ts.map +1 -1
  32. package/dist/core/settings-manager.js +101 -37
  33. package/dist/core/settings-manager.js.map +1 -1
  34. package/dist/core/skills.d.ts +7 -1
  35. package/dist/core/skills.d.ts.map +1 -1
  36. package/dist/core/skills.js +7 -5
  37. package/dist/core/skills.js.map +1 -1
  38. package/dist/core/slash-commands.d.ts +9 -3
  39. package/dist/core/slash-commands.d.ts.map +1 -1
  40. package/dist/core/slash-commands.js +10 -7
  41. package/dist/core/slash-commands.js.map +1 -1
  42. package/dist/core/system-prompt.d.ts +24 -2
  43. package/dist/core/system-prompt.d.ts.map +1 -1
  44. package/dist/core/system-prompt.js +18 -16
  45. package/dist/core/system-prompt.js.map +1 -1
  46. package/dist/core/tools/index.d.ts +12 -22
  47. package/dist/core/tools/index.d.ts.map +1 -1
  48. package/dist/core/tools/index.js +2 -0
  49. package/dist/core/tools/index.js.map +1 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +12 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/main.d.ts +4 -1
  55. package/dist/main.d.ts.map +1 -1
  56. package/dist/main.js +142 -312
  57. package/dist/main.js.map +1 -1
  58. package/dist/modes/interactive/components/session-selector.d.ts +3 -12
  59. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/session-selector.js +1 -3
  61. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  62. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  63. package/dist/modes/interactive/interactive-mode.js +3 -2
  64. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  65. package/dist/utils/shell.d.ts.map +1 -1
  66. package/dist/utils/shell.js +1 -1
  67. package/dist/utils/shell.js.map +1 -1
  68. package/docs/sdk.md +819 -0
  69. package/examples/README.md +29 -0
  70. package/examples/sdk/01-minimal.ts +22 -0
  71. package/examples/sdk/02-custom-model.ts +36 -0
  72. package/examples/sdk/03-custom-prompt.ts +44 -0
  73. package/examples/sdk/04-skills.ts +44 -0
  74. package/examples/sdk/05-tools.ts +67 -0
  75. package/examples/sdk/06-hooks.ts +61 -0
  76. package/examples/sdk/07-context-files.ts +36 -0
  77. package/examples/sdk/08-slash-commands.ts +37 -0
  78. package/examples/sdk/09-api-keys-and-oauth.ts +45 -0
  79. package/examples/sdk/10-settings.ts +38 -0
  80. package/examples/sdk/11-sessions.ts +46 -0
  81. package/examples/sdk/12-full-control.ts +91 -0
  82. package/examples/sdk/README.md +138 -0
  83. 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 === "session") {
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
- enabled = true;
120
- sessionInitialized = false;
121
- pendingEntries = [];
122
- // In-memory entries for --no-session mode (when enabled=false)
138
+ cwd;
139
+ persist;
140
+ flushed = false;
123
141
  inMemoryEntries = [];
124
- constructor(continueSession = false, customSessionPath) {
125
- this.sessionDir = this.getSessionDirectory();
126
- if (customSessionPath) {
127
- // Use custom session file path
128
- this.sessionFile = resolve(customSessionPath);
129
- this.loadSessionId();
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.initNewSession();
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
- /** Disable session saving (for --no-session mode) */
160
- disable() {
161
- this.enabled = false;
162
- }
163
- /** Check if session persistence is enabled */
164
- isEnabled() {
165
- return this.enabled;
166
- }
167
- getSessionDirectory() {
168
- const cwd = process.cwd();
169
- // Replace all path separators and colons (for Windows drive letters) with dashes
170
- const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
171
- const configDir = getAgentDir();
172
- const sessionDir = join(configDir, "sessions", safePath);
173
- if (!existsSync(sessionDir)) {
174
- mkdirSync(sessionDir, { recursive: true });
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
- initNewSession() {
179
- this.sessionId = uuidv4();
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
- /** Reset to a fresh session. Clears pending entries and starts a new session file. */
184
- reset() {
185
- this.pendingEntries = [];
186
- this.inMemoryEntries = [];
187
- this.sessionInitialized = false;
188
- this.initNewSession();
181
+ getCwd() {
182
+ return this.cwd;
189
183
  }
190
- findMostRecentlyModifiedSession() {
191
- try {
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
- loadSessionId() {
207
- if (!existsSync(this.sessionFile))
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
- startSession(state) {
225
- if (this.sessionInitialized)
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.sessionInitialized = true;
228
- const entry = {
229
- type: "session",
230
- id: this.sessionId,
231
- timestamp: new Date().toISOString(),
232
- cwd: process.cwd(),
233
- provider: state.model.provider,
234
- modelId: state.model.id,
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
- this.pendingEntries = [];
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
- if (!this.sessionInitialized) {
258
- this.pendingEntries.push(entry);
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
- if (!this.sessionInitialized) {
276
- this.pendingEntries.push(entry);
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
- if (!this.sessionInitialized) {
295
- this.pendingEntries.push(entry);
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
- // Write to file only if enabled
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
- getSessionId() {
340
- return this.sessionId;
341
- }
342
- getSessionFile() {
343
- return this.sessionFile;
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
- * Load entries directly from the session file (internal helper).
347
- */
348
- loadEntriesFromFile() {
349
- if (!existsSync(this.sessionFile))
350
- return [];
351
- const content = readFileSync(this.sessionFile, "utf8");
352
- const entries = [];
353
- const lines = content.trim().split("\n");
354
- for (const line of lines) {
355
- if (!line.trim())
356
- continue;
357
- try {
358
- const entry = JSON.parse(line);
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
- catch {
362
- // Skip malformed lines
288
+ else {
289
+ newEntries.push(entry);
363
290
  }
364
291
  }
365
- return entries;
366
- }
367
- /**
368
- * Load all entries from the session file or in-memory store.
369
- * When file persistence is enabled, reads from file (source of truth for resumed sessions).
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
- // Otherwise return in-memory entries (for --no-session mode)
378
- return [...this.inMemoryEntries];
379
- }
380
- /**
381
- * Load all sessions for the current directory with metadata
382
- */
383
- loadAllSessions() {
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(this.sessionDir)
332
+ const files = readdirSync(sessionDir)
387
333
  .filter((f) => f.endsWith(".jsonl"))
388
- .map((f) => join(this.sessionDir, f));
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 (error) {
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 (error) {
449
- console.error("Failed to load sessions:", error);
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