pi2dsh 0.2.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.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/README.zh.md +122 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +128 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/compat/pi-ai.d.mts +2597 -0
- package/dist/compat/pi-ai.d.mts.map +1 -0
- package/dist/compat/pi-ai.mjs +4669 -0
- package/dist/compat/pi-ai.mjs.map +1 -0
- package/dist/compat/pi-coding-agent.d.mts +745 -0
- package/dist/compat/pi-coding-agent.d.mts.map +1 -0
- package/dist/compat/pi-coding-agent.mjs +4 -0
- package/dist/compat/pi-tui.d.mts +3 -0
- package/dist/compat/pi-tui.mjs +3622 -0
- package/dist/compat/pi-tui.mjs.map +1 -0
- package/dist/host.d.mts +35 -0
- package/dist/host.d.mts.map +1 -0
- package/dist/host.mjs +197 -0
- package/dist/host.mjs.map +1 -0
- package/dist/index.d.mts +69 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/mcp-config-jL9w70It.mjs +1535 -0
- package/dist/mcp-config-jL9w70It.mjs.map +1 -0
- package/dist/pi-coding-agent-Dsg6_0ua.mjs +2060 -0
- package/dist/pi-coding-agent-Dsg6_0ua.mjs.map +1 -0
- package/dist/pi-config-shim-CZ1wFzqM.mjs +27 -0
- package/dist/pi-config-shim-CZ1wFzqM.mjs.map +1 -0
- package/dist/pi-tui-iHoF2tFc.d.mts +1043 -0
- package/dist/pi-tui-iHoF2tFc.d.mts.map +1 -0
- package/dist/pi-tui-utils-CcaVtm-3.mjs +895 -0
- package/dist/pi-tui-utils-CcaVtm-3.mjs.map +1 -0
- package/dist/pi-types-KazmR2O5.d.mts +62 -0
- package/dist/pi-types-KazmR2O5.d.mts.map +1 -0
- package/dist/pi-uuid-Db8ShZsK.mjs +47 -0
- package/dist/pi-uuid-Db8ShZsK.mjs.map +1 -0
- package/dist/rolldown-runtime-C2Q2p085.mjs +15 -0
- package/dist/runtime-D84Hv_3m.mjs +1499 -0
- package/dist/runtime-D84Hv_3m.mjs.map +1 -0
- package/dist/runtime.d.mts +31 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +3 -0
- package/dist/source-D7Ir-rPT.mjs +154 -0
- package/dist/source-D7Ir-rPT.mjs.map +1 -0
- package/dist/types-7IWJPPvS.d.mts +59 -0
- package/dist/types-7IWJPPvS.d.mts.map +1 -0
- package/package.json +135 -0
|
@@ -0,0 +1,2060 @@
|
|
|
1
|
+
|
|
2
|
+
import { i as resolvePath, n as getSessionsDir, r as normalizePath, t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
|
|
3
|
+
import { t as uuidv7 } from "./pi-uuid-Db8ShZsK.mjs";
|
|
4
|
+
import { g as visibleWidth, h as truncateToWidth } from "./pi-tui-utils-CcaVtm-3.mjs";
|
|
5
|
+
import { realpath } from "node:fs/promises";
|
|
6
|
+
import { delimiter, join, resolve } from "node:path";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
import { appendFileSync as appendFileSync$1, closeSync, createReadStream, existsSync as existsSync$1, mkdirSync as mkdirSync$1, openSync, readSync, readdirSync, statSync, writeFileSync as writeFileSync$1 } from "fs";
|
|
11
|
+
import { readdir as readdir$1, stat as stat$1 } from "fs/promises";
|
|
12
|
+
import { join as join$1, resolve as resolve$1 } from "path";
|
|
13
|
+
import { createInterface } from "readline";
|
|
14
|
+
import { StringDecoder } from "string_decoder";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
//#region src/compat/vendor/pi-messages.ts
|
|
17
|
+
const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
|
18
|
+
|
|
19
|
+
<summary>
|
|
20
|
+
`;
|
|
21
|
+
const COMPACTION_SUMMARY_SUFFIX = `
|
|
22
|
+
</summary>`;
|
|
23
|
+
const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
|
|
24
|
+
|
|
25
|
+
<summary>
|
|
26
|
+
`;
|
|
27
|
+
const BRANCH_SUMMARY_SUFFIX = `</summary>`;
|
|
28
|
+
/**
|
|
29
|
+
* Convert a BashExecutionMessage to user message text for LLM context.
|
|
30
|
+
*/
|
|
31
|
+
function bashExecutionToText(msg) {
|
|
32
|
+
let text = `Ran \`${msg.command}\`\n`;
|
|
33
|
+
if (msg.output) text += `\`\`\`\n${msg.output}\n\`\`\``;
|
|
34
|
+
else text += "(no output)";
|
|
35
|
+
if (msg.cancelled) text += "\n\n(command cancelled)";
|
|
36
|
+
else if (msg.exitCode !== null && msg.exitCode !== void 0 && msg.exitCode !== 0) text += `\n\nCommand exited with code ${msg.exitCode}`;
|
|
37
|
+
if (msg.truncated && msg.fullOutputPath) text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
function createBranchSummaryMessage(summary, fromId, timestamp) {
|
|
41
|
+
return {
|
|
42
|
+
role: "branchSummary",
|
|
43
|
+
summary,
|
|
44
|
+
fromId,
|
|
45
|
+
timestamp: new Date(timestamp).getTime()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createCompactionSummaryMessage(summary, tokensBefore, timestamp) {
|
|
49
|
+
return {
|
|
50
|
+
role: "compactionSummary",
|
|
51
|
+
summary,
|
|
52
|
+
tokensBefore,
|
|
53
|
+
timestamp: new Date(timestamp).getTime()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Convert CustomMessageEntry to AgentMessage format */
|
|
57
|
+
function createCustomMessage(customType, content, display, details, timestamp) {
|
|
58
|
+
return {
|
|
59
|
+
role: "custom",
|
|
60
|
+
customType,
|
|
61
|
+
content,
|
|
62
|
+
display,
|
|
63
|
+
details,
|
|
64
|
+
timestamp: new Date(timestamp).getTime()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Transform AgentMessages (including custom types) to LLM-compatible Messages.
|
|
69
|
+
*
|
|
70
|
+
* This is used by:
|
|
71
|
+
* - Agent's transormToLlm option (for prompt calls and queued messages)
|
|
72
|
+
* - Compaction's generateSummary (for summarization)
|
|
73
|
+
* - Custom extensions and tools
|
|
74
|
+
*/
|
|
75
|
+
function convertToLlm(messages) {
|
|
76
|
+
return messages.map((m) => {
|
|
77
|
+
switch (m.role) {
|
|
78
|
+
case "bashExecution":
|
|
79
|
+
if (m.excludeFromContext) return;
|
|
80
|
+
return {
|
|
81
|
+
role: "user",
|
|
82
|
+
content: [{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: bashExecutionToText(m)
|
|
85
|
+
}],
|
|
86
|
+
timestamp: m.timestamp
|
|
87
|
+
};
|
|
88
|
+
case "custom": return {
|
|
89
|
+
role: "user",
|
|
90
|
+
content: typeof m.content === "string" ? [{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: m.content
|
|
93
|
+
}] : m.content,
|
|
94
|
+
timestamp: m.timestamp
|
|
95
|
+
};
|
|
96
|
+
case "branchSummary": return {
|
|
97
|
+
role: "user",
|
|
98
|
+
content: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX
|
|
101
|
+
}],
|
|
102
|
+
timestamp: m.timestamp
|
|
103
|
+
};
|
|
104
|
+
case "compactionSummary": return {
|
|
105
|
+
role: "user",
|
|
106
|
+
content: [{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX
|
|
109
|
+
}],
|
|
110
|
+
timestamp: m.timestamp
|
|
111
|
+
};
|
|
112
|
+
case "user":
|
|
113
|
+
case "assistant":
|
|
114
|
+
case "toolResult": return m;
|
|
115
|
+
default: return;
|
|
116
|
+
}
|
|
117
|
+
}).filter((m) => m !== void 0);
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/compat/vendor/pi-session-manager.ts
|
|
121
|
+
const CURRENT_SESSION_VERSION = 3;
|
|
122
|
+
function createSessionId() {
|
|
123
|
+
return uuidv7();
|
|
124
|
+
}
|
|
125
|
+
function assertValidSessionId(id) {
|
|
126
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) throw new Error("Session id must be non-empty, contain only alphanumeric characters, '-', '_', and '.', and start and end with an alphanumeric character");
|
|
127
|
+
}
|
|
128
|
+
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
|
129
|
+
function generateId(byId) {
|
|
130
|
+
for (let i = 0; i < 100; i++) {
|
|
131
|
+
const id = randomUUID().slice(0, 8);
|
|
132
|
+
if (!byId.has(id)) return id;
|
|
133
|
+
}
|
|
134
|
+
return randomUUID();
|
|
135
|
+
}
|
|
136
|
+
/** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */
|
|
137
|
+
function migrateV1ToV2(entries) {
|
|
138
|
+
const ids = /* @__PURE__ */ new Set();
|
|
139
|
+
let prevId = null;
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.type === "session") {
|
|
142
|
+
entry.version = 2;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
entry.id = generateId(ids);
|
|
146
|
+
entry.parentId = prevId;
|
|
147
|
+
prevId = entry.id;
|
|
148
|
+
if (entry.type === "compaction") {
|
|
149
|
+
const comp = entry;
|
|
150
|
+
if (typeof comp.firstKeptEntryIndex === "number") {
|
|
151
|
+
const targetEntry = entries[comp.firstKeptEntryIndex];
|
|
152
|
+
if (targetEntry && targetEntry.type !== "session") comp.firstKeptEntryId = targetEntry.id;
|
|
153
|
+
delete comp.firstKeptEntryIndex;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Migrate v2 → v3: rename hookMessage role to custom. Mutates in place. */
|
|
159
|
+
function migrateV2ToV3(entries) {
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (entry.type === "session") {
|
|
162
|
+
entry.version = 3;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (entry.type === "message") {
|
|
166
|
+
const msgEntry = entry;
|
|
167
|
+
if (msgEntry.message && msgEntry.message.role === "hookMessage") msgEntry.message.role = "custom";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Run all necessary migrations to bring entries to current version.
|
|
173
|
+
* Mutates entries in place. Returns true if any migration was applied.
|
|
174
|
+
*/
|
|
175
|
+
function migrateToCurrentVersion(entries) {
|
|
176
|
+
const version = entries.find((e) => e.type === "session")?.version ?? 1;
|
|
177
|
+
if (version >= 3) return false;
|
|
178
|
+
if (version < 2) migrateV1ToV2(entries);
|
|
179
|
+
if (version < 3) migrateV2ToV3(entries);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/** Exported for testing */
|
|
183
|
+
function migrateSessionEntries(entries) {
|
|
184
|
+
migrateToCurrentVersion(entries);
|
|
185
|
+
}
|
|
186
|
+
/** Exported for compaction.test.ts */
|
|
187
|
+
function parseSessionEntries(content) {
|
|
188
|
+
const entries = [];
|
|
189
|
+
const lines = content.trim().split("\n");
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (!line.trim()) continue;
|
|
192
|
+
try {
|
|
193
|
+
const entry = JSON.parse(line);
|
|
194
|
+
entries.push(entry);
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
return entries;
|
|
198
|
+
}
|
|
199
|
+
function getLatestCompactionEntry(entries) {
|
|
200
|
+
for (let i = entries.length - 1; i >= 0; i--) if (entries[i].type === "compaction") return entries[i];
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function buildEntryIndex(entries, byId) {
|
|
204
|
+
if (byId) return byId;
|
|
205
|
+
const index = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const entry of entries) index.set(entry.id, entry);
|
|
207
|
+
return index;
|
|
208
|
+
}
|
|
209
|
+
function buildSessionPath(entries, leafId, byId) {
|
|
210
|
+
const index = buildEntryIndex(entries, byId);
|
|
211
|
+
let leaf;
|
|
212
|
+
if (leafId === null) return [];
|
|
213
|
+
if (leafId) leaf = index.get(leafId);
|
|
214
|
+
leaf ??= entries[entries.length - 1];
|
|
215
|
+
if (!leaf) return [];
|
|
216
|
+
const path = [];
|
|
217
|
+
let current = leaf;
|
|
218
|
+
while (current) {
|
|
219
|
+
path.push(current);
|
|
220
|
+
current = current.parentId ? index.get(current.parentId) : void 0;
|
|
221
|
+
}
|
|
222
|
+
path.reverse();
|
|
223
|
+
return path;
|
|
224
|
+
}
|
|
225
|
+
function getSessionContextSettings(path) {
|
|
226
|
+
let thinkingLevel = "off";
|
|
227
|
+
let model = null;
|
|
228
|
+
for (const entry of path) if (entry.type === "thinking_level_change") thinkingLevel = entry.thinkingLevel;
|
|
229
|
+
else if (entry.type === "model_change") model = {
|
|
230
|
+
provider: entry.provider,
|
|
231
|
+
modelId: entry.modelId
|
|
232
|
+
};
|
|
233
|
+
else if (entry.type === "message" && entry.message.role === "assistant") model = {
|
|
234
|
+
provider: entry.message.provider,
|
|
235
|
+
modelId: entry.message.model
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
thinkingLevel,
|
|
239
|
+
model
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Project one selected session entry into LLM/runtime messages.
|
|
244
|
+
* Plain custom entries are display/state entries and do not participate in context.
|
|
245
|
+
*/
|
|
246
|
+
function sessionEntryToContextMessages(entry) {
|
|
247
|
+
if (entry.type === "message") {
|
|
248
|
+
const message = entry.message;
|
|
249
|
+
if ((message.role === "user" || message.role === "assistant" || message.role === "toolResult") && message.content == null) return [{
|
|
250
|
+
...message,
|
|
251
|
+
content: []
|
|
252
|
+
}];
|
|
253
|
+
return [message];
|
|
254
|
+
}
|
|
255
|
+
if (entry.type === "custom_message") return [createCustomMessage(entry.customType, entry.content ?? [], entry.display, entry.details, entry.timestamp)];
|
|
256
|
+
if (entry.type === "branch_summary" && entry.summary) return [createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)];
|
|
257
|
+
if (entry.type === "compaction") return [createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp)];
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Build the active, compaction-aware session entry list.
|
|
262
|
+
*
|
|
263
|
+
* This follows the current leaf path. If the path contains compaction entries,
|
|
264
|
+
* the latest compaction is represented by the compaction entry itself, followed
|
|
265
|
+
* by the kept entries starting at firstKeptEntryId and all entries after the
|
|
266
|
+
* compaction entry. Older summarized entries are omitted.
|
|
267
|
+
*/
|
|
268
|
+
function buildContextEntries(entries, leafId, byId) {
|
|
269
|
+
const path = buildSessionPath(entries, leafId, byId);
|
|
270
|
+
let compaction = null;
|
|
271
|
+
for (const entry of path) if (entry.type === "compaction") compaction = entry;
|
|
272
|
+
if (!compaction) return path;
|
|
273
|
+
const compactionIdx = path.findIndex((entry) => entry.id === compaction.id);
|
|
274
|
+
if (compactionIdx < 0) return path;
|
|
275
|
+
const contextEntries = [compaction];
|
|
276
|
+
let foundFirstKept = false;
|
|
277
|
+
for (let i = 0; i < compactionIdx; i++) {
|
|
278
|
+
const entry = path[i];
|
|
279
|
+
if (entry.id === compaction.firstKeptEntryId) foundFirstKept = true;
|
|
280
|
+
if (foundFirstKept) contextEntries.push(entry);
|
|
281
|
+
}
|
|
282
|
+
contextEntries.push(...path.slice(compactionIdx + 1));
|
|
283
|
+
return contextEntries;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Build the session context from entries using tree traversal.
|
|
287
|
+
* If leafId is provided, walks from that entry to root.
|
|
288
|
+
* Handles compaction and branch summaries along the path.
|
|
289
|
+
*/
|
|
290
|
+
function buildSessionContext(entries, leafId, byId) {
|
|
291
|
+
const { thinkingLevel, model } = getSessionContextSettings(buildSessionPath(entries, leafId, byId));
|
|
292
|
+
return {
|
|
293
|
+
messages: buildContextEntries(entries, leafId, byId).flatMap(sessionEntryToContextMessages),
|
|
294
|
+
thinkingLevel,
|
|
295
|
+
model
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Compute the default session directory for a cwd.
|
|
300
|
+
* Encodes cwd into a safe directory name under ~/.pi/agent/sessions/.
|
|
301
|
+
*/
|
|
302
|
+
function getDefaultSessionDirPath(cwd, agentDir = getAgentDir()) {
|
|
303
|
+
const resolvedCwd = resolvePath(cwd);
|
|
304
|
+
const resolvedAgentDir = resolvePath(agentDir);
|
|
305
|
+
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
306
|
+
return join$1(resolvedAgentDir, "sessions", safePath);
|
|
307
|
+
}
|
|
308
|
+
function getDefaultSessionDir(cwd, agentDir = getAgentDir()) {
|
|
309
|
+
const sessionDir = getDefaultSessionDirPath(cwd, agentDir);
|
|
310
|
+
if (!existsSync$1(sessionDir)) mkdirSync$1(sessionDir, { recursive: true });
|
|
311
|
+
return sessionDir;
|
|
312
|
+
}
|
|
313
|
+
const SESSION_READ_BUFFER_SIZE = 1048576;
|
|
314
|
+
const SESSION_HEADER_READ_BUFFER_SIZE = 4096;
|
|
315
|
+
/** Bound synchronous header discovery while allowing large cwd and custom metadata fields. */
|
|
316
|
+
const MAX_SESSION_HEADER_SCAN_BYTES = 1048576;
|
|
317
|
+
var SessionHeaderScanLimitError = class extends Error {
|
|
318
|
+
constructor(filePath) {
|
|
319
|
+
super(`Session header exceeds ${MAX_SESSION_HEADER_SCAN_BYTES}-byte scan limit: ${filePath}`);
|
|
320
|
+
this.name = "SessionHeaderScanLimitError";
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
function parseSessionEntryLine(line) {
|
|
324
|
+
if (!line.trim()) return null;
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(line);
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Exported for testing */
|
|
332
|
+
function loadEntriesFromFile(filePath) {
|
|
333
|
+
const resolvedFilePath = normalizePath(filePath);
|
|
334
|
+
if (!existsSync$1(resolvedFilePath)) return [];
|
|
335
|
+
const entries = [];
|
|
336
|
+
const fd = openSync(resolvedFilePath, "r");
|
|
337
|
+
try {
|
|
338
|
+
const decoder = new StringDecoder("utf8");
|
|
339
|
+
const buffer = Buffer.allocUnsafe(SESSION_READ_BUFFER_SIZE);
|
|
340
|
+
let pending = "";
|
|
341
|
+
while (true) {
|
|
342
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
|
|
343
|
+
if (bytesRead === 0) break;
|
|
344
|
+
pending += decoder.write(buffer.subarray(0, bytesRead));
|
|
345
|
+
let lineStart = 0;
|
|
346
|
+
let newlineIndex = pending.indexOf("\n", lineStart);
|
|
347
|
+
while (newlineIndex !== -1) {
|
|
348
|
+
const entry = parseSessionEntryLine(pending.slice(lineStart, newlineIndex));
|
|
349
|
+
if (entry) entries.push(entry);
|
|
350
|
+
lineStart = newlineIndex + 1;
|
|
351
|
+
newlineIndex = pending.indexOf("\n", lineStart);
|
|
352
|
+
}
|
|
353
|
+
pending = pending.slice(lineStart);
|
|
354
|
+
}
|
|
355
|
+
pending += decoder.end();
|
|
356
|
+
const finalEntry = parseSessionEntryLine(pending);
|
|
357
|
+
if (finalEntry) entries.push(finalEntry);
|
|
358
|
+
} finally {
|
|
359
|
+
closeSync(fd);
|
|
360
|
+
}
|
|
361
|
+
if (entries.length === 0) return entries;
|
|
362
|
+
const header = entries[0];
|
|
363
|
+
if (header.type !== "session" || typeof header.id !== "string") return [];
|
|
364
|
+
return entries;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Inspect a physical line while searching for the first parsed session entry.
|
|
368
|
+
* Blank and malformed lines are skipped to match loadEntriesFromFile().
|
|
369
|
+
* Returns undefined to keep scanning, null for a parsed non-header entry, or the header.
|
|
370
|
+
*/
|
|
371
|
+
function parseSessionHeaderCandidate(line) {
|
|
372
|
+
if (!line.trim()) return void 0;
|
|
373
|
+
const entry = parseSessionEntryLine(line);
|
|
374
|
+
if (!entry) return void 0;
|
|
375
|
+
if (entry.type !== "session" || typeof entry.id !== "string") return null;
|
|
376
|
+
return entry;
|
|
377
|
+
}
|
|
378
|
+
function readSessionHeader(filePath) {
|
|
379
|
+
const fd = openSync(filePath, "r");
|
|
380
|
+
try {
|
|
381
|
+
const decoder = new StringDecoder("utf8");
|
|
382
|
+
const buffer = Buffer.allocUnsafe(SESSION_HEADER_READ_BUFFER_SIZE);
|
|
383
|
+
const lineChunks = [];
|
|
384
|
+
let scannedBytes = 0;
|
|
385
|
+
while (scannedBytes < MAX_SESSION_HEADER_SCAN_BYTES) {
|
|
386
|
+
const readLength = Math.min(buffer.length, MAX_SESSION_HEADER_SCAN_BYTES - scannedBytes);
|
|
387
|
+
const bytesRead = readSync(fd, buffer, 0, readLength, null);
|
|
388
|
+
if (bytesRead === 0) {
|
|
389
|
+
lineChunks.push(decoder.end());
|
|
390
|
+
return parseSessionHeaderCandidate(lineChunks.join("")) ?? null;
|
|
391
|
+
}
|
|
392
|
+
scannedBytes += bytesRead;
|
|
393
|
+
const chunk = decoder.write(buffer.subarray(0, bytesRead));
|
|
394
|
+
let lineStart = 0;
|
|
395
|
+
let newlineIndex = chunk.indexOf("\n", lineStart);
|
|
396
|
+
while (newlineIndex !== -1) {
|
|
397
|
+
lineChunks.push(chunk.slice(lineStart, newlineIndex));
|
|
398
|
+
const header = parseSessionHeaderCandidate(lineChunks.join(""));
|
|
399
|
+
if (header !== void 0) return header;
|
|
400
|
+
lineChunks.length = 0;
|
|
401
|
+
lineStart = newlineIndex + 1;
|
|
402
|
+
newlineIndex = chunk.indexOf("\n", lineStart);
|
|
403
|
+
}
|
|
404
|
+
lineChunks.push(chunk.slice(lineStart));
|
|
405
|
+
}
|
|
406
|
+
const probe = Buffer.allocUnsafe(1);
|
|
407
|
+
if (readSync(fd, probe, 0, probe.length, null) === 0) {
|
|
408
|
+
lineChunks.push(decoder.end());
|
|
409
|
+
return parseSessionHeaderCandidate(lineChunks.join("")) ?? null;
|
|
410
|
+
}
|
|
411
|
+
throw new SessionHeaderScanLimitError(filePath);
|
|
412
|
+
} finally {
|
|
413
|
+
closeSync(fd);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function readSessionHeaderForDiscovery(filePath) {
|
|
417
|
+
try {
|
|
418
|
+
return readSessionHeader(filePath);
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function getSessionHeaderCwd(header) {
|
|
424
|
+
const cwd = header.cwd;
|
|
425
|
+
return typeof cwd === "string" ? cwd : void 0;
|
|
426
|
+
}
|
|
427
|
+
function sessionCwdMatches(cwd, resolvedCwd) {
|
|
428
|
+
return cwd !== void 0 && cwd !== "" && resolvePath(cwd) === resolvedCwd;
|
|
429
|
+
}
|
|
430
|
+
/** Exported for testing */
|
|
431
|
+
function findMostRecentSession(sessionDir, cwd) {
|
|
432
|
+
const resolvedSessionDir = normalizePath(sessionDir);
|
|
433
|
+
const resolvedCwd = cwd ? resolvePath(cwd) : void 0;
|
|
434
|
+
try {
|
|
435
|
+
return readdirSync(resolvedSessionDir).filter((f) => f.endsWith(".jsonl")).map((f) => join$1(resolvedSessionDir, f)).map((path) => ({
|
|
436
|
+
path,
|
|
437
|
+
header: readSessionHeaderForDiscovery(path)
|
|
438
|
+
})).filter((file) => file.header !== null && (!resolvedCwd || sessionCwdMatches(getSessionHeaderCwd(file.header), resolvedCwd))).map(({ path }) => ({
|
|
439
|
+
path,
|
|
440
|
+
mtime: statSync(path).mtime
|
|
441
|
+
})).sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0]?.path || null;
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function isMessageWithContent(message) {
|
|
447
|
+
return typeof message.role === "string" && "content" in message;
|
|
448
|
+
}
|
|
449
|
+
function extractTextContent(message) {
|
|
450
|
+
const content = message.content;
|
|
451
|
+
if (typeof content === "string") return content;
|
|
452
|
+
return content.filter((block) => block.type === "text").map((block) => block.text).join(" ");
|
|
453
|
+
}
|
|
454
|
+
function getMessageActivityTime(entry) {
|
|
455
|
+
const message = entry.message;
|
|
456
|
+
if (!isMessageWithContent(message)) return void 0;
|
|
457
|
+
if (message.role !== "user" && message.role !== "assistant") return void 0;
|
|
458
|
+
const msgTimestamp = message.timestamp;
|
|
459
|
+
if (typeof msgTimestamp === "number") return msgTimestamp;
|
|
460
|
+
const t = new Date(entry.timestamp).getTime();
|
|
461
|
+
return Number.isNaN(t) ? void 0 : t;
|
|
462
|
+
}
|
|
463
|
+
async function buildSessionInfo(filePath) {
|
|
464
|
+
try {
|
|
465
|
+
const stats = await stat$1(filePath);
|
|
466
|
+
let header = null;
|
|
467
|
+
let messageCount = 0;
|
|
468
|
+
let firstMessage = "";
|
|
469
|
+
const allMessages = [];
|
|
470
|
+
let name;
|
|
471
|
+
let lastActivityTime;
|
|
472
|
+
const rl = createInterface({
|
|
473
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
474
|
+
crlfDelay: Infinity
|
|
475
|
+
});
|
|
476
|
+
for await (const line of rl) {
|
|
477
|
+
const entry = parseSessionEntryLine(line);
|
|
478
|
+
if (!entry) continue;
|
|
479
|
+
if (!header) {
|
|
480
|
+
if (entry.type !== "session") return null;
|
|
481
|
+
header = entry;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (entry.type === "session_info") name = entry.name?.trim() || void 0;
|
|
485
|
+
if (entry.type !== "message") continue;
|
|
486
|
+
messageCount++;
|
|
487
|
+
const activityTime = getMessageActivityTime(entry);
|
|
488
|
+
if (typeof activityTime === "number") lastActivityTime = Math.max(lastActivityTime ?? 0, activityTime);
|
|
489
|
+
const message = entry.message;
|
|
490
|
+
if (!isMessageWithContent(message)) continue;
|
|
491
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
492
|
+
const textContent = extractTextContent(message);
|
|
493
|
+
if (!textContent) continue;
|
|
494
|
+
allMessages.push(textContent);
|
|
495
|
+
if (!firstMessage && message.role === "user") firstMessage = textContent;
|
|
496
|
+
}
|
|
497
|
+
if (!header) return null;
|
|
498
|
+
const cwd = typeof header.cwd === "string" ? header.cwd : "";
|
|
499
|
+
const parentSessionPath = header.parentSession;
|
|
500
|
+
const headerTime = typeof header.timestamp === "string" ? new Date(header.timestamp).getTime() : NaN;
|
|
501
|
+
const modified = typeof lastActivityTime === "number" && lastActivityTime > 0 ? new Date(lastActivityTime) : !Number.isNaN(headerTime) ? new Date(headerTime) : stats.mtime;
|
|
502
|
+
return {
|
|
503
|
+
path: filePath,
|
|
504
|
+
id: header.id,
|
|
505
|
+
cwd,
|
|
506
|
+
name,
|
|
507
|
+
parentSessionPath,
|
|
508
|
+
created: new Date(header.timestamp),
|
|
509
|
+
modified,
|
|
510
|
+
messageCount,
|
|
511
|
+
firstMessage: firstMessage || "(no messages)",
|
|
512
|
+
allMessagesText: allMessages.join(" ")
|
|
513
|
+
};
|
|
514
|
+
} catch {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const MAX_CONCURRENT_SESSION_INFO_LOADS = 10;
|
|
519
|
+
async function buildSessionInfosWithConcurrency(files, onLoaded) {
|
|
520
|
+
const results = new Array(files.length).fill(null);
|
|
521
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
522
|
+
let nextIndex = 0;
|
|
523
|
+
const startNext = () => {
|
|
524
|
+
const index = nextIndex++;
|
|
525
|
+
const file = files[index];
|
|
526
|
+
if (!file) return;
|
|
527
|
+
let task;
|
|
528
|
+
task = buildSessionInfo(file).then((info) => {
|
|
529
|
+
results[index] = info;
|
|
530
|
+
}).catch(() => {
|
|
531
|
+
results[index] = null;
|
|
532
|
+
}).finally(() => {
|
|
533
|
+
inFlight.delete(task);
|
|
534
|
+
onLoaded();
|
|
535
|
+
});
|
|
536
|
+
inFlight.add(task);
|
|
537
|
+
};
|
|
538
|
+
while (nextIndex < files.length || inFlight.size > 0) {
|
|
539
|
+
while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) startNext();
|
|
540
|
+
if (inFlight.size > 0) await Promise.race(inFlight);
|
|
541
|
+
}
|
|
542
|
+
return results;
|
|
543
|
+
}
|
|
544
|
+
async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progressTotal) {
|
|
545
|
+
const sessions = [];
|
|
546
|
+
if (!existsSync$1(dir)) return sessions;
|
|
547
|
+
try {
|
|
548
|
+
const files = (await readdir$1(dir)).filter((f) => f.endsWith(".jsonl")).map((f) => join$1(dir, f));
|
|
549
|
+
const total = progressTotal ?? files.length;
|
|
550
|
+
let loaded = 0;
|
|
551
|
+
const results = await buildSessionInfosWithConcurrency(files, () => {
|
|
552
|
+
loaded++;
|
|
553
|
+
onProgress?.(progressOffset + loaded, total);
|
|
554
|
+
});
|
|
555
|
+
for (const info of results) if (info) sessions.push(info);
|
|
556
|
+
} catch {}
|
|
557
|
+
return sessions;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Manages conversation sessions as append-only trees stored in JSONL files.
|
|
561
|
+
*
|
|
562
|
+
* Each session entry has an id and parentId forming a tree structure. The "leaf"
|
|
563
|
+
* pointer tracks the current position. Appending creates a child of the current leaf.
|
|
564
|
+
* Branching moves the leaf to an earlier entry, allowing new branches without
|
|
565
|
+
* modifying history.
|
|
566
|
+
*
|
|
567
|
+
* Use buildSessionContext() to get the resolved message list for the LLM, which
|
|
568
|
+
* handles compaction summaries and follows the path from root to current leaf.
|
|
569
|
+
*/
|
|
570
|
+
var SessionManager = class SessionManager {
|
|
571
|
+
sessionId = "";
|
|
572
|
+
sessionFile;
|
|
573
|
+
sessionDir;
|
|
574
|
+
cwd;
|
|
575
|
+
persist;
|
|
576
|
+
flushed = false;
|
|
577
|
+
fileEntries = [];
|
|
578
|
+
byId = /* @__PURE__ */ new Map();
|
|
579
|
+
labelsById = /* @__PURE__ */ new Map();
|
|
580
|
+
labelTimestampsById = /* @__PURE__ */ new Map();
|
|
581
|
+
leafId = null;
|
|
582
|
+
constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions, preloadedFileEntries) {
|
|
583
|
+
this.cwd = resolvePath(cwd);
|
|
584
|
+
this.sessionDir = normalizePath(sessionDir);
|
|
585
|
+
this.persist = persist;
|
|
586
|
+
if (persist && this.sessionDir && !existsSync$1(this.sessionDir)) mkdirSync$1(this.sessionDir, { recursive: true });
|
|
587
|
+
if (sessionFile) this._setSessionFile(sessionFile, preloadedFileEntries);
|
|
588
|
+
else this.newSession(newSessionOptions);
|
|
589
|
+
}
|
|
590
|
+
/** Switch to a different session file (used for resume and branching) */
|
|
591
|
+
setSessionFile(sessionFile) {
|
|
592
|
+
this._setSessionFile(sessionFile);
|
|
593
|
+
}
|
|
594
|
+
_setSessionFile(sessionFile, preloadedFileEntries) {
|
|
595
|
+
this.sessionFile = resolvePath(sessionFile);
|
|
596
|
+
if (existsSync$1(this.sessionFile)) {
|
|
597
|
+
this.fileEntries = preloadedFileEntries ?? loadEntriesFromFile(this.sessionFile);
|
|
598
|
+
if (this.fileEntries.length === 0) {
|
|
599
|
+
const explicitPath = this.sessionFile;
|
|
600
|
+
if (statSync(explicitPath).size > 0) throw new Error(`Session file is not a valid pi session: ${explicitPath}`);
|
|
601
|
+
this.newSession();
|
|
602
|
+
this.sessionFile = explicitPath;
|
|
603
|
+
this._rewriteFile();
|
|
604
|
+
this.flushed = true;
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const header = this.fileEntries.find((e) => e.type === "session");
|
|
608
|
+
this.sessionId = header?.id ?? createSessionId();
|
|
609
|
+
if (migrateToCurrentVersion(this.fileEntries)) this._rewriteFile();
|
|
610
|
+
this._buildIndex();
|
|
611
|
+
this.flushed = true;
|
|
612
|
+
} else {
|
|
613
|
+
const explicitPath = this.sessionFile;
|
|
614
|
+
this.newSession();
|
|
615
|
+
this.sessionFile = explicitPath;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
newSession(options) {
|
|
619
|
+
if (options?.id !== void 0) assertValidSessionId(options.id);
|
|
620
|
+
this.sessionId = options?.id ?? createSessionId();
|
|
621
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
622
|
+
const header = {
|
|
623
|
+
type: "session",
|
|
624
|
+
version: 3,
|
|
625
|
+
id: this.sessionId,
|
|
626
|
+
timestamp,
|
|
627
|
+
cwd: this.cwd,
|
|
628
|
+
parentSession: options?.parentSession
|
|
629
|
+
};
|
|
630
|
+
this.fileEntries = [header];
|
|
631
|
+
this.byId.clear();
|
|
632
|
+
this.labelsById.clear();
|
|
633
|
+
this.labelTimestampsById.clear();
|
|
634
|
+
this.leafId = null;
|
|
635
|
+
this.flushed = false;
|
|
636
|
+
if (this.persist) {
|
|
637
|
+
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
638
|
+
this.sessionFile = join$1(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
639
|
+
}
|
|
640
|
+
return this.sessionFile;
|
|
641
|
+
}
|
|
642
|
+
_buildIndex() {
|
|
643
|
+
this.byId.clear();
|
|
644
|
+
this.labelsById.clear();
|
|
645
|
+
this.labelTimestampsById.clear();
|
|
646
|
+
this.leafId = null;
|
|
647
|
+
for (const entry of this.fileEntries) {
|
|
648
|
+
if (entry.type === "session") continue;
|
|
649
|
+
this.byId.set(entry.id, entry);
|
|
650
|
+
this.leafId = entry.id;
|
|
651
|
+
if (entry.type === "label") {
|
|
652
|
+
if (entry.label) {
|
|
653
|
+
this.labelsById.set(entry.targetId, entry.label);
|
|
654
|
+
this.labelTimestampsById.set(entry.targetId, entry.timestamp);
|
|
655
|
+
} else {
|
|
656
|
+
this.labelsById.delete(entry.targetId);
|
|
657
|
+
this.labelTimestampsById.delete(entry.targetId);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
_rewriteFile() {
|
|
663
|
+
if (!this.persist || !this.sessionFile) return;
|
|
664
|
+
const fd = openSync(this.sessionFile, "w");
|
|
665
|
+
try {
|
|
666
|
+
for (const entry of this.fileEntries) writeFileSync$1(fd, `${JSON.stringify(entry)}\n`);
|
|
667
|
+
} finally {
|
|
668
|
+
closeSync(fd);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
isPersisted() {
|
|
672
|
+
return this.persist;
|
|
673
|
+
}
|
|
674
|
+
getCwd() {
|
|
675
|
+
return this.cwd;
|
|
676
|
+
}
|
|
677
|
+
getSessionDir() {
|
|
678
|
+
return this.sessionDir;
|
|
679
|
+
}
|
|
680
|
+
usesDefaultSessionDir() {
|
|
681
|
+
return this.sessionDir === getDefaultSessionDirPath(this.cwd);
|
|
682
|
+
}
|
|
683
|
+
getSessionId() {
|
|
684
|
+
return this.sessionId;
|
|
685
|
+
}
|
|
686
|
+
getSessionFile() {
|
|
687
|
+
return this.sessionFile;
|
|
688
|
+
}
|
|
689
|
+
_persist(entry) {
|
|
690
|
+
if (!this.persist || !this.sessionFile) return;
|
|
691
|
+
if (!this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant")) {
|
|
692
|
+
if (this.flushed) appendFileSync$1(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
693
|
+
else this.flushed = false;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (!this.flushed) {
|
|
697
|
+
const fd = openSync(this.sessionFile, "wx");
|
|
698
|
+
try {
|
|
699
|
+
for (const e of this.fileEntries) writeFileSync$1(fd, `${JSON.stringify(e)}\n`);
|
|
700
|
+
} finally {
|
|
701
|
+
closeSync(fd);
|
|
702
|
+
}
|
|
703
|
+
this.flushed = true;
|
|
704
|
+
} else appendFileSync$1(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
|
705
|
+
}
|
|
706
|
+
_appendEntry(entry) {
|
|
707
|
+
this.fileEntries.push(entry);
|
|
708
|
+
this.byId.set(entry.id, entry);
|
|
709
|
+
this.leafId = entry.id;
|
|
710
|
+
this._persist(entry);
|
|
711
|
+
}
|
|
712
|
+
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
713
|
+
* Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
|
|
714
|
+
* Reason: we want these to be top-level entries in the session, not message session entries,
|
|
715
|
+
* so it is easier to find them.
|
|
716
|
+
* These need to be appended via appendCompaction() and appendBranchSummary() methods.
|
|
717
|
+
*/
|
|
718
|
+
appendMessage(message) {
|
|
719
|
+
const entry = {
|
|
720
|
+
type: "message",
|
|
721
|
+
id: generateId(this.byId),
|
|
722
|
+
parentId: this.leafId,
|
|
723
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
724
|
+
message
|
|
725
|
+
};
|
|
726
|
+
this._appendEntry(entry);
|
|
727
|
+
return entry.id;
|
|
728
|
+
}
|
|
729
|
+
/** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
|
|
730
|
+
appendThinkingLevelChange(thinkingLevel) {
|
|
731
|
+
const entry = {
|
|
732
|
+
type: "thinking_level_change",
|
|
733
|
+
id: generateId(this.byId),
|
|
734
|
+
parentId: this.leafId,
|
|
735
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
736
|
+
thinkingLevel
|
|
737
|
+
};
|
|
738
|
+
this._appendEntry(entry);
|
|
739
|
+
return entry.id;
|
|
740
|
+
}
|
|
741
|
+
/** Append a model change as child of current leaf, then advance leaf. Returns entry id. */
|
|
742
|
+
appendModelChange(provider, modelId) {
|
|
743
|
+
const entry = {
|
|
744
|
+
type: "model_change",
|
|
745
|
+
id: generateId(this.byId),
|
|
746
|
+
parentId: this.leafId,
|
|
747
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
748
|
+
provider,
|
|
749
|
+
modelId
|
|
750
|
+
};
|
|
751
|
+
this._appendEntry(entry);
|
|
752
|
+
return entry.id;
|
|
753
|
+
}
|
|
754
|
+
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
755
|
+
appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook, usage) {
|
|
756
|
+
const entry = {
|
|
757
|
+
type: "compaction",
|
|
758
|
+
id: generateId(this.byId),
|
|
759
|
+
parentId: this.leafId,
|
|
760
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
761
|
+
summary,
|
|
762
|
+
firstKeptEntryId,
|
|
763
|
+
tokensBefore,
|
|
764
|
+
details,
|
|
765
|
+
usage,
|
|
766
|
+
fromHook
|
|
767
|
+
};
|
|
768
|
+
this._appendEntry(entry);
|
|
769
|
+
return entry.id;
|
|
770
|
+
}
|
|
771
|
+
/** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
|
|
772
|
+
appendCustomEntry(customType, data) {
|
|
773
|
+
const entry = {
|
|
774
|
+
type: "custom",
|
|
775
|
+
customType,
|
|
776
|
+
data,
|
|
777
|
+
id: generateId(this.byId),
|
|
778
|
+
parentId: this.leafId,
|
|
779
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
780
|
+
};
|
|
781
|
+
this._appendEntry(entry);
|
|
782
|
+
return entry.id;
|
|
783
|
+
}
|
|
784
|
+
/** Append a session info entry (e.g., display name). Returns entry id. */
|
|
785
|
+
appendSessionInfo(name) {
|
|
786
|
+
const sanitizedName = name.replace(/[\r\n]+/g, " ").trim();
|
|
787
|
+
const entry = {
|
|
788
|
+
type: "session_info",
|
|
789
|
+
id: generateId(this.byId),
|
|
790
|
+
parentId: this.leafId,
|
|
791
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
792
|
+
name: sanitizedName
|
|
793
|
+
};
|
|
794
|
+
this._appendEntry(entry);
|
|
795
|
+
return entry.id;
|
|
796
|
+
}
|
|
797
|
+
/** Get the current session name from the latest session_info entry, if any. */
|
|
798
|
+
getSessionName() {
|
|
799
|
+
const entries = this.getEntries();
|
|
800
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
801
|
+
const entry = entries[i];
|
|
802
|
+
if (entry.type === "session_info") return entry.name?.trim() || void 0;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Append a custom message entry (for extensions) that participates in LLM context.
|
|
807
|
+
* @param customType Extension identifier for filtering on reload
|
|
808
|
+
* @param content Message content (string or TextContent/ImageContent array)
|
|
809
|
+
* @param display Whether to show in TUI (true = styled display, false = hidden)
|
|
810
|
+
* @param details Optional extension-specific metadata (not sent to LLM)
|
|
811
|
+
* @returns Entry id
|
|
812
|
+
*/
|
|
813
|
+
appendCustomMessageEntry(customType, content, display, details) {
|
|
814
|
+
const entry = {
|
|
815
|
+
type: "custom_message",
|
|
816
|
+
customType,
|
|
817
|
+
content,
|
|
818
|
+
display,
|
|
819
|
+
details,
|
|
820
|
+
id: generateId(this.byId),
|
|
821
|
+
parentId: this.leafId,
|
|
822
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
823
|
+
};
|
|
824
|
+
this._appendEntry(entry);
|
|
825
|
+
return entry.id;
|
|
826
|
+
}
|
|
827
|
+
getLeafId() {
|
|
828
|
+
return this.leafId;
|
|
829
|
+
}
|
|
830
|
+
getLeafEntry() {
|
|
831
|
+
return this.leafId ? this.byId.get(this.leafId) : void 0;
|
|
832
|
+
}
|
|
833
|
+
getEntry(id) {
|
|
834
|
+
return this.byId.get(id);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Get all direct children of an entry.
|
|
838
|
+
*/
|
|
839
|
+
getChildren(parentId) {
|
|
840
|
+
const children = [];
|
|
841
|
+
for (const entry of this.byId.values()) if (entry.parentId === parentId) children.push(entry);
|
|
842
|
+
return children;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Get the label for an entry, if any.
|
|
846
|
+
*/
|
|
847
|
+
getLabel(id) {
|
|
848
|
+
return this.labelsById.get(id);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Set or clear a label on an entry.
|
|
852
|
+
* Labels are user-defined markers for bookmarking/navigation.
|
|
853
|
+
* Pass undefined or empty string to clear the label.
|
|
854
|
+
*/
|
|
855
|
+
appendLabelChange(targetId, label) {
|
|
856
|
+
if (!this.byId.has(targetId)) throw new Error(`Entry ${targetId} not found`);
|
|
857
|
+
const entry = {
|
|
858
|
+
type: "label",
|
|
859
|
+
id: generateId(this.byId),
|
|
860
|
+
parentId: this.leafId,
|
|
861
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
862
|
+
targetId,
|
|
863
|
+
label
|
|
864
|
+
};
|
|
865
|
+
this._appendEntry(entry);
|
|
866
|
+
if (label) {
|
|
867
|
+
this.labelsById.set(targetId, label);
|
|
868
|
+
this.labelTimestampsById.set(targetId, entry.timestamp);
|
|
869
|
+
} else {
|
|
870
|
+
this.labelsById.delete(targetId);
|
|
871
|
+
this.labelTimestampsById.delete(targetId);
|
|
872
|
+
}
|
|
873
|
+
return entry.id;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Walk from entry to root, returning all entries in path order.
|
|
877
|
+
* Includes all entry types (messages, compaction, model changes, etc.).
|
|
878
|
+
* Use buildSessionContext() to get the resolved messages for the LLM.
|
|
879
|
+
*/
|
|
880
|
+
getBranch(fromId) {
|
|
881
|
+
const path = [];
|
|
882
|
+
const startId = fromId ?? this.leafId;
|
|
883
|
+
let current = startId ? this.byId.get(startId) : void 0;
|
|
884
|
+
while (current) {
|
|
885
|
+
path.push(current);
|
|
886
|
+
current = current.parentId ? this.byId.get(current.parentId) : void 0;
|
|
887
|
+
}
|
|
888
|
+
path.reverse();
|
|
889
|
+
return path;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Build the active, compaction-aware entry list for context/rendering.
|
|
893
|
+
* Uses tree traversal from current leaf.
|
|
894
|
+
*/
|
|
895
|
+
buildContextEntries() {
|
|
896
|
+
return buildContextEntries(this.getEntries(), this.leafId, this.byId);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Build the session context (what gets sent to the LLM).
|
|
900
|
+
* Uses tree traversal from current leaf.
|
|
901
|
+
*/
|
|
902
|
+
buildSessionContext() {
|
|
903
|
+
return buildSessionContext(this.getEntries(), this.leafId, this.byId);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Get session header.
|
|
907
|
+
*/
|
|
908
|
+
getHeader() {
|
|
909
|
+
const h = this.fileEntries.find((e) => e.type === "session");
|
|
910
|
+
return h ? h : null;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get all session entries (excludes header). Returns a shallow copy.
|
|
914
|
+
* The session is append-only: use appendXXX() to add entries, branch() to
|
|
915
|
+
* change the leaf pointer. Entries cannot be modified or deleted.
|
|
916
|
+
*/
|
|
917
|
+
getEntries() {
|
|
918
|
+
return this.fileEntries.filter((e) => e.type !== "session");
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get the session as a tree structure. Returns a shallow defensive copy of all entries.
|
|
922
|
+
* A well-formed session has exactly one root (first entry with parentId === null).
|
|
923
|
+
* Orphaned entries (broken parent chain) are also returned as roots.
|
|
924
|
+
*/
|
|
925
|
+
getTree() {
|
|
926
|
+
const entries = this.getEntries();
|
|
927
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
928
|
+
const roots = [];
|
|
929
|
+
for (const entry of entries) {
|
|
930
|
+
const label = this.labelsById.get(entry.id);
|
|
931
|
+
const labelTimestamp = this.labelTimestampsById.get(entry.id);
|
|
932
|
+
nodeMap.set(entry.id, {
|
|
933
|
+
entry,
|
|
934
|
+
children: [],
|
|
935
|
+
label,
|
|
936
|
+
labelTimestamp
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
const node = nodeMap.get(entry.id);
|
|
941
|
+
if (entry.parentId === null || entry.parentId === entry.id) roots.push(node);
|
|
942
|
+
else {
|
|
943
|
+
const parent = nodeMap.get(entry.parentId);
|
|
944
|
+
if (parent) parent.children.push(node);
|
|
945
|
+
else roots.push(node);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const stack = [...roots];
|
|
949
|
+
while (stack.length > 0) {
|
|
950
|
+
const node = stack.pop();
|
|
951
|
+
node.children.sort((a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime());
|
|
952
|
+
stack.push(...node.children);
|
|
953
|
+
}
|
|
954
|
+
return roots;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Start a new branch from an earlier entry.
|
|
958
|
+
* Moves the leaf pointer to the specified entry. The next appendXXX() call
|
|
959
|
+
* will create a child of that entry, forming a new branch. Existing entries
|
|
960
|
+
* are not modified or deleted.
|
|
961
|
+
*/
|
|
962
|
+
branch(branchFromId) {
|
|
963
|
+
if (!this.byId.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
964
|
+
this.leafId = branchFromId;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Reset the leaf pointer to null (before any entries).
|
|
968
|
+
* The next appendXXX() call will create a new root entry (parentId = null).
|
|
969
|
+
* Use this when navigating to re-edit the first user message.
|
|
970
|
+
*/
|
|
971
|
+
resetLeaf() {
|
|
972
|
+
this.leafId = null;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Start a new branch with a summary of the abandoned path.
|
|
976
|
+
* Same as branch(), but also appends a branch_summary entry that captures
|
|
977
|
+
* context from the abandoned conversation path.
|
|
978
|
+
*/
|
|
979
|
+
branchWithSummary(branchFromId, summary, details, fromHook, usage) {
|
|
980
|
+
if (branchFromId !== null && !this.byId.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
981
|
+
this.leafId = branchFromId;
|
|
982
|
+
const entry = {
|
|
983
|
+
type: "branch_summary",
|
|
984
|
+
id: generateId(this.byId),
|
|
985
|
+
parentId: branchFromId,
|
|
986
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
987
|
+
fromId: branchFromId ?? "root",
|
|
988
|
+
summary,
|
|
989
|
+
details,
|
|
990
|
+
usage,
|
|
991
|
+
fromHook
|
|
992
|
+
};
|
|
993
|
+
this._appendEntry(entry);
|
|
994
|
+
return entry.id;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Create a new session file containing only the path from root to the specified leaf.
|
|
998
|
+
* Useful for extracting a single conversation path from a branched session.
|
|
999
|
+
* Returns the new session file path, or undefined if not persisting.
|
|
1000
|
+
*/
|
|
1001
|
+
createBranchedSession(leafId) {
|
|
1002
|
+
const previousSessionFile = this.sessionFile;
|
|
1003
|
+
const path = this.getBranch(leafId);
|
|
1004
|
+
if (path.length === 0) throw new Error(`Entry ${leafId} not found`);
|
|
1005
|
+
const pathWithoutLabels = [];
|
|
1006
|
+
let pathParentId = null;
|
|
1007
|
+
for (const entry of path) {
|
|
1008
|
+
if (entry.type === "label") continue;
|
|
1009
|
+
pathWithoutLabels.push({
|
|
1010
|
+
...entry,
|
|
1011
|
+
parentId: pathParentId
|
|
1012
|
+
});
|
|
1013
|
+
pathParentId = entry.id;
|
|
1014
|
+
}
|
|
1015
|
+
const newSessionId = createSessionId();
|
|
1016
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1017
|
+
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1018
|
+
const newSessionFile = join$1(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
1019
|
+
const header = {
|
|
1020
|
+
type: "session",
|
|
1021
|
+
version: 3,
|
|
1022
|
+
id: newSessionId,
|
|
1023
|
+
timestamp,
|
|
1024
|
+
cwd: this.cwd,
|
|
1025
|
+
parentSession: this.persist ? previousSessionFile : void 0
|
|
1026
|
+
};
|
|
1027
|
+
const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id));
|
|
1028
|
+
const labelsToWrite = [];
|
|
1029
|
+
for (const [targetId, label] of this.labelsById) if (pathEntryIds.has(targetId)) labelsToWrite.push({
|
|
1030
|
+
targetId,
|
|
1031
|
+
label,
|
|
1032
|
+
timestamp: this.labelTimestampsById.get(targetId)
|
|
1033
|
+
});
|
|
1034
|
+
if (this.persist) {
|
|
1035
|
+
let parentId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
|
|
1036
|
+
const labelEntries = [];
|
|
1037
|
+
for (const { targetId, label, timestamp: labelTimestamp } of labelsToWrite) {
|
|
1038
|
+
const labelEntry = {
|
|
1039
|
+
type: "label",
|
|
1040
|
+
id: generateId(new Set(pathEntryIds)),
|
|
1041
|
+
parentId,
|
|
1042
|
+
timestamp: labelTimestamp,
|
|
1043
|
+
targetId,
|
|
1044
|
+
label
|
|
1045
|
+
};
|
|
1046
|
+
pathEntryIds.add(labelEntry.id);
|
|
1047
|
+
labelEntries.push(labelEntry);
|
|
1048
|
+
parentId = labelEntry.id;
|
|
1049
|
+
}
|
|
1050
|
+
this.fileEntries = [
|
|
1051
|
+
header,
|
|
1052
|
+
...pathWithoutLabels,
|
|
1053
|
+
...labelEntries
|
|
1054
|
+
];
|
|
1055
|
+
this.sessionId = newSessionId;
|
|
1056
|
+
this.sessionFile = newSessionFile;
|
|
1057
|
+
this._buildIndex();
|
|
1058
|
+
if (this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant")) {
|
|
1059
|
+
this._rewriteFile();
|
|
1060
|
+
this.flushed = true;
|
|
1061
|
+
} else this.flushed = false;
|
|
1062
|
+
return newSessionFile;
|
|
1063
|
+
}
|
|
1064
|
+
const labelEntries = [];
|
|
1065
|
+
let parentId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
|
|
1066
|
+
for (const { targetId, label, timestamp: labelTimestamp } of labelsToWrite) {
|
|
1067
|
+
const labelEntry = {
|
|
1068
|
+
type: "label",
|
|
1069
|
+
id: generateId(/* @__PURE__ */ new Set([...pathEntryIds, ...labelEntries.map((e) => e.id)])),
|
|
1070
|
+
parentId,
|
|
1071
|
+
timestamp: labelTimestamp,
|
|
1072
|
+
targetId,
|
|
1073
|
+
label
|
|
1074
|
+
};
|
|
1075
|
+
labelEntries.push(labelEntry);
|
|
1076
|
+
parentId = labelEntry.id;
|
|
1077
|
+
}
|
|
1078
|
+
this.fileEntries = [
|
|
1079
|
+
header,
|
|
1080
|
+
...pathWithoutLabels,
|
|
1081
|
+
...labelEntries
|
|
1082
|
+
];
|
|
1083
|
+
this.sessionId = newSessionId;
|
|
1084
|
+
this._buildIndex();
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Create a new session.
|
|
1088
|
+
* @param cwd Working directory (stored in session header)
|
|
1089
|
+
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
|
1090
|
+
*/
|
|
1091
|
+
static create(cwd, sessionDir, options) {
|
|
1092
|
+
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1093
|
+
return new SessionManager(cwd, dir, void 0, true, options);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Open a specific session file.
|
|
1097
|
+
* @param path Path to session file
|
|
1098
|
+
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
|
|
1099
|
+
* @param cwdOverride Optional cwd override instead of the session header cwd.
|
|
1100
|
+
*/
|
|
1101
|
+
static open(path, sessionDir, cwdOverride) {
|
|
1102
|
+
const resolvedPath = resolvePath(path);
|
|
1103
|
+
let header = null;
|
|
1104
|
+
let preloadedFileEntries;
|
|
1105
|
+
if (cwdOverride === void 0 && existsSync$1(resolvedPath)) try {
|
|
1106
|
+
header = readSessionHeader(resolvedPath);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (!(error instanceof SessionHeaderScanLimitError)) throw error;
|
|
1109
|
+
preloadedFileEntries = loadEntriesFromFile(resolvedPath);
|
|
1110
|
+
const firstEntry = preloadedFileEntries[0];
|
|
1111
|
+
header = firstEntry?.type === "session" ? firstEntry : null;
|
|
1112
|
+
}
|
|
1113
|
+
const cwd = cwdOverride ?? (header ? getSessionHeaderCwd(header) : void 0) ?? process.cwd();
|
|
1114
|
+
const dir = sessionDir ? normalizePath(sessionDir) : resolve$1(resolvedPath, "..");
|
|
1115
|
+
return new SessionManager(cwd, dir, resolvedPath, true, void 0, preloadedFileEntries);
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Continue the most recent session, or create new if none.
|
|
1119
|
+
* @param cwd Working directory
|
|
1120
|
+
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
|
1121
|
+
*/
|
|
1122
|
+
static continueRecent(cwd, sessionDir) {
|
|
1123
|
+
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1124
|
+
const mostRecent = findMostRecentSession(dir, sessionDir !== void 0 && dir !== getDefaultSessionDirPath(cwd) ? cwd : void 0);
|
|
1125
|
+
if (mostRecent) return new SessionManager(cwd, dir, mostRecent, true);
|
|
1126
|
+
return new SessionManager(cwd, dir, void 0, true);
|
|
1127
|
+
}
|
|
1128
|
+
/** Create an in-memory session (no file persistence) */
|
|
1129
|
+
static inMemory(cwd = process.cwd(), options) {
|
|
1130
|
+
return new SessionManager(cwd, "", void 0, false, options);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Fork a session from another project directory into the current project.
|
|
1134
|
+
* Creates a new session in the target cwd with the full history from the source session.
|
|
1135
|
+
* @param sourcePath Path to the source session file
|
|
1136
|
+
* @param targetCwd Target working directory (where the new session will be stored)
|
|
1137
|
+
* @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
|
|
1138
|
+
*/
|
|
1139
|
+
static forkFrom(sourcePath, targetCwd, sessionDir, options) {
|
|
1140
|
+
const resolvedSourcePath = resolvePath(sourcePath);
|
|
1141
|
+
const resolvedTargetCwd = resolvePath(targetCwd);
|
|
1142
|
+
const sourceEntries = loadEntriesFromFile(resolvedSourcePath);
|
|
1143
|
+
if (sourceEntries.length === 0) throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`);
|
|
1144
|
+
if (!sourceEntries.find((e) => e.type === "session")) throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`);
|
|
1145
|
+
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd);
|
|
1146
|
+
if (!existsSync$1(dir)) mkdirSync$1(dir, { recursive: true });
|
|
1147
|
+
if (options?.id !== void 0) assertValidSessionId(options.id);
|
|
1148
|
+
const newSessionId = options?.id ?? createSessionId();
|
|
1149
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1150
|
+
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1151
|
+
const newSessionFile = join$1(dir, `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
1152
|
+
writeFileSync$1(newSessionFile, `${JSON.stringify({
|
|
1153
|
+
type: "session",
|
|
1154
|
+
version: 3,
|
|
1155
|
+
id: newSessionId,
|
|
1156
|
+
timestamp,
|
|
1157
|
+
cwd: resolvedTargetCwd,
|
|
1158
|
+
parentSession: resolvedSourcePath
|
|
1159
|
+
})}\n`, { flag: "wx" });
|
|
1160
|
+
for (const entry of sourceEntries) if (entry.type !== "session") appendFileSync$1(newSessionFile, `${JSON.stringify(entry)}\n`);
|
|
1161
|
+
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* List all sessions for a directory.
|
|
1165
|
+
* @param cwd Working directory (used to compute default session directory)
|
|
1166
|
+
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
|
|
1167
|
+
* @param onProgress Optional callback for progress updates (loaded, total)
|
|
1168
|
+
*/
|
|
1169
|
+
static async list(cwd, sessionDir, onProgress) {
|
|
1170
|
+
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1171
|
+
const filterCwd = sessionDir !== void 0 && dir !== getDefaultSessionDirPath(cwd);
|
|
1172
|
+
const resolvedCwd = resolvePath(cwd);
|
|
1173
|
+
const sessions = (await listSessionsFromDir(dir, onProgress)).filter((session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd));
|
|
1174
|
+
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1175
|
+
return sessions;
|
|
1176
|
+
}
|
|
1177
|
+
static async listAll(sessionDirOrOnProgress, onProgress) {
|
|
1178
|
+
const customSessionDir = typeof sessionDirOrOnProgress === "string" ? normalizePath(sessionDirOrOnProgress) : void 0;
|
|
1179
|
+
const progress = typeof sessionDirOrOnProgress === "function" ? sessionDirOrOnProgress : onProgress;
|
|
1180
|
+
if (customSessionDir) {
|
|
1181
|
+
const sessions = await listSessionsFromDir(customSessionDir, progress);
|
|
1182
|
+
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1183
|
+
return sessions;
|
|
1184
|
+
}
|
|
1185
|
+
const sessionsDir = getSessionsDir();
|
|
1186
|
+
try {
|
|
1187
|
+
if (!existsSync$1(sessionsDir)) return [];
|
|
1188
|
+
const dirs = (await readdir$1(sessionsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => join$1(sessionsDir, entry.name));
|
|
1189
|
+
let totalFiles = 0;
|
|
1190
|
+
const dirFiles = [];
|
|
1191
|
+
for (const dir of dirs) try {
|
|
1192
|
+
const files = (await readdir$1(dir)).filter((f) => f.endsWith(".jsonl"));
|
|
1193
|
+
dirFiles.push(files.map((f) => join$1(dir, f)));
|
|
1194
|
+
totalFiles += files.length;
|
|
1195
|
+
} catch {
|
|
1196
|
+
dirFiles.push([]);
|
|
1197
|
+
}
|
|
1198
|
+
let loaded = 0;
|
|
1199
|
+
const sessions = [];
|
|
1200
|
+
const results = await buildSessionInfosWithConcurrency(dirFiles.flat(), () => {
|
|
1201
|
+
loaded++;
|
|
1202
|
+
progress?.(loaded, totalFiles);
|
|
1203
|
+
});
|
|
1204
|
+
for (const info of results) if (info) sessions.push(info);
|
|
1205
|
+
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1206
|
+
return sessions;
|
|
1207
|
+
} catch {
|
|
1208
|
+
return [];
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
//#endregion
|
|
1213
|
+
//#region src/compat/vendor/pi-truncate.ts
|
|
1214
|
+
/**
|
|
1215
|
+
* Shared truncation utilities for tool outputs.
|
|
1216
|
+
*
|
|
1217
|
+
* Truncation is based on two independent limits - whichever is hit first wins:
|
|
1218
|
+
* - Line limit (default: 2000 lines)
|
|
1219
|
+
* - Byte limit (default: 50KB)
|
|
1220
|
+
*
|
|
1221
|
+
* Never returns partial lines (except bash tail truncation edge case).
|
|
1222
|
+
*/
|
|
1223
|
+
const DEFAULT_MAX_LINES = 2e3;
|
|
1224
|
+
const DEFAULT_MAX_BYTES = 51200;
|
|
1225
|
+
function splitLinesForCounting(content) {
|
|
1226
|
+
if (content.length === 0) return [];
|
|
1227
|
+
const lines = content.split("\n");
|
|
1228
|
+
if (content.endsWith("\n")) lines.pop();
|
|
1229
|
+
return lines;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Format bytes as human-readable size.
|
|
1233
|
+
*/
|
|
1234
|
+
function formatSize(bytes) {
|
|
1235
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1236
|
+
else if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1237
|
+
else return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Truncate content from the head (keep first N lines/bytes).
|
|
1241
|
+
* Suitable for file reads where you want to see the beginning.
|
|
1242
|
+
*
|
|
1243
|
+
* Never returns partial lines. If first line exceeds byte limit,
|
|
1244
|
+
* returns empty content with firstLineExceedsLimit=true.
|
|
1245
|
+
*/
|
|
1246
|
+
function truncateHead(content, options = {}) {
|
|
1247
|
+
const maxLines = options.maxLines ?? 2e3;
|
|
1248
|
+
const maxBytes = options.maxBytes ?? 51200;
|
|
1249
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
1250
|
+
const lines = splitLinesForCounting(content);
|
|
1251
|
+
const totalLines = lines.length;
|
|
1252
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) return {
|
|
1253
|
+
content,
|
|
1254
|
+
truncated: false,
|
|
1255
|
+
truncatedBy: null,
|
|
1256
|
+
totalLines,
|
|
1257
|
+
totalBytes,
|
|
1258
|
+
outputLines: totalLines,
|
|
1259
|
+
outputBytes: totalBytes,
|
|
1260
|
+
lastLinePartial: false,
|
|
1261
|
+
firstLineExceedsLimit: false,
|
|
1262
|
+
maxLines,
|
|
1263
|
+
maxBytes
|
|
1264
|
+
};
|
|
1265
|
+
if (Buffer.byteLength(lines[0], "utf-8") > maxBytes) return {
|
|
1266
|
+
content: "",
|
|
1267
|
+
truncated: true,
|
|
1268
|
+
truncatedBy: "bytes",
|
|
1269
|
+
totalLines,
|
|
1270
|
+
totalBytes,
|
|
1271
|
+
outputLines: 0,
|
|
1272
|
+
outputBytes: 0,
|
|
1273
|
+
lastLinePartial: false,
|
|
1274
|
+
firstLineExceedsLimit: true,
|
|
1275
|
+
maxLines,
|
|
1276
|
+
maxBytes
|
|
1277
|
+
};
|
|
1278
|
+
const outputLinesArr = [];
|
|
1279
|
+
let outputBytesCount = 0;
|
|
1280
|
+
let truncatedBy = "lines";
|
|
1281
|
+
for (let i = 0; i < lines.length && i < maxLines; i++) {
|
|
1282
|
+
const line = lines[i];
|
|
1283
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
|
|
1284
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
1285
|
+
truncatedBy = "bytes";
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
outputLinesArr.push(line);
|
|
1289
|
+
outputBytesCount += lineBytes;
|
|
1290
|
+
}
|
|
1291
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) truncatedBy = "lines";
|
|
1292
|
+
const outputContent = outputLinesArr.join("\n");
|
|
1293
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
1294
|
+
return {
|
|
1295
|
+
content: outputContent,
|
|
1296
|
+
truncated: true,
|
|
1297
|
+
truncatedBy,
|
|
1298
|
+
totalLines,
|
|
1299
|
+
totalBytes,
|
|
1300
|
+
outputLines: outputLinesArr.length,
|
|
1301
|
+
outputBytes: finalOutputBytes,
|
|
1302
|
+
lastLinePartial: false,
|
|
1303
|
+
firstLineExceedsLimit: false,
|
|
1304
|
+
maxLines,
|
|
1305
|
+
maxBytes
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Truncate content from the tail (keep last N lines/bytes).
|
|
1310
|
+
* Suitable for bash output where you want to see the end (errors, final results).
|
|
1311
|
+
*
|
|
1312
|
+
* May return partial first line if the last line of original content exceeds byte limit.
|
|
1313
|
+
*/
|
|
1314
|
+
function truncateTail(content, options = {}) {
|
|
1315
|
+
const maxLines = options.maxLines ?? 2e3;
|
|
1316
|
+
const maxBytes = options.maxBytes ?? 51200;
|
|
1317
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
1318
|
+
const lines = splitLinesForCounting(content);
|
|
1319
|
+
const totalLines = lines.length;
|
|
1320
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) return {
|
|
1321
|
+
content,
|
|
1322
|
+
truncated: false,
|
|
1323
|
+
truncatedBy: null,
|
|
1324
|
+
totalLines,
|
|
1325
|
+
totalBytes,
|
|
1326
|
+
outputLines: totalLines,
|
|
1327
|
+
outputBytes: totalBytes,
|
|
1328
|
+
lastLinePartial: false,
|
|
1329
|
+
firstLineExceedsLimit: false,
|
|
1330
|
+
maxLines,
|
|
1331
|
+
maxBytes
|
|
1332
|
+
};
|
|
1333
|
+
const outputLinesArr = [];
|
|
1334
|
+
let outputBytesCount = 0;
|
|
1335
|
+
let truncatedBy = "lines";
|
|
1336
|
+
let lastLinePartial = false;
|
|
1337
|
+
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
|
|
1338
|
+
const line = lines[i];
|
|
1339
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
|
|
1340
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
1341
|
+
truncatedBy = "bytes";
|
|
1342
|
+
if (outputLinesArr.length === 0) {
|
|
1343
|
+
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
|
1344
|
+
outputLinesArr.unshift(truncatedLine);
|
|
1345
|
+
outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
|
|
1346
|
+
lastLinePartial = true;
|
|
1347
|
+
}
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
outputLinesArr.unshift(line);
|
|
1351
|
+
outputBytesCount += lineBytes;
|
|
1352
|
+
}
|
|
1353
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) truncatedBy = "lines";
|
|
1354
|
+
const outputContent = outputLinesArr.join("\n");
|
|
1355
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
1356
|
+
return {
|
|
1357
|
+
content: outputContent,
|
|
1358
|
+
truncated: true,
|
|
1359
|
+
truncatedBy,
|
|
1360
|
+
totalLines,
|
|
1361
|
+
totalBytes,
|
|
1362
|
+
outputLines: outputLinesArr.length,
|
|
1363
|
+
outputBytes: finalOutputBytes,
|
|
1364
|
+
lastLinePartial,
|
|
1365
|
+
firstLineExceedsLimit: false,
|
|
1366
|
+
maxLines,
|
|
1367
|
+
maxBytes
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Truncate a string to fit within a byte limit (from the end).
|
|
1372
|
+
* Handles multi-byte UTF-8 characters correctly.
|
|
1373
|
+
*/
|
|
1374
|
+
function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
1375
|
+
const buf = Buffer.from(str, "utf-8");
|
|
1376
|
+
if (buf.length <= maxBytes) return str;
|
|
1377
|
+
let start = buf.length - maxBytes;
|
|
1378
|
+
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
1379
|
+
return buf.slice(start).toString("utf-8");
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Truncate a single line to max characters, adding [truncated] suffix.
|
|
1383
|
+
* Used for grep match lines.
|
|
1384
|
+
*/
|
|
1385
|
+
function truncateLine(line, maxChars = 500) {
|
|
1386
|
+
if (line.length <= maxChars) return {
|
|
1387
|
+
text: line,
|
|
1388
|
+
wasTruncated: false
|
|
1389
|
+
};
|
|
1390
|
+
return {
|
|
1391
|
+
text: `${line.slice(0, maxChars)}... [truncated]`,
|
|
1392
|
+
wasTruncated: true
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region src/compat/vendor/pi-file-mutation-queue.ts
|
|
1397
|
+
const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
1398
|
+
let registrationQueue = Promise.resolve();
|
|
1399
|
+
function isMissingPathError(error) {
|
|
1400
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
1401
|
+
}
|
|
1402
|
+
async function getMutationQueueKey(filePath) {
|
|
1403
|
+
const resolvedPath = resolve(filePath);
|
|
1404
|
+
try {
|
|
1405
|
+
return await realpath(resolvedPath);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
if (isMissingPathError(error)) return resolvedPath;
|
|
1408
|
+
throw error;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Serialize file mutation operations targeting the same file.
|
|
1413
|
+
* Operations for different files still run in parallel.
|
|
1414
|
+
*/
|
|
1415
|
+
async function withFileMutationQueue(filePath, fn) {
|
|
1416
|
+
const registration = registrationQueue.then(async () => {
|
|
1417
|
+
const key = await getMutationQueueKey(filePath);
|
|
1418
|
+
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
1419
|
+
let releaseNext;
|
|
1420
|
+
const nextQueue = new Promise((resolveQueue) => {
|
|
1421
|
+
releaseNext = resolveQueue;
|
|
1422
|
+
});
|
|
1423
|
+
const chainedQueue = currentQueue.then(() => nextQueue);
|
|
1424
|
+
fileMutationQueues.set(key, chainedQueue);
|
|
1425
|
+
return {
|
|
1426
|
+
key,
|
|
1427
|
+
currentQueue,
|
|
1428
|
+
chainedQueue,
|
|
1429
|
+
releaseNext
|
|
1430
|
+
};
|
|
1431
|
+
});
|
|
1432
|
+
registrationQueue = registration.then(() => void 0, () => void 0);
|
|
1433
|
+
const { key, currentQueue, chainedQueue, releaseNext } = await registration;
|
|
1434
|
+
await currentQueue;
|
|
1435
|
+
try {
|
|
1436
|
+
return await fn();
|
|
1437
|
+
} finally {
|
|
1438
|
+
releaseNext();
|
|
1439
|
+
if (fileMutationQueues.get(key) === chainedQueue) fileMutationQueues.delete(key);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/compat/pi-coding-agent.ts
|
|
1444
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
1445
|
+
const VERSION = "pi2dsh-compat";
|
|
1446
|
+
function defineTool(tool) {
|
|
1447
|
+
return tool;
|
|
1448
|
+
}
|
|
1449
|
+
function unsupportedRuntime(name) {
|
|
1450
|
+
throw new Error(`pi2dsh: ${name} belongs to Pi's internal agent runtime and has no verified DSH mapping; use the DSH-native service instead (see the pi2dsh compatibility report)`);
|
|
1451
|
+
}
|
|
1452
|
+
const identity = (text) => text;
|
|
1453
|
+
var Theme = class {
|
|
1454
|
+
name;
|
|
1455
|
+
constructor(name = "pi2dsh-headless") {
|
|
1456
|
+
this.name = name;
|
|
1457
|
+
}
|
|
1458
|
+
fg(_color, text) {
|
|
1459
|
+
return text;
|
|
1460
|
+
}
|
|
1461
|
+
bg(_color, text) {
|
|
1462
|
+
return text;
|
|
1463
|
+
}
|
|
1464
|
+
bold(text) {
|
|
1465
|
+
return text;
|
|
1466
|
+
}
|
|
1467
|
+
italic(text) {
|
|
1468
|
+
return text;
|
|
1469
|
+
}
|
|
1470
|
+
underline(text) {
|
|
1471
|
+
return text;
|
|
1472
|
+
}
|
|
1473
|
+
inverse(text) {
|
|
1474
|
+
return text;
|
|
1475
|
+
}
|
|
1476
|
+
strikethrough(text) {
|
|
1477
|
+
return text;
|
|
1478
|
+
}
|
|
1479
|
+
getFgAnsi(_color) {
|
|
1480
|
+
return "";
|
|
1481
|
+
}
|
|
1482
|
+
getBgAnsi(_color) {
|
|
1483
|
+
return "";
|
|
1484
|
+
}
|
|
1485
|
+
getColorMode() {
|
|
1486
|
+
return "none";
|
|
1487
|
+
}
|
|
1488
|
+
getThinkingBorderColor(_level) {
|
|
1489
|
+
return identity;
|
|
1490
|
+
}
|
|
1491
|
+
getBashModeBorderColor() {
|
|
1492
|
+
return identity;
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
const theme = new Theme();
|
|
1496
|
+
function initTheme(_themeName, _enableWatcher = false) {}
|
|
1497
|
+
function getSettingsListTheme() {
|
|
1498
|
+
return {
|
|
1499
|
+
label: (text, _selected) => text,
|
|
1500
|
+
value: (text, _selected) => text,
|
|
1501
|
+
description: (text) => text,
|
|
1502
|
+
cursor: "→ ",
|
|
1503
|
+
hint: (text) => text
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function getSelectListTheme() {
|
|
1507
|
+
return {
|
|
1508
|
+
selectedPrefix: "→ ",
|
|
1509
|
+
selectedText: identity,
|
|
1510
|
+
description: identity,
|
|
1511
|
+
scrollInfo: identity,
|
|
1512
|
+
noMatch: identity
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function getMarkdownTheme() {
|
|
1516
|
+
return {
|
|
1517
|
+
heading: identity,
|
|
1518
|
+
link: identity,
|
|
1519
|
+
linkUrl: identity,
|
|
1520
|
+
code: identity,
|
|
1521
|
+
codeBlock: identity,
|
|
1522
|
+
codeBlockBorder: identity,
|
|
1523
|
+
quote: identity,
|
|
1524
|
+
quoteBorder: identity,
|
|
1525
|
+
hr: identity,
|
|
1526
|
+
listBullet: identity,
|
|
1527
|
+
bold: identity,
|
|
1528
|
+
italic: identity,
|
|
1529
|
+
underline: identity,
|
|
1530
|
+
strikethrough: identity,
|
|
1531
|
+
highlightCode: (code) => code.split("\n")
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
const LANGUAGE_BY_EXTENSION = {
|
|
1535
|
+
".ts": "typescript",
|
|
1536
|
+
".tsx": "typescript",
|
|
1537
|
+
".js": "javascript",
|
|
1538
|
+
".jsx": "javascript",
|
|
1539
|
+
".mjs": "javascript",
|
|
1540
|
+
".cjs": "javascript",
|
|
1541
|
+
".py": "python",
|
|
1542
|
+
".rb": "ruby",
|
|
1543
|
+
".go": "go",
|
|
1544
|
+
".rs": "rust",
|
|
1545
|
+
".java": "java",
|
|
1546
|
+
".c": "c",
|
|
1547
|
+
".h": "c",
|
|
1548
|
+
".cpp": "cpp",
|
|
1549
|
+
".hpp": "cpp",
|
|
1550
|
+
".cs": "csharp",
|
|
1551
|
+
".sh": "bash",
|
|
1552
|
+
".bash": "bash",
|
|
1553
|
+
".zsh": "bash",
|
|
1554
|
+
".json": "json",
|
|
1555
|
+
".yaml": "yaml",
|
|
1556
|
+
".yml": "yaml",
|
|
1557
|
+
".toml": "ini",
|
|
1558
|
+
".md": "markdown",
|
|
1559
|
+
".html": "xml",
|
|
1560
|
+
".xml": "xml",
|
|
1561
|
+
".css": "css",
|
|
1562
|
+
".scss": "scss",
|
|
1563
|
+
".sql": "sql",
|
|
1564
|
+
".php": "php",
|
|
1565
|
+
".swift": "swift",
|
|
1566
|
+
".kt": "kotlin",
|
|
1567
|
+
".scala": "scala",
|
|
1568
|
+
".lua": "lua",
|
|
1569
|
+
".r": "r"
|
|
1570
|
+
};
|
|
1571
|
+
function getLanguageFromPath(filePath) {
|
|
1572
|
+
const dot = filePath.lastIndexOf(".");
|
|
1573
|
+
if (dot === -1) return void 0;
|
|
1574
|
+
return LANGUAGE_BY_EXTENSION[filePath.slice(dot).toLowerCase()];
|
|
1575
|
+
}
|
|
1576
|
+
function highlightCode(code, _lang) {
|
|
1577
|
+
return code.split("\n");
|
|
1578
|
+
}
|
|
1579
|
+
var DynamicBorder = class {
|
|
1580
|
+
color;
|
|
1581
|
+
constructor(color = identity) {
|
|
1582
|
+
this.color = color;
|
|
1583
|
+
}
|
|
1584
|
+
render(width) {
|
|
1585
|
+
return [this.color("─".repeat(Math.max(1, width)))];
|
|
1586
|
+
}
|
|
1587
|
+
invalidate() {}
|
|
1588
|
+
};
|
|
1589
|
+
function contentChars(content) {
|
|
1590
|
+
if (typeof content === "string") return content.length;
|
|
1591
|
+
if (!Array.isArray(content)) return 0;
|
|
1592
|
+
let chars = 0;
|
|
1593
|
+
for (const block of content) {
|
|
1594
|
+
if (typeof block !== "object" || block === null) continue;
|
|
1595
|
+
const record = block;
|
|
1596
|
+
if (typeof record.text === "string") chars += record.text.length;
|
|
1597
|
+
else if (typeof record.thinking === "string") chars += record.thinking.length;
|
|
1598
|
+
else if (record.type === "image") chars += 1600;
|
|
1599
|
+
else if (record.arguments !== void 0) chars += JSON.stringify(record.arguments ?? {}).length;
|
|
1600
|
+
}
|
|
1601
|
+
return chars;
|
|
1602
|
+
}
|
|
1603
|
+
function estimateTokens(message) {
|
|
1604
|
+
const record = message;
|
|
1605
|
+
let chars = contentChars(record.content);
|
|
1606
|
+
if (typeof record.summary === "string") chars += record.summary.length;
|
|
1607
|
+
if (record.role === "bashExecution") chars += String(record.command ?? "").length + String(record.output ?? "").length;
|
|
1608
|
+
return Math.ceil(chars / 4);
|
|
1609
|
+
}
|
|
1610
|
+
function calculateContextTokens(messages) {
|
|
1611
|
+
return messages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
1612
|
+
}
|
|
1613
|
+
const DEFAULT_COMPACTION_SETTINGS = Object.freeze({
|
|
1614
|
+
enabled: true,
|
|
1615
|
+
reserveTokens: 3e4,
|
|
1616
|
+
keepRecentTokens: 2e4
|
|
1617
|
+
});
|
|
1618
|
+
function shouldCompact(..._args) {
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
function compact(..._args) {
|
|
1622
|
+
return unsupportedRuntime("compact()");
|
|
1623
|
+
}
|
|
1624
|
+
function findCutPoint(..._args) {
|
|
1625
|
+
return unsupportedRuntime("findCutPoint()");
|
|
1626
|
+
}
|
|
1627
|
+
function generateSummary(..._args) {
|
|
1628
|
+
return unsupportedRuntime("generateSummary()");
|
|
1629
|
+
}
|
|
1630
|
+
function generateSummaryWithUsage(..._args) {
|
|
1631
|
+
return unsupportedRuntime("generateSummaryWithUsage()");
|
|
1632
|
+
}
|
|
1633
|
+
function generateBranchSummary(..._args) {
|
|
1634
|
+
return unsupportedRuntime("generateBranchSummary()");
|
|
1635
|
+
}
|
|
1636
|
+
function serializeConversation(messages) {
|
|
1637
|
+
return JSON.stringify(messages);
|
|
1638
|
+
}
|
|
1639
|
+
function parseFrontmatter(text) {
|
|
1640
|
+
const normalized = text.replace(/\r\n?/gu, "\n");
|
|
1641
|
+
if (!normalized.startsWith("---")) return {
|
|
1642
|
+
attributes: {},
|
|
1643
|
+
body: normalized
|
|
1644
|
+
};
|
|
1645
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
1646
|
+
if (endIndex === -1) return {
|
|
1647
|
+
attributes: {},
|
|
1648
|
+
body: normalized
|
|
1649
|
+
};
|
|
1650
|
+
const attributes = {};
|
|
1651
|
+
for (const line of normalized.slice(4, endIndex).split("\n")) {
|
|
1652
|
+
const separator = line.indexOf(":");
|
|
1653
|
+
if (separator === -1) continue;
|
|
1654
|
+
const key = line.slice(0, separator).trim();
|
|
1655
|
+
let value = line.slice(separator + 1).trim();
|
|
1656
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1657
|
+
if (key.length > 0) attributes[key] = value;
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
attributes,
|
|
1661
|
+
body: normalized.slice(endIndex + 4).replace(/^\n/u, "")
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function stripFrontmatter(text) {
|
|
1665
|
+
return parseFrontmatter(text).body;
|
|
1666
|
+
}
|
|
1667
|
+
async function copyToClipboard(text) {
|
|
1668
|
+
const attempt = (command, args) => new Promise((resolve) => {
|
|
1669
|
+
const child = execFile(command, args, (error) => resolve(error === null));
|
|
1670
|
+
child.stdin?.write(text);
|
|
1671
|
+
child.stdin?.end();
|
|
1672
|
+
});
|
|
1673
|
+
if (process.platform === "darwin") return attempt("pbcopy", []);
|
|
1674
|
+
if (process.platform === "win32") return attempt("clip", []);
|
|
1675
|
+
if (await attempt("wl-copy", [])) return true;
|
|
1676
|
+
return attempt("xclip", ["-selection", "clipboard"]);
|
|
1677
|
+
}
|
|
1678
|
+
async function resizeImage(data, mimeType, _options = {}) {
|
|
1679
|
+
return {
|
|
1680
|
+
data,
|
|
1681
|
+
mimeType
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
async function convertToPng(data, mimeType) {
|
|
1685
|
+
if (mimeType === "image/png") return {
|
|
1686
|
+
data,
|
|
1687
|
+
mimeType
|
|
1688
|
+
};
|
|
1689
|
+
return unsupportedRuntime("convertToPng() for non-PNG input");
|
|
1690
|
+
}
|
|
1691
|
+
function getShellConfig() {
|
|
1692
|
+
if (process.platform === "win32") return {
|
|
1693
|
+
shell: process.env.COMSPEC ?? "cmd.exe",
|
|
1694
|
+
args: [
|
|
1695
|
+
"/d",
|
|
1696
|
+
"/s",
|
|
1697
|
+
"/c"
|
|
1698
|
+
]
|
|
1699
|
+
};
|
|
1700
|
+
const preferred = process.env.SHELL;
|
|
1701
|
+
if (preferred !== void 0 && preferred.length > 0 && existsSync(preferred)) return {
|
|
1702
|
+
shell: preferred,
|
|
1703
|
+
args: ["-c"]
|
|
1704
|
+
};
|
|
1705
|
+
for (const candidate of [
|
|
1706
|
+
"/bin/bash",
|
|
1707
|
+
"/bin/zsh",
|
|
1708
|
+
"/bin/sh"
|
|
1709
|
+
]) if (existsSync(candidate)) return {
|
|
1710
|
+
shell: candidate,
|
|
1711
|
+
args: ["-c"]
|
|
1712
|
+
};
|
|
1713
|
+
return {
|
|
1714
|
+
shell: "sh",
|
|
1715
|
+
args: ["-c"]
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function getPackageDir() {
|
|
1719
|
+
const override = process.env.PI_PACKAGE_DIR;
|
|
1720
|
+
if (override !== void 0 && override.length > 0) return override;
|
|
1721
|
+
return join(getAgentDir(), "package");
|
|
1722
|
+
}
|
|
1723
|
+
function readStoredCredential(providerId, authPath = join(getAgentDir(), "auth.json")) {
|
|
1724
|
+
try {
|
|
1725
|
+
return JSON.parse(readFileSync(authPath, "utf8"))[providerId];
|
|
1726
|
+
} catch {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function parseSkillBlock(text) {
|
|
1731
|
+
const match = /<skill_content\b[^>]*\bname="([^"]+)"[^>]*>([\s\S]*?)<\/skill_content>/u.exec(text);
|
|
1732
|
+
if (match === null) return null;
|
|
1733
|
+
return {
|
|
1734
|
+
name: match[1],
|
|
1735
|
+
content: match[2].trim()
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function wrapRegisteredTool(..._args) {
|
|
1739
|
+
return unsupportedRuntime("wrapRegisteredTool()");
|
|
1740
|
+
}
|
|
1741
|
+
function getBinDir() {
|
|
1742
|
+
return (process.env.PATH ?? "").split(delimiter)[0] ?? join(homedir(), ".local", "bin");
|
|
1743
|
+
}
|
|
1744
|
+
var SettingsManager = class SettingsManager {
|
|
1745
|
+
globalSettings;
|
|
1746
|
+
projectSettings;
|
|
1747
|
+
constructor(globalSettings, projectSettings) {
|
|
1748
|
+
this.globalSettings = globalSettings;
|
|
1749
|
+
this.projectSettings = projectSettings;
|
|
1750
|
+
}
|
|
1751
|
+
static create(_cwd, _agentDir, options = {}) {
|
|
1752
|
+
return SettingsManager.inMemory({}, options);
|
|
1753
|
+
}
|
|
1754
|
+
static fromStorage(_storage, options = {}) {
|
|
1755
|
+
return SettingsManager.inMemory({}, options);
|
|
1756
|
+
}
|
|
1757
|
+
static inMemory(settings = {}, _options = {}) {
|
|
1758
|
+
return new SettingsManager({ ...settings }, {});
|
|
1759
|
+
}
|
|
1760
|
+
get merged() {
|
|
1761
|
+
return {
|
|
1762
|
+
...this.globalSettings,
|
|
1763
|
+
...this.projectSettings
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
read(key, fallback) {
|
|
1767
|
+
const value = this.merged[key];
|
|
1768
|
+
return value === void 0 ? fallback : value;
|
|
1769
|
+
}
|
|
1770
|
+
async reload() {}
|
|
1771
|
+
async flush() {}
|
|
1772
|
+
drainErrors() {
|
|
1773
|
+
return [];
|
|
1774
|
+
}
|
|
1775
|
+
applyOverrides(overrides) {
|
|
1776
|
+
Object.assign(this.globalSettings, overrides);
|
|
1777
|
+
}
|
|
1778
|
+
getGlobalSettings() {
|
|
1779
|
+
return { ...this.globalSettings };
|
|
1780
|
+
}
|
|
1781
|
+
getProjectSettings() {
|
|
1782
|
+
return { ...this.projectSettings };
|
|
1783
|
+
}
|
|
1784
|
+
isProjectTrusted() {
|
|
1785
|
+
return this.read("projectTrusted", false);
|
|
1786
|
+
}
|
|
1787
|
+
setProjectTrusted(trusted) {
|
|
1788
|
+
this.projectSettings.projectTrusted = trusted;
|
|
1789
|
+
}
|
|
1790
|
+
getDefaultProjectTrust() {
|
|
1791
|
+
return this.read("defaultProjectTrust", "ask");
|
|
1792
|
+
}
|
|
1793
|
+
getDefaultProvider() {
|
|
1794
|
+
return this.read("defaultProvider", void 0);
|
|
1795
|
+
}
|
|
1796
|
+
getDefaultModel() {
|
|
1797
|
+
return this.read("defaultModel", void 0);
|
|
1798
|
+
}
|
|
1799
|
+
setDefaultProvider(provider) {
|
|
1800
|
+
this.globalSettings.defaultProvider = provider;
|
|
1801
|
+
}
|
|
1802
|
+
setDefaultModel(model) {
|
|
1803
|
+
this.globalSettings.defaultModel = model;
|
|
1804
|
+
}
|
|
1805
|
+
setDefaultModelAndProvider(model, provider) {
|
|
1806
|
+
this.setDefaultModel(model);
|
|
1807
|
+
this.setDefaultProvider(provider);
|
|
1808
|
+
}
|
|
1809
|
+
getDefaultThinkingLevel() {
|
|
1810
|
+
return this.read("defaultThinkingLevel", void 0);
|
|
1811
|
+
}
|
|
1812
|
+
getThemeSetting() {
|
|
1813
|
+
return this.read("theme", void 0);
|
|
1814
|
+
}
|
|
1815
|
+
getTheme() {
|
|
1816
|
+
return this.getThemeSetting();
|
|
1817
|
+
}
|
|
1818
|
+
setTheme(themeName) {
|
|
1819
|
+
this.globalSettings.theme = themeName;
|
|
1820
|
+
}
|
|
1821
|
+
getCompactionSettings() {
|
|
1822
|
+
return this.read("compaction", { ...DEFAULT_COMPACTION_SETTINGS });
|
|
1823
|
+
}
|
|
1824
|
+
getBranchSummarySettings() {
|
|
1825
|
+
return this.read("branchSummary", {
|
|
1826
|
+
reserveTokens: 8e3,
|
|
1827
|
+
skipPrompt: false
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
getRetrySettings() {
|
|
1831
|
+
return this.read("retry", {
|
|
1832
|
+
enabled: true,
|
|
1833
|
+
maxRetries: 3,
|
|
1834
|
+
baseDelayMs: 1e3
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
getProviderRetrySettings() {
|
|
1838
|
+
return this.getRetrySettings();
|
|
1839
|
+
}
|
|
1840
|
+
getHttpIdleTimeoutMs() {
|
|
1841
|
+
return this.read("httpIdleTimeoutMs", 12e4);
|
|
1842
|
+
}
|
|
1843
|
+
getWebSocketConnectTimeoutMs() {
|
|
1844
|
+
return this.read("webSocketConnectTimeoutMs", 3e4);
|
|
1845
|
+
}
|
|
1846
|
+
getPackages() {
|
|
1847
|
+
return this.read("packages", []);
|
|
1848
|
+
}
|
|
1849
|
+
getExtensionPaths() {
|
|
1850
|
+
return this.read("extensions", []);
|
|
1851
|
+
}
|
|
1852
|
+
getSkillPaths() {
|
|
1853
|
+
return this.read("skills", []);
|
|
1854
|
+
}
|
|
1855
|
+
getPromptTemplatePaths() {
|
|
1856
|
+
return this.read("prompts", []);
|
|
1857
|
+
}
|
|
1858
|
+
getThemePaths() {
|
|
1859
|
+
return this.read("themes", []);
|
|
1860
|
+
}
|
|
1861
|
+
getTuiMode() {
|
|
1862
|
+
return this.read("tuiMode", "regular");
|
|
1863
|
+
}
|
|
1864
|
+
getShowImages() {
|
|
1865
|
+
return this.read("showImages", false);
|
|
1866
|
+
}
|
|
1867
|
+
getImageWidthCells() {
|
|
1868
|
+
return this.read("imageWidthCells", 40);
|
|
1869
|
+
}
|
|
1870
|
+
getClearOnShrink() {
|
|
1871
|
+
return this.read("clearOnShrink", false);
|
|
1872
|
+
}
|
|
1873
|
+
getShowTerminalProgress() {
|
|
1874
|
+
return this.read("showTerminalProgress", false);
|
|
1875
|
+
}
|
|
1876
|
+
getHideThinkingBlock() {
|
|
1877
|
+
return this.read("hideThinkingBlock", false);
|
|
1878
|
+
}
|
|
1879
|
+
getExternalEditorCommand() {
|
|
1880
|
+
return this.read("externalEditorCommand", void 0);
|
|
1881
|
+
}
|
|
1882
|
+
getSteeringMode() {
|
|
1883
|
+
return this.read("steeringMode", "all");
|
|
1884
|
+
}
|
|
1885
|
+
getFollowUpMode() {
|
|
1886
|
+
return this.read("followUpMode", "queue");
|
|
1887
|
+
}
|
|
1888
|
+
getShellPath() {
|
|
1889
|
+
return this.read("shellPath", void 0);
|
|
1890
|
+
}
|
|
1891
|
+
getShellCommandPrefix() {
|
|
1892
|
+
return this.read("shellCommandPrefix", void 0);
|
|
1893
|
+
}
|
|
1894
|
+
getNpmCommand() {
|
|
1895
|
+
return this.read("npmCommand", void 0);
|
|
1896
|
+
}
|
|
1897
|
+
getEnableSkillCommands() {
|
|
1898
|
+
return this.read("enableSkillCommands", true);
|
|
1899
|
+
}
|
|
1900
|
+
getEnableAnalytics() {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
getTrackingId() {}
|
|
1904
|
+
};
|
|
1905
|
+
var InMemorySettingsStorage = class {
|
|
1906
|
+
settings;
|
|
1907
|
+
constructor(settings = {}) {
|
|
1908
|
+
this.settings = settings;
|
|
1909
|
+
}
|
|
1910
|
+
async withLock(_scope, fn) {
|
|
1911
|
+
return fn();
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
var FileSettingsStorage = class extends InMemorySettingsStorage {};
|
|
1915
|
+
function runtimeStubClass(name) {
|
|
1916
|
+
return class {
|
|
1917
|
+
constructor() {
|
|
1918
|
+
return unsupportedRuntime(`new ${name}()`);
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
const ProjectTrustStore = runtimeStubClass("ProjectTrustStore");
|
|
1923
|
+
const DefaultResourceLoader = runtimeStubClass("DefaultResourceLoader");
|
|
1924
|
+
const DefaultPackageManager = runtimeStubClass("DefaultPackageManager");
|
|
1925
|
+
const ModelRuntime = runtimeStubClass("ModelRuntime");
|
|
1926
|
+
var ModelRegistry = class {
|
|
1927
|
+
models = /* @__PURE__ */ new Map();
|
|
1928
|
+
register(id, model) {
|
|
1929
|
+
this.models.set(id, model);
|
|
1930
|
+
}
|
|
1931
|
+
get(id) {
|
|
1932
|
+
return this.models.get(id);
|
|
1933
|
+
}
|
|
1934
|
+
list() {
|
|
1935
|
+
return [...this.models.values()];
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
function createAgentSession(..._args) {
|
|
1939
|
+
return unsupportedRuntime("createAgentSession()");
|
|
1940
|
+
}
|
|
1941
|
+
function createCodingTools(..._args) {
|
|
1942
|
+
return unsupportedRuntime("createCodingTools()");
|
|
1943
|
+
}
|
|
1944
|
+
function createReadOnlyTools(..._args) {
|
|
1945
|
+
return unsupportedRuntime("createReadOnlyTools()");
|
|
1946
|
+
}
|
|
1947
|
+
function createBashTool(..._args) {
|
|
1948
|
+
return unsupportedRuntime("createBashTool()");
|
|
1949
|
+
}
|
|
1950
|
+
function createReadTool(..._args) {
|
|
1951
|
+
return unsupportedRuntime("createReadTool()");
|
|
1952
|
+
}
|
|
1953
|
+
function createEditTool(..._args) {
|
|
1954
|
+
return unsupportedRuntime("createEditTool()");
|
|
1955
|
+
}
|
|
1956
|
+
function createWriteTool(..._args) {
|
|
1957
|
+
return unsupportedRuntime("createWriteTool()");
|
|
1958
|
+
}
|
|
1959
|
+
function createGrepTool(..._args) {
|
|
1960
|
+
return unsupportedRuntime("createGrepTool()");
|
|
1961
|
+
}
|
|
1962
|
+
function createFindTool(..._args) {
|
|
1963
|
+
return unsupportedRuntime("createFindTool()");
|
|
1964
|
+
}
|
|
1965
|
+
function createLsTool(..._args) {
|
|
1966
|
+
return unsupportedRuntime("createLsTool()");
|
|
1967
|
+
}
|
|
1968
|
+
function loadSkills(..._args) {
|
|
1969
|
+
return unsupportedRuntime("loadSkills()");
|
|
1970
|
+
}
|
|
1971
|
+
function loadSkillsFromDir(..._args) {
|
|
1972
|
+
return unsupportedRuntime("loadSkillsFromDir()");
|
|
1973
|
+
}
|
|
1974
|
+
function formatSkillsForPrompt(..._args) {
|
|
1975
|
+
return unsupportedRuntime("formatSkillsForPrompt()");
|
|
1976
|
+
}
|
|
1977
|
+
function createEventBus() {
|
|
1978
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1979
|
+
return {
|
|
1980
|
+
emit(channel, data) {
|
|
1981
|
+
for (const handler of handlers.get(channel) ?? []) Promise.resolve().then(() => handler(data)).catch((error) => console.error(error));
|
|
1982
|
+
},
|
|
1983
|
+
on(channel, handler) {
|
|
1984
|
+
const set = handlers.get(channel) ?? /* @__PURE__ */ new Set();
|
|
1985
|
+
set.add(handler);
|
|
1986
|
+
handlers.set(channel, set);
|
|
1987
|
+
return () => {
|
|
1988
|
+
set.delete(handler);
|
|
1989
|
+
};
|
|
1990
|
+
},
|
|
1991
|
+
clear() {
|
|
1992
|
+
handlers.clear();
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
var HeadlessComponent = class {
|
|
1997
|
+
render(_width) {
|
|
1998
|
+
return [];
|
|
1999
|
+
}
|
|
2000
|
+
invalidate() {}
|
|
2001
|
+
};
|
|
2002
|
+
var ToolExecutionComponent = class extends HeadlessComponent {};
|
|
2003
|
+
var FooterComponent = class extends HeadlessComponent {};
|
|
2004
|
+
var BorderedLoader = class extends HeadlessComponent {
|
|
2005
|
+
start() {}
|
|
2006
|
+
stop() {}
|
|
2007
|
+
dispose() {}
|
|
2008
|
+
};
|
|
2009
|
+
var CustomMessageComponent = class extends HeadlessComponent {};
|
|
2010
|
+
var AssistantMessageComponent = class extends HeadlessComponent {};
|
|
2011
|
+
var UserMessageComponent = class extends HeadlessComponent {};
|
|
2012
|
+
var ExtensionSelectorComponent = class extends HeadlessComponent {};
|
|
2013
|
+
var ExtensionInputComponent = class extends HeadlessComponent {};
|
|
2014
|
+
var ExtensionEditorComponent = class extends HeadlessComponent {};
|
|
2015
|
+
var SettingsSelectorComponent = class extends HeadlessComponent {};
|
|
2016
|
+
var CustomEditor = class extends HeadlessComponent {
|
|
2017
|
+
text = "";
|
|
2018
|
+
onSubmit;
|
|
2019
|
+
onChange;
|
|
2020
|
+
getText() {
|
|
2021
|
+
return this.text;
|
|
2022
|
+
}
|
|
2023
|
+
setText(text) {
|
|
2024
|
+
this.text = text;
|
|
2025
|
+
this.onChange?.(text);
|
|
2026
|
+
}
|
|
2027
|
+
handleInput(data) {
|
|
2028
|
+
if (data === "\r" || data === "\n") {
|
|
2029
|
+
this.onSubmit?.(this.text);
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (data >= " ") this.setText(this.text + data);
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
function renderDiff(oldText, newText, _options = {}) {
|
|
2036
|
+
const removed = oldText.split("\n").map((line) => `- ${line}`);
|
|
2037
|
+
const added = newText.split("\n").map((line) => `+ ${line}`);
|
|
2038
|
+
return [...removed, ...added];
|
|
2039
|
+
}
|
|
2040
|
+
function truncateToVisualLines(text, maxVisualLines, width, paddingX = 0) {
|
|
2041
|
+
const inner = Math.max(1, width - paddingX * 2);
|
|
2042
|
+
const lines = text.split("\n").map((line) => visibleWidth(line) > inner ? truncateToWidth(line, inner) : line);
|
|
2043
|
+
return {
|
|
2044
|
+
visualLines: lines.slice(0, Math.max(0, maxVisualLines)),
|
|
2045
|
+
skippedCount: Math.max(0, lines.length - maxVisualLines)
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
function keyHint(keybinding, description) {
|
|
2049
|
+
return `${keybinding} ${description}`;
|
|
2050
|
+
}
|
|
2051
|
+
function keyText(keybinding) {
|
|
2052
|
+
return keybinding;
|
|
2053
|
+
}
|
|
2054
|
+
function rawKeyHint(key, description) {
|
|
2055
|
+
return `${key} ${description}`;
|
|
2056
|
+
}
|
|
2057
|
+
//#endregion
|
|
2058
|
+
export { getShellConfig as $, createBashTool as A, findMostRecentSession as At, defineTool as B, COMPACTION_SUMMARY_SUFFIX as Bt, UserMessageComponent as C, truncateLine as Ct, convertToPng as D, assertValidSessionId as Dt, compact as E, SessionManager as Et, createGrepTool as F, parseSessionEntries as Ft, generateSummary as G, createCustomMessage as Gt, findCutPoint as H, convertToLlm as Ht, createLsTool as I, sessionEntryToContextMessages as It, getLanguageFromPath as J, generateSummaryWithUsage as K, createReadOnlyTools as L, BRANCH_SUMMARY_PREFIX as Lt, createEditTool as M, getLatestCompactionEntry as Mt, createEventBus as N, loadEntriesFromFile as Nt, copyToClipboard as O, buildContextEntries as Ot, createFindTool as P, migrateSessionEntries as Pt, getSettingsListTheme as Q, createReadTool as R, BRANCH_SUMMARY_SUFFIX as Rt, ToolExecutionComponent as S, truncateHead as St, calculateContextTokens as T, CURRENT_SESSION_VERSION as Tt, formatSkillsForPrompt as U, createBranchSummaryMessage as Ut, estimateTokens as V, bashExecutionToText as Vt, generateBranchSummary as W, createCompactionSummaryMessage as Wt, getPackageDir as X, getMarkdownTheme as Y, getSelectListTheme as Z, ModelRuntime as _, wrapRegisteredTool as _t, CustomMessageComponent as a, loadSkillsFromDir as at, SettingsSelectorComponent as b, DEFAULT_MAX_LINES as bt, DefaultResourceLoader as c, rawKeyHint as ct, ExtensionInputComponent as d, resizeImage as dt, highlightCode as et, ExtensionSelectorComponent as f, serializeConversation as ft, ModelRegistry as g, truncateToVisualLines as gt, InMemorySettingsStorage as h, theme as ht, CustomEditor as i, loadSkills as it, createCodingTools as j, getDefaultSessionDir as jt, createAgentSession as k, buildSessionContext as kt, DynamicBorder as l, readStoredCredential as lt, FooterComponent as m, stripFrontmatter as mt, BorderedLoader as n, keyHint as nt, DEFAULT_COMPACTION_SETTINGS as o, parseFrontmatter as ot, FileSettingsStorage as p, shouldCompact as pt, getBinDir as q, CONFIG_DIR_NAME as r, keyText as rt, DefaultPackageManager as s, parseSkillBlock as st, AssistantMessageComponent as t, initTheme as tt, ExtensionEditorComponent as u, renderDiff as ut, ProjectTrustStore as v, withFileMutationQueue as vt, VERSION as w, truncateTail as wt, Theme as x, formatSize as xt, SettingsManager as y, DEFAULT_MAX_BYTES as yt, createWriteTool as z, COMPACTION_SUMMARY_PREFIX as zt };
|
|
2059
|
+
|
|
2060
|
+
//# sourceMappingURL=pi-coding-agent-Dsg6_0ua.mjs.map
|