pi-webdesk 0.1.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/README.md +111 -0
- package/dist/apps/daemon/src/appearance-preferences.js +218 -0
- package/dist/apps/daemon/src/auth.js +88 -0
- package/dist/apps/daemon/src/bin.js +123 -0
- package/dist/apps/daemon/src/cli.js +48 -0
- package/dist/apps/daemon/src/event-hub.js +155 -0
- package/dist/apps/daemon/src/index.js +102 -0
- package/dist/apps/daemon/src/launcher-control.js +114 -0
- package/dist/apps/daemon/src/launcher.js +73 -0
- package/dist/apps/daemon/src/pi-auth.js +290 -0
- package/dist/apps/daemon/src/pi-resources.js +182 -0
- package/dist/apps/daemon/src/pi-runtime-factory.js +19 -0
- package/dist/apps/daemon/src/pi-sessions.js +265 -0
- package/dist/apps/daemon/src/runtime-process.js +241 -0
- package/dist/apps/daemon/src/secret.js +71 -0
- package/dist/apps/daemon/src/server.js +1662 -0
- package/dist/apps/daemon/src/session-projection.js +117 -0
- package/dist/apps/daemon/src/state-lock.js +31 -0
- package/dist/apps/daemon/src/static-web.js +53 -0
- package/dist/apps/daemon/src/task-archive.js +152 -0
- package/dist/apps/daemon/src/task-commit.js +503 -0
- package/dist/apps/daemon/src/task-merge.js +912 -0
- package/dist/apps/daemon/src/task-review.js +204 -0
- package/dist/apps/daemon/src/task-runtime.js +1124 -0
- package/dist/apps/daemon/src/task-validation.js +352 -0
- package/dist/apps/daemon/src/workspace-store.js +140 -0
- package/dist/apps/daemon/src/workspace.js +795 -0
- package/dist/extensions/webdesk.js +34 -0
- package/dist/packages/git/src/commit.js +675 -0
- package/dist/packages/git/src/errors.js +55 -0
- package/dist/packages/git/src/fingerprint.js +286 -0
- package/dist/packages/git/src/index.js +123 -0
- package/dist/packages/git/src/merge.js +1008 -0
- package/dist/packages/git/src/paths.js +58 -0
- package/dist/packages/git/src/repository.js +77 -0
- package/dist/packages/git/src/review.js +396 -0
- package/dist/packages/git/src/runner.js +110 -0
- package/dist/packages/git/src/validation.js +263 -0
- package/dist/packages/git/src/worktree.js +233 -0
- package/dist/packages/pi-bridge/extensions/pita-policy.js +117 -0
- package/dist/packages/pi-bridge/src/auth.js +80 -0
- package/dist/packages/pi-bridge/src/errors.js +19 -0
- package/dist/packages/pi-bridge/src/handshake.js +43 -0
- package/dist/packages/pi-bridge/src/index.js +76 -0
- package/dist/packages/pi-bridge/src/jsonl.js +105 -0
- package/dist/packages/pi-bridge/src/policy-approval.js +62 -0
- package/dist/packages/pi-bridge/src/resolve.js +59 -0
- package/dist/packages/pi-bridge/src/resources-child.mjs +23 -0
- package/dist/packages/pi-bridge/src/resources.js +481 -0
- package/dist/packages/pi-bridge/src/rpc/client.js +480 -0
- package/dist/packages/pi-bridge/src/rpc/runtime.js +496 -0
- package/dist/packages/pi-bridge/src/rpc/supervisor.mjs +129 -0
- package/dist/packages/pi-bridge/src/rpc/tool-events.js +78 -0
- package/dist/packages/pi-bridge/src/rpc/wire.js +263 -0
- package/dist/packages/pi-bridge/src/runtime.js +0 -0
- package/dist/packages/pi-bridge/src/sessions-child.mjs +38 -0
- package/dist/packages/pi-bridge/src/sessions.js +314 -0
- package/dist/packages/pi-bridge/src/tool-activity.js +56 -0
- package/dist/packages/protocol/src/index.js +1863 -0
- package/dist/web/assets/index-BOw_fhvO.css +2 -0
- package/dist/web/assets/index-oXs7yAAo.js +119 -0
- package/dist/web/index.html +14 -0
- package/package.json +69 -0
- package/scripts/prepare.mjs +7 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// packages/pi-bridge/src/rpc/wire.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { PiBridgeError } from "../errors.js";
|
|
4
|
+
var rpcResponseSchema = z.looseObject({
|
|
5
|
+
type: z.literal("response"),
|
|
6
|
+
id: z.string().optional(),
|
|
7
|
+
command: z.string(),
|
|
8
|
+
success: z.boolean(),
|
|
9
|
+
data: z.unknown().optional(),
|
|
10
|
+
error: z.string().optional()
|
|
11
|
+
});
|
|
12
|
+
var rpcExtensionUiRequestSchema = z.looseObject({
|
|
13
|
+
type: z.literal("extension_ui_request"),
|
|
14
|
+
id: z.string(),
|
|
15
|
+
method: z.string().min(1),
|
|
16
|
+
title: z.string().optional(),
|
|
17
|
+
message: z.string().optional(),
|
|
18
|
+
options: z.array(z.string()).optional(),
|
|
19
|
+
placeholder: z.string().optional(),
|
|
20
|
+
prefill: z.string().optional(),
|
|
21
|
+
timeout: z.number().optional(),
|
|
22
|
+
notifyType: z.enum(["info", "warning", "error"]).optional(),
|
|
23
|
+
statusKey: z.string().optional(),
|
|
24
|
+
statusText: z.string().optional()
|
|
25
|
+
});
|
|
26
|
+
var rpcEventSchema = z.looseObject({
|
|
27
|
+
type: z.string().min(1)
|
|
28
|
+
});
|
|
29
|
+
var rpcExtensionErrorEventSchema = z.looseObject({
|
|
30
|
+
type: z.literal("extension_error"),
|
|
31
|
+
extensionPath: z.string().optional(),
|
|
32
|
+
event: z.string().optional(),
|
|
33
|
+
error: z.string().optional()
|
|
34
|
+
});
|
|
35
|
+
function classifyRpcRecord(value) {
|
|
36
|
+
const base = rpcEventSchema.safeParse(value);
|
|
37
|
+
if (!base.success) {
|
|
38
|
+
throw new PiBridgeError(
|
|
39
|
+
"PI_PROTOCOL_VIOLATION",
|
|
40
|
+
"Pi RPC record is not an object with a string `type`",
|
|
41
|
+
{ details: { record: value } }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (base.data.type === "response") {
|
|
45
|
+
const response = rpcResponseSchema.safeParse(value);
|
|
46
|
+
if (!response.success) {
|
|
47
|
+
throw new PiBridgeError(
|
|
48
|
+
"PI_PROTOCOL_VIOLATION",
|
|
49
|
+
`Pi RPC response record is malformed: ${response.error.message}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return { kind: "response", record: response.data };
|
|
53
|
+
}
|
|
54
|
+
if (base.data.type === "extension_ui_request") {
|
|
55
|
+
const request = rpcExtensionUiRequestSchema.safeParse(value);
|
|
56
|
+
if (!request.success) {
|
|
57
|
+
throw new PiBridgeError(
|
|
58
|
+
"PI_PROTOCOL_VIOLATION",
|
|
59
|
+
`Pi RPC extension_ui_request record is malformed: ${request.error.message}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return { kind: "extension_ui_request", record: request.data };
|
|
63
|
+
}
|
|
64
|
+
return { kind: "event", record: base.data };
|
|
65
|
+
}
|
|
66
|
+
var getStateDataSchema = z.looseObject({
|
|
67
|
+
model: z.looseObject({
|
|
68
|
+
id: z.string(),
|
|
69
|
+
name: z.string().optional(),
|
|
70
|
+
provider: z.string().optional()
|
|
71
|
+
}).nullable().optional(),
|
|
72
|
+
isStreaming: z.boolean(),
|
|
73
|
+
messageCount: z.number().int().min(0),
|
|
74
|
+
sessionFile: z.string().nullable().optional(),
|
|
75
|
+
sessionId: z.string().nullable().optional(),
|
|
76
|
+
sessionName: z.string().optional()
|
|
77
|
+
});
|
|
78
|
+
var wireModelSchema = z.looseObject({
|
|
79
|
+
id: z.string().min(1),
|
|
80
|
+
name: z.string().optional(),
|
|
81
|
+
provider: z.string().optional()
|
|
82
|
+
});
|
|
83
|
+
var getAvailableModelsDataSchema = z.looseObject({
|
|
84
|
+
models: z.array(wireModelSchema)
|
|
85
|
+
});
|
|
86
|
+
var entryMessageSchema = z.looseObject({
|
|
87
|
+
role: z.string().min(1).max(100),
|
|
88
|
+
content: z.unknown().optional()
|
|
89
|
+
});
|
|
90
|
+
var entrySchema = z.looseObject({
|
|
91
|
+
type: z.string().min(1).max(100),
|
|
92
|
+
id: z.string().min(1).max(500),
|
|
93
|
+
parentId: z.string().min(1).max(500).nullable().optional(),
|
|
94
|
+
message: entryMessageSchema.optional()
|
|
95
|
+
});
|
|
96
|
+
function extractEntryText(content) {
|
|
97
|
+
if (typeof content === "string") return content;
|
|
98
|
+
if (!Array.isArray(content)) return null;
|
|
99
|
+
let text = "";
|
|
100
|
+
for (const block of content) {
|
|
101
|
+
if (typeof block === "object" && block !== null && block["type"] === "text" && typeof block["text"] === "string") {
|
|
102
|
+
text += block["text"];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return text === "" ? null : text;
|
|
106
|
+
}
|
|
107
|
+
var getEntriesDataSchema = z.looseObject({
|
|
108
|
+
entries: z.array(entrySchema),
|
|
109
|
+
leafId: z.string().nullable()
|
|
110
|
+
});
|
|
111
|
+
var MAX_WIRE_SESSION_TREE_NODES = 500;
|
|
112
|
+
var MAX_WIRE_SESSION_TREE_DEPTH = 64;
|
|
113
|
+
var MAX_WIRE_SESSION_TREE_SCAN_NODES = 1e5;
|
|
114
|
+
var treeEnvelopeSchema = z.looseObject({
|
|
115
|
+
tree: z.array(z.unknown()),
|
|
116
|
+
leafId: z.string().min(1).max(500).nullable()
|
|
117
|
+
});
|
|
118
|
+
var shallowTreeNodeSchema = z.looseObject({
|
|
119
|
+
entry: entrySchema,
|
|
120
|
+
children: z.array(z.unknown()),
|
|
121
|
+
label: z.string().max(500).optional()
|
|
122
|
+
});
|
|
123
|
+
var getTreeDataSchema = {
|
|
124
|
+
safeParse(value) {
|
|
125
|
+
const envelope = treeEnvelopeSchema.safeParse(value);
|
|
126
|
+
if (!envelope.success) return envelope;
|
|
127
|
+
const flat = [];
|
|
128
|
+
const rootIndexes = [];
|
|
129
|
+
const byId = /* @__PURE__ */ new Map();
|
|
130
|
+
const pending = [];
|
|
131
|
+
let maxDepth = 0;
|
|
132
|
+
for (let index = envelope.data.tree.length - 1; index >= 0; index--) {
|
|
133
|
+
pending.push({
|
|
134
|
+
raw: envelope.data.tree[index],
|
|
135
|
+
parentIndex: null,
|
|
136
|
+
depth: 0
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
while (pending.length > 0 && flat.length < MAX_WIRE_SESSION_TREE_SCAN_NODES) {
|
|
140
|
+
const next = pending.pop();
|
|
141
|
+
const parsed = shallowTreeNodeSchema.safeParse(next.raw);
|
|
142
|
+
if (!parsed.success) {
|
|
143
|
+
return { success: false, error: { message: parsed.error.message } };
|
|
144
|
+
}
|
|
145
|
+
const nodeIndex = flat.length;
|
|
146
|
+
flat.push({
|
|
147
|
+
entry: parsed.data.entry,
|
|
148
|
+
children: [],
|
|
149
|
+
...parsed.data.label === void 0 ? {} : { label: parsed.data.label },
|
|
150
|
+
depth: next.depth
|
|
151
|
+
});
|
|
152
|
+
maxDepth = Math.max(maxDepth, next.depth);
|
|
153
|
+
if (!byId.has(parsed.data.entry.id)) byId.set(parsed.data.entry.id, nodeIndex);
|
|
154
|
+
if (next.parentIndex === null) {
|
|
155
|
+
rootIndexes.push(nodeIndex);
|
|
156
|
+
} else {
|
|
157
|
+
flat[next.parentIndex].children.push(nodeIndex);
|
|
158
|
+
}
|
|
159
|
+
for (let index = parsed.data.children.length - 1; index >= 0; index--) {
|
|
160
|
+
pending.push({
|
|
161
|
+
raw: parsed.data.children[index],
|
|
162
|
+
parentIndex: nodeIndex,
|
|
163
|
+
depth: next.depth + 1
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const completeAndBounded = pending.length === 0 && flat.length <= MAX_WIRE_SESSION_TREE_NODES && maxDepth < MAX_WIRE_SESSION_TREE_DEPTH;
|
|
168
|
+
const makeNode = (index) => {
|
|
169
|
+
const source = flat[index];
|
|
170
|
+
return {
|
|
171
|
+
entry: source.entry,
|
|
172
|
+
children: [],
|
|
173
|
+
...source.label === void 0 ? {} : { label: source.label }
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const project = (indexes) => {
|
|
177
|
+
const roots2 = indexes.map(makeNode);
|
|
178
|
+
const work = indexes.map((index, position) => ({
|
|
179
|
+
index,
|
|
180
|
+
target: roots2[position]
|
|
181
|
+
}));
|
|
182
|
+
while (work.length > 0) {
|
|
183
|
+
const { index, target } = work.pop();
|
|
184
|
+
target.children = flat[index].children.map(makeNode);
|
|
185
|
+
for (let child = 0; child < flat[index].children.length; child++) {
|
|
186
|
+
work.push({
|
|
187
|
+
index: flat[index].children[child],
|
|
188
|
+
target: target.children[child]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return roots2;
|
|
193
|
+
};
|
|
194
|
+
let roots;
|
|
195
|
+
if (completeAndBounded) {
|
|
196
|
+
roots = project(rootIndexes);
|
|
197
|
+
} else {
|
|
198
|
+
const activePath = [];
|
|
199
|
+
const visited = /* @__PURE__ */ new Set();
|
|
200
|
+
let cursor = envelope.data.leafId;
|
|
201
|
+
while (cursor !== null && !visited.has(cursor)) {
|
|
202
|
+
visited.add(cursor);
|
|
203
|
+
const index = byId.get(cursor);
|
|
204
|
+
if (index === void 0) break;
|
|
205
|
+
activePath.push(index);
|
|
206
|
+
cursor = flat[index].entry.parentId ?? null;
|
|
207
|
+
}
|
|
208
|
+
activePath.reverse();
|
|
209
|
+
const retainedPath = activePath.slice(-MAX_WIRE_SESSION_TREE_DEPTH);
|
|
210
|
+
if (retainedPath.length > 0) {
|
|
211
|
+
roots = [makeNode(retainedPath[0])];
|
|
212
|
+
let target = roots[0];
|
|
213
|
+
for (let index = 1; index < retainedPath.length; index++) {
|
|
214
|
+
const child = makeNode(retainedPath[index]);
|
|
215
|
+
target.children = [child];
|
|
216
|
+
target = child;
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
roots = [];
|
|
220
|
+
const work = rootIndexes.slice().reverse().map((index) => ({ index, target: roots, depth: 0 }));
|
|
221
|
+
let emitted = 0;
|
|
222
|
+
while (work.length > 0 && emitted < MAX_WIRE_SESSION_TREE_NODES) {
|
|
223
|
+
const { index, target, depth } = work.pop();
|
|
224
|
+
const node = makeNode(index);
|
|
225
|
+
target.push(node);
|
|
226
|
+
emitted++;
|
|
227
|
+
if (depth + 1 >= MAX_WIRE_SESSION_TREE_DEPTH) continue;
|
|
228
|
+
const children = flat[index].children;
|
|
229
|
+
for (let child = children.length - 1; child >= 0; child--) {
|
|
230
|
+
work.push({
|
|
231
|
+
index: children[child],
|
|
232
|
+
target: node.children,
|
|
233
|
+
depth: depth + 1
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
data: {
|
|
242
|
+
tree: roots,
|
|
243
|
+
leafId: envelope.data.leafId,
|
|
244
|
+
truncated: !completeAndBounded
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
export {
|
|
250
|
+
MAX_WIRE_SESSION_TREE_DEPTH,
|
|
251
|
+
MAX_WIRE_SESSION_TREE_NODES,
|
|
252
|
+
classifyRpcRecord,
|
|
253
|
+
extractEntryText,
|
|
254
|
+
getAvailableModelsDataSchema,
|
|
255
|
+
getEntriesDataSchema,
|
|
256
|
+
getStateDataSchema,
|
|
257
|
+
getTreeDataSchema,
|
|
258
|
+
rpcEventSchema,
|
|
259
|
+
rpcExtensionErrorEventSchema,
|
|
260
|
+
rpcExtensionUiRequestSchema,
|
|
261
|
+
rpcResponseSchema,
|
|
262
|
+
wireModelSchema
|
|
263
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// packages/pi-bridge/src/sessions-child.mjs
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import {
|
|
4
|
+
forkPiSessionInProcess,
|
|
5
|
+
inspectPiSessionsInProcess
|
|
6
|
+
} from "./sessions.js";
|
|
7
|
+
var MAX_REQUEST_BYTES = 16 * 1024;
|
|
8
|
+
var input = "";
|
|
9
|
+
process.stdin.setEncoding("utf8");
|
|
10
|
+
for await (const chunk of process.stdin) {
|
|
11
|
+
input += chunk;
|
|
12
|
+
if (Buffer.byteLength(input, "utf8") > MAX_REQUEST_BYTES) {
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (process.exitCode !== 1) {
|
|
18
|
+
try {
|
|
19
|
+
const request = JSON.parse(input);
|
|
20
|
+
if (request?.operation === "list") {
|
|
21
|
+
process.stdout.write(JSON.stringify(await inspectPiSessionsInProcess()));
|
|
22
|
+
} else if (request?.operation === "fork" && typeof request.sourcePath === "string" && typeof request.targetCwd === "string" && (request.sessionId === void 0 || typeof request.sessionId === "string")) {
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
JSON.stringify(
|
|
25
|
+
await forkPiSessionInProcess({
|
|
26
|
+
sourcePath: request.sourcePath,
|
|
27
|
+
targetCwd: request.targetCwd,
|
|
28
|
+
...request.sessionId === void 0 ? {} : { sessionId: request.sessionId }
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error("invalid operation");
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// packages/pi-bridge/src/sessions.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { open } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
SessionManager,
|
|
9
|
+
SettingsManager,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
parseSessionEntries
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { PiBridgeError } from "./errors.js";
|
|
15
|
+
var DEFAULT_LIST_TIMEOUT_MS = 15e3;
|
|
16
|
+
var DEFAULT_FORK_TIMEOUT_MS = 3e4;
|
|
17
|
+
var MAX_CHILD_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
var MAX_CHILD_ERROR_BYTES = 64 * 1024;
|
|
19
|
+
var MAX_CATALOG_SERIALIZED_ENTRY_BYTES = 14e5;
|
|
20
|
+
var MAX_SOURCE_SESSION_BYTES = 128 * 1024 * 1024;
|
|
21
|
+
var MAX_SOURCE_HEADER_BYTES = 64 * 1024;
|
|
22
|
+
var MAX_PI_SESSION_CATALOG_ENTRIES = 200;
|
|
23
|
+
var PI_SESSION_DIRECTORY_ENV = "PI_CODING_AGENT_SESSION_DIR";
|
|
24
|
+
var catalogEntrySchema = z.object({
|
|
25
|
+
path: z.string().min(1).max(4096).refine(path.isAbsolute),
|
|
26
|
+
id: z.string().min(1).max(500),
|
|
27
|
+
cwd: z.string().min(1).max(4096).refine(path.isAbsolute),
|
|
28
|
+
name: z.string().min(1).max(200).nullable(),
|
|
29
|
+
createdAtMs: z.number().int().nonnegative(),
|
|
30
|
+
modifiedAtMs: z.number().int().nonnegative(),
|
|
31
|
+
messageCount: z.number().int().nonnegative(),
|
|
32
|
+
firstMessage: z.string().max(500)
|
|
33
|
+
}).strict();
|
|
34
|
+
var sourceHeaderSchema = z.object({
|
|
35
|
+
type: z.literal("session"),
|
|
36
|
+
version: z.literal(3),
|
|
37
|
+
id: z.string().min(1).max(500),
|
|
38
|
+
cwd: z.string().min(1).max(4096).refine(path.isAbsolute)
|
|
39
|
+
}).passthrough();
|
|
40
|
+
var catalogInspectionSchema = z.object({
|
|
41
|
+
sessionsRoot: z.string(),
|
|
42
|
+
sessions: z.array(catalogEntrySchema).max(MAX_PI_SESSION_CATALOG_ENTRIES),
|
|
43
|
+
omitted: z.number().int().nonnegative()
|
|
44
|
+
}).strict();
|
|
45
|
+
var forkResultSchema = z.object({
|
|
46
|
+
sessionFile: z.string(),
|
|
47
|
+
sessionId: z.string(),
|
|
48
|
+
cwd: z.string(),
|
|
49
|
+
parentSession: z.string().nullable(),
|
|
50
|
+
sourceSessionId: z.string(),
|
|
51
|
+
sourceCwd: z.string()
|
|
52
|
+
}).strict();
|
|
53
|
+
function expandHome(value) {
|
|
54
|
+
if (value === "~") return os.homedir();
|
|
55
|
+
if (value.startsWith(`~${path.sep}`)) return path.join(os.homedir(), value.slice(2));
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function configuredSessionDir(explicit, env = process.env, cwd = process.cwd(), agentDir = getAgentDir()) {
|
|
59
|
+
const explicitDirectory = explicit?.trim() ? explicit : void 0;
|
|
60
|
+
const environmentDirectory = env[PI_SESSION_DIRECTORY_ENV]?.trim() ? env[PI_SESSION_DIRECTORY_ENV] : void 0;
|
|
61
|
+
const configured = explicitDirectory ?? environmentDirectory ?? SettingsManager.create(cwd, agentDir).getSessionDir();
|
|
62
|
+
return configured === void 0 || configured.trim() === "" ? void 0 : path.resolve(cwd, expandHome(configured));
|
|
63
|
+
}
|
|
64
|
+
function sessionsRoot(sessionDir) {
|
|
65
|
+
return sessionDir ?? path.join(getAgentDir(), "sessions");
|
|
66
|
+
}
|
|
67
|
+
async function inspectPiSessionsInProcess(options = {}) {
|
|
68
|
+
const sessionDir = configuredSessionDir(
|
|
69
|
+
options.sessionDir,
|
|
70
|
+
options.env,
|
|
71
|
+
options.cwd,
|
|
72
|
+
options.agentDir
|
|
73
|
+
);
|
|
74
|
+
const all = await SessionManager.listAll(sessionDir);
|
|
75
|
+
const selected = [];
|
|
76
|
+
let serializedEntryBytes = 0;
|
|
77
|
+
for (const session of all.slice(0, MAX_PI_SESSION_CATALOG_ENTRIES)) {
|
|
78
|
+
try {
|
|
79
|
+
await inspectSourceHeaderReadOnly(path.normalize(session.path));
|
|
80
|
+
const createdAtMs = session.created.getTime();
|
|
81
|
+
const modifiedAtMs = session.modified.getTime();
|
|
82
|
+
if (!Number.isSafeInteger(createdAtMs) || createdAtMs < 0 || !Number.isSafeInteger(modifiedAtMs) || modifiedAtMs < 0 || !Number.isSafeInteger(session.messageCount) || session.messageCount < 0) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const parsed = catalogEntrySchema.safeParse({
|
|
86
|
+
path: path.normalize(session.path),
|
|
87
|
+
id: session.id,
|
|
88
|
+
cwd: path.normalize(session.cwd),
|
|
89
|
+
name: session.name === void 0 || session.name === null ? null : session.name.slice(0, 200) || null,
|
|
90
|
+
createdAtMs,
|
|
91
|
+
modifiedAtMs,
|
|
92
|
+
messageCount: session.messageCount,
|
|
93
|
+
firstMessage: session.firstMessage.slice(0, 500)
|
|
94
|
+
});
|
|
95
|
+
if (!parsed.success) continue;
|
|
96
|
+
const entryBytes = Buffer.byteLength(JSON.stringify(parsed.data), "utf8");
|
|
97
|
+
if (serializedEntryBytes + entryBytes > MAX_CATALOG_SERIALIZED_ENTRY_BYTES) continue;
|
|
98
|
+
serializedEntryBytes += entryBytes;
|
|
99
|
+
selected.push(parsed.data);
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
sessionsRoot: path.normalize(sessionsRoot(sessionDir)),
|
|
105
|
+
sessions: selected,
|
|
106
|
+
omitted: Math.max(0, all.length - selected.length)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function inspectSourceHeaderReadOnly(sourcePath) {
|
|
110
|
+
const handle = await open(sourcePath, "r");
|
|
111
|
+
try {
|
|
112
|
+
const details = await handle.stat();
|
|
113
|
+
if (!details.isFile() || details.size > MAX_SOURCE_SESSION_BYTES) {
|
|
114
|
+
throw new PiBridgeError(
|
|
115
|
+
"PI_INVALID_CONFIGURATION",
|
|
116
|
+
"Pi source session is not a bounded regular file"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const buffer = Buffer.alloc(Math.min(MAX_SOURCE_HEADER_BYTES, Math.max(1, details.size)));
|
|
120
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
121
|
+
const entries = parseSessionEntries(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
122
|
+
const parsed = sourceHeaderSchema.safeParse(entries[0]);
|
|
123
|
+
if (!parsed.success) {
|
|
124
|
+
throw new PiBridgeError(
|
|
125
|
+
"PI_PROTOCOL_VIOLATION",
|
|
126
|
+
"Pi source session is not in the supported current format",
|
|
127
|
+
{ cause: parsed.error }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return { id: parsed.data.id, cwd: path.normalize(parsed.data.cwd) };
|
|
131
|
+
} finally {
|
|
132
|
+
await handle.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function forkPiSessionInProcess(options) {
|
|
136
|
+
if (!path.isAbsolute(options.sourcePath) || !path.isAbsolute(options.targetCwd)) {
|
|
137
|
+
throw new PiBridgeError(
|
|
138
|
+
"PI_INVALID_CONFIGURATION",
|
|
139
|
+
"Pi session continuation requires absolute source and target paths"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const sessionDir = configuredSessionDir(
|
|
143
|
+
options.sessionDir,
|
|
144
|
+
options.env,
|
|
145
|
+
options.cwd,
|
|
146
|
+
options.agentDir
|
|
147
|
+
);
|
|
148
|
+
const source = await inspectSourceHeaderReadOnly(path.normalize(options.sourcePath));
|
|
149
|
+
const manager = SessionManager.forkFrom(
|
|
150
|
+
path.normalize(options.sourcePath),
|
|
151
|
+
path.normalize(options.targetCwd),
|
|
152
|
+
sessionDir,
|
|
153
|
+
options.sessionId === void 0 ? void 0 : { id: options.sessionId }
|
|
154
|
+
);
|
|
155
|
+
const sessionFile = manager.getSessionFile();
|
|
156
|
+
if (sessionFile === void 0) {
|
|
157
|
+
throw new PiBridgeError(
|
|
158
|
+
"PI_PROTOCOL_VIOLATION",
|
|
159
|
+
"Pi did not return a persistent child session file"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const header = manager.getHeader();
|
|
163
|
+
return {
|
|
164
|
+
sessionFile: path.normalize(sessionFile),
|
|
165
|
+
sessionId: manager.getSessionId(),
|
|
166
|
+
cwd: manager.getCwd(),
|
|
167
|
+
parentSession: header?.parentSession ?? null,
|
|
168
|
+
sourceSessionId: source.id,
|
|
169
|
+
sourceCwd: source.cwd
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function validateTimeout(value, label) {
|
|
173
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 6e4) {
|
|
174
|
+
throw new PiBridgeError("PI_INVALID_CONFIGURATION", `${label} timeout is invalid`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function killChild(child) {
|
|
178
|
+
try {
|
|
179
|
+
child.kill("SIGKILL");
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function runChild(options) {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const child = options.spawnProcess(process.execPath, [options.childPath], {
|
|
186
|
+
env: { ...options.env, PI_OFFLINE: "1" },
|
|
187
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
188
|
+
});
|
|
189
|
+
let stdout = Buffer.alloc(0);
|
|
190
|
+
let stderrBytes = 0;
|
|
191
|
+
let settled = false;
|
|
192
|
+
const finish = (operation) => {
|
|
193
|
+
if (settled) return;
|
|
194
|
+
settled = true;
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
operation();
|
|
197
|
+
};
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
killChild(child);
|
|
200
|
+
finish(
|
|
201
|
+
() => reject(new PiBridgeError("PI_REQUEST_TIMEOUT", `${options.operation} timed out`))
|
|
202
|
+
);
|
|
203
|
+
}, options.timeoutMs);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
child.stdout.on("data", (chunk) => {
|
|
206
|
+
if (settled) return;
|
|
207
|
+
if (stdout.length + chunk.length > MAX_CHILD_OUTPUT_BYTES) {
|
|
208
|
+
killChild(child);
|
|
209
|
+
finish(
|
|
210
|
+
() => reject(
|
|
211
|
+
new PiBridgeError(
|
|
212
|
+
"PI_PROTOCOL_VIOLATION",
|
|
213
|
+
`${options.operation} exceeded its output bound`
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
stdout = Buffer.concat([stdout, chunk]);
|
|
220
|
+
});
|
|
221
|
+
child.stderr.on("data", (chunk) => {
|
|
222
|
+
stderrBytes += chunk.length;
|
|
223
|
+
if (stderrBytes > MAX_CHILD_ERROR_BYTES) killChild(child);
|
|
224
|
+
});
|
|
225
|
+
child.on("error", (error) => {
|
|
226
|
+
finish(
|
|
227
|
+
() => reject(
|
|
228
|
+
new PiBridgeError("PI_SPAWN_FAILED", `${options.operation} could not start`, {
|
|
229
|
+
cause: error
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
child.on("close", (code, signal) => {
|
|
235
|
+
finish(() => {
|
|
236
|
+
if (code !== 0 || signal !== null) {
|
|
237
|
+
reject(
|
|
238
|
+
new PiBridgeError("PI_RUNTIME_EXITED", `${options.operation} did not complete`, {
|
|
239
|
+
details: { code, signal }
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let decoded;
|
|
245
|
+
try {
|
|
246
|
+
decoded = JSON.parse(stdout.toString("utf8"));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
reject(
|
|
249
|
+
new PiBridgeError("PI_PROTOCOL_VIOLATION", `${options.operation} returned invalid JSON`, {
|
|
250
|
+
cause: error
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const parsed = options.schema.safeParse(decoded);
|
|
256
|
+
if (!parsed.success) {
|
|
257
|
+
reject(
|
|
258
|
+
new PiBridgeError(
|
|
259
|
+
"PI_PROTOCOL_VIOLATION",
|
|
260
|
+
`${options.operation} returned incompatible data`,
|
|
261
|
+
{ cause: parsed.error }
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
resolve(parsed.data);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
child.stdin.on("error", () => {
|
|
270
|
+
});
|
|
271
|
+
child.stdin.end(JSON.stringify(options.request));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function createPiSessionCatalog(options = {}) {
|
|
275
|
+
const listTimeoutMs = options.listTimeoutMs ?? DEFAULT_LIST_TIMEOUT_MS;
|
|
276
|
+
const forkTimeoutMs = options.forkTimeoutMs ?? DEFAULT_FORK_TIMEOUT_MS;
|
|
277
|
+
validateTimeout(listTimeoutMs, "Pi session discovery");
|
|
278
|
+
validateTimeout(forkTimeoutMs, "Pi session continuation");
|
|
279
|
+
const childPath = options.childPath ?? fileURLToPath(new URL("./sessions-child.mjs", import.meta.url));
|
|
280
|
+
const spawnProcess = options.spawnProcess ?? spawn;
|
|
281
|
+
const env = options.env ?? process.env;
|
|
282
|
+
return {
|
|
283
|
+
list: () => runChild({
|
|
284
|
+
childPath,
|
|
285
|
+
spawnProcess,
|
|
286
|
+
env,
|
|
287
|
+
timeoutMs: listTimeoutMs,
|
|
288
|
+
request: { operation: "list" },
|
|
289
|
+
schema: catalogInspectionSchema,
|
|
290
|
+
operation: "Pi session discovery"
|
|
291
|
+
}),
|
|
292
|
+
fork: (sourcePath, targetCwd, sessionId) => runChild({
|
|
293
|
+
childPath,
|
|
294
|
+
spawnProcess,
|
|
295
|
+
env,
|
|
296
|
+
timeoutMs: forkTimeoutMs,
|
|
297
|
+
request: {
|
|
298
|
+
operation: "fork",
|
|
299
|
+
sourcePath: path.normalize(sourcePath),
|
|
300
|
+
targetCwd: path.normalize(targetCwd),
|
|
301
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
302
|
+
},
|
|
303
|
+
schema: forkResultSchema,
|
|
304
|
+
operation: "Pi session continuation"
|
|
305
|
+
})
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
MAX_PI_SESSION_CATALOG_ENTRIES,
|
|
310
|
+
PI_SESSION_DIRECTORY_ENV,
|
|
311
|
+
createPiSessionCatalog,
|
|
312
|
+
forkPiSessionInProcess,
|
|
313
|
+
inspectPiSessionsInProcess
|
|
314
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// packages/pi-bridge/src/tool-activity.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { sanitizeApprovalDisplayText } from "./policy-approval.js";
|
|
4
|
+
var callSchema = z.object({
|
|
5
|
+
type: z.literal("toolCall"),
|
|
6
|
+
id: z.string().min(1).max(500),
|
|
7
|
+
name: z.string().min(1).max(200),
|
|
8
|
+
arguments: z.unknown().optional()
|
|
9
|
+
});
|
|
10
|
+
function boundedToolText(raw, limit = 2e4) {
|
|
11
|
+
const prefix = raw.slice(0, limit);
|
|
12
|
+
const safe = sanitizeApprovalDisplayText(prefix);
|
|
13
|
+
return { text: safe.slice(0, limit), omitted: raw.length - prefix.length + Math.max(0, safe.length - limit) };
|
|
14
|
+
}
|
|
15
|
+
function toolCallProjection(id, name, args) {
|
|
16
|
+
const input = boundedToolText(JSON.stringify(args ?? {}, null, 2));
|
|
17
|
+
const path = args !== null && typeof args === "object" && "path" in args && typeof args.path === "string" ? boundedToolText(args.path, 4096).text : void 0;
|
|
18
|
+
return {
|
|
19
|
+
toolCallId: id,
|
|
20
|
+
name: boundedToolText(name, 200).text,
|
|
21
|
+
status: "queued",
|
|
22
|
+
input: input.text,
|
|
23
|
+
output: "",
|
|
24
|
+
inputOmittedChars: input.omitted,
|
|
25
|
+
outputOmittedChars: 0,
|
|
26
|
+
...path === void 0 ? {} : { path }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function extractToolCalls(content) {
|
|
30
|
+
if (!Array.isArray(content)) return [];
|
|
31
|
+
return content.flatMap((block) => {
|
|
32
|
+
const call = callSchema.safeParse(block);
|
|
33
|
+
return call.success ? [toolCallProjection(call.data.id, call.data.name, call.data.arguments)] : [];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function toolResultText(content) {
|
|
37
|
+
if (typeof content === "string") return boundedToolText(content);
|
|
38
|
+
if (!Array.isArray(content)) return { text: "", omitted: 0 };
|
|
39
|
+
let text = "";
|
|
40
|
+
let omitted = 0;
|
|
41
|
+
for (const block of content) {
|
|
42
|
+
if (block === null || typeof block !== "object") continue;
|
|
43
|
+
const value = block.type === "text" && typeof block.text === "string" ? block.text : block.type === "image" ? "[Image result]" : "";
|
|
44
|
+
if (!value) continue;
|
|
45
|
+
const projected = boundedToolText(`${text || omitted ? "\n" : ""}${value}`, Math.max(0, 2e4 - text.length));
|
|
46
|
+
text += projected.text;
|
|
47
|
+
omitted += projected.omitted;
|
|
48
|
+
}
|
|
49
|
+
return { text, omitted };
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
boundedToolText,
|
|
53
|
+
extractToolCalls,
|
|
54
|
+
toolCallProjection,
|
|
55
|
+
toolResultText
|
|
56
|
+
};
|