@jmfederico/pi-web 1.202607.2 → 1.202608.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 +1 -1
- package/dist/client/assets/{CodeViewer-B4M13447.js → CodeViewer-D91Sp61M.js} +1 -1
- package/dist/client/assets/{TerminalPanel-Bmvj-Ecn.js → TerminalPanel-fYrKc3jh.js} +2 -2
- package/dist/client/assets/{UnifiedDiffViewer-C0hygwRo.js → UnifiedDiffViewer-BafLWGtF.js} +1 -1
- package/dist/client/assets/index-CA8q9_o7.js +4025 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -1
- package/dist/pi-web-plugins/info/infoInternals.js +208 -0
- package/dist/pi-web-plugins/info/pi-web-plugin.js +12 -15
- package/dist/pi-web-plugins/relays/markdownDocument.js +81 -0
- package/dist/pi-web-plugins/relays/package.json +9 -0
- package/dist/pi-web-plugins/relays/pi-web-plugin.js +44 -0
- package/dist/pi-web-plugins/relays/relayDiscovery.js +228 -0
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +550 -0
- package/dist/pi-web-plugins/relays/vendor/README.md +24 -0
- package/dist/pi-web-plugins/relays/vendor/marked.esm.js +76 -0
- package/dist/plugin-api.d.ts +7 -1
- package/dist/server/app.js +0 -22
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +0 -2
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/configRoutes.js +13 -11
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +2 -13
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/piWebStatus.js +12 -38
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
- package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
- package/dist/server/sessiond/agentProcessEnvironment.js +62 -0
- package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js +33 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -0
- package/dist/server/sessiond.js +145 -116
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/askUserTool.js +102 -0
- package/dist/server/sessions/askUserTool.js.map +1 -0
- package/dist/server/sessions/authRoutes.js +0 -10
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +0 -24
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/extensionDialogWaiters.js +76 -0
- package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
- package/dist/server/sessions/globalProviderPolicy.js +79 -10
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
- package/dist/server/sessions/messagePaging.js +2 -2
- package/dist/server/sessions/messagePaging.js.map +1 -1
- package/dist/server/sessions/modelCatalogRefresher.js +8 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +2 -5
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/parentSessionLocator.js +75 -0
- package/dist/server/sessions/parentSessionLocator.js.map +1 -0
- package/dist/server/sessions/pendingAskStore.js +275 -0
- package/dist/server/sessions/pendingAskStore.js.map +1 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +182 -2
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +721 -203
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionFileHeader.js +117 -0
- package/dist/server/sessions/sessionFileHeader.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +169 -54
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionSummaryScanner.js +619 -0
- package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +8 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +7 -1
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/spawnTargetResolver.js +3 -16
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -1
- package/dist/server/workspaces/effectivePathAccess.js +0 -19
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -1
- package/dist/server/workspaces/gitWorktreeDiscovery.js +9 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -1
- package/dist/server/workspaces/projectWorkspaceCwds.js +22 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js +30 -4
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/server/workspaces/workspaceService.js +15 -2
- package/dist/server/workspaces/workspaceService.js.map +1 -1
- package/dist/sessiond/config.js +2 -2
- package/dist/sessiond/config.js.map +1 -1
- package/dist/shared/activity.js +9 -1
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +300 -24
- package/dist/shared/apiTypes.js +30 -15
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +10 -41
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +4 -3
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/config.md +115 -11
- package/docs/plugins.md +82 -14
- package/package.json +11 -9
- package/dist/client/assets/index-TvPsRyS1.js +0 -3545
- package/dist/server/sessiond/sessionDaemonStartup.js +0 -49
- package/dist/server/sessiond/sessionDaemonStartup.js.map +0 -1
- package/dist/server/sessions/sessionArchiveMigration.js +0 -590
- package/dist/server/sessions/sessionArchiveMigration.js.map +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
/** Chunk size for streaming a session file's header region. */
|
|
3
|
+
const HEADER_READ_CHUNK_BYTES = 8192;
|
|
4
|
+
/**
|
|
5
|
+
* How far to look for the first parseable line. Headers are single-line and
|
|
6
|
+
* written at creation; the bound keeps a pathological file with a huge
|
|
7
|
+
* non-header prefix from dragging the listing and open paths.
|
|
8
|
+
*/
|
|
9
|
+
const HEADER_READ_CAP_BYTES = 262144;
|
|
10
|
+
const NEWLINE = 0x0a; // `\n`
|
|
11
|
+
const CARRIAGE_RETURN = 0x0d; // `\r`
|
|
12
|
+
/**
|
|
13
|
+
* Read a Pi session file's header without loading the whole transcript.
|
|
14
|
+
*
|
|
15
|
+
* Streams the file in chunks and stops at the first parseable line, mirroring
|
|
16
|
+
* the SDK rule the summary scanner follows: unparseable lines are skipped
|
|
17
|
+
* until one parses, and a parseable non-session entry disqualifies the file.
|
|
18
|
+
* The search is bounded to the header region; if no parseable line completes
|
|
19
|
+
* within the bound, the file is treated as headerless.
|
|
20
|
+
*
|
|
21
|
+
* Returns undefined for any unreadable file or file without a usable session
|
|
22
|
+
* header: callers use this to verify links between sessions, so an unusable
|
|
23
|
+
* header must behave the same as a missing one rather than throwing.
|
|
24
|
+
*/
|
|
25
|
+
export async function readSessionHeaderSummary(sessionFile) {
|
|
26
|
+
let file;
|
|
27
|
+
try {
|
|
28
|
+
file = await open(sessionFile, "r");
|
|
29
|
+
return await streamHeaderSummary(file);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await file?.close().catch(() => undefined);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function streamHeaderSummary(file) {
|
|
39
|
+
let pending = Buffer.alloc(0); // bytes of the current line, not yet newline-terminated
|
|
40
|
+
let position = 0;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const chunk = Buffer.alloc(HEADER_READ_CHUNK_BYTES);
|
|
43
|
+
const { bytesRead } = await file.read(chunk, 0, chunk.length, position);
|
|
44
|
+
if (bytesRead === 0) {
|
|
45
|
+
// End of file: an unterminated tail is the final line (readline parity).
|
|
46
|
+
const outcome = classifyHeaderLine(pending);
|
|
47
|
+
return outcome === "skip" ? undefined : outcome;
|
|
48
|
+
}
|
|
49
|
+
position += bytesRead;
|
|
50
|
+
let chunkStart = 0;
|
|
51
|
+
for (;;) {
|
|
52
|
+
const newlineAt = chunk.indexOf(NEWLINE, chunkStart);
|
|
53
|
+
if (newlineAt === -1) {
|
|
54
|
+
pending = Buffer.concat([pending, chunk.subarray(chunkStart, bytesRead)]);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const line = Buffer.concat([pending, chunk.subarray(chunkStart, newlineAt)]);
|
|
58
|
+
const outcome = classifyHeaderLine(line);
|
|
59
|
+
if (outcome !== "skip")
|
|
60
|
+
return outcome;
|
|
61
|
+
pending = Buffer.alloc(0);
|
|
62
|
+
chunkStart = newlineAt + 1;
|
|
63
|
+
}
|
|
64
|
+
// Bounded search: once the cap is consumed, leave the unterminated tail
|
|
65
|
+
// unclassified and report no header rather than reading without limit.
|
|
66
|
+
if (position >= HEADER_READ_CAP_BYTES)
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Classify one candidate line, mirroring the SDK's first-parseable-entry rule
|
|
72
|
+
* that the summary scanner applies: unparseable lines are skipped until one
|
|
73
|
+
* parses, and the first parseable line decides the file's fate. Returns
|
|
74
|
+
* "skip" to keep looking, undefined to reject, or the parsed header.
|
|
75
|
+
*/
|
|
76
|
+
function classifyHeaderLine(lineBytes) {
|
|
77
|
+
// Readline parity: a CRLF file yields lines without their trailing `\r`.
|
|
78
|
+
let end = lineBytes.length;
|
|
79
|
+
if (end > 0 && lineBytes[end - 1] === CARRIAGE_RETURN)
|
|
80
|
+
end -= 1;
|
|
81
|
+
const entry = tryParseEntry(lineBytes.toString("utf8", 0, end));
|
|
82
|
+
if (entry === undefined)
|
|
83
|
+
return "skip";
|
|
84
|
+
if (entry["type"] !== "session")
|
|
85
|
+
return undefined;
|
|
86
|
+
const id = entry["id"];
|
|
87
|
+
// The scanner skips headers without a usable id; the reader must agree so a
|
|
88
|
+
// link verified here can never point at an unusable session.
|
|
89
|
+
if (typeof id !== "string" || id === "")
|
|
90
|
+
return undefined;
|
|
91
|
+
const cwd = nonEmptyStringField(entry, "cwd");
|
|
92
|
+
const parentSession = nonEmptyStringField(entry, "parentSession");
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
96
|
+
...(parentSession === undefined ? {} : { parentSession }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function tryParseEntry(line) {
|
|
100
|
+
if (line.trim() === "")
|
|
101
|
+
return undefined;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(line);
|
|
104
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function nonEmptyStringField(header, key) {
|
|
111
|
+
const value = header[key];
|
|
112
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
113
|
+
}
|
|
114
|
+
function isRecord(value) {
|
|
115
|
+
return typeof value === "object" && value !== null;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=sessionFileHeader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionFileHeader.js","sourceRoot":"","sources":["../../../src/server/sessions/sessionFileHeader.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_CATALOG_ID_MAX_LENGTH, SESSION_UNREAD_CWD_MAX_LENGTH, SESSION_UNREAD_SESSION_ID_MAX_LENGTH } from "../../shared/apiTypes.js";
|
|
1
|
+
import { ASK_USER_ID_MAX_LENGTH, ASK_USER_OPTION_LIMIT, ASK_USER_OTHER_TEXT_MAX_LENGTH, ASK_USER_QUESTION_LIMIT, EXTENSION_DIALOG_ID_MAX_LENGTH, EXTENSION_DIALOG_INPUT_MAX_LENGTH, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_CATALOG_ID_MAX_LENGTH, SESSION_UNREAD_CWD_MAX_LENGTH, SESSION_UNREAD_SESSION_ID_MAX_LENGTH } from "../../shared/apiTypes.js";
|
|
2
2
|
import { projectBrowserMessageResponse } from "../browserMessageProjection.js";
|
|
3
3
|
import { normalizeRequestCwd } from "../workingDirectory.js";
|
|
4
4
|
import { normalizeSessionCleanupRequest } from "./sessionCleanup.js";
|
|
@@ -20,7 +20,11 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
20
20
|
app.post(`${prefix}/sessions`, async (request, reply) => {
|
|
21
21
|
try {
|
|
22
22
|
const body = requireRecord(request.body);
|
|
23
|
-
|
|
23
|
+
// An opaque label the caller uses to recognise its own construction's
|
|
24
|
+
// startup reports. Optional: only a browser row waiting for a session id
|
|
25
|
+
// has anything to correlate.
|
|
26
|
+
const startupToken = body["startupToken"] === undefined ? undefined : requireNonEmptyString(body, "startupToken");
|
|
27
|
+
return await sessions.start(normalizeRequestCwd(requireString(body, "cwd")), optionalField("startupToken", startupToken));
|
|
24
28
|
}
|
|
25
29
|
catch (error) {
|
|
26
30
|
return reply.code(400).send({ error: errorMessage(error) });
|
|
@@ -132,9 +136,12 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
132
136
|
}
|
|
133
137
|
});
|
|
134
138
|
app.get(`${prefix}/sessions/:sessionId/messages`, async (request, reply) => {
|
|
139
|
+
const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
|
|
140
|
+
if (ref === undefined)
|
|
141
|
+
return reply;
|
|
135
142
|
try {
|
|
136
143
|
const page = { ...optionalField("before", optionalNumber(request.query.before)), ...optionalField("limit", optionalNumber(request.query.limit)) };
|
|
137
|
-
const messages = await sessions.messages(
|
|
144
|
+
const messages = await sessions.messages(ref, page);
|
|
138
145
|
return projectBrowserMessageResponse(messages);
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
|
@@ -142,24 +149,33 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
142
149
|
}
|
|
143
150
|
});
|
|
144
151
|
app.get(`${prefix}/sessions/:sessionId/status`, async (request, reply) => {
|
|
152
|
+
const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
|
|
153
|
+
if (ref === undefined)
|
|
154
|
+
return reply;
|
|
145
155
|
try {
|
|
146
|
-
return await sessions.status(
|
|
156
|
+
return await sessions.status(ref);
|
|
147
157
|
}
|
|
148
158
|
catch (error) {
|
|
149
159
|
return reply.code(404).send({ error: errorMessage(error) });
|
|
150
160
|
}
|
|
151
161
|
});
|
|
152
162
|
app.get(`${prefix}/sessions/:sessionId/stream-snapshot`, async (request, reply) => {
|
|
163
|
+
const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
|
|
164
|
+
if (ref === undefined)
|
|
165
|
+
return reply;
|
|
153
166
|
try {
|
|
154
|
-
return await sessions.streamSnapshot(
|
|
167
|
+
return await sessions.streamSnapshot(ref);
|
|
155
168
|
}
|
|
156
169
|
catch (error) {
|
|
157
170
|
return reply.code(404).send({ error: errorMessage(error) });
|
|
158
171
|
}
|
|
159
172
|
});
|
|
160
173
|
app.get(`${prefix}/sessions/:sessionId/models`, async (request, reply) => {
|
|
174
|
+
const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
|
|
175
|
+
if (ref === undefined)
|
|
176
|
+
return reply;
|
|
161
177
|
try {
|
|
162
|
-
return { models: await sessions.availableModels(
|
|
178
|
+
return { models: await sessions.availableModels(ref) };
|
|
163
179
|
}
|
|
164
180
|
catch (error) {
|
|
165
181
|
return reply.code(404).send({ error: errorMessage(error) });
|
|
@@ -168,7 +184,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
168
184
|
app.post(`${prefix}/sessions/:sessionId/model`, async (request, reply) => {
|
|
169
185
|
try {
|
|
170
186
|
const body = optionalRecord(request.body);
|
|
171
|
-
return await sessions.setModel(
|
|
187
|
+
return await sessions.setModel(sessionRefFromBody(request.params.sessionId, body), requireString(body, "provider"), requireString(body, "modelId"));
|
|
172
188
|
}
|
|
173
189
|
catch (error) {
|
|
174
190
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -180,15 +196,18 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
180
196
|
const direction = body["direction"];
|
|
181
197
|
if (direction !== undefined && direction !== "forward" && direction !== "backward")
|
|
182
198
|
throw new Error("direction must be forward or backward");
|
|
183
|
-
return await sessions.cycleModel(
|
|
199
|
+
return await sessions.cycleModel(sessionRefFromBody(request.params.sessionId, body), direction ?? "forward");
|
|
184
200
|
}
|
|
185
201
|
catch (error) {
|
|
186
202
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
187
203
|
}
|
|
188
204
|
});
|
|
189
205
|
app.get(`${prefix}/sessions/:sessionId/thinking-levels`, async (request, reply) => {
|
|
206
|
+
const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
|
|
207
|
+
if (ref === undefined)
|
|
208
|
+
return reply;
|
|
190
209
|
try {
|
|
191
|
-
return { levels: await sessions.availableThinkingLevels(
|
|
210
|
+
return { levels: await sessions.availableThinkingLevels(ref) };
|
|
192
211
|
}
|
|
193
212
|
catch (error) {
|
|
194
213
|
return reply.code(404).send({ error: errorMessage(error) });
|
|
@@ -199,7 +218,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
199
218
|
const body = optionalRecord(request.body);
|
|
200
219
|
// The level string is validated against the session's live available levels
|
|
201
220
|
// in the service, so it stays correct if pi changes the set.
|
|
202
|
-
return await sessions.setThinkingLevel(
|
|
221
|
+
return await sessions.setThinkingLevel(sessionRefFromBody(request.params.sessionId, body), requireThinkingLevel(body["level"]));
|
|
203
222
|
}
|
|
204
223
|
catch (error) {
|
|
205
224
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -208,15 +227,18 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
208
227
|
app.post(`${prefix}/sessions/:sessionId/thinking-level/cycle`, async (request, reply) => {
|
|
209
228
|
try {
|
|
210
229
|
const body = optionalRecord(request.body);
|
|
211
|
-
return await sessions.cycleThinkingLevel(
|
|
230
|
+
return await sessions.cycleThinkingLevel(sessionRefFromBody(request.params.sessionId, body));
|
|
212
231
|
}
|
|
213
232
|
catch (error) {
|
|
214
233
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
215
234
|
}
|
|
216
235
|
});
|
|
217
236
|
app.get(`${prefix}/sessions/:sessionId/commands`, async (request, reply) => {
|
|
237
|
+
const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
|
|
238
|
+
if (ref === undefined)
|
|
239
|
+
return reply;
|
|
218
240
|
try {
|
|
219
|
-
return await sessions.commands(
|
|
241
|
+
return await sessions.commands(ref);
|
|
220
242
|
}
|
|
221
243
|
catch (error) {
|
|
222
244
|
return reply.code(404).send({ error: errorMessage(error) });
|
|
@@ -225,7 +247,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
225
247
|
app.post(`${prefix}/sessions/:sessionId/prompt`, async (request, reply) => {
|
|
226
248
|
try {
|
|
227
249
|
const body = optionalRecord(request.body);
|
|
228
|
-
await sessions.prompt(
|
|
250
|
+
await sessions.prompt(sessionRefFromBody(request.params.sessionId, body), body["text"], body["streamingBehavior"], body["attachments"]);
|
|
229
251
|
return { accepted: true };
|
|
230
252
|
}
|
|
231
253
|
catch (error) {
|
|
@@ -234,7 +256,46 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
234
256
|
});
|
|
235
257
|
app.post(`${prefix}/sessions/:sessionId/queue/clear`, async (request, reply) => {
|
|
236
258
|
try {
|
|
237
|
-
return await sessions.clearQueue(
|
|
259
|
+
return await sessions.clearQueue(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
app.post(`${prefix}/sessions/:sessionId/ask/submit`, async (request, reply) => {
|
|
266
|
+
try {
|
|
267
|
+
const body = requireRecord(request.body);
|
|
268
|
+
const askId = requireBoundedId(body["askId"], "askId");
|
|
269
|
+
return await sessions.submitAsk(sessionRefFromBody(request.params.sessionId, body), askId, askUserSubmissionFromBody(body));
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
app.post(`${prefix}/sessions/:sessionId/ask/cancel`, async (request, reply) => {
|
|
276
|
+
try {
|
|
277
|
+
const body = requireRecord(request.body);
|
|
278
|
+
return await sessions.cancelAsk(sessionRefFromBody(request.params.sessionId, body), requireBoundedId(body["askId"], "askId"));
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
app.post(`${prefix}/sessions/:sessionId/dialogs/answer`, async (request, reply) => {
|
|
285
|
+
try {
|
|
286
|
+
const body = requireRecord(request.body);
|
|
287
|
+
const answer = extensionDialogAnswerFromBody(body);
|
|
288
|
+
return await sessions.answerDialog(sessionRefFromBody(request.params.sessionId, body), answer.dialogId, answer.value);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
app.post(`${prefix}/sessions/:sessionId/dialogs/cancel`, async (request, reply) => {
|
|
295
|
+
try {
|
|
296
|
+
const body = requireRecord(request.body);
|
|
297
|
+
const cancel = extensionDialogCancelFromBody(body);
|
|
298
|
+
return await sessions.cancelDialog(sessionRefFromBody(request.params.sessionId, body), cancel.dialogId);
|
|
238
299
|
}
|
|
239
300
|
catch (error) {
|
|
240
301
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -243,7 +304,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
243
304
|
app.post(`${prefix}/sessions/:sessionId/warnings/dismiss`, async (request, reply) => {
|
|
244
305
|
try {
|
|
245
306
|
const body = optionalRecord(request.body);
|
|
246
|
-
return await sessions.dismissWarning(
|
|
307
|
+
return await sessions.dismissWarning(sessionRefFromBody(request.params.sessionId, body), requireString(body, "dismissId"));
|
|
247
308
|
}
|
|
248
309
|
catch (error) {
|
|
249
310
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -255,7 +316,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
255
316
|
const folder = body["folder"];
|
|
256
317
|
if (folder !== undefined && typeof folder !== "string")
|
|
257
318
|
throw new Error("folder field must be a string");
|
|
258
|
-
const attachments = await sessions.saveAttachments(
|
|
319
|
+
const attachments = await sessions.saveAttachments(sessionRefFromBody(request.params.sessionId, body), body["attachments"], folder);
|
|
259
320
|
return { attachments };
|
|
260
321
|
}
|
|
261
322
|
catch (error) {
|
|
@@ -265,7 +326,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
265
326
|
app.post(`${prefix}/sessions/:sessionId/shell`, async (request, reply) => {
|
|
266
327
|
try {
|
|
267
328
|
const body = optionalRecord(request.body);
|
|
268
|
-
await sessions.shell(
|
|
329
|
+
await sessions.shell(sessionRefFromBody(request.params.sessionId, body), requireString(body, "text"));
|
|
269
330
|
return { accepted: true };
|
|
270
331
|
}
|
|
271
332
|
catch (error) {
|
|
@@ -275,7 +336,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
275
336
|
app.post(`${prefix}/sessions/:sessionId/commands/run`, async (request, reply) => {
|
|
276
337
|
try {
|
|
277
338
|
const body = optionalRecord(request.body);
|
|
278
|
-
return await sessions.runCommand(
|
|
339
|
+
return await sessions.runCommand(sessionRefFromBody(request.params.sessionId, body), requireString(body, "text"));
|
|
279
340
|
}
|
|
280
341
|
catch (error) {
|
|
281
342
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -284,7 +345,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
284
345
|
app.post(`${prefix}/sessions/:sessionId/commands/respond`, async (request, reply) => {
|
|
285
346
|
try {
|
|
286
347
|
const body = optionalRecord(request.body);
|
|
287
|
-
return await sessions.respondToCommand(
|
|
348
|
+
return await sessions.respondToCommand(sessionRefFromBody(request.params.sessionId, body), requireString(body, "requestId"), requireString(body, "value"));
|
|
288
349
|
}
|
|
289
350
|
catch (error) {
|
|
290
351
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -293,7 +354,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
293
354
|
app.post(`${prefix}/sessions/:sessionId/tree/navigate`, async (request, reply) => {
|
|
294
355
|
try {
|
|
295
356
|
const body = requireRecord(request.body);
|
|
296
|
-
return await sessions.navigateTree(
|
|
357
|
+
return await sessions.navigateTree(sessionRefFromBody(request.params.sessionId, body), sessionTreeNavigateRequestFromBody(body));
|
|
297
358
|
}
|
|
298
359
|
catch (error) {
|
|
299
360
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -301,7 +362,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
301
362
|
});
|
|
302
363
|
app.post(`${prefix}/sessions/:sessionId/abort`, async (request, reply) => {
|
|
303
364
|
try {
|
|
304
|
-
await sessions.abort(
|
|
365
|
+
await sessions.abort(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
305
366
|
return { aborted: true };
|
|
306
367
|
}
|
|
307
368
|
catch (error) {
|
|
@@ -310,7 +371,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
310
371
|
});
|
|
311
372
|
app.post(`${prefix}/sessions/:sessionId/stop`, async (request, reply) => {
|
|
312
373
|
try {
|
|
313
|
-
await sessions.stop(
|
|
374
|
+
await sessions.stop(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
314
375
|
return { stopped: true };
|
|
315
376
|
}
|
|
316
377
|
catch (error) {
|
|
@@ -319,7 +380,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
319
380
|
});
|
|
320
381
|
app.post(`${prefix}/sessions/:sessionId/archive`, async (request, reply) => {
|
|
321
382
|
try {
|
|
322
|
-
await sessions.archive(
|
|
383
|
+
await sessions.archive(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
323
384
|
return { archived: true };
|
|
324
385
|
}
|
|
325
386
|
catch (error) {
|
|
@@ -328,7 +389,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
328
389
|
});
|
|
329
390
|
app.post(`${prefix}/sessions/:sessionId/archive-tree`, async (request, reply) => {
|
|
330
391
|
try {
|
|
331
|
-
return await sessions.archiveTree(
|
|
392
|
+
return await sessions.archiveTree(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
332
393
|
}
|
|
333
394
|
catch (error) {
|
|
334
395
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
@@ -336,25 +397,16 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
336
397
|
});
|
|
337
398
|
app.post(`${prefix}/sessions/:sessionId/restore`, async (request, reply) => {
|
|
338
399
|
try {
|
|
339
|
-
await sessions.restore(
|
|
400
|
+
await sessions.restore(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
340
401
|
return { restored: true };
|
|
341
402
|
}
|
|
342
403
|
catch (error) {
|
|
343
404
|
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
344
405
|
}
|
|
345
406
|
});
|
|
346
|
-
app.delete(`${prefix}/sessions/:sessionId`, async (request, reply) => {
|
|
347
|
-
try {
|
|
348
|
-
await sessions.deleteArchived(sessionLookupFromQuery(request.params.sessionId, request.query));
|
|
349
|
-
return { deleted: true };
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
407
|
app.post(`${prefix}/sessions/:sessionId/reload`, async (request, reply) => {
|
|
356
408
|
try {
|
|
357
|
-
await sessions.reload(
|
|
409
|
+
await sessions.reload(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
358
410
|
return { reloaded: true };
|
|
359
411
|
}
|
|
360
412
|
catch (error) {
|
|
@@ -363,7 +415,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
|
|
|
363
415
|
});
|
|
364
416
|
app.post(`${prefix}/sessions/:sessionId/detach-parent`, async (request, reply) => {
|
|
365
417
|
try {
|
|
366
|
-
await sessions.detachParent(
|
|
418
|
+
await sessions.detachParent(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
|
|
367
419
|
return { detached: true };
|
|
368
420
|
}
|
|
369
421
|
catch (error) {
|
|
@@ -394,12 +446,7 @@ function parseBulkMutationRef(value) {
|
|
|
394
446
|
const id = requireString(record, "id").trim();
|
|
395
447
|
if (id === "")
|
|
396
448
|
throw new Error("id field must not be empty");
|
|
397
|
-
|
|
398
|
-
if (cwd === undefined || cwd === "")
|
|
399
|
-
return { id };
|
|
400
|
-
if (typeof cwd !== "string")
|
|
401
|
-
throw new Error("cwd field must be a string");
|
|
402
|
-
return { id, cwd: normalizeRequestCwd(cwd) };
|
|
449
|
+
return { id, cwd: requireRefCwd(record["cwd"]) };
|
|
403
450
|
}
|
|
404
451
|
function notificationRefFromQuery(id, query) {
|
|
405
452
|
const cwd = requireNonEmptyBoundedString(query.cwd, "cwd", MAX_NOTIFICATION_CWD_LENGTH);
|
|
@@ -415,21 +462,35 @@ function notificationRef(id, cwd) {
|
|
|
415
462
|
cwd: normalizeRequestCwd(cwd),
|
|
416
463
|
};
|
|
417
464
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
function sessionLookupFromBody(id, body) {
|
|
422
|
-
const cwd = body["cwd"];
|
|
465
|
+
function sessionRefFromQuery(id, query) {
|
|
466
|
+
const cwd = query.cwd;
|
|
423
467
|
if (cwd === undefined || cwd === "")
|
|
424
|
-
|
|
425
|
-
if (typeof cwd !== "string")
|
|
426
|
-
throw new Error("cwd field must be a string");
|
|
468
|
+
throw new Error("cwd query parameter is required");
|
|
427
469
|
return { id, cwd: normalizeRequestCwd(cwd) };
|
|
428
470
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
471
|
+
// Every read-only per-session GET route starts with this parse; a malformed
|
|
472
|
+
// query is a 400, sent here so the handlers stay flat.
|
|
473
|
+
function sessionRefFromQueryOr400(id, query, reply) {
|
|
474
|
+
try {
|
|
475
|
+
return sessionRefFromQuery(id, query);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
reply.code(400).send({ error: errorMessage(error) });
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function sessionRefFromBody(id, body) {
|
|
483
|
+
return { id, cwd: requireRefCwd(body["cwd"]) };
|
|
484
|
+
}
|
|
485
|
+
// Every per-session ref is cwd-scoped: the workspace path picks the session
|
|
486
|
+
// store the id resolves in, and it is normalized here so everything past the
|
|
487
|
+
// route layer sees canonical paths.
|
|
488
|
+
function requireRefCwd(cwd) {
|
|
489
|
+
if (typeof cwd !== "string")
|
|
490
|
+
throw new Error("cwd field must be a string");
|
|
491
|
+
if (cwd === "")
|
|
492
|
+
throw new Error("cwd field must not be empty");
|
|
493
|
+
return normalizeRequestCwd(cwd);
|
|
433
494
|
}
|
|
434
495
|
function sessionTreeNavigateRequestFromBody(body) {
|
|
435
496
|
const targetId = requireNonEmptyString(body, "targetId");
|
|
@@ -463,6 +524,60 @@ function requireExactFields(record, fields, label) {
|
|
|
463
524
|
if (unexpected !== undefined)
|
|
464
525
|
throw new Error(`${label} field contains unsupported property: ${unexpected}`);
|
|
465
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Shape-check one submitted answer set. Only transport-level checks belong here:
|
|
529
|
+
* whether the answers fit the questions that were actually asked is the pending
|
|
530
|
+
* ask store's job, since only it knows the open ask.
|
|
531
|
+
*/
|
|
532
|
+
function askUserSubmissionFromBody(body) {
|
|
533
|
+
const answers = body["answers"];
|
|
534
|
+
if (!Array.isArray(answers))
|
|
535
|
+
throw new Error("answers field must be an array");
|
|
536
|
+
if (answers.length > ASK_USER_QUESTION_LIMIT)
|
|
537
|
+
throw new Error("answers field has too many entries");
|
|
538
|
+
return { answers: answers.map(askUserAnswerFromValue) };
|
|
539
|
+
}
|
|
540
|
+
function askUserAnswerFromValue(value) {
|
|
541
|
+
const record = requireRecord(value);
|
|
542
|
+
const values = record["values"];
|
|
543
|
+
if (!Array.isArray(values))
|
|
544
|
+
throw new Error("values field must be an array");
|
|
545
|
+
if (values.length > ASK_USER_OPTION_LIMIT)
|
|
546
|
+
throw new Error("values field has too many entries");
|
|
547
|
+
const otherText = record["otherText"];
|
|
548
|
+
if (otherText !== undefined && typeof otherText !== "string")
|
|
549
|
+
throw new Error("otherText field must be a string");
|
|
550
|
+
if (typeof otherText === "string" && otherText.length > ASK_USER_OTHER_TEXT_MAX_LENGTH)
|
|
551
|
+
throw new Error("otherText field is too long");
|
|
552
|
+
return {
|
|
553
|
+
id: requireBoundedId(record["id"], "id"),
|
|
554
|
+
values: values.map((entry) => requireBoundedId(entry, "values entry")),
|
|
555
|
+
...(otherText === undefined ? {} : { otherText }),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function requireBoundedId(value, field) {
|
|
559
|
+
return requireNonEmptyBoundedString(value, field, ASK_USER_ID_MAX_LENGTH);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Shape-check one dialog answer. Only transport-level checks belong here:
|
|
563
|
+
* whether the value fits the answered dialog's kind is the pending dialog
|
|
564
|
+
* store's job, since only it knows the open dialog.
|
|
565
|
+
*/
|
|
566
|
+
function extensionDialogAnswerFromBody(body) {
|
|
567
|
+
const dialogId = requireNonEmptyBoundedString(body["dialogId"], "dialogId", EXTENSION_DIALOG_ID_MAX_LENGTH);
|
|
568
|
+
const value = body["value"];
|
|
569
|
+
if (typeof value === "boolean")
|
|
570
|
+
return { dialogId, value };
|
|
571
|
+
if (typeof value === "string") {
|
|
572
|
+
if (value.length > EXTENSION_DIALOG_INPUT_MAX_LENGTH)
|
|
573
|
+
throw new Error("value field is too long");
|
|
574
|
+
return { dialogId, value };
|
|
575
|
+
}
|
|
576
|
+
throw new Error("value field must be a string or a boolean");
|
|
577
|
+
}
|
|
578
|
+
function extensionDialogCancelFromBody(body) {
|
|
579
|
+
return { dialogId: requireNonEmptyBoundedString(body["dialogId"], "dialogId", EXTENSION_DIALOG_ID_MAX_LENGTH) };
|
|
580
|
+
}
|
|
466
581
|
function optionalRecord(value) {
|
|
467
582
|
if (value === undefined || value === null)
|
|
468
583
|
return {};
|