dsh-phone-bridge 0.2.3 → 0.2.5
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 +2 -0
- package/index.js +132 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ trash panel at the bottom of the sidebar where each entry can be restored or pur
|
|
|
37
37
|
| `POST /stop` | cancel the session's running turn |
|
|
38
38
|
| `GET /models` | the deployment model catalog |
|
|
39
39
|
| `POST /model` | switch the model a session uses |
|
|
40
|
+
| `GET /health` | whether this DSH build still has every `sessionController` method the plugin calls, and which are missing if not |
|
|
41
|
+
| `GET /recent?sessionId=&limit=` | the user's own last few messages in a session, read through `sessionController.page()` |
|
|
40
42
|
|
|
41
43
|
Deleting **moves** a session to `~/.dsh/deleted-sessions/` instead of unlinking it,
|
|
42
44
|
so a delete stays recoverable and purging is a separate, explicit step.
|
package/index.js
CHANGED
|
@@ -227,6 +227,23 @@ const WORKSPACE_FILE = join(DSH_HOME, "storages", "workspace.json");
|
|
|
227
227
|
// TRASH_ROOT is assigned in apply() from Config, because it is the one path a
|
|
228
228
|
// deployment may reasonably want to move somewhere else.
|
|
229
229
|
|
|
230
|
+
// Every sessionController method this plugin calls. DSH is pre-1.0 and this set
|
|
231
|
+
// is an internal surface, so it is checked at load and reported by /health
|
|
232
|
+
// rather than discovered one failing route at a time.
|
|
233
|
+
const REQUIRED_CONTROLLER_METHODS = [
|
|
234
|
+
"cancel",
|
|
235
|
+
"create",
|
|
236
|
+
"follow",
|
|
237
|
+
"inspect",
|
|
238
|
+
"list",
|
|
239
|
+
"modelCatalog",
|
|
240
|
+
"page",
|
|
241
|
+
"prompt",
|
|
242
|
+
"rename",
|
|
243
|
+
"search",
|
|
244
|
+
"selectModel",
|
|
245
|
+
];
|
|
246
|
+
|
|
230
247
|
// A session lives at sessions/<slug-of-cwd>/<sessionId>/, where the slug is a
|
|
231
248
|
// lossy encoding of the working directory. Locate it by scanning instead of
|
|
232
249
|
// trying to re-derive that encoding.
|
|
@@ -257,6 +274,27 @@ function listOnDiskSessionIds() {
|
|
|
257
274
|
return ids;
|
|
258
275
|
}
|
|
259
276
|
|
|
277
|
+
// Plain text out of a message's content blocks.
|
|
278
|
+
//
|
|
279
|
+
// The block shape is not a stable surface either, so only text-ish fields are
|
|
280
|
+
// read and anything unrecognised is skipped rather than stringified into the
|
|
281
|
+
// output as "[object Object]".
|
|
282
|
+
function textFromContentBlocks(content) {
|
|
283
|
+
if (typeof content === "string") return content.trim();
|
|
284
|
+
if (!Array.isArray(content)) return "";
|
|
285
|
+
const parts = [];
|
|
286
|
+
for (const block of content) {
|
|
287
|
+
if (typeof block === "string") {
|
|
288
|
+
if (block.trim()) parts.push(block.trim());
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!block || typeof block !== "object") continue;
|
|
292
|
+
const text = [block.text, block.content].find((v) => typeof v === "string" && v.trim());
|
|
293
|
+
if (text) parts.push(text.trim());
|
|
294
|
+
}
|
|
295
|
+
return parts.join("\n").trim();
|
|
296
|
+
}
|
|
297
|
+
|
|
260
298
|
// Pull just the title out of a session's projection bag.
|
|
261
299
|
//
|
|
262
300
|
// The bag also carries token counts, context pressure, the turn outline and the
|
|
@@ -879,6 +917,89 @@ export function apply(ctx, config = {}) {
|
|
|
879
917
|
},
|
|
880
918
|
}));
|
|
881
919
|
|
|
920
|
+
// What this build of DSH is expected to hand us. The plugin drives sessions
|
|
921
|
+
// through sessionController, which is an internal surface that can change
|
|
922
|
+
// between releases; when it does, the symptom would otherwise be a bare 500 on
|
|
923
|
+
// whichever route happens to touch the missing method, with nothing said about
|
|
924
|
+
// why. Reporting it up front turns that into one clear line.
|
|
925
|
+
// The user's own last few messages in a session, so the phone can answer "where
|
|
926
|
+
// did this conversation get to" without scrolling a chat app.
|
|
927
|
+
//
|
|
928
|
+
// Read through sessionController.page(), which is the supported history read and
|
|
929
|
+
// does not activate a cold session. The event shape is not a stable surface, so
|
|
930
|
+
// every field access is guarded and an unknown payload degrades to fewer lines
|
|
931
|
+
// rather than throwing.
|
|
932
|
+
disposers.push(ctx.webServer.register({
|
|
933
|
+
kind: "exact",
|
|
934
|
+
path: `${ROUTE_PATH}/recent`,
|
|
935
|
+
handler: async (req, res) => {
|
|
936
|
+
try {
|
|
937
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
938
|
+
const sessionId = (url.searchParams.get("sessionId") ?? "").trim();
|
|
939
|
+
const limit = Math.min(Math.max(Number(url.searchParams.get("limit")) || 3, 1), 20);
|
|
940
|
+
if (!sessionId) {
|
|
941
|
+
sendJson(res, 400, { ok: false, error: "sessionId is required" });
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const sessionController = ctx.get("sessionController");
|
|
945
|
+
if (!sessionController) throw new Error("sessionController is unavailable");
|
|
946
|
+
if (typeof sessionController.page !== "function") {
|
|
947
|
+
throw new Error("this DSH build does not expose sessionController.page");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const signal = new AbortController().signal;
|
|
951
|
+
// throughSeq: -1 means "the newest page"; maxMessages stays well above the
|
|
952
|
+
// requested count because assistant and tool events sit between the user's.
|
|
953
|
+
const page = await sessionController.page({
|
|
954
|
+
address: { kind: "session", sessionId },
|
|
955
|
+
throughSeq: -1,
|
|
956
|
+
maxMessages: 200,
|
|
957
|
+
}, signal);
|
|
958
|
+
|
|
959
|
+
const texts = [];
|
|
960
|
+
for (const record of page?.records ?? []) {
|
|
961
|
+
const event = record?.event;
|
|
962
|
+
if (event?.type !== "user/message") continue;
|
|
963
|
+
if (event?.data?.source?.kind !== "user") continue;
|
|
964
|
+
const text = textFromContentBlocks(event.data.content);
|
|
965
|
+
if (!text) continue;
|
|
966
|
+
texts.push({ at: event.time ?? null, text });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
sendJson(res, 200, {
|
|
970
|
+
ok: true,
|
|
971
|
+
sessionId,
|
|
972
|
+
total: texts.length,
|
|
973
|
+
// Newest last, so the caller can slice from the end.
|
|
974
|
+
messages: texts.slice(-limit),
|
|
975
|
+
});
|
|
976
|
+
} catch (error) {
|
|
977
|
+
sendJson(res, 500, { ok: false, error: String(error?.message ?? error) });
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
}));
|
|
981
|
+
|
|
982
|
+
disposers.push(ctx.webServer.register({
|
|
983
|
+
kind: "exact",
|
|
984
|
+
path: `${ROUTE_PATH}/health`,
|
|
985
|
+
handler: async (_req, res) => {
|
|
986
|
+
try {
|
|
987
|
+
const sessionController = ctx.get("sessionController");
|
|
988
|
+
const missing = sessionController
|
|
989
|
+
? REQUIRED_CONTROLLER_METHODS.filter((name) => typeof sessionController[name] !== "function")
|
|
990
|
+
: REQUIRED_CONTROLLER_METHODS.slice();
|
|
991
|
+
sendJson(res, 200, {
|
|
992
|
+
ok: missing.length === 0,
|
|
993
|
+
controllerAvailable: Boolean(sessionController),
|
|
994
|
+
required: REQUIRED_CONTROLLER_METHODS,
|
|
995
|
+
missing,
|
|
996
|
+
});
|
|
997
|
+
} catch (error) {
|
|
998
|
+
sendJson(res, 500, { ok: false, error: String(error?.message ?? error) });
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
}));
|
|
1002
|
+
|
|
882
1003
|
disposers.push(ctx.webServer.register({
|
|
883
1004
|
kind: "exact",
|
|
884
1005
|
path: ROUTE_PATH,
|
|
@@ -886,6 +1007,17 @@ export function apply(ctx, config = {}) {
|
|
|
886
1007
|
}));
|
|
887
1008
|
|
|
888
1009
|
ctx.logger?.info?.(`phone-bridge listening on ${ROUTE_PATH}`);
|
|
1010
|
+
{
|
|
1011
|
+
const controller = ctx.get("sessionController");
|
|
1012
|
+
const missing = controller
|
|
1013
|
+
? REQUIRED_CONTROLLER_METHODS.filter((name) => typeof controller[name] !== "function")
|
|
1014
|
+
: REQUIRED_CONTROLLER_METHODS.slice();
|
|
1015
|
+
if (missing.length > 0) {
|
|
1016
|
+
ctx.logger?.warn?.(
|
|
1017
|
+
`phone-bridge: this DSH build is missing ${missing.join(", ")} on sessionController - the routes that use them will fail. GET ${ROUTE_PATH}/health reports the same list.`,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
889
1021
|
|
|
890
1022
|
return async () => {
|
|
891
1023
|
for (const dispose of disposers) {
|