@getpaseo/server 0.1.91-beta.1 → 0.1.91
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/dist/scripts/supervisor.js +21 -0
- package/dist/server/server/agent/agent-manager.d.ts +1 -1
- package/dist/server/server/agent/agent-manager.js +23 -48
- package/dist/server/server/agent/agent-sdk-types.d.ts +15 -0
- package/dist/server/server/agent/prompt-attachments.js +8 -0
- package/dist/server/server/agent/provider-registry.d.ts +0 -1
- package/dist/server/server/agent/provider-registry.js +22 -4
- package/dist/server/server/agent/provider-snapshot-manager.js +19 -1
- package/dist/server/server/agent/providers/claude/agent.d.ts +5 -2
- package/dist/server/server/agent/providers/claude/agent.js +6 -2
- package/dist/server/server/agent/providers/claude/models.d.ts +1 -1
- package/dist/server/server/agent/providers/claude/models.js +6 -6
- package/dist/server/server/agent/providers/codex-app-server-agent.js +9 -5
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +1 -0
- package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +4 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +16 -2
- package/dist/server/server/agent/providers/opencode-agent.js +75 -4
- package/dist/server/server/agent/providers/pi/agent.d.ts +23 -1
- package/dist/server/server/agent/providers/pi/agent.js +219 -13
- package/dist/server/server/agent/providers/pi/cli-runtime.js +9 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +9 -0
- package/dist/server/server/agent/providers/pi/runtime.d.ts +2 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +12 -0
- package/dist/server/server/agent/providers/pi/session-descriptor.js +304 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +8 -0
- package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +22 -0
- package/dist/server/server/agent/runtime-mcp-config.d.ts +8 -0
- package/dist/server/server/agent/runtime-mcp-config.js +50 -0
- package/dist/server/server/auth.js +16 -1
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +2 -2
- package/dist/server/server/daemon-worker.js +84 -1
- package/dist/server/server/file-upload/index.d.ts +27 -0
- package/dist/server/server/file-upload/index.js +158 -0
- package/dist/server/server/loop-service.d.ts +12 -12
- package/dist/server/server/persisted-config.d.ts +11 -0
- package/dist/server/server/persisted-config.js +2 -1
- package/dist/server/server/persistence-hooks.js +6 -4
- package/dist/server/server/session.d.ts +5 -2
- package/dist/server/server/session.js +20 -2
- package/dist/server/server/speech/providers/local/runtime.js +1 -0
- package/dist/server/server/speech/providers/local/worker-client.d.ts +14 -1
- package/dist/server/server/speech/providers/local/worker-client.js +169 -7
- package/dist/server/server/websocket-server.d.ts +2 -0
- package/dist/server/server/websocket-server.js +20 -7
- package/dist/server/server/workspace-registry.d.ts +4 -4
- package/dist/server/utils/directory-suggestions.js +10 -5
- package/dist/server/utils/worktree.d.ts +4 -0
- package/dist/server/utils/worktree.js +19 -2
- package/dist/src/server/persisted-config.js +2 -1
- package/package.json +5 -5
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { open, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRealpathAwarePathMatcher } from "../../../../utils/path.js";
|
|
5
|
+
const PI_PROVIDER = "pi";
|
|
6
|
+
const PI_CONFIG_DIR_NAME = ".pi";
|
|
7
|
+
const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
8
|
+
const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
|
|
9
|
+
const HEAD_BYTES = 64 * 1024;
|
|
10
|
+
const TAIL_BYTES = 256 * 1024;
|
|
11
|
+
const FULL_SCAN_LINE_LIMIT = 2000;
|
|
12
|
+
export async function listPiPersistedAgents(options = {}) {
|
|
13
|
+
const provider = options.provider ?? PI_PROVIDER;
|
|
14
|
+
const sessionsDir = await resolvePiSessionsDir(options);
|
|
15
|
+
const files = await walkJsonlFiles(sessionsDir);
|
|
16
|
+
const matchesCwd = options.cwd ? createRealpathAwarePathMatcher(options.cwd) : null;
|
|
17
|
+
const limit = options.limit ?? 20;
|
|
18
|
+
const descriptors = [];
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const descriptor = await readPiSessionDescriptor(file, provider);
|
|
21
|
+
if (!descriptor)
|
|
22
|
+
continue;
|
|
23
|
+
if (matchesCwd && !matchesCwd(descriptor.cwd))
|
|
24
|
+
continue;
|
|
25
|
+
descriptors.push(descriptor);
|
|
26
|
+
}
|
|
27
|
+
return descriptors
|
|
28
|
+
.sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
|
|
29
|
+
.slice(0, limit);
|
|
30
|
+
}
|
|
31
|
+
async function resolvePiSessionsDir(options) {
|
|
32
|
+
const env = options.env ?? process.env;
|
|
33
|
+
const homeDir = options.homeDir ?? homedir();
|
|
34
|
+
const baseDir = options.cwd ?? process.cwd();
|
|
35
|
+
if (options.sessionDir?.trim()) {
|
|
36
|
+
return resolveConfigPath(options.sessionDir, { baseDir, homeDir });
|
|
37
|
+
}
|
|
38
|
+
const agentDir = resolvePiAgentDir({ runtimeSettings: options.runtimeSettings, env, homeDir });
|
|
39
|
+
const envSessionDir = options.runtimeSettings?.env?.[PI_SESSION_DIR_ENV] ?? env[PI_SESSION_DIR_ENV];
|
|
40
|
+
if (envSessionDir?.trim()) {
|
|
41
|
+
return resolveConfigPath(envSessionDir, { baseDir, homeDir });
|
|
42
|
+
}
|
|
43
|
+
const settingsSessionDir = await readConfiguredSessionDir({
|
|
44
|
+
agentDir,
|
|
45
|
+
cwd: options.cwd,
|
|
46
|
+
});
|
|
47
|
+
if (settingsSessionDir?.trim()) {
|
|
48
|
+
return resolveConfigPath(settingsSessionDir, { baseDir, homeDir });
|
|
49
|
+
}
|
|
50
|
+
return path.join(agentDir, "sessions");
|
|
51
|
+
}
|
|
52
|
+
function resolvePiAgentDir(input) {
|
|
53
|
+
const configured = input.runtimeSettings?.env?.[PI_AGENT_DIR_ENV] ?? input.env[PI_AGENT_DIR_ENV];
|
|
54
|
+
if (configured?.trim()) {
|
|
55
|
+
return resolveConfigPath(configured, { baseDir: process.cwd(), homeDir: input.homeDir });
|
|
56
|
+
}
|
|
57
|
+
return path.join(input.homeDir, PI_CONFIG_DIR_NAME, "agent");
|
|
58
|
+
}
|
|
59
|
+
async function readConfiguredSessionDir(input) {
|
|
60
|
+
const values = await Promise.all([
|
|
61
|
+
readSessionDirFromSettings(path.join(input.agentDir, "settings.json")),
|
|
62
|
+
input.cwd
|
|
63
|
+
? readSessionDirFromSettings(path.join(input.cwd, PI_CONFIG_DIR_NAME, "settings.json"))
|
|
64
|
+
: null,
|
|
65
|
+
]);
|
|
66
|
+
return values[1] ?? values[0] ?? null;
|
|
67
|
+
}
|
|
68
|
+
async function readSessionDirFromSettings(settingsPath) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(await readFile(settingsPath, "utf8"));
|
|
71
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const sessionDir = Reflect.get(parsed, "sessionDir");
|
|
75
|
+
return typeof sessionDir === "string" && sessionDir.trim() ? sessionDir : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function resolveConfigPath(value, options) {
|
|
82
|
+
if (value === "~") {
|
|
83
|
+
return options.homeDir;
|
|
84
|
+
}
|
|
85
|
+
if (value.startsWith("~/")) {
|
|
86
|
+
return path.join(options.homeDir, value.slice(2));
|
|
87
|
+
}
|
|
88
|
+
return path.isAbsolute(value) ? value : path.resolve(options.baseDir, value);
|
|
89
|
+
}
|
|
90
|
+
async function walkJsonlFiles(root) {
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
99
|
+
const entryPath = path.join(root, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
return await walkJsonlFiles(entryPath);
|
|
102
|
+
}
|
|
103
|
+
return entry.isFile() && entry.name.endsWith(".jsonl") ? [entryPath] : [];
|
|
104
|
+
}));
|
|
105
|
+
return files.flat();
|
|
106
|
+
}
|
|
107
|
+
async function readPiSessionDescriptor(filePath, provider) {
|
|
108
|
+
const firstLine = await readFirstLine(filePath);
|
|
109
|
+
if (!firstLine)
|
|
110
|
+
return null;
|
|
111
|
+
const header = parseSessionHeader(firstLine);
|
|
112
|
+
if (!header)
|
|
113
|
+
return null;
|
|
114
|
+
const tail = await readTail(filePath).catch(() => "");
|
|
115
|
+
const tailInfo = parseSessionTail(tail);
|
|
116
|
+
const headInfo = await scanSessionHead(filePath);
|
|
117
|
+
const title = tailInfo.title ?? headInfo.title ?? headInfo.firstUserMessage;
|
|
118
|
+
const lastActivityAt = tailInfo.lastActivityAt ?? (await readFileMtime(filePath)) ?? header.createdAt ?? new Date(0);
|
|
119
|
+
const timeline = buildPreviewTimeline({
|
|
120
|
+
firstUserMessage: headInfo.firstUserMessage,
|
|
121
|
+
lastUserMessage: tailInfo.lastUserMessage,
|
|
122
|
+
});
|
|
123
|
+
const persistence = {
|
|
124
|
+
provider,
|
|
125
|
+
sessionId: header.sessionId,
|
|
126
|
+
nativeHandle: filePath,
|
|
127
|
+
metadata: {
|
|
128
|
+
provider,
|
|
129
|
+
cwd: header.cwd,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
provider,
|
|
134
|
+
sessionId: header.sessionId,
|
|
135
|
+
cwd: header.cwd,
|
|
136
|
+
title,
|
|
137
|
+
lastActivityAt,
|
|
138
|
+
persistence,
|
|
139
|
+
timeline,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function readFirstLine(filePath) {
|
|
143
|
+
const handle = await open(filePath, "r").catch(() => null);
|
|
144
|
+
if (!handle)
|
|
145
|
+
return null;
|
|
146
|
+
try {
|
|
147
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
148
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
149
|
+
if (bytesRead <= 0)
|
|
150
|
+
return null;
|
|
151
|
+
const chunk = buffer.subarray(0, bytesRead).toString("utf8");
|
|
152
|
+
const newlineIndex = chunk.indexOf("\n");
|
|
153
|
+
return (newlineIndex === -1 ? chunk : chunk.slice(0, newlineIndex)).trim();
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await handle.close().catch(() => undefined);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function readTail(filePath) {
|
|
160
|
+
const fileStats = await stat(filePath);
|
|
161
|
+
const start = Math.max(0, fileStats.size - TAIL_BYTES);
|
|
162
|
+
const length = fileStats.size - start;
|
|
163
|
+
const handle = await open(filePath, "r");
|
|
164
|
+
try {
|
|
165
|
+
const buffer = Buffer.alloc(length);
|
|
166
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
167
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await handle.close().catch(() => undefined);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function readFileMtime(filePath) {
|
|
174
|
+
try {
|
|
175
|
+
return (await stat(filePath)).mtime;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function parseSessionHeader(firstLine) {
|
|
182
|
+
const entry = parseJsonRecord(firstLine);
|
|
183
|
+
if (!entry || entry.type !== "session")
|
|
184
|
+
return null;
|
|
185
|
+
const sessionId = typeof entry.id === "string" ? entry.id : null;
|
|
186
|
+
const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
|
|
187
|
+
if (!sessionId || !cwd)
|
|
188
|
+
return null;
|
|
189
|
+
const createdAt = parseDate(entry.timestamp);
|
|
190
|
+
return { sessionId, cwd, createdAt };
|
|
191
|
+
}
|
|
192
|
+
function parseSessionTail(tail) {
|
|
193
|
+
const lines = tail.split(/\r?\n/u);
|
|
194
|
+
let title = null;
|
|
195
|
+
let lastActivityAt = null;
|
|
196
|
+
let fallbackTimestamp = null;
|
|
197
|
+
let lastUserMessage = null;
|
|
198
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
199
|
+
const entry = parseJsonRecord(lines[index].trim());
|
|
200
|
+
if (!entry)
|
|
201
|
+
continue;
|
|
202
|
+
if (!title && entry.type === "session_info") {
|
|
203
|
+
title = readNonEmptyString(entry.name);
|
|
204
|
+
}
|
|
205
|
+
const entryTimestamp = parseDate(entry.timestamp);
|
|
206
|
+
if (!fallbackTimestamp && entryTimestamp) {
|
|
207
|
+
fallbackTimestamp = entryTimestamp;
|
|
208
|
+
}
|
|
209
|
+
if (entry.type !== "message") {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!lastActivityAt && entryTimestamp) {
|
|
213
|
+
lastActivityAt = entryTimestamp;
|
|
214
|
+
}
|
|
215
|
+
if (!lastUserMessage && isRecord(entry.message) && entry.message.role === "user") {
|
|
216
|
+
lastUserMessage = extractMessageText(entry.message.content);
|
|
217
|
+
}
|
|
218
|
+
if (title && lastActivityAt && lastUserMessage) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { title, lastActivityAt: lastActivityAt ?? fallbackTimestamp, lastUserMessage };
|
|
223
|
+
}
|
|
224
|
+
async function scanSessionHead(filePath) {
|
|
225
|
+
let content;
|
|
226
|
+
try {
|
|
227
|
+
content = await readFile(filePath, "utf8");
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { title: null, firstUserMessage: null };
|
|
231
|
+
}
|
|
232
|
+
let title = null;
|
|
233
|
+
let firstUserMessage = null;
|
|
234
|
+
let lineCount = 0;
|
|
235
|
+
for (const rawLine of content.split(/\r?\n/u)) {
|
|
236
|
+
lineCount += 1;
|
|
237
|
+
const entry = parseJsonRecord(rawLine.trim());
|
|
238
|
+
if (!entry)
|
|
239
|
+
continue;
|
|
240
|
+
if (entry.type === "session_info") {
|
|
241
|
+
title = readNonEmptyString(entry.name) ?? title;
|
|
242
|
+
}
|
|
243
|
+
if (!firstUserMessage && entry.type === "message" && isRecord(entry.message)) {
|
|
244
|
+
if (entry.message.role === "user") {
|
|
245
|
+
firstUserMessage = extractMessageText(entry.message.content);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (title && firstUserMessage) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (lineCount >= FULL_SCAN_LINE_LIMIT && firstUserMessage) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { title, firstUserMessage };
|
|
256
|
+
}
|
|
257
|
+
function buildPreviewTimeline(input) {
|
|
258
|
+
const items = [];
|
|
259
|
+
if (input.firstUserMessage) {
|
|
260
|
+
items.push({ type: "user_message", text: input.firstUserMessage });
|
|
261
|
+
}
|
|
262
|
+
if (input.lastUserMessage && input.lastUserMessage !== input.firstUserMessage) {
|
|
263
|
+
items.push({ type: "user_message", text: input.lastUserMessage });
|
|
264
|
+
}
|
|
265
|
+
return items;
|
|
266
|
+
}
|
|
267
|
+
function parseJsonRecord(line) {
|
|
268
|
+
if (!line)
|
|
269
|
+
return null;
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(line);
|
|
272
|
+
return isRecord(parsed) ? parsed : null;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function isRecord(value) {
|
|
279
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
280
|
+
}
|
|
281
|
+
function readNonEmptyString(value) {
|
|
282
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
283
|
+
}
|
|
284
|
+
function parseDate(value) {
|
|
285
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const date = new Date(value);
|
|
289
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
290
|
+
}
|
|
291
|
+
function extractMessageText(content) {
|
|
292
|
+
if (typeof content === "string") {
|
|
293
|
+
return content.trim() || null;
|
|
294
|
+
}
|
|
295
|
+
if (!Array.isArray(content)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const text = content
|
|
299
|
+
.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : [])
|
|
300
|
+
.join("\n\n")
|
|
301
|
+
.trim();
|
|
302
|
+
return text || null;
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=session-descriptor.js.map
|
|
@@ -15,6 +15,10 @@ export declare class FakePiSession implements PiRuntimeSession {
|
|
|
15
15
|
message: string;
|
|
16
16
|
imageCount: number;
|
|
17
17
|
}>;
|
|
18
|
+
readonly compactRequests: Array<{
|
|
19
|
+
customInstructions?: string;
|
|
20
|
+
}>;
|
|
21
|
+
readonly setAutoCompactionRequests: boolean[];
|
|
18
22
|
readonly setModelRequests: Array<{
|
|
19
23
|
provider: string;
|
|
20
24
|
modelId: string;
|
|
@@ -41,6 +45,8 @@ export declare class FakePiSession implements PiRuntimeSession {
|
|
|
41
45
|
messages: PiAgentMessage[];
|
|
42
46
|
stats: PiSessionStats;
|
|
43
47
|
commands: PiRpcSlashCommand[];
|
|
48
|
+
compactError: Error | null;
|
|
49
|
+
emitCompactEnd: boolean;
|
|
44
50
|
state: PiSessionState;
|
|
45
51
|
private readonly subscribers;
|
|
46
52
|
constructor(launch: PiRuntimeLaunch);
|
|
@@ -50,6 +56,8 @@ export declare class FakePiSession implements PiRuntimeSession {
|
|
|
50
56
|
data: string;
|
|
51
57
|
mimeType: string;
|
|
52
58
|
}>): Promise<void>;
|
|
59
|
+
compact(customInstructions?: string): Promise<void>;
|
|
60
|
+
setAutoCompaction(enabled: boolean): Promise<void>;
|
|
53
61
|
abort(): Promise<void>;
|
|
54
62
|
getState(): Promise<PiSessionState>;
|
|
55
63
|
getMessages(): Promise<PiAgentMessage[]>;
|
|
@@ -31,6 +31,8 @@ export class FakePi {
|
|
|
31
31
|
export class FakePiSession {
|
|
32
32
|
constructor(launch) {
|
|
33
33
|
this.prompts = [];
|
|
34
|
+
this.compactRequests = [];
|
|
35
|
+
this.setAutoCompactionRequests = [];
|
|
34
36
|
this.setModelRequests = [];
|
|
35
37
|
this.setThinkingLevelRequests = [];
|
|
36
38
|
this.treeNavigationRequests = [];
|
|
@@ -46,12 +48,15 @@ export class FakePiSession {
|
|
|
46
48
|
cost: 0,
|
|
47
49
|
};
|
|
48
50
|
this.commands = [];
|
|
51
|
+
this.compactError = null;
|
|
52
|
+
this.emitCompactEnd = true;
|
|
49
53
|
this.subscribers = new Set();
|
|
50
54
|
this.state = {
|
|
51
55
|
model: null,
|
|
52
56
|
thinkingLevel: "medium",
|
|
53
57
|
isStreaming: false,
|
|
54
58
|
isCompacting: false,
|
|
59
|
+
autoCompactionEnabled: true,
|
|
55
60
|
sessionFile: launch.session ?? "/tmp/pi-session",
|
|
56
61
|
sessionId: "pi-session-1",
|
|
57
62
|
messageCount: 0,
|
|
@@ -69,6 +74,23 @@ export class FakePiSession {
|
|
|
69
74
|
this.handleTreeNavigationCommand(message);
|
|
70
75
|
this.handleEntryCaptureCommand(message);
|
|
71
76
|
}
|
|
77
|
+
async compact(customInstructions) {
|
|
78
|
+
this.compactRequests.push(customInstructions === undefined ? {} : { customInstructions });
|
|
79
|
+
this.emit({ type: "compaction_start", reason: "manual" });
|
|
80
|
+
if (this.emitCompactEnd) {
|
|
81
|
+
this.emit({ type: "compaction_end", reason: "manual" });
|
|
82
|
+
}
|
|
83
|
+
if (this.compactError) {
|
|
84
|
+
throw this.compactError;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async setAutoCompaction(enabled) {
|
|
88
|
+
this.setAutoCompactionRequests.push(enabled);
|
|
89
|
+
this.state = {
|
|
90
|
+
...this.state,
|
|
91
|
+
autoCompactionEnabled: enabled,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
72
94
|
async abort() {
|
|
73
95
|
this.abortRequested = true;
|
|
74
96
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentSessionConfig } from "./agent-sdk-types.js";
|
|
2
|
+
export declare function stripInternalPaseoMcpServer(config: AgentSessionConfig): AgentSessionConfig;
|
|
3
|
+
export declare function withRuntimePaseoMcpServer(params: {
|
|
4
|
+
config: AgentSessionConfig;
|
|
5
|
+
agentId: string;
|
|
6
|
+
mcpBaseUrl: string | null;
|
|
7
|
+
}): AgentSessionConfig;
|
|
8
|
+
//# sourceMappingURL=runtime-mcp-config.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const PASEO_MCP_SERVER_NAME = "paseo";
|
|
2
|
+
const PASEO_MCP_PATHNAME = "/mcp/agents";
|
|
3
|
+
export function stripInternalPaseoMcpServer(config) {
|
|
4
|
+
const mcpServers = config.mcpServers;
|
|
5
|
+
if (!mcpServers) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
const paseoServer = mcpServers[PASEO_MCP_SERVER_NAME];
|
|
9
|
+
if (!paseoServer || !isInternalPaseoMcpServer(paseoServer)) {
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
12
|
+
const nextMcpServers = { ...mcpServers };
|
|
13
|
+
delete nextMcpServers[PASEO_MCP_SERVER_NAME];
|
|
14
|
+
const next = { ...config };
|
|
15
|
+
if (Object.keys(nextMcpServers).length > 0) {
|
|
16
|
+
next.mcpServers = nextMcpServers;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
delete next.mcpServers;
|
|
20
|
+
}
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
export function withRuntimePaseoMcpServer(params) {
|
|
24
|
+
const storedConfig = stripInternalPaseoMcpServer(params.config);
|
|
25
|
+
if (!params.mcpBaseUrl || storedConfig.mcpServers?.[PASEO_MCP_SERVER_NAME]) {
|
|
26
|
+
return storedConfig;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...storedConfig,
|
|
30
|
+
mcpServers: {
|
|
31
|
+
[PASEO_MCP_SERVER_NAME]: {
|
|
32
|
+
type: "http",
|
|
33
|
+
url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`,
|
|
34
|
+
},
|
|
35
|
+
...storedConfig.mcpServers,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function isInternalPaseoMcpServer(config) {
|
|
40
|
+
if (config.type !== "http" && config.type !== "sse") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return new URL(config.url).pathname === PASEO_MCP_PATHNAME;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=runtime-mcp-config.js.map
|
|
@@ -84,10 +84,25 @@ export function createRequireBearerMiddleware(auth, onReject) {
|
|
|
84
84
|
})();
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
// Routes that authenticate via their own capability and therefore must not be
|
|
88
|
+
// gated a second time behind the daemon password.
|
|
89
|
+
const BEARER_AUTH_BYPASS_PATHS = new Set([
|
|
90
|
+
// Unauthenticated liveness probe.
|
|
91
|
+
"/api/health",
|
|
92
|
+
// Guarded by a single-use download token (crypto-random UUID, 60s TTL,
|
|
93
|
+
// consumed on first use) that is only ever issued over the
|
|
94
|
+
// already-authenticated WebSocket. The token IS the capability for this
|
|
95
|
+
// route. Requiring the daemon password on top of it breaks browser and
|
|
96
|
+
// Electron downloads: those trigger the download via an anchor navigation,
|
|
97
|
+
// which cannot attach an `Authorization` header. The download endpoint still
|
|
98
|
+
// rejects requests without a valid token (400/403), so dropping the bearer
|
|
99
|
+
// here does not make the route unauthenticated.
|
|
100
|
+
"/api/files/download",
|
|
101
|
+
]);
|
|
87
102
|
export function shouldBypassBearerAuth(method, path) {
|
|
88
103
|
if (method === "OPTIONS") {
|
|
89
104
|
return true;
|
|
90
105
|
}
|
|
91
|
-
return path
|
|
106
|
+
return BEARER_AUTH_BYPASS_PATHS.has(path);
|
|
92
107
|
}
|
|
93
108
|
//# sourceMappingURL=auth.js.map
|
|
@@ -34,10 +34,10 @@ export async function archiveIfSafe(input) {
|
|
|
34
34
|
if (!snapshot) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
if (snapshot.git.isDirty === true
|
|
37
|
+
if (snapshot.git.isDirty === true) {
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
if (snapshot.git.aheadOfOrigin > 0) {
|
|
40
|
+
if (typeof snapshot.git.aheadOfOrigin === "number" && snapshot.git.aheadOfOrigin > 0) {
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
const ownership = await deps.isPaseoOwnedWorktreeCwd(cwd, {
|
|
@@ -1,8 +1,39 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { createPaseoDaemon } from "./bootstrap.js";
|
|
2
4
|
import { loadConfig } from "./config.js";
|
|
3
5
|
import { resolvePaseoHome } from "./paseo-home.js";
|
|
4
6
|
import { createRootLogger } from "./logger.js";
|
|
5
7
|
process.title = "Paseo Daemon";
|
|
8
|
+
function isPidAlive(pid) {
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "EPERM") {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeWorkerLifecycleLog(paseoHome, message, fields = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const logPath = path.join(paseoHome, "daemon.log");
|
|
23
|
+
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
24
|
+
appendFileSync(logPath, `${JSON.stringify({
|
|
25
|
+
level: "warn",
|
|
26
|
+
time: new Date().toISOString(),
|
|
27
|
+
pid: process.pid,
|
|
28
|
+
name: "DaemonWorker",
|
|
29
|
+
msg: message,
|
|
30
|
+
...fields,
|
|
31
|
+
})}\n`, "utf8");
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Exit-reason logging must never prevent the worker from exiting.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
6
37
|
function bootstrapFromEnvironment() {
|
|
7
38
|
try {
|
|
8
39
|
const paseoHome = resolvePaseoHome();
|
|
@@ -31,7 +62,7 @@ function applyCliFlagOverrides(config) {
|
|
|
31
62
|
}
|
|
32
63
|
}
|
|
33
64
|
async function main() {
|
|
34
|
-
const { logger, config } = bootstrapFromEnvironment();
|
|
65
|
+
const { paseoHome, logger, config } = bootstrapFromEnvironment();
|
|
35
66
|
let daemon = null;
|
|
36
67
|
let shutdownPromise = null;
|
|
37
68
|
let exitHookInstalled = false;
|
|
@@ -107,6 +138,58 @@ async function main() {
|
|
|
107
138
|
}
|
|
108
139
|
beginShutdown("restart lifecycle intent", { successExitCode: 0 });
|
|
109
140
|
};
|
|
141
|
+
const installSupervisorLivenessGuard = () => {
|
|
142
|
+
if (typeof process.send !== "function") {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const supervisorPid = process.ppid;
|
|
146
|
+
let lastSupervisorHeartbeatAt = Date.now();
|
|
147
|
+
let supervisorExitRequested = false;
|
|
148
|
+
const exitAfterSupervisorLoss = (reason) => {
|
|
149
|
+
if (supervisorExitRequested) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
supervisorExitRequested = true;
|
|
153
|
+
writeWorkerLifecycleLog(paseoHome, "Supervisor liveness lost; worker exiting", {
|
|
154
|
+
reason,
|
|
155
|
+
supervisorPid,
|
|
156
|
+
currentParentPid: process.ppid,
|
|
157
|
+
ipcConnected: typeof process.connected === "boolean" ? process.connected : null,
|
|
158
|
+
heartbeatAgeMs: Date.now() - lastSupervisorHeartbeatAt,
|
|
159
|
+
});
|
|
160
|
+
// The supervisor owns the worker's stdout/stderr pipes. Once it is gone,
|
|
161
|
+
// logging during graceful shutdown can block on the broken pipe and leave
|
|
162
|
+
// the daemon orphaned, so supervisor loss is a hard process boundary.
|
|
163
|
+
process.exit(0);
|
|
164
|
+
};
|
|
165
|
+
process.on("message", (message) => {
|
|
166
|
+
if (typeof message === "object" &&
|
|
167
|
+
message !== null &&
|
|
168
|
+
"type" in message &&
|
|
169
|
+
message.type === "paseo:supervisor-heartbeat") {
|
|
170
|
+
lastSupervisorHeartbeatAt = Date.now();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
process.on("disconnect", () => exitAfterSupervisorLoss("ipc_disconnect_event"));
|
|
174
|
+
const timer = setInterval(() => {
|
|
175
|
+
const ipcConnected = typeof process.connected === "boolean" ? process.connected : true;
|
|
176
|
+
const heartbeatExpired = Date.now() - lastSupervisorHeartbeatAt > 3500;
|
|
177
|
+
const supervisorChanged = process.ppid !== supervisorPid;
|
|
178
|
+
if (ipcConnected === false) {
|
|
179
|
+
exitAfterSupervisorLoss("ipc_disconnected");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (supervisorChanged) {
|
|
183
|
+
exitAfterSupervisorLoss("supervisor_parent_pid_changed");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (heartbeatExpired && !isPidAlive(supervisorPid)) {
|
|
187
|
+
exitAfterSupervisorLoss("supervisor_pid_dead");
|
|
188
|
+
}
|
|
189
|
+
}, 1000);
|
|
190
|
+
timer.unref();
|
|
191
|
+
};
|
|
192
|
+
installSupervisorLivenessGuard();
|
|
110
193
|
try {
|
|
111
194
|
daemon = await createPaseoDaemon({
|
|
112
195
|
...config,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type FileTransferFrame } from "@getpaseo/protocol/binary-frames/index";
|
|
2
|
+
import type { FileUploadRequest, FileUploadResponse } from "../messages.js";
|
|
3
|
+
interface FileUploadStoreOptions {
|
|
4
|
+
paseoHome: string;
|
|
5
|
+
staleUploadTimeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class FileUploadStore {
|
|
8
|
+
private static readonly defaultStaleUploadTimeoutMs;
|
|
9
|
+
private readonly paseoHome;
|
|
10
|
+
private readonly staleUploadTimeoutMs;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
constructor(options: FileUploadStoreOptions);
|
|
13
|
+
beginUpload(request: FileUploadRequest): void;
|
|
14
|
+
receiveFrame(frame: FileTransferFrame): Promise<FileUploadResponse | null>;
|
|
15
|
+
private applyFrame;
|
|
16
|
+
private startWriting;
|
|
17
|
+
private writeChunk;
|
|
18
|
+
private completeUpload;
|
|
19
|
+
private createStaleUploadTimeout;
|
|
20
|
+
private refreshStaleUploadTimeout;
|
|
21
|
+
private expireStaleUpload;
|
|
22
|
+
private clearPendingUpload;
|
|
23
|
+
private removeFailedUpload;
|
|
24
|
+
private removeUploadDirectory;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|