pipane 0.1.5 → 0.1.7

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.
Files changed (138) hide show
  1. package/README.md +45 -1
  2. package/THIRD_PARTY_NOTICES.md +29 -0
  3. package/bin/pipane-rendezvous.js +21 -0
  4. package/bin/pipane.js +21 -1
  5. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/dist/client/assets/__vite-browser-external-DtLbhLGx.js +1 -0
  65. package/dist/client/assets/app-runtime-Rw-O-AwW.js +1 -0
  66. package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +1 -0
  67. package/dist/client/assets/index-DwbBcYUf.js +2 -0
  68. package/dist/client/assets/index-iblQYBAl.css +1 -0
  69. package/dist/client/assets/main-LGItV9Aj.js +2807 -0
  70. package/dist/client/assets/pairing-page-D-WxlWZR.js +1 -0
  71. package/dist/client/assets/remote-backend-manager-BWuGh30I.js +1 -0
  72. package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +4 -0
  73. package/dist/client/assets/webrtc-frame-transport-BijNnA7N.js +1 -0
  74. package/dist/client/assets/ws-agent-adapter-BRysf-Y7.js +6 -0
  75. package/dist/client/index.html +2 -2
  76. package/dist/server/rendezvous/rendezvous-hub.js +426 -0
  77. package/dist/server/rendezvous/server.js +247 -0
  78. package/dist/server/rendezvous/trust-store.js +432 -0
  79. package/dist/server/server/auth-guard.js +61 -0
  80. package/dist/server/server/backend-connection-authorizer.js +29 -0
  81. package/dist/server/server/backend-identity.js +167 -0
  82. package/dist/server/server/backend-protocol-handler.js +132 -0
  83. package/dist/server/server/backend-trust-store.js +157 -0
  84. package/dist/server/server/backend-webrtc.js +239 -0
  85. package/dist/server/server/bundled-extensions.js +6 -0
  86. package/dist/server/server/conversation-file-access.js +97 -0
  87. package/dist/server/server/extension-status.js +31 -0
  88. package/dist/server/server/frame-connection.js +48 -0
  89. package/dist/server/server/frame-router.js +45 -0
  90. package/dist/server/server/ice-servers.js +24 -0
  91. package/dist/server/server/local-backend-api.js +389 -0
  92. package/dist/server/server/local-settings.js +28 -0
  93. package/dist/server/server/pi-rpc-protocol.js +407 -0
  94. package/dist/server/server/pi-runtime.js +1 -1
  95. package/dist/server/server/process-pool.js +179 -136
  96. package/dist/server/server/rendezvous-client.js +282 -0
  97. package/dist/server/server/rest-api.js +121 -231
  98. package/dist/server/server/server.js +188 -134
  99. package/dist/server/server/session-actor.js +202 -0
  100. package/dist/server/server/session-index.js +111 -18
  101. package/dist/server/server/session-jsonl.js +128 -8
  102. package/dist/server/server/session-path.js +145 -0
  103. package/dist/server/server/session-registry.js +67 -0
  104. package/dist/server/server/update-api.js +28 -0
  105. package/dist/server/server/update-check.js +33 -13
  106. package/dist/server/server/update-manager.js +231 -0
  107. package/dist/server/server/worktree-name.js +124 -0
  108. package/dist/server/server/ws-handler.js +784 -478
  109. package/dist/server/shared/backend-api.js +1 -0
  110. package/dist/server/shared/backend-protocol.js +164 -0
  111. package/dist/server/shared/jsonl-sync.js +41 -0
  112. package/dist/server/shared/node-trust-crypto.js +61 -0
  113. package/dist/server/shared/rendezvous-protocol.js +243 -0
  114. package/dist/server/shared/rpc-timeouts.js +4 -0
  115. package/dist/server/shared/thinking-levels.js +94 -0
  116. package/dist/server/shared/tool-runtime.js +1 -0
  117. package/dist/server/shared/trust-protocol.js +97 -0
  118. package/dist/server/shared/updates.js +4 -0
  119. package/dist/server/shared/ws-protocol.js +473 -0
  120. package/docs/protocol.md +127 -0
  121. package/extensions/canvas.ts +1 -1
  122. package/package.json +42 -17
  123. package/dist/client/assets/_node-stub_node_crypto-C_7epg3G.js +0 -1
  124. package/dist/client/assets/_node-stub_node_fs-B-VOzeXW.js +0 -1
  125. package/dist/client/assets/_node-stub_node_http-CROXaVGJ.js +0 -1
  126. package/dist/client/assets/_node-stub_node_os-TTKJha15.js +0 -1
  127. package/dist/client/assets/_node-stub_node_path-DJCJiKv7.js +0 -1
  128. package/dist/client/assets/_node-stub_stream-DDexIdn_.js +0 -1
  129. package/dist/client/assets/index-BtDAXAo1.css +0 -1
  130. package/dist/client/assets/index-CAQqlK61.js +0 -131
  131. package/dist/client/assets/index-CJEeBmBj.js +0 -3
  132. package/dist/client/assets/index-DUEhKmPQ.js +0 -1
  133. package/dist/client/assets/index-XEVKscP-.js +0 -4443
  134. package/dist/client/assets/index-woI4agCy.js +0 -45
  135. package/dist/server/server/attached-session.js +0 -209
  136. package/dist/server/server/load-trace-store.js +0 -48
  137. package/dist/server/server/session-lifecycle.js +0 -155
  138. package/patches/@mariozechner+pi-web-ui+0.55.3.patch +0 -3437
