ctxpool 0.1.1 → 0.5.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 +107 -99
- package/dist/cli.js +336 -183
- package/dist/cli.js.map +1 -1
- package/dist/clipboard.js +8 -10
- package/dist/clipboard.js.map +1 -1
- package/dist/format.d.ts +32 -0
- package/dist/format.js +110 -0
- package/dist/format.js.map +1 -0
- package/dist/handoff.d.ts +18 -13
- package/dist/handoff.js +495 -706
- package/dist/handoff.js.map +1 -1
- package/dist/mcp.js +99 -221
- package/dist/mcp.js.map +1 -1
- package/dist/pack.d.ts +14 -0
- package/dist/pack.js +121 -0
- package/dist/pack.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.js +35 -5
- package/dist/paths.js.map +1 -1
- package/dist/redact.d.ts +6 -0
- package/dist/redact.js +85 -0
- package/dist/redact.js.map +1 -0
- package/dist/repo.d.ts +6 -1
- package/dist/repo.js +109 -123
- package/dist/repo.js.map +1 -1
- package/dist/session.d.ts +45 -1
- package/dist/session.js +1307 -302
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +14 -6
- package/dist/store.js +90 -115
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +6 -0
- package/dist/text.js +25 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +92 -7
- package/dist/ui.d.ts +10 -1
- package/dist/ui.js +355 -382
- package/dist/ui.js.map +1 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +40 -10
- package/dist/util.js.map +1 -1
- package/dist/verify.d.ts +16 -0
- package/dist/verify.js +74 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist-web/assets/Geist-Variable-B48wpBRb.woff2 +0 -0
- package/dist-web/assets/GeistMono-Variable-BRXCGrxA.woff2 +0 -0
- package/dist-web/assets/antigravity-Vc0oU6JN.svg +1 -0
- package/dist-web/assets/contextpool-mark-4vSRiUeZ.svg +1 -0
- package/dist-web/assets/index-BztZuBPJ.js +21 -0
- package/dist-web/assets/index-LvoT6mmM.css +2 -0
- package/dist-web/index.html +4 -3
- package/package.json +18 -17
- package/dist/logs.d.ts +0 -9
- package/dist/logs.js +0 -82
- package/dist/logs.js.map +0 -1
- package/dist-web/assets/contextpool-mark-Bu_00i5t.svg +0 -16
- package/dist-web/assets/index-BfHRocKl.js +0 -14
- package/dist-web/assets/index-l1QjYqDb.css +0 -2
package/dist/ui.js
CHANGED
|
@@ -1,273 +1,266 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
-
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { createReadStream, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { networkInterfaces } from "node:os";
|
|
3
4
|
import { spawnSync } from "node:child_process";
|
|
4
5
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { buildAgentContext, buildTeamTimelineExport,
|
|
7
|
+
import { buildAgentContext, buildTeamTimelineExport, deriveContextBlocks, summarizeHandoff } from "./handoff.js";
|
|
8
|
+
import { createContextPack, renderContextPackOutput } from "./pack.js";
|
|
9
|
+
import { redactSecrets } from "./redact.js";
|
|
10
|
+
import { SOURCE_AGENTS, sourceSessionDirs } from "./session.js";
|
|
7
11
|
import { openStore } from "./store.js";
|
|
12
|
+
import { CtxError } from "./util.js";
|
|
13
|
+
import { VERSION } from "./version.js";
|
|
8
14
|
const MAX_FILE_VIEW_BYTES = 512 * 1024;
|
|
9
|
-
const MAX_JSON_BODY_BYTES = 1024 * 1024;
|
|
15
|
+
const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
17
|
+
class HttpError extends Error {
|
|
18
|
+
status;
|
|
19
|
+
constructor(status, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.status = status;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
10
24
|
export function isValidContextPackBody(body) {
|
|
11
25
|
return Boolean(parseContextPackBody(body));
|
|
12
26
|
}
|
|
13
27
|
export async function startUi(options = {}) {
|
|
14
28
|
const host = options.host || "127.0.0.1";
|
|
15
29
|
const requestedPort = options.port ?? 4377;
|
|
16
|
-
const ports = options.
|
|
30
|
+
const ports = options.fallback === false ? [requestedPort] : Array.from({ length: 11 }, (_, index) => requestedPort + index);
|
|
31
|
+
const allowedHosts = allowedHostnames(host);
|
|
17
32
|
for (const port of ports) {
|
|
18
|
-
const server =
|
|
33
|
+
const server = createServer((req, res) => {
|
|
34
|
+
handle(req, res, allowedHosts).catch((error) => {
|
|
35
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
36
|
+
if (!res.headersSent)
|
|
37
|
+
sendJson(res, status, { error: error instanceof Error ? error.message : "Unknown error" });
|
|
38
|
+
else
|
|
39
|
+
res.end();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
19
42
|
const result = await listen(server, host, port);
|
|
20
43
|
if (result.ok) {
|
|
21
|
-
|
|
22
|
-
|
|
44
|
+
const address = server.address();
|
|
45
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
46
|
+
const displayHost = host.includes(":") ? `[${host}]` : host;
|
|
47
|
+
return { url: `http://${displayHost}:${boundPort}`, host, port: boundPort, close: () => closeServer(server) };
|
|
23
48
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
throw new Error(`Unable to start ContextPool UI on ${host}:${port}: ${result.code}`);
|
|
49
|
+
if (result.code !== "EADDRINUSE" || ports.length === 1) {
|
|
50
|
+
throw new CtxError(`Could not start the UI on ${host}:${port} (${result.code}).`, result.code === "EADDRINUSE" ? "Pick another port with --port." : undefined);
|
|
27
51
|
}
|
|
28
52
|
}
|
|
29
|
-
throw new
|
|
30
|
-
}
|
|
31
|
-
function createUiServer() {
|
|
32
|
-
return createServer(async (req, res) => {
|
|
33
|
-
try {
|
|
34
|
-
await route(req, res);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
sendJson(res, 500, { error: error instanceof Error ? error.message : "Unknown error" });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
53
|
+
throw new CtxError(`Ports ${ports[0]}–${ports[ports.length - 1]} are all in use.`, "Pick another port with --port.");
|
|
40
54
|
}
|
|
41
55
|
function listen(server, host, port) {
|
|
42
|
-
return new Promise((
|
|
43
|
-
server.once("error", (error) => {
|
|
44
|
-
resolve({ ok: false, code: error.code || error.message });
|
|
45
|
-
});
|
|
56
|
+
return new Promise((resolvePromise) => {
|
|
57
|
+
server.once("error", (error) => resolvePromise({ ok: false, code: error.code || error.message }));
|
|
46
58
|
server.listen(port, host, () => {
|
|
47
59
|
server.removeAllListeners("error");
|
|
48
|
-
|
|
60
|
+
resolvePromise({ ok: true });
|
|
49
61
|
});
|
|
50
62
|
});
|
|
51
63
|
}
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
function closeServer(server) {
|
|
65
|
+
return new Promise((resolvePromise) => {
|
|
66
|
+
server.closeAllConnections?.();
|
|
67
|
+
server.close(() => resolvePromise());
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Routing
|
|
72
|
+
async function handle(req, res, allowedHosts) {
|
|
73
|
+
setSecurityHeaders(res);
|
|
74
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
54
75
|
const method = req.method || "GET";
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
store.close();
|
|
74
|
-
}
|
|
76
|
+
// Reject requests addressed to any other hostname. This blocks DNS-rebinding pages from reading local data.
|
|
77
|
+
if (!isAllowedRequest(req, allowedHosts))
|
|
78
|
+
return sendJson(res, 403, { error: "Forbidden host or origin" });
|
|
79
|
+
if (isMutatingMethod(method) && !isJsonRequest(req))
|
|
80
|
+
return sendJson(res, 415, { error: "Use Content-Type: application/json" });
|
|
81
|
+
const path = url.pathname;
|
|
82
|
+
const param = (prefix, suffix = "") => decodeURIComponent(path.slice(prefix.length, suffix ? -suffix.length : undefined));
|
|
83
|
+
if (path === "/api/status")
|
|
84
|
+
return withStore((store) => sendJson(res, 200, uiStatus(store.getStatus())));
|
|
85
|
+
if (path === "/api/tasks")
|
|
86
|
+
return withStore((store) => sendJson(res, 200, { tasks: store.listTasks() }));
|
|
87
|
+
if (path === "/api/handoffs") {
|
|
88
|
+
return withStore((store) => {
|
|
89
|
+
const packets = store.listHandoffPackets(url.searchParams.get("task") || undefined);
|
|
90
|
+
// The list view only needs the compact summary; full packets carry transcripts and diffs.
|
|
91
|
+
sendJson(res, 200, { handoffs: url.searchParams.get("view") === "list" ? packets.map(summarizeHandoff) : packets });
|
|
92
|
+
});
|
|
75
93
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
store.close();
|
|
86
|
-
}
|
|
94
|
+
if (path === "/api/context-blocks") {
|
|
95
|
+
return withStore((store) => {
|
|
96
|
+
const handoffId = url.searchParams.get("handoff");
|
|
97
|
+
const handoff = handoffId ? store.getHandoffById(handoffId) : undefined;
|
|
98
|
+
if (handoffId && !handoff)
|
|
99
|
+
return sendJson(res, 404, { error: "Handoff not found" });
|
|
100
|
+
const packets = handoff ? [handoff] : store.listHandoffPackets(url.searchParams.get("task") || undefined);
|
|
101
|
+
sendJson(res, 200, { blocks: packets.flatMap(deriveContextBlocks) });
|
|
102
|
+
});
|
|
87
103
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
finally {
|
|
104
|
-
store.close();
|
|
105
|
-
}
|
|
104
|
+
if (path === "/api/context-packs") {
|
|
105
|
+
if (method === "GET")
|
|
106
|
+
return withStore((store) => sendJson(res, 200, { packs: store.listContextPacks() }));
|
|
107
|
+
if (method !== "POST")
|
|
108
|
+
return sendJson(res, 405, { error: "Use GET or POST" });
|
|
109
|
+
const body = await readJsonBody(req);
|
|
110
|
+
const explicit = parseContextPackBody(body);
|
|
111
|
+
if (!explicit && Array.isArray(body.blocks) && !body.blocks.every(isContextBlock))
|
|
112
|
+
return sendJson(res, 400, { error: "Invalid context blocks" });
|
|
113
|
+
const pack = explicit ||
|
|
114
|
+
createContextPack(typeof body.name === "string" ? body.name : "Untitled pack", Array.isArray(body.blocks) ? body.blocks : [], {
|
|
115
|
+
includeRawTranscript: body.includeRawTranscript === true
|
|
116
|
+
});
|
|
117
|
+
return withStore((store) => sendJson(res, 200, { pack: store.saveContextPack({ ...pack, updatedAt: new Date().toISOString() }) }));
|
|
106
118
|
}
|
|
107
|
-
if (
|
|
108
|
-
const id =
|
|
109
|
-
|
|
110
|
-
return sendJson(res, 400, { error: "Missing context pack id" });
|
|
111
|
-
const store = await openStore();
|
|
112
|
-
try {
|
|
119
|
+
if (path.startsWith("/api/context-packs/") && path.endsWith("/export")) {
|
|
120
|
+
const id = param("/api/context-packs/", "/export");
|
|
121
|
+
return withStore((store) => {
|
|
113
122
|
const pack = store.getContextPackById(id);
|
|
114
123
|
if (!pack)
|
|
115
124
|
return sendJson(res, 404, { error: "Context pack not found" });
|
|
116
125
|
const output = renderContextPackOutput(pack);
|
|
117
126
|
return url.searchParams.get("format") === "markdown" ? sendText(res, output.markdown) : sendJson(res, 200, output);
|
|
118
|
-
}
|
|
119
|
-
finally {
|
|
120
|
-
store.close();
|
|
121
|
-
}
|
|
127
|
+
});
|
|
122
128
|
}
|
|
123
|
-
if (
|
|
124
|
-
const id =
|
|
125
|
-
if (
|
|
126
|
-
return
|
|
127
|
-
const store = await openStore();
|
|
128
|
-
try {
|
|
129
|
-
if (req.method === "GET") {
|
|
129
|
+
if (path.startsWith("/api/context-packs/")) {
|
|
130
|
+
const id = param("/api/context-packs/");
|
|
131
|
+
if (method === "GET") {
|
|
132
|
+
return withStore((store) => {
|
|
130
133
|
const pack = store.getContextPackById(id);
|
|
131
134
|
return sendJson(res, pack ? 200 : 404, pack ? { pack } : { error: "Context pack not found" });
|
|
132
|
-
}
|
|
133
|
-
if (req.method === "DELETE") {
|
|
134
|
-
return sendJson(res, store.deleteContextPack(id) ? 200 : 404, { ok: true });
|
|
135
|
-
}
|
|
136
|
-
if (req.method !== "PUT")
|
|
137
|
-
return sendJson(res, 405, { error: "Use GET, PUT, or DELETE" });
|
|
138
|
-
const body = await readJsonBody(req);
|
|
139
|
-
const pack = parseContextPackBody(body);
|
|
140
|
-
if (!pack || pack.id !== id)
|
|
141
|
-
return sendJson(res, 400, { error: "Invalid context pack body" });
|
|
142
|
-
return sendJson(res, 200, { pack: store.saveContextPack({ ...pack, updatedAt: new Date().toISOString() }) });
|
|
143
|
-
}
|
|
144
|
-
finally {
|
|
145
|
-
store.close();
|
|
135
|
+
});
|
|
146
136
|
}
|
|
137
|
+
if (method === "DELETE")
|
|
138
|
+
return withStore((store) => (store.deleteContextPack(id) ? sendJson(res, 200, { ok: true }) : sendJson(res, 404, { error: "Context pack not found" })));
|
|
139
|
+
if (method !== "PUT")
|
|
140
|
+
return sendJson(res, 405, { error: "Use GET, PUT, or DELETE" });
|
|
141
|
+
const pack = parseContextPackBody(await readJsonBody(req));
|
|
142
|
+
if (!pack || pack.id !== id)
|
|
143
|
+
return sendJson(res, 400, { error: "Invalid context pack body" });
|
|
144
|
+
return withStore((store) => sendJson(res, 200, { pack: store.saveContextPack({ ...pack, updatedAt: new Date().toISOString() }) }));
|
|
147
145
|
}
|
|
148
|
-
if (
|
|
149
|
-
const id =
|
|
150
|
-
|
|
151
|
-
return sendJson(res, 400, { error: "Missing handoff id" });
|
|
152
|
-
const store = await openStore();
|
|
153
|
-
try {
|
|
146
|
+
if (path.startsWith("/api/handoffs/") && path.endsWith("/agent-context")) {
|
|
147
|
+
const id = param("/api/handoffs/", "/agent-context");
|
|
148
|
+
return withStore((store) => {
|
|
154
149
|
const handoff = store.getHandoffById(id);
|
|
155
150
|
if (!handoff)
|
|
156
151
|
return sendJson(res, 404, { error: "Handoff not found" });
|
|
157
152
|
const context = buildAgentContext(handoff);
|
|
158
153
|
return url.searchParams.get("format") === "json" ? sendJson(res, 200, context) : sendText(res, context.markdown);
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
store.close();
|
|
162
|
-
}
|
|
154
|
+
});
|
|
163
155
|
}
|
|
164
|
-
if (
|
|
165
|
-
const id =
|
|
166
|
-
|
|
167
|
-
return sendJson(res, 400, { error: "Missing handoff id" });
|
|
168
|
-
const store = await openStore();
|
|
169
|
-
try {
|
|
156
|
+
if (path.startsWith("/api/handoffs/") && path.endsWith("/team-export")) {
|
|
157
|
+
const id = param("/api/handoffs/", "/team-export");
|
|
158
|
+
return withStore((store) => {
|
|
170
159
|
const handoff = store.getHandoffById(id);
|
|
171
160
|
if (!handoff)
|
|
172
161
|
return sendJson(res, 404, { error: "Handoff not found" });
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
store.close();
|
|
178
|
-
}
|
|
162
|
+
return sendJson(res, 200, buildTeamTimelineExport(handoff, { includeRawTranscript: url.searchParams.get("includeRawTranscript") === "true" }));
|
|
163
|
+
});
|
|
179
164
|
}
|
|
180
|
-
if (
|
|
181
|
-
const id =
|
|
182
|
-
|
|
183
|
-
return sendJson(res, 400, { error: "Missing handoff id" });
|
|
184
|
-
const store = await openStore();
|
|
185
|
-
try {
|
|
165
|
+
if (path.startsWith("/api/handoffs/")) {
|
|
166
|
+
const id = param("/api/handoffs/");
|
|
167
|
+
return withStore((store) => {
|
|
186
168
|
const handoff = store.getHandoffById(id);
|
|
187
169
|
return sendJson(res, handoff ? 200 : 404, handoff ? { handoff } : { error: "Handoff not found" });
|
|
188
|
-
}
|
|
189
|
-
finally {
|
|
190
|
-
store.close();
|
|
191
|
-
}
|
|
170
|
+
});
|
|
192
171
|
}
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
try {
|
|
172
|
+
if (path === "/api/latest") {
|
|
173
|
+
return withStore((store) => {
|
|
196
174
|
const handoff = store.getLatestHandoff(url.searchParams.get("task") || undefined);
|
|
197
175
|
return sendJson(res, handoff ? 200 : 404, handoff ? { handoff } : { error: "No handoff found" });
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
store.close();
|
|
201
|
-
}
|
|
176
|
+
});
|
|
202
177
|
}
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
178
|
+
if (path === "/api/files/view") {
|
|
179
|
+
if (method !== "GET")
|
|
205
180
|
return sendJson(res, 405, { ok: false, error: "Use GET" });
|
|
206
181
|
const repoPath = url.searchParams.get("repoPath") || "";
|
|
207
|
-
const filePath = url.searchParams.get("path") || "";
|
|
208
182
|
if (!(await isKnownRepoPath(repoPath)))
|
|
209
|
-
return sendJson(res, 403, { ok: false, error: "Repository
|
|
210
|
-
const result = readRepoFileForView(repoPath,
|
|
183
|
+
return sendJson(res, 403, { ok: false, error: "Repository is not in any saved handoff" });
|
|
184
|
+
const result = readRepoFileForView(repoPath, url.searchParams.get("path") || "");
|
|
211
185
|
return sendJson(res, result.ok ? 200 : fileViewStatus(result.error), result);
|
|
212
186
|
}
|
|
213
|
-
if (
|
|
214
|
-
if (
|
|
187
|
+
if (path === "/api/files/open") {
|
|
188
|
+
if (method !== "POST")
|
|
215
189
|
return sendJson(res, 405, { error: "Use POST" });
|
|
216
190
|
const body = await readJsonBody(req);
|
|
217
191
|
const repoPath = typeof body.repoPath === "string" ? body.repoPath : "";
|
|
218
|
-
const filePath = typeof body.path === "string" ? body.path : "";
|
|
219
|
-
const line = typeof body.line === "number" ? body.line : undefined;
|
|
220
|
-
const mode = body.mode === "reveal" ? "reveal" : "open";
|
|
221
192
|
if (!(await isKnownRepoPath(repoPath)))
|
|
222
|
-
return sendJson(res, 403, { ok: false, error: "Repository
|
|
223
|
-
const safe = resolveSafeRepoPath(repoPath,
|
|
193
|
+
return sendJson(res, 403, { ok: false, error: "Repository is not in any saved handoff" });
|
|
194
|
+
const safe = resolveSafeRepoPath(repoPath, typeof body.path === "string" ? body.path : "");
|
|
224
195
|
if (!safe.ok)
|
|
225
196
|
return sendJson(res, 400, { ok: false, error: safe.error });
|
|
226
197
|
if (!existsSync(safe.path))
|
|
227
198
|
return sendJson(res, 404, { ok: false, error: "File does not exist on disk" });
|
|
228
|
-
|
|
199
|
+
const line = typeof body.line === "number" && Number.isInteger(body.line) && body.line > 0 ? body.line : undefined;
|
|
200
|
+
return sendJson(res, 200, openLocalFile(safe.path, line, body.mode === "reveal" ? "reveal" : "open"));
|
|
229
201
|
}
|
|
230
|
-
if (
|
|
202
|
+
if (path.startsWith("/api/"))
|
|
203
|
+
return sendJson(res, 404, { error: "Not found" });
|
|
204
|
+
if (method !== "GET" && method !== "HEAD")
|
|
205
|
+
return sendJson(res, 405, { error: "Use GET" });
|
|
206
|
+
if (trySendStaticAsset(path, res))
|
|
231
207
|
return;
|
|
232
|
-
|
|
233
|
-
return sendWebApp(res);
|
|
234
|
-
sendJson(res, 404, { error: "Not found" });
|
|
235
|
-
}
|
|
236
|
-
function isHandoffPage(pathname) {
|
|
237
|
-
const parts = pathname.split("/").filter(Boolean);
|
|
238
|
-
return parts.length === 2 && parts[0] === "handoffs" && parts[1].length > 0;
|
|
239
|
-
}
|
|
240
|
-
function sendJson(res, status, data) {
|
|
241
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
242
|
-
res.end(JSON.stringify(data, null, 2));
|
|
208
|
+
return sendWebApp(res);
|
|
243
209
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
210
|
+
function uiStatus(status) {
|
|
211
|
+
const dirs = sourceSessionDirs();
|
|
212
|
+
return {
|
|
213
|
+
version: VERSION,
|
|
214
|
+
home: status.home,
|
|
215
|
+
handoffCount: status.handoffCount,
|
|
216
|
+
taskCount: status.taskCount,
|
|
217
|
+
packCount: status.packCount,
|
|
218
|
+
latestAt: status.latestAt,
|
|
219
|
+
agents: Object.fromEntries(SOURCE_AGENTS.map((source) => [source, existsSync(dirs[source])]))
|
|
220
|
+
};
|
|
247
221
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
222
|
+
async function withStore(fn) {
|
|
223
|
+
const store = await openStore();
|
|
224
|
+
try {
|
|
225
|
+
fn(store);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
store.close();
|
|
229
|
+
}
|
|
251
230
|
}
|
|
252
|
-
|
|
253
|
-
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Request guards
|
|
233
|
+
function allowedHostnames(boundHost) {
|
|
234
|
+
const hosts = new Set(LOOPBACK);
|
|
235
|
+
if (boundHost === "0.0.0.0" || boundHost === "::") {
|
|
236
|
+
for (const addresses of Object.values(networkInterfaces()))
|
|
237
|
+
for (const address of addresses || [])
|
|
238
|
+
hosts.add(address.address);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
hosts.add(boundHost);
|
|
242
|
+
}
|
|
243
|
+
return hosts;
|
|
254
244
|
}
|
|
255
245
|
export function isLocalRequest(req) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
return isAllowedRequest(req, LOOPBACK);
|
|
247
|
+
}
|
|
248
|
+
function isAllowedRequest(req, allowed) {
|
|
249
|
+
const host = parseHost(req.headers.host);
|
|
250
|
+
if (!host || !allowed.has(host.hostname))
|
|
259
251
|
return false;
|
|
252
|
+
const origin = typeof req.headers.origin === "string" ? req.headers.origin : "";
|
|
260
253
|
if (!origin)
|
|
261
254
|
return true;
|
|
262
255
|
try {
|
|
263
256
|
const parsed = new URL(origin);
|
|
264
|
-
return
|
|
257
|
+
return allowed.has(parsed.hostname) && (parsed.port || "80") === (host.port || "80");
|
|
265
258
|
}
|
|
266
259
|
catch {
|
|
267
260
|
return false;
|
|
268
261
|
}
|
|
269
262
|
}
|
|
270
|
-
function
|
|
263
|
+
function parseHost(value) {
|
|
271
264
|
const raw = Array.isArray(value) ? value[0] : value;
|
|
272
265
|
if (!raw)
|
|
273
266
|
return undefined;
|
|
@@ -279,8 +272,13 @@ function normalizeHost(value) {
|
|
|
279
272
|
return undefined;
|
|
280
273
|
}
|
|
281
274
|
}
|
|
282
|
-
function
|
|
283
|
-
return
|
|
275
|
+
function isMutatingMethod(method) {
|
|
276
|
+
return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
|
|
277
|
+
}
|
|
278
|
+
function isJsonRequest(req) {
|
|
279
|
+
if (req.method === "DELETE")
|
|
280
|
+
return true;
|
|
281
|
+
return /^application\/json\b/i.test(req.headers["content-type"] || "");
|
|
284
282
|
}
|
|
285
283
|
async function isKnownRepoPath(repoPath) {
|
|
286
284
|
if (!repoPath)
|
|
@@ -288,95 +286,12 @@ async function isKnownRepoPath(repoPath) {
|
|
|
288
286
|
const requested = resolve(repoPath);
|
|
289
287
|
const store = await openStore();
|
|
290
288
|
try {
|
|
291
|
-
return store.
|
|
289
|
+
return store.listRepoPaths().some((path) => resolve(path) === requested);
|
|
292
290
|
}
|
|
293
291
|
finally {
|
|
294
292
|
store.close();
|
|
295
293
|
}
|
|
296
294
|
}
|
|
297
|
-
function sendWebApp(res) {
|
|
298
|
-
const indexPath = join(webDistDir(), "index.html");
|
|
299
|
-
if (existsSync(indexPath))
|
|
300
|
-
return sendHtml(res, readFileSync(indexPath, "utf8"));
|
|
301
|
-
return sendHtml(res, `<!doctype html><html><head><title>ContextPool</title><style>body{font-family:ui-sans-serif,system-ui;margin:40px;color:#09090b}code{background:#f4f4f5;padding:2px 4px;border-radius:4px}</style></head><body><h1>ContextPool UI assets are not built</h1><p>Run <code>npm run build:web</code> or <code>npm run build</code>, then restart <code>ctxpool ui</code>.</p></body></html>`);
|
|
302
|
-
}
|
|
303
|
-
function trySendStaticAsset(pathname, res) {
|
|
304
|
-
const extension = extname(pathname);
|
|
305
|
-
if (!extension)
|
|
306
|
-
return false;
|
|
307
|
-
const root = webDistDir();
|
|
308
|
-
const requested = resolve(root, `.${decodeURIComponent(pathname)}`);
|
|
309
|
-
const rel = relative(root, requested);
|
|
310
|
-
if (rel.startsWith("..") || isAbsolute(rel) || !existsSync(requested))
|
|
311
|
-
return false;
|
|
312
|
-
const stats = statSync(requested);
|
|
313
|
-
if (!stats.isFile())
|
|
314
|
-
return false;
|
|
315
|
-
res.writeHead(200, { "content-type": contentType(extension) });
|
|
316
|
-
createReadStream(requested).pipe(res);
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
function webDistDir() {
|
|
320
|
-
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
321
|
-
return resolve(moduleDir, "..", "dist-web");
|
|
322
|
-
}
|
|
323
|
-
function contentType(extension) {
|
|
324
|
-
switch (extension) {
|
|
325
|
-
case ".html":
|
|
326
|
-
return "text/html; charset=utf-8";
|
|
327
|
-
case ".js":
|
|
328
|
-
return "text/javascript; charset=utf-8";
|
|
329
|
-
case ".css":
|
|
330
|
-
return "text/css; charset=utf-8";
|
|
331
|
-
case ".svg":
|
|
332
|
-
return "image/svg+xml";
|
|
333
|
-
case ".png":
|
|
334
|
-
return "image/png";
|
|
335
|
-
case ".jpg":
|
|
336
|
-
case ".jpeg":
|
|
337
|
-
return "image/jpeg";
|
|
338
|
-
case ".webp":
|
|
339
|
-
return "image/webp";
|
|
340
|
-
default:
|
|
341
|
-
return "application/octet-stream";
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
export function readRepoFileForView(repoPath, filePath) {
|
|
345
|
-
const safe = resolveSafeRepoPath(repoPath, filePath);
|
|
346
|
-
if (!safe.ok)
|
|
347
|
-
return { ok: false, error: safe.error };
|
|
348
|
-
if (!existsSync(safe.path))
|
|
349
|
-
return { ok: false, path: filePath, error: "File does not exist on disk" };
|
|
350
|
-
const stats = statSync(safe.path);
|
|
351
|
-
if (!stats.isFile())
|
|
352
|
-
return { ok: false, path: filePath, error: "Path is not a file" };
|
|
353
|
-
if (stats.size > MAX_FILE_VIEW_BYTES) {
|
|
354
|
-
return { ok: false, path: filePath, size: stats.size, error: `File is larger than ${MAX_FILE_VIEW_BYTES} bytes` };
|
|
355
|
-
}
|
|
356
|
-
const buffer = readFileSync(safe.path);
|
|
357
|
-
if (isBinaryFile(filePath, buffer))
|
|
358
|
-
return { ok: false, path: filePath, size: stats.size, error: "Binary files cannot be previewed" };
|
|
359
|
-
const redacted = redactSensitivePreview(filePath, buffer.toString("utf8"));
|
|
360
|
-
return {
|
|
361
|
-
ok: true,
|
|
362
|
-
path: filePath,
|
|
363
|
-
absolutePath: safe.path,
|
|
364
|
-
size: stats.size,
|
|
365
|
-
language: languageForPath(filePath),
|
|
366
|
-
content: redacted.content,
|
|
367
|
-
truncated: false,
|
|
368
|
-
redacted: redacted.redacted
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
function fileViewStatus(error) {
|
|
372
|
-
if (/outside|Missing|Repository/.test(error))
|
|
373
|
-
return 400;
|
|
374
|
-
if (/does not exist/.test(error))
|
|
375
|
-
return 404;
|
|
376
|
-
if (/larger|Binary|not a file/.test(error))
|
|
377
|
-
return 415;
|
|
378
|
-
return 400;
|
|
379
|
-
}
|
|
380
295
|
async function readJsonBody(req) {
|
|
381
296
|
const chunks = [];
|
|
382
297
|
let size = 0;
|
|
@@ -384,173 +299,231 @@ async function readJsonBody(req) {
|
|
|
384
299
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
385
300
|
size += buffer.length;
|
|
386
301
|
if (size > MAX_JSON_BODY_BYTES)
|
|
387
|
-
throw new
|
|
302
|
+
throw new HttpError(413, `Body exceeds ${MAX_JSON_BODY_BYTES} bytes`);
|
|
388
303
|
chunks.push(buffer);
|
|
389
304
|
}
|
|
390
|
-
if (chunks.length
|
|
305
|
+
if (!chunks.length)
|
|
391
306
|
return {};
|
|
392
307
|
try {
|
|
393
|
-
|
|
308
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
309
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
310
|
+
throw new Error();
|
|
311
|
+
return parsed;
|
|
394
312
|
}
|
|
395
313
|
catch {
|
|
396
|
-
|
|
314
|
+
throw new HttpError(400, "Body must be a JSON object");
|
|
397
315
|
}
|
|
398
316
|
}
|
|
399
317
|
function parseContextPackBody(body) {
|
|
400
318
|
if (typeof body.id !== "string" || typeof body.name !== "string" || !Array.isArray(body.items))
|
|
401
319
|
return undefined;
|
|
402
|
-
|
|
403
|
-
const items = body.items.filter(isContextPackItem);
|
|
404
|
-
if (items.length !== body.items.length)
|
|
320
|
+
if (!body.items.every(isContextPackItem))
|
|
405
321
|
return undefined;
|
|
322
|
+
const now = new Date().toISOString();
|
|
406
323
|
return {
|
|
407
324
|
id: body.id,
|
|
408
325
|
name: body.name,
|
|
409
326
|
createdAt: typeof body.createdAt === "string" ? body.createdAt : now,
|
|
410
327
|
updatedAt: typeof body.updatedAt === "string" ? body.updatedAt : now,
|
|
411
328
|
includeRawTranscript: body.includeRawTranscript === true,
|
|
412
|
-
items
|
|
329
|
+
items: body.items
|
|
413
330
|
};
|
|
414
331
|
}
|
|
415
332
|
function isContextPackItem(value) {
|
|
416
333
|
if (!value || typeof value !== "object")
|
|
417
334
|
return false;
|
|
418
335
|
const item = value;
|
|
419
|
-
return
|
|
420
|
-
typeof item.blockId === "string" &&
|
|
421
|
-
typeof item.order === "number" &&
|
|
422
|
-
typeof item.enabled === "boolean" &&
|
|
423
|
-
isContextBlock(item.block));
|
|
336
|
+
return typeof item.id === "string" && typeof item.blockId === "string" && typeof item.order === "number" && typeof item.enabled === "boolean" && isContextBlock(item.block);
|
|
424
337
|
}
|
|
338
|
+
const CATEGORIES = new Set(["intent", "instruction", "decision", "constraint", "verification", "risk", "next-action", "changed-file", "transcript"]);
|
|
339
|
+
const PROVENANCE = new Set(["user-authored", "transcript-derived", "diff-derived", "command-derived", "ai-inferred"]);
|
|
340
|
+
const CONFIDENCE = new Set(["high", "medium", "low"]);
|
|
425
341
|
function isContextBlock(value) {
|
|
426
342
|
if (!value || typeof value !== "object")
|
|
427
343
|
return false;
|
|
428
344
|
const block = value;
|
|
429
|
-
const provenance = block.provenance;
|
|
345
|
+
const provenance = (block.provenance || {});
|
|
430
346
|
return (typeof block.id === "string" &&
|
|
431
347
|
typeof block.sourceHandoffId === "string" &&
|
|
432
348
|
typeof block.sourceTask === "string" &&
|
|
433
349
|
typeof block.sourceCreatedAt === "string" &&
|
|
434
|
-
|
|
350
|
+
CATEGORIES.has(block.category) &&
|
|
435
351
|
typeof block.title === "string" &&
|
|
436
352
|
typeof block.text === "string" &&
|
|
437
353
|
typeof block.estimatedChars === "number" &&
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
354
|
+
PROVENANCE.has(provenance.kind) &&
|
|
355
|
+
CONFIDENCE.has(provenance.confidence) &&
|
|
356
|
+
typeof provenance.source === "string");
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Responses
|
|
360
|
+
function setSecurityHeaders(res) {
|
|
361
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
362
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
363
|
+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
364
|
+
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
365
|
+
}
|
|
366
|
+
function sendJson(res, status, data) {
|
|
367
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
368
|
+
res.end(JSON.stringify(data));
|
|
369
|
+
}
|
|
370
|
+
function sendText(res, text) {
|
|
371
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
372
|
+
res.end(text);
|
|
373
|
+
}
|
|
374
|
+
function sendWebApp(res) {
|
|
375
|
+
const indexPath = join(webDistDir(), "index.html");
|
|
376
|
+
const html = existsSync(indexPath)
|
|
377
|
+
? readFileSync(indexPath, "utf8")
|
|
378
|
+
: `<!doctype html><html><head><meta charset="utf-8"><title>ContextPool</title><style>body{font:15px/1.6 ui-sans-serif,system-ui;margin:15vh auto;max-width:34rem;padding:0 24px;color:#37352f}code{background:#f1f1ef;padding:2px 6px;border-radius:4px}</style></head><body><h1 style="font-weight:600;font-size:20px">UI assets are not built</h1><p>Run <code>npm run build</code>, then restart <code>ctxpool ui</code>.</p></body></html>`;
|
|
379
|
+
res.writeHead(200, {
|
|
380
|
+
"content-type": "text/html; charset=utf-8",
|
|
381
|
+
"cache-control": "no-store",
|
|
382
|
+
"content-security-policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
|
|
383
|
+
});
|
|
384
|
+
res.end(html);
|
|
385
|
+
}
|
|
386
|
+
function trySendStaticAsset(pathname, res) {
|
|
387
|
+
const extension = extname(pathname);
|
|
388
|
+
if (!extension)
|
|
389
|
+
return false;
|
|
390
|
+
const root = webDistDir();
|
|
391
|
+
const requested = resolve(root, `.${decodeURIComponent(pathname)}`);
|
|
392
|
+
const rel = relative(root, requested);
|
|
393
|
+
if (rel.startsWith("..") || isAbsolute(rel) || !existsSync(requested) || !statSync(requested).isFile())
|
|
394
|
+
return false;
|
|
395
|
+
res.writeHead(200, {
|
|
396
|
+
"content-type": contentType(extension),
|
|
397
|
+
"cache-control": rel.startsWith("assets") ? "public, max-age=31536000, immutable" : "no-cache"
|
|
398
|
+
});
|
|
399
|
+
createReadStream(requested).pipe(res);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
function webDistDir() {
|
|
403
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "dist-web");
|
|
404
|
+
}
|
|
405
|
+
function contentType(extension) {
|
|
406
|
+
const types = {
|
|
407
|
+
".html": "text/html; charset=utf-8",
|
|
408
|
+
".js": "text/javascript; charset=utf-8",
|
|
409
|
+
".css": "text/css; charset=utf-8",
|
|
410
|
+
".svg": "image/svg+xml",
|
|
411
|
+
".png": "image/png",
|
|
412
|
+
".jpg": "image/jpeg",
|
|
413
|
+
".jpeg": "image/jpeg",
|
|
414
|
+
".webp": "image/webp",
|
|
415
|
+
".woff2": "font/woff2",
|
|
416
|
+
".ico": "image/x-icon"
|
|
417
|
+
};
|
|
418
|
+
return types[extension] || "application/octet-stream";
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// File preview
|
|
422
|
+
export function readRepoFileForView(repoPath, filePath) {
|
|
423
|
+
const safe = resolveSafeRepoPath(repoPath, filePath);
|
|
424
|
+
if (!safe.ok)
|
|
425
|
+
return { ok: false, error: safe.error };
|
|
426
|
+
if (!existsSync(safe.path))
|
|
427
|
+
return { ok: false, path: filePath, error: "File does not exist on disk" };
|
|
428
|
+
const stats = statSync(safe.path);
|
|
429
|
+
if (!stats.isFile())
|
|
430
|
+
return { ok: false, path: filePath, error: "Path is not a file" };
|
|
431
|
+
if (stats.size > MAX_FILE_VIEW_BYTES)
|
|
432
|
+
return { ok: false, path: filePath, size: stats.size, error: `File is larger than ${MAX_FILE_VIEW_BYTES} bytes` };
|
|
433
|
+
const buffer = readFileSync(safe.path);
|
|
434
|
+
if (isBinaryFile(filePath, buffer))
|
|
435
|
+
return { ok: false, path: filePath, size: stats.size, error: "Binary files cannot be previewed" };
|
|
436
|
+
const preview = redactPreview(filePath, buffer.toString("utf8"));
|
|
437
|
+
return { ok: true, path: filePath, absolutePath: safe.path, size: stats.size, language: languageForPath(filePath), content: preview.content, truncated: false, redacted: preview.redacted };
|
|
459
438
|
}
|
|
439
|
+
/** Resolve `filePath` inside `repoPath`, following symlinks so a link cannot point outside the repository. */
|
|
460
440
|
export function resolveSafeRepoPath(repoPath, filePath) {
|
|
461
441
|
if (!repoPath || !filePath)
|
|
462
442
|
return { ok: false, error: "Missing repoPath or path" };
|
|
463
443
|
const repo = resolve(repoPath);
|
|
464
444
|
if (!existsSync(repo))
|
|
465
445
|
return { ok: false, error: "Repository path does not exist" };
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return { ok: true, path: resolve(filePath) };
|
|
471
|
-
}
|
|
472
|
-
const fullPath = resolve(repo, filePath);
|
|
473
|
-
const rel = relative(repo, fullPath);
|
|
474
|
-
if (rel.startsWith("..") || isAbsolute(rel))
|
|
446
|
+
const target = isAbsolute(filePath) ? resolve(filePath) : resolve(repo, filePath);
|
|
447
|
+
if (!isInside(target, repo))
|
|
448
|
+
return { ok: false, error: "File is outside the repository" };
|
|
449
|
+
if (existsSync(target) && !isInside(realpathSync(target), realpathSync(repo)))
|
|
475
450
|
return { ok: false, error: "File is outside the repository" };
|
|
476
|
-
return { ok: true, path:
|
|
451
|
+
return { ok: true, path: target };
|
|
452
|
+
}
|
|
453
|
+
function isInside(path, root) {
|
|
454
|
+
const rel = relative(root, path);
|
|
455
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
456
|
+
}
|
|
457
|
+
function fileViewStatus(error) {
|
|
458
|
+
if (/does not exist/.test(error))
|
|
459
|
+
return 404;
|
|
460
|
+
if (/larger|Binary|not a file/.test(error))
|
|
461
|
+
return 415;
|
|
462
|
+
return 400;
|
|
477
463
|
}
|
|
478
464
|
function isBinaryFile(path, buffer) {
|
|
479
465
|
if (/\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|tar|tgz|woff2?|ttf|otf|mp4|mov|mp3|wasm|sqlite|db)$/i.test(path))
|
|
480
466
|
return true;
|
|
481
|
-
|
|
482
|
-
return sample.includes(0);
|
|
467
|
+
return buffer.subarray(0, Math.min(buffer.length, 8192)).includes(0);
|
|
483
468
|
}
|
|
484
469
|
function languageForPath(path) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
case ".py":
|
|
508
|
-
return "python";
|
|
509
|
-
case ".go":
|
|
510
|
-
return "go";
|
|
511
|
-
case ".rs":
|
|
512
|
-
return "rust";
|
|
513
|
-
case ".yml":
|
|
514
|
-
case ".yaml":
|
|
515
|
-
return "yaml";
|
|
516
|
-
case ".toml":
|
|
517
|
-
return "toml";
|
|
518
|
-
case ".sh":
|
|
519
|
-
return "shell";
|
|
520
|
-
default:
|
|
521
|
-
return "text";
|
|
522
|
-
}
|
|
470
|
+
const languages = {
|
|
471
|
+
".ts": "typescript",
|
|
472
|
+
".tsx": "tsx",
|
|
473
|
+
".js": "javascript",
|
|
474
|
+
".mjs": "javascript",
|
|
475
|
+
".cjs": "javascript",
|
|
476
|
+
".jsx": "jsx",
|
|
477
|
+
".json": "json",
|
|
478
|
+
".css": "css",
|
|
479
|
+
".html": "html",
|
|
480
|
+
".md": "markdown",
|
|
481
|
+
".mdx": "markdown",
|
|
482
|
+
".sql": "sql",
|
|
483
|
+
".py": "python",
|
|
484
|
+
".go": "go",
|
|
485
|
+
".rs": "rust",
|
|
486
|
+
".yml": "yaml",
|
|
487
|
+
".yaml": "yaml",
|
|
488
|
+
".toml": "toml",
|
|
489
|
+
".sh": "shell"
|
|
490
|
+
};
|
|
491
|
+
return languages[extname(path).toLowerCase()] || "text";
|
|
523
492
|
}
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return `${prefix}${value}`;
|
|
531
|
-
redacted = true;
|
|
532
|
-
return `${prefix}[redacted]`;
|
|
533
|
-
});
|
|
534
|
-
return { content: next, redacted };
|
|
493
|
+
function redactPreview(path, content) {
|
|
494
|
+
let next = redactSecrets(content);
|
|
495
|
+
if (/(^|\/)\.env(?:\.|$)/i.test(path)) {
|
|
496
|
+
next = next.replace(/^(\s*(?:export\s+)?[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|DATABASE_URL|URL|DSN)[A-Z0-9_]*\s*=\s*)(.+)$/gim, (match, prefix, value) => value.trim() && !value.trim().startsWith("#") ? `${prefix}[redacted]` : match);
|
|
497
|
+
}
|
|
498
|
+
return { content: next, redacted: next !== content };
|
|
535
499
|
}
|
|
500
|
+
// Opening these with the OS handler would execute them; reveal them instead.
|
|
501
|
+
const EXECUTABLE = /\.(app|command|tool|terminal|sh|bash|zsh|scpt|applescript|workflow|pkg|dmg|exe|bat|cmd|ps1|msi|desktop|jar)$/i;
|
|
536
502
|
function openLocalFile(path, line, mode) {
|
|
537
|
-
const editor = process.env.CTXPOOL_EDITOR;
|
|
538
503
|
const editorTarget = line ? `${path}:${line}` : path;
|
|
539
|
-
const
|
|
540
|
-
?
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
]
|
|
549
|
-
|
|
550
|
-
|
|
504
|
+
const reveal = process.platform === "darwin"
|
|
505
|
+
? [{ cmd: "open", args: ["-R", path], method: "open -R" }]
|
|
506
|
+
: process.platform === "win32"
|
|
507
|
+
? [{ cmd: "explorer", args: [`/select,${path}`], method: "explorer" }]
|
|
508
|
+
: [{ cmd: "xdg-open", args: [dirname(path)], method: "xdg-open" }];
|
|
509
|
+
const editors = [
|
|
510
|
+
...(process.env.CTXPOOL_EDITOR ? [{ cmd: process.env.CTXPOOL_EDITOR, args: [editorTarget], method: "CTXPOOL_EDITOR" }] : []),
|
|
511
|
+
{ cmd: "cursor", args: ["-g", editorTarget], method: "cursor" },
|
|
512
|
+
{ cmd: "code", args: ["-g", editorTarget], method: "code" },
|
|
513
|
+
{ cmd: "zed", args: [editorTarget], method: "zed" }
|
|
514
|
+
];
|
|
515
|
+
const systemOpen = EXECUTABLE.test(path)
|
|
516
|
+
? reveal
|
|
517
|
+
: process.platform === "darwin"
|
|
518
|
+
? [{ cmd: "open", args: ["-t", path], method: "open -t" }]
|
|
519
|
+
: process.platform === "win32"
|
|
520
|
+
? []
|
|
521
|
+
: [{ cmd: "xdg-open", args: [path], method: "xdg-open" }];
|
|
522
|
+
for (const candidate of mode === "reveal" ? reveal : [...editors, ...systemOpen]) {
|
|
523
|
+
const result = spawnSync(candidate.cmd, candidate.args, { stdio: "ignore", timeout: 5_000 });
|
|
551
524
|
if (!result.error && (result.status === 0 || result.status === null))
|
|
552
525
|
return { ok: true, method: candidate.method };
|
|
553
526
|
}
|
|
554
|
-
return { ok: false, error: "No
|
|
527
|
+
return { ok: false, error: "No editor or file opener succeeded" };
|
|
555
528
|
}
|
|
556
529
|
//# sourceMappingURL=ui.js.map
|