create-interview-cockpit 0.17.1 → 0.17.3
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/package.json +1 -1
- package/template/client/src/api.ts +2 -9
- package/template/client/src/components/ChatMessage.tsx +23 -14
- package/template/client/src/components/ChatView.tsx +8 -1
- package/template/client/src/components/CodeRunnerModal.tsx +132 -86
- package/template/client/src/components/InfraLabModal.tsx +1 -1
- package/template/client/src/components/WorkspaceSwitcher.tsx +3 -2
- package/template/client/src/store.ts +2 -10
- package/template/client/tsconfig.tsbuildinfo +1 -1
- package/template/cockpit.json +1 -1
- package/template/server/src/google-drive.ts +272 -83
- package/template/server/src/index.ts +219 -92
- package/template/server/src/storage.ts +15 -0
|
@@ -56,9 +56,26 @@ const upload = multer({
|
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const MODEL_READABLE_IMAGE_EXTS = new Set([
|
|
60
|
+
"png",
|
|
61
|
+
"jpg",
|
|
62
|
+
"jpeg",
|
|
63
|
+
"gif",
|
|
64
|
+
"webp",
|
|
65
|
+
]);
|
|
66
|
+
const MAX_IMAGE_BYTES_FOR_MODEL = 10 * 1024 * 1024;
|
|
67
|
+
|
|
68
|
+
function extensionForFilename(filename: string): string {
|
|
69
|
+
return filename.split(".").pop()?.toLowerCase() ?? "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isModelReadableImage(filename: string): boolean {
|
|
73
|
+
return MODEL_READABLE_IMAGE_EXTS.has(extensionForFilename(filename));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract text from uploaded files (docx, pdf, plain text; images stay visual assets)
|
|
60
77
|
async function extractText(buffer: Buffer, filename: string): Promise<string> {
|
|
61
|
-
const ext = filename
|
|
78
|
+
const ext = extensionForFilename(filename);
|
|
62
79
|
if (ext === "docx") {
|
|
63
80
|
try {
|
|
64
81
|
const result = await mammoth.extractRawText({ buffer });
|
|
@@ -76,17 +93,14 @@ async function extractText(buffer: Buffer, filename: string): Promise<string> {
|
|
|
76
93
|
return `[PDF extraction failed: ${e?.message ?? "unknown error"}. The original file is stored and can be downloaded.]`;
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Images can't be read as text; store original for download.
|
|
82
|
-
// Return a descriptor so the LLM knows the file exists.
|
|
83
|
-
return `[Image file: ${filename} — the original is stored and available for download, but cannot be read as text by the AI.]`;
|
|
96
|
+
if (isModelReadableImage(filename)) {
|
|
97
|
+
return `[Image file: ${filename} — call readFile for this file id to inspect the image visually.]`;
|
|
84
98
|
}
|
|
85
99
|
return buffer.toString("utf-8");
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
function mimeForFilename(filename: string): string {
|
|
89
|
-
const ext = filename
|
|
103
|
+
const ext = extensionForFilename(filename);
|
|
90
104
|
const map: Record<string, string> = {
|
|
91
105
|
pdf: "application/pdf",
|
|
92
106
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
@@ -106,6 +120,130 @@ function mimeForFilename(filename: string): string {
|
|
|
106
120
|
return map[ext] ?? "application/octet-stream";
|
|
107
121
|
}
|
|
108
122
|
|
|
123
|
+
type ReferenceFileEntry = {
|
|
124
|
+
label: string;
|
|
125
|
+
originalName: string;
|
|
126
|
+
reader: () => Promise<string>;
|
|
127
|
+
mediaType?: string;
|
|
128
|
+
imageReader?: () => Promise<Buffer | null>;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function makeReferenceFileEntry({
|
|
132
|
+
scope,
|
|
133
|
+
file,
|
|
134
|
+
reader,
|
|
135
|
+
blobReader,
|
|
136
|
+
}: {
|
|
137
|
+
scope: "workspace" | "topic" | "question";
|
|
138
|
+
file: storage.ContextFile;
|
|
139
|
+
reader: () => Promise<string>;
|
|
140
|
+
blobReader: () => Promise<Buffer>;
|
|
141
|
+
}): ReferenceFileEntry {
|
|
142
|
+
const label = `[${scope}] ${file.originalName}`;
|
|
143
|
+
return {
|
|
144
|
+
label,
|
|
145
|
+
originalName: file.originalName,
|
|
146
|
+
reader,
|
|
147
|
+
...(isModelReadableImage(file.originalName)
|
|
148
|
+
? {
|
|
149
|
+
mediaType: mimeForFilename(file.originalName),
|
|
150
|
+
imageReader: async () =>
|
|
151
|
+
(await storage.readOriginalBlob(file.id)) ?? (await blobReader()),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function makeCodeReferenceFileEntry(
|
|
158
|
+
label: string,
|
|
159
|
+
reader: () => Promise<string>,
|
|
160
|
+
): ReferenceFileEntry {
|
|
161
|
+
return { label, originalName: label, reader };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createReadFileTool(fileRegistry: Map<string, ReferenceFileEntry>) {
|
|
165
|
+
return tool({
|
|
166
|
+
description:
|
|
167
|
+
"Read an available reference file. Text documents return text; images return visual image data to inspect.",
|
|
168
|
+
inputSchema: z.object({
|
|
169
|
+
fileId: z
|
|
170
|
+
.string()
|
|
171
|
+
.describe("The id of the file to read, from the available files list."),
|
|
172
|
+
}),
|
|
173
|
+
execute: async ({ fileId }) => {
|
|
174
|
+
const entry = fileRegistry.get(fileId);
|
|
175
|
+
if (!entry) return { type: "error", error: "File not found" };
|
|
176
|
+
try {
|
|
177
|
+
if (entry.imageReader) {
|
|
178
|
+
return {
|
|
179
|
+
type: "image",
|
|
180
|
+
fileId,
|
|
181
|
+
fileName: entry.label,
|
|
182
|
+
mediaType: entry.mediaType ?? mimeForFilename(entry.originalName),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const content = await entry.reader();
|
|
186
|
+
return { type: "text", fileName: entry.label, content };
|
|
187
|
+
} catch {
|
|
188
|
+
return { type: "error", error: "Could not read file" };
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
toModelOutput: async ({ output }) => {
|
|
192
|
+
if ((output as any)?.type === "image") {
|
|
193
|
+
const imageOutput = output as {
|
|
194
|
+
fileId: string;
|
|
195
|
+
fileName: string;
|
|
196
|
+
mediaType: string;
|
|
197
|
+
};
|
|
198
|
+
const entry = fileRegistry.get(imageOutput.fileId);
|
|
199
|
+
let buffer: Buffer | null | undefined;
|
|
200
|
+
try {
|
|
201
|
+
buffer = await entry?.imageReader?.();
|
|
202
|
+
} catch {
|
|
203
|
+
buffer = null;
|
|
204
|
+
}
|
|
205
|
+
if (!buffer) {
|
|
206
|
+
return {
|
|
207
|
+
type: "text",
|
|
208
|
+
value: `${imageOutput.fileName}\n\n[Image bytes are unavailable.]`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (buffer.byteLength > MAX_IMAGE_BYTES_FOR_MODEL) {
|
|
212
|
+
return {
|
|
213
|
+
type: "text",
|
|
214
|
+
value: `${imageOutput.fileName}\n\n[Image is ${(buffer.byteLength / (1024 * 1024)).toFixed(1)} MB, which is too large to send to the model. Ask the user to upload a smaller image.]`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
type: "content",
|
|
219
|
+
value: [
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: `${imageOutput.fileName}\nInspect the attached image directly and answer using what is visible in it.`,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: "image-data",
|
|
226
|
+
data: buffer.toString("base64"),
|
|
227
|
+
mediaType: imageOutput.mediaType,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if ((output as any)?.type === "text") {
|
|
234
|
+
const textOutput = output as { fileName: string; content: string };
|
|
235
|
+
return {
|
|
236
|
+
type: "text",
|
|
237
|
+
value: `${textOutput.fileName}\n\n${textOutput.content}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const error = (output as any)?.error ?? "Could not read file";
|
|
242
|
+
return { type: "text", value: `[readFile error: ${error}]` };
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
109
247
|
const PORT = process.env.PORT || 3001;
|
|
110
248
|
const CODE_CONTEXT_DIR = process.env.CODE_CONTEXT_DIR || "";
|
|
111
249
|
|
|
@@ -1374,20 +1512,22 @@ app.post("/api/chat", async (req, res) => {
|
|
|
1374
1512
|
}
|
|
1375
1513
|
}
|
|
1376
1514
|
|
|
1377
|
-
// Build a file registry: id →
|
|
1515
|
+
// Build a file registry: id → reference entry.
|
|
1378
1516
|
// The model sees the list of file names and can call readFile(id) for any of them.
|
|
1379
|
-
const fileRegistry = new Map<
|
|
1380
|
-
string,
|
|
1381
|
-
{ label: string; reader: () => Promise<string> }
|
|
1382
|
-
>();
|
|
1517
|
+
const fileRegistry = new Map<string, ReferenceFileEntry>();
|
|
1383
1518
|
|
|
1384
1519
|
// Workspace-level uploaded files (apply to all topics)
|
|
1385
1520
|
const workspaceFiles = await storage.getWorkspaceContextFiles();
|
|
1386
1521
|
for (const cf of workspaceFiles) {
|
|
1387
|
-
fileRegistry.set(
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1522
|
+
fileRegistry.set(
|
|
1523
|
+
cf.id,
|
|
1524
|
+
makeReferenceFileEntry({
|
|
1525
|
+
scope: "workspace",
|
|
1526
|
+
file: cf,
|
|
1527
|
+
reader: () => storage.readWorkspaceContextFileContent(cf.id),
|
|
1528
|
+
blobReader: () => storage.readWorkspaceContextFileBlob(cf.id),
|
|
1529
|
+
}),
|
|
1530
|
+
);
|
|
1391
1531
|
}
|
|
1392
1532
|
|
|
1393
1533
|
// Topic-level uploaded files + topic-wide system prompt
|
|
@@ -1399,10 +1539,15 @@ app.post("/api/chat", async (req, res) => {
|
|
|
1399
1539
|
}
|
|
1400
1540
|
if (topic?.contextFiles?.length) {
|
|
1401
1541
|
for (const cf of topic.contextFiles) {
|
|
1402
|
-
fileRegistry.set(
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1542
|
+
fileRegistry.set(
|
|
1543
|
+
cf.id,
|
|
1544
|
+
makeReferenceFileEntry({
|
|
1545
|
+
scope: "topic",
|
|
1546
|
+
file: cf,
|
|
1547
|
+
reader: () => storage.readContextFileContent(cf.id),
|
|
1548
|
+
blobReader: () => storage.readContextFileBlob(cf.id),
|
|
1549
|
+
}),
|
|
1550
|
+
);
|
|
1406
1551
|
}
|
|
1407
1552
|
}
|
|
1408
1553
|
}
|
|
@@ -1414,10 +1559,15 @@ app.post("/api/chat", async (req, res) => {
|
|
|
1414
1559
|
for (const cf of question.contextFiles.filter(
|
|
1415
1560
|
(c) => c.inContext !== false,
|
|
1416
1561
|
)) {
|
|
1417
|
-
fileRegistry.set(
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1562
|
+
fileRegistry.set(
|
|
1563
|
+
cf.id,
|
|
1564
|
+
makeReferenceFileEntry({
|
|
1565
|
+
scope: "question",
|
|
1566
|
+
file: cf,
|
|
1567
|
+
reader: () => storage.readContextFileContent(cf.id),
|
|
1568
|
+
blobReader: () => storage.readContextFileBlob(cf.id),
|
|
1569
|
+
}),
|
|
1570
|
+
);
|
|
1421
1571
|
}
|
|
1422
1572
|
}
|
|
1423
1573
|
}
|
|
@@ -1428,10 +1578,12 @@ app.post("/api/chat", async (req, res) => {
|
|
|
1428
1578
|
const fullPath = path.join(CODE_CONTEXT_DIR, filePath);
|
|
1429
1579
|
const resolved = path.resolve(fullPath);
|
|
1430
1580
|
if (!resolved.startsWith(path.resolve(CODE_CONTEXT_DIR))) continue;
|
|
1431
|
-
fileRegistry.set(
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1581
|
+
fileRegistry.set(
|
|
1582
|
+
`code:${filePath}`,
|
|
1583
|
+
makeCodeReferenceFileEntry(`[code] ${filePath}`, () =>
|
|
1584
|
+
fs.readFile(resolved, "utf-8"),
|
|
1585
|
+
),
|
|
1586
|
+
);
|
|
1435
1587
|
}
|
|
1436
1588
|
}
|
|
1437
1589
|
|
|
@@ -1445,6 +1597,7 @@ app.post("/api/chat", async (req, res) => {
|
|
|
1445
1597
|
|
|
1446
1598
|
system += `\n\n--- Available Reference Files ---
|
|
1447
1599
|
The following files are available to you. Use the readFile tool to retrieve a file's content when it would help answer the question. Only read files that are relevant — you do not need to read them all.
|
|
1600
|
+
For image files, readFile returns visual image data so you can inspect what is visible rather than treating the image as text.
|
|
1448
1601
|
|
|
1449
1602
|
`;
|
|
1450
1603
|
for (const [id, { label }] of fileRegistry) {
|
|
@@ -1581,27 +1734,7 @@ Examples (illustrative only — use real ids and names from the list above):
|
|
|
1581
1734
|
}),
|
|
1582
1735
|
...(fileRegistry.size > 0
|
|
1583
1736
|
? {
|
|
1584
|
-
readFile:
|
|
1585
|
-
description:
|
|
1586
|
-
"Read the content of an available reference file. Use this to get file contents when they are relevant to the user's question.",
|
|
1587
|
-
inputSchema: z.object({
|
|
1588
|
-
fileId: z
|
|
1589
|
-
.string()
|
|
1590
|
-
.describe(
|
|
1591
|
-
"The id of the file to read, from the available files list.",
|
|
1592
|
-
),
|
|
1593
|
-
}),
|
|
1594
|
-
execute: async ({ fileId }) => {
|
|
1595
|
-
const entry = fileRegistry.get(fileId);
|
|
1596
|
-
if (!entry) return { error: "File not found" };
|
|
1597
|
-
try {
|
|
1598
|
-
const content = await entry.reader();
|
|
1599
|
-
return { fileName: entry.label, content };
|
|
1600
|
-
} catch {
|
|
1601
|
-
return { error: "Could not read file" };
|
|
1602
|
-
}
|
|
1603
|
-
},
|
|
1604
|
-
}),
|
|
1737
|
+
readFile: createReadFileTool(fileRegistry),
|
|
1605
1738
|
}
|
|
1606
1739
|
: {}),
|
|
1607
1740
|
},
|
|
@@ -1881,17 +2014,19 @@ app.post("/api/code-line-ask", async (req, res) => {
|
|
|
1881
2014
|
const aiSettings = await storage.getAiSettings();
|
|
1882
2015
|
|
|
1883
2016
|
// Build a file registry identical to /api/chat so the model has the same context
|
|
1884
|
-
const fileRegistry = new Map<
|
|
1885
|
-
string,
|
|
1886
|
-
{ label: string; reader: () => Promise<string> }
|
|
1887
|
-
>();
|
|
2017
|
+
const fileRegistry = new Map<string, ReferenceFileEntry>();
|
|
1888
2018
|
|
|
1889
2019
|
const workspaceFiles = await storage.getWorkspaceContextFiles();
|
|
1890
2020
|
for (const cf of workspaceFiles) {
|
|
1891
|
-
fileRegistry.set(
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2021
|
+
fileRegistry.set(
|
|
2022
|
+
cf.id,
|
|
2023
|
+
makeReferenceFileEntry({
|
|
2024
|
+
scope: "workspace",
|
|
2025
|
+
file: cf,
|
|
2026
|
+
reader: () => storage.readWorkspaceContextFileContent(cf.id),
|
|
2027
|
+
blobReader: () => storage.readWorkspaceContextFileBlob(cf.id),
|
|
2028
|
+
}),
|
|
2029
|
+
);
|
|
1895
2030
|
}
|
|
1896
2031
|
|
|
1897
2032
|
if (topicId) {
|
|
@@ -1899,10 +2034,15 @@ app.post("/api/code-line-ask", async (req, res) => {
|
|
|
1899
2034
|
const topic = topics.find((t: any) => t.id === topicId);
|
|
1900
2035
|
if (topic?.contextFiles?.length) {
|
|
1901
2036
|
for (const cf of topic.contextFiles) {
|
|
1902
|
-
fileRegistry.set(
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2037
|
+
fileRegistry.set(
|
|
2038
|
+
cf.id,
|
|
2039
|
+
makeReferenceFileEntry({
|
|
2040
|
+
scope: "topic",
|
|
2041
|
+
file: cf,
|
|
2042
|
+
reader: () => storage.readContextFileContent(cf.id),
|
|
2043
|
+
blobReader: () => storage.readContextFileBlob(cf.id),
|
|
2044
|
+
}),
|
|
2045
|
+
);
|
|
1906
2046
|
}
|
|
1907
2047
|
}
|
|
1908
2048
|
}
|
|
@@ -1911,10 +2051,15 @@ app.post("/api/code-line-ask", async (req, res) => {
|
|
|
1911
2051
|
const question = await storage.getQuestion(questionId);
|
|
1912
2052
|
if (question?.contextFiles?.length) {
|
|
1913
2053
|
for (const cf of question.contextFiles) {
|
|
1914
|
-
fileRegistry.set(
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2054
|
+
fileRegistry.set(
|
|
2055
|
+
cf.id,
|
|
2056
|
+
makeReferenceFileEntry({
|
|
2057
|
+
scope: "question",
|
|
2058
|
+
file: cf,
|
|
2059
|
+
reader: () => storage.readContextFileContent(cf.id),
|
|
2060
|
+
blobReader: () => storage.readContextFileBlob(cf.id),
|
|
2061
|
+
}),
|
|
2062
|
+
);
|
|
1918
2063
|
}
|
|
1919
2064
|
}
|
|
1920
2065
|
}
|
|
@@ -1924,10 +2069,12 @@ app.post("/api/code-line-ask", async (req, res) => {
|
|
|
1924
2069
|
const fullPath = path.join(CODE_CONTEXT_DIR, fp);
|
|
1925
2070
|
const resolved = path.resolve(fullPath);
|
|
1926
2071
|
if (!resolved.startsWith(path.resolve(CODE_CONTEXT_DIR))) continue;
|
|
1927
|
-
fileRegistry.set(
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2072
|
+
fileRegistry.set(
|
|
2073
|
+
`code:${fp}`,
|
|
2074
|
+
makeCodeReferenceFileEntry(`[code] ${fp}`, () =>
|
|
2075
|
+
fs.readFile(resolved, "utf-8"),
|
|
2076
|
+
),
|
|
2077
|
+
);
|
|
1931
2078
|
}
|
|
1932
2079
|
}
|
|
1933
2080
|
|
|
@@ -1950,7 +2097,7 @@ app.post("/api/code-line-ask", async (req, res) => {
|
|
|
1950
2097
|
codeFilePaths.push(id.slice("code:".length));
|
|
1951
2098
|
}
|
|
1952
2099
|
|
|
1953
|
-
system += `\n\n--- Available Reference Files ---\nThe following files are available to you. Use the readFile tool to retrieve a file's content when it would help answer the question. Only read files that are relevant.\n\n`;
|
|
2100
|
+
system += `\n\n--- Available Reference Files ---\nThe following files are available to you. Use the readFile tool to retrieve a file's content when it would help answer the question. Only read files that are relevant. For image files, readFile returns visual image data so you can inspect what is visible.\n\n`;
|
|
1954
2101
|
for (const [id, { label }] of fileRegistry) {
|
|
1955
2102
|
system += `• ${label} (id: "${id}")\n`;
|
|
1956
2103
|
}
|
|
@@ -1992,27 +2139,7 @@ app.post("/api/code-line-ask", async (req, res) => {
|
|
|
1992
2139
|
tools:
|
|
1993
2140
|
fileRegistry.size > 0
|
|
1994
2141
|
? {
|
|
1995
|
-
readFile:
|
|
1996
|
-
description:
|
|
1997
|
-
"Read the content of an available reference file. Use this to get file contents when relevant to the question.",
|
|
1998
|
-
inputSchema: z.object({
|
|
1999
|
-
fileId: z
|
|
2000
|
-
.string()
|
|
2001
|
-
.describe(
|
|
2002
|
-
"The id of the file to read, from the available files list.",
|
|
2003
|
-
),
|
|
2004
|
-
}),
|
|
2005
|
-
execute: async ({ fileId }) => {
|
|
2006
|
-
const entry = fileRegistry.get(fileId);
|
|
2007
|
-
if (!entry) return { error: "File not found" };
|
|
2008
|
-
try {
|
|
2009
|
-
const content = await entry.reader();
|
|
2010
|
-
return { fileName: entry.label, content };
|
|
2011
|
-
} catch {
|
|
2012
|
-
return { error: "Could not read file" };
|
|
2013
|
-
}
|
|
2014
|
-
},
|
|
2015
|
-
}),
|
|
2142
|
+
readFile: createReadFileTool(fileRegistry),
|
|
2016
2143
|
}
|
|
2017
2144
|
: undefined,
|
|
2018
2145
|
stopWhen: stepCountIs(4),
|
|
@@ -434,6 +434,7 @@ export async function clearWorkspaceData(workspaceId: string): Promise<void> {
|
|
|
434
434
|
await ensureWorkspaceDirs(workspaceId);
|
|
435
435
|
const wid = workspaceId;
|
|
436
436
|
await fs.writeFile(topicsFilePath(wid), JSON.stringify([], null, 2));
|
|
437
|
+
await fs.writeFile(workspaceFilesFilePath(wid), JSON.stringify([], null, 2));
|
|
437
438
|
await Promise.all([
|
|
438
439
|
fs
|
|
439
440
|
.readdir(questionsDirPath(wid))
|
|
@@ -534,6 +535,13 @@ export async function readContextFileContent(fileId: string): Promise<string> {
|
|
|
534
535
|
return fs.readFile(path.join(contextFilesDirPath(), fileId), "utf-8");
|
|
535
536
|
}
|
|
536
537
|
|
|
538
|
+
export async function readContextFileBlob(
|
|
539
|
+
fileId: string,
|
|
540
|
+
workspaceId = _activeWorkspaceId,
|
|
541
|
+
): Promise<Buffer> {
|
|
542
|
+
return fs.readFile(path.join(contextFilesDirPath(workspaceId), fileId));
|
|
543
|
+
}
|
|
544
|
+
|
|
537
545
|
/**
|
|
538
546
|
* Store the raw original file bytes at {fileId}.orig so downloads can serve the
|
|
539
547
|
* real file rather than the extracted-text version used by the LLM.
|
|
@@ -646,6 +654,13 @@ export async function readWorkspaceContextFileContent(
|
|
|
646
654
|
);
|
|
647
655
|
}
|
|
648
656
|
|
|
657
|
+
export async function readWorkspaceContextFileBlob(
|
|
658
|
+
fileId: string,
|
|
659
|
+
workspaceId = _activeWorkspaceId,
|
|
660
|
+
): Promise<Buffer> {
|
|
661
|
+
return fs.readFile(path.join(contextFilesDirPath(workspaceId), fileId));
|
|
662
|
+
}
|
|
663
|
+
|
|
649
664
|
export async function getQuestion(id: string): Promise<Question | null> {
|
|
650
665
|
await ensureWorkspaceDirs();
|
|
651
666
|
try {
|