@@ -1,289 +1,179 @@
1
- /**
2
- * REST API endpoints for pipane.
3
- *
4
- * Stateless handlers that read session data from JSONL files on disk.
5
- */
6
- import { existsSync, readFileSync, readdirSync, watchFile } from "node:fs";
7
- import { unlink } from "node:fs/promises";
8
- import path from "node:path";
9
- import { buildSessionContext, parseSessionEntries } from "@mariozechner/pi-coding-agent";
10
- import { SessionIndex } from "./session-index.js";
11
- import { LocalSettingsStore } from "./local-settings.js";
12
- let localSettingsStore;
13
- let sessionIndex;
14
- let localSettingsWatcherStarted = false;
15
- function startLocalSettingsWatcher(onLocalSettingsReloaded) {
16
- if (localSettingsWatcherStarted)
17
- return;
18
- localSettingsWatcherStarted = true;
19
- let debounceTimer = null;
20
- watchFile(localSettingsStore.path, { interval: 500 }, () => {
21
- if (debounceTimer)
22
- clearTimeout(debounceTimer);
23
- debounceTimer = setTimeout(async () => {
24
- const changed = localSettingsStore.reloadFromDiskIfValid();
25
- if (!changed)
26
- return;
27
- await sessionIndex.invalidateAll();
28
- onLocalSettingsReloaded?.();
29
- }, 150);
30
- });
31
- }
32
- async function readJsonBody(req) {
1
+ /** HTTP compatibility facade over the carrier-neutral backend service. */
2
+ import { LocalBackendApi, LocalBackendApiError } from "./local-backend-api.js";
3
+ async function readJsonBody(req, maxBytes = Number.POSITIVE_INFINITY) {
33
4
  const chunks = [];
34
- for await (const chunk of req)
5
+ let size = 0;
6
+ for await (const chunk of req) {
7
+ size += chunk.length;
8
+ if (size > maxBytes)
9
+ throw new LocalBackendApiError("Request body is too large", 413, "invalid_request");
35
10
  chunks.push(chunk);
36
- const raw = Buffer.concat(chunks).toString();
37
- return JSON.parse(raw || "{}");
11
+ }
12
+ return JSON.parse(Buffer.concat(chunks).toString() || "{}");
13
+ }
14
+ function sendError(res, error) {
15
+ if (error instanceof LocalBackendApiError) {
16
+ res.status(error.status).json({ error: error.message, code: error.code });
17
+ return;
18
+ }
19
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
38
20
  }
39
21
  export function registerRestApi(app, options = {}) {
40
- const traceStore = options.traceStore;
41
- localSettingsStore = options.localSettingsStore ?? new LocalSettingsStore();
42
- sessionIndex = new SessionIndex({
43
- cwdDisplayFormatter: (cwd) => localSettingsStore.formatCwdTitle(cwd),
44
- });
45
- startLocalSettingsWatcher(options.onLocalSettingsReloaded);
46
- app.post("/api/debug/load-trace/event", async (req, res) => {
22
+ const api = options.api ?? new LocalBackendApi(options);
23
+ app.get("/api/sessions", async (_req, res) => {
47
24
  try {
48
- if (!traceStore) {
49
- res.status(404).json({ error: "Tracing disabled" });
50
- return;
51
- }
52
- const chunks = [];
53
- for await (const chunk of req)
54
- chunks.push(chunk);
55
- const body = JSON.parse(Buffer.concat(chunks).toString() || "{}");
56
- const traceId = String(body.traceId || req.headers["x-pi-trace-id"] || "");
57
- if (!traceId) {
58
- res.status(400).json({ error: "Missing traceId" });
59
- return;
60
- }
61
- traceStore.record(traceId, {
62
- ts: new Date().toISOString(),
63
- source: "frontend",
64
- kind: body.durationMs != null ? "span" : "instant",
65
- name: String(body.name || "frontend_event"),
66
- durationMs: typeof body.durationMs === "number" ? body.durationMs : undefined,
67
- attrs: body.attrs && typeof body.attrs === "object" ? body.attrs : undefined,
68
- });
69
- res.json({ ok: true });
25
+ res.json(await api.listSessions());
70
26
  }
71
- catch (err) {
72
- res.status(500).json({ error: err.message });
27
+ catch (error) {
28
+ sendError(res, error);
73
29
  }
74
30
  });
75
- app.get("/api/debug/load-trace/latest", (_req, res) => {
76
- if (!traceStore) {
77
- res.status(404).json({ error: "Tracing disabled" });
78
- return;
31
+ app.delete("/api/sessions", async (req, res) => {
32
+ try {
33
+ const body = await readJsonBody(req);
34
+ if (typeof body.path !== "string")
35
+ throw new LocalBackendApiError("Missing 'path' string", 400, "invalid_request");
36
+ await api.deleteSession(body.path);
37
+ res.json({ success: true });
38
+ }
39
+ catch (error) {
40
+ sendError(res, error);
79
41
  }
80
- res.json({ traces: traceStore.getLatest() });
81
42
  });
82
- app.get("/api/debug/load-trace/:traceId", (req, res) => {
83
- if (!traceStore) {
84
- res.status(404).json({ error: "Tracing disabled" });
85
- return;
43
+ app.get("/api/sessions/fork-messages", async (req, res) => {
44
+ try {
45
+ if (typeof req.query.path !== "string")
46
+ throw new LocalBackendApiError("Missing 'path' query parameter", 400, "invalid_request");
47
+ res.json({ messages: await api.listForkMessages(req.query.path) });
86
48
  }
87
- const trace = traceStore.get(req.params.traceId);
88
- if (!trace) {
89
- res.status(404).json({ error: "Trace not found" });
90
- return;
49
+ catch (error) {
50
+ sendError(res, error);
91
51
  }
92
- res.json(trace);
93
52
  });
94
- app.get("/api/sessions", async (_req, res) => {
53
+ app.get("/api/sessions/raw", async (req, res) => {
95
54
  try {
96
- res.json(await sessionIndex.listSessions());
55
+ if (typeof req.query.path !== "string")
56
+ throw new LocalBackendApiError("Missing 'path' query parameter", 400, "invalid_request");
57
+ res.type("text/plain").send(await api.getRawSession(req.query.path));
97
58
  }
98
- catch (err) {
99
- res.status(500).json({ error: err.message });
59
+ catch (error) {
60
+ sendError(res, error);
100
61
  }
101
62
  });
102
- app.get("/api/settings/local", (_req, res) => {
63
+ app.get("/api/files/content", async (req, res) => {
103
64
  try {
104
- res.json(localSettingsStore.read());
65
+ if (typeof req.query.sessionPath !== "string" || typeof req.query.path !== "string") {
66
+ throw new LocalBackendApiError("Missing 'sessionPath' or 'path' query parameter", 400, "invalid_request");
67
+ }
68
+ res.json(await api.getFileContent(req.query.sessionPath, req.query.path));
105
69
  }
106
- catch (err) {
107
- res.status(500).json({ error: err.message });
70
+ catch (error) {
71
+ sendError(res, error);
108
72
  }
109
73
  });
110
- app.post("/api/settings/local/validate", async (req, res) => {
74
+ app.post("/api/files/uploads", async (req, res) => {
111
75
  try {
112
- const body = await readJsonBody(req);
113
- if (typeof body.content !== "string") {
114
- res.status(400).json({ error: "Missing 'content' string" });
115
- return;
76
+ const body = await readJsonBody(req, 512 * 1024);
77
+ if (typeof body.fileName !== "string" || typeof body.mimeType !== "string" || typeof body.size !== "number") {
78
+ throw new LocalBackendApiError("Missing file upload metadata", 400, "invalid_request");
116
79
  }
117
- res.json(localSettingsStore.validate(body.content));
80
+ res.status(201).json(await api.createFileUpload({
81
+ fileName: body.fileName,
82
+ mimeType: body.mimeType,
83
+ size: body.size,
84
+ }));
118
85
  }
119
- catch (err) {
120
- res.status(500).json({ error: err.message });
86
+ catch (error) {
87
+ sendError(res, error);
121
88
  }
122
89
  });
123
- app.patch("/api/settings/local", async (req, res) => {
90
+ app.post("/api/files/uploads/:uploadId/chunks", async (req, res) => {
124
91
  try {
125
- const body = await readJsonBody(req);
126
- if (!body || typeof body !== "object") {
127
- res.status(400).json({ error: "Request body must be a JSON object" });
128
- return;
92
+ const body = await readJsonBody(req, 512 * 1024);
93
+ if (typeof body.offset !== "number" || typeof body.data !== "string") {
94
+ throw new LocalBackendApiError("Missing file upload chunk", 400, "invalid_request");
129
95
  }
130
- const result = localSettingsStore.patch(body);
131
- if (!result.valid) {
132
- res.status(400).json(result);
133
- return;
134
- }
135
- await sessionIndex.invalidateAll();
136
- options.onLocalSettingsReloaded?.();
137
- res.json(result);
96
+ res.json(await api.appendFileUpload({
97
+ uploadId: req.params.uploadId,
98
+ offset: body.offset,
99
+ data: body.data,
100
+ }));
138
101
  }
139
- catch (err) {
140
- res.status(500).json({ error: err.message });
102
+ catch (error) {
103
+ sendError(res, error);
141
104
  }
142
105
  });
143
- app.put("/api/settings/local", async (req, res) => {
106
+ app.post("/api/files/uploads/:uploadId/complete", async (req, res) => {
144
107
  try {
145
- const body = await readJsonBody(req);
146
- if (typeof body.content !== "string") {
147
- res.status(400).json({ error: "Missing 'content' string" });
148
- return;
149
- }
150
- const result = localSettingsStore.save(body.content);
151
- if (!result.valid) {
152
- res.status(400).json(result);
153
- return;
154
- }
155
- await sessionIndex.invalidateAll();
156
- options.onLocalSettingsReloaded?.();
157
- res.json(result);
108
+ res.json(await api.completeFileUpload(req.params.uploadId));
158
109
  }
159
- catch (err) {
160
- res.status(500).json({ error: err.message });
110
+ catch (error) {
111
+ sendError(res, error);
161
112
  }
162
113
  });
163
- app.delete("/api/sessions", async (req, res) => {
114
+ app.get("/api/browse", async (req, res) => {
164
115
  try {
165
- const chunks = [];
166
- for await (const chunk of req)
167
- chunks.push(chunk);
168
- const body = JSON.parse(Buffer.concat(chunks).toString());
169
- const { path: sessionPath } = body;
170
- if (!sessionPath || typeof sessionPath !== "string") {
171
- res.status(400).json({ error: "Missing session path" });
172
- return;
173
- }
174
- if (!sessionPath.endsWith(".jsonl") || !existsSync(sessionPath)) {
175
- res.status(404).json({ error: "Session not found" });
176
- return;
177
- }
178
- await unlink(sessionPath);
179
- res.json({ success: true });
116
+ res.json(await api.browseDirectory(typeof req.query.path === "string" ? req.query.path : ""));
180
117
  }
181
- catch (err) {
182
- res.status(500).json({ error: err.message });
118
+ catch (error) {
119
+ sendError(res, error);
183
120
  }
184
121
  });
185
- app.get("/api/sessions/messages", (req, res) => {
122
+ app.post("/api/directories", async (req, res) => {
186
123
  try {
187
- const sessionPath = req.query.path;
188
- if (!sessionPath || !sessionPath.endsWith(".jsonl")) {
189
- res.status(400).json({ error: "Missing or invalid session path" });
190
- return;
191
- }
192
- if (!existsSync(sessionPath)) {
193
- res.status(404).json({ error: "Session file not found" });
194
- return;
124
+ const body = await readJsonBody(req, 16 * 1024);
125
+ if (typeof body.parentPath !== "string" || typeof body.name !== "string") {
126
+ throw new LocalBackendApiError("Missing 'parentPath' or 'name' string", 400, "invalid_request");
195
127
  }
196
- const content = readFileSync(sessionPath, "utf8");
197
- const entries = parseSessionEntries(content);
198
- const context = buildSessionContext(entries);
199
- res.json({
200
- messages: context.messages,
201
- model: context.model,
202
- thinkingLevel: context.thinkingLevel,
203
- });
128
+ res.status(201).json(await api.createDirectory(body.parentPath, body.name));
204
129
  }
205
- catch (err) {
206
- res.status(500).json({ error: err.message });
130
+ catch (error) {
131
+ sendError(res, error);
207
132
  }
208
133
  });
209
- app.get("/api/sessions/fork-messages", (req, res) => {
134
+ app.get("/api/settings/local", async (_req, res) => {
210
135
  try {
211
- const sessionPath = req.query.path;
212
- if (!sessionPath || !sessionPath.endsWith(".jsonl")) {
213
- res.status(400).json({ error: "Missing or invalid session path" });
214
- return;
215
- }
216
- if (!existsSync(sessionPath)) {
217
- res.status(404).json({ error: "Session file not found" });
218
- return;
219
- }
220
- const content = readFileSync(sessionPath, "utf8");
221
- const entries = parseSessionEntries(content);
222
- const messages = [];
223
- for (const entry of entries) {
224
- if (entry.type !== "message")
225
- continue;
226
- const msg = entry.message;
227
- if (!msg || msg.role !== "user")
228
- continue;
229
- let text = "";
230
- if (typeof msg.content === "string") {
231
- text = msg.content;
232
- }
233
- else if (Array.isArray(msg.content)) {
234
- text = msg.content
235
- .filter((c) => c.type === "text")
236
- .map((c) => c.text)
237
- .join("");
238
- }
239
- if (text && entry.id) {
240
- messages.push({ entryId: entry.id, text });
241
- }
242
- }
243
- res.json({ messages });
136
+ res.json(await api.getLocalSettings());
244
137
  }
245
- catch (err) {
246
- res.status(500).json({ error: err.message });
138
+ catch (error) {
139
+ sendError(res, error);
247
140
  }
248
141
  });
249
- app.get("/api/sessions/raw", (req, res) => {
142
+ app.post("/api/settings/local/validate", async (req, res) => {
250
143
  try {
251
- const sessionPath = req.query.path;
252
- if (!sessionPath || !sessionPath.endsWith(".jsonl")) {
253
- res.status(400).json({ error: "Missing or invalid session path" });
254
- return;
255
- }
256
- if (!existsSync(sessionPath)) {
257
- res.status(404).json({ error: "Session file not found" });
258
- return;
259
- }
260
- const content = readFileSync(sessionPath, "utf8");
261
- res.type("text/plain").send(content);
144
+ const body = await readJsonBody(req);
145
+ if (typeof body.content !== "string")
146
+ throw new LocalBackendApiError("Missing 'content' string", 400, "invalid_request");
147
+ res.json(await api.validateLocalSettings(body.content));
262
148
  }
263
- catch (err) {
264
- res.status(500).json({ error: err.message });
149
+ catch (error) {
150
+ sendError(res, error);
265
151
  }
266
152
  });
267
- app.get("/api/browse", (req, res) => {
153
+ app.patch("/api/settings/local", async (req, res) => {
268
154
  try {
269
- const requestedPath = req.query.path || process.env.HOME || "/";
270
- const resolved = path.resolve(requestedPath.replace(/^~/, process.env.HOME || "/"));
271
- if (!existsSync(resolved)) {
272
- res.status(404).json({ error: "Path not found" });
273
- return;
155
+ const body = await readJsonBody(req);
156
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
157
+ throw new LocalBackendApiError("Request body must be a JSON object", 400, "invalid_request");
274
158
  }
275
- const entries = readdirSync(resolved, { withFileTypes: true });
276
- const dirs = entries
277
- .filter((e) => e.isDirectory() && !e.name.startsWith("."))
278
- .map((e) => ({
279
- name: e.name,
280
- path: path.join(resolved, e.name),
281
- }))
282
- .sort((a, b) => a.name.localeCompare(b.name));
283
- res.json({ path: resolved, dirs });
159
+ const result = await api.patchLocalSettings(body);
160
+ res.status(result.valid ? 200 : 400).json(result);
161
+ }
162
+ catch (error) {
163
+ sendError(res, error);
164
+ }
165
+ });
166
+ app.put("/api/settings/local", async (req, res) => {
167
+ try {
168
+ const body = await readJsonBody(req);
169
+ if (typeof body.content !== "string")
170
+ throw new LocalBackendApiError("Missing 'content' string", 400, "invalid_request");
171
+ const result = await api.saveLocalSettings(body.content);
172
+ res.status(result.valid ? 200 : 400).json(result);
284
173
  }
285
- catch (err) {
286
- res.status(500).json({ error: err.message });
174
+ catch (error) {
175
+ sendError(res, error);
287
176
  }
288
177
  });
178
+ return api;
289
179
  }