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