@hachej/boring-diagram 0.1.67
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 +38 -0
- package/dist/front/index.d.ts +5 -0
- package/dist/front/index.js +1042 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.js +255 -0
- package/dist/shared/index.d.ts +8 -0
- package/dist/shared/index.js +28 -0
- package/package.json +81 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
|
|
2
|
+
|
|
3
|
+
declare function defaultDiagramServerPlugin(_options: unknown, ctx: {
|
|
4
|
+
workspaceRoot: string;
|
|
5
|
+
}): WorkspaceServerPlugin;
|
|
6
|
+
declare function createDiagramServerPlugin(options: {
|
|
7
|
+
workspaceRoot: string;
|
|
8
|
+
}): WorkspaceServerPlugin;
|
|
9
|
+
|
|
10
|
+
export { createDiagramServerPlugin, defaultDiagramServerPlugin as default };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// src/server/index.ts
|
|
2
|
+
import { constants } from "fs";
|
|
3
|
+
import { lstat, open } from "fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
5
|
+
import { defineServerPlugin } from "@hachej/boring-workspace/server";
|
|
6
|
+
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { generateImages, getImageModels, getImageProviders, getEnvApiKey } from "@earendil-works/pi-ai";
|
|
8
|
+
|
|
9
|
+
// src/shared/index.ts
|
|
10
|
+
var DIAGRAM_PLUGIN_ID = "diagram";
|
|
11
|
+
function saveTargetFor(path) {
|
|
12
|
+
if (path.toLowerCase().endsWith(".excalidraw.png")) return path.replace(/\.png$/i, "");
|
|
13
|
+
return path || "untitled.excalidraw";
|
|
14
|
+
}
|
|
15
|
+
function renderTargetFor(path) {
|
|
16
|
+
const source = saveTargetFor(path);
|
|
17
|
+
if (source.toLowerCase().endsWith(".excalidraw")) return source.replace(/\.excalidraw$/i, ".render.png");
|
|
18
|
+
return `${source}.render.png`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/server/index.ts
|
|
22
|
+
var DEFAULT_IMAGE_MODEL_KEY = "openrouter:openrouter/auto";
|
|
23
|
+
var MAX_PROMPT_LENGTH = 4e3;
|
|
24
|
+
var MAX_IMAGE_BASE64_LENGTH = 20 * 1024 * 1024;
|
|
25
|
+
function defaultDiagramServerPlugin(_options, ctx) {
|
|
26
|
+
return createDiagramServerPlugin({ workspaceRoot: ctx.workspaceRoot });
|
|
27
|
+
}
|
|
28
|
+
function createDiagramServerPlugin(options) {
|
|
29
|
+
const authStorage = AuthStorage.create();
|
|
30
|
+
const routes = async (app) => {
|
|
31
|
+
app.get("/api/v1/plugins/diagram/render/models", async () => {
|
|
32
|
+
const models = listRenderableImageModels();
|
|
33
|
+
const defaultModel = resolveDefaultModel(models);
|
|
34
|
+
const responseModels = await Promise.all(models.map(async (model) => {
|
|
35
|
+
const configured = Boolean(await resolveImageApiKey(authStorage, model.provider));
|
|
36
|
+
return {
|
|
37
|
+
provider: model.provider,
|
|
38
|
+
id: model.id,
|
|
39
|
+
label: model.name || model.id,
|
|
40
|
+
available: configured,
|
|
41
|
+
configured
|
|
42
|
+
};
|
|
43
|
+
}));
|
|
44
|
+
const authConfigured = responseModels.some((model) => model.available);
|
|
45
|
+
return {
|
|
46
|
+
models: responseModels,
|
|
47
|
+
defaultModel,
|
|
48
|
+
authConfigured,
|
|
49
|
+
...authConfigured ? {} : { authHint: "Configure an API key for a Pi image provider, for example OPENROUTER_API_KEY, to enable Diagram image rendering." }
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
app.post("/api/v1/plugins/diagram/render", async (request, reply) => {
|
|
53
|
+
const body = request.body ?? {};
|
|
54
|
+
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
55
|
+
if (!prompt) return sendRenderError(reply, 400, "validation_error", "prompt is required");
|
|
56
|
+
if (prompt.length > MAX_PROMPT_LENGTH) return sendRenderError(reply, 400, "validation_error", "prompt is too long");
|
|
57
|
+
const sketchPngBase64 = typeof body.sketchPngBase64 === "string" ? body.sketchPngBase64 : "";
|
|
58
|
+
if (!sketchPngBase64) return sendRenderError(reply, 400, "validation_error", "sketchPngBase64 is required");
|
|
59
|
+
if (sketchPngBase64.length > MAX_IMAGE_BASE64_LENGTH) return sendRenderError(reply, 413, "validation_error", "sketch image is too large");
|
|
60
|
+
const models = listRenderableImageModels();
|
|
61
|
+
const model = resolveRequestedModel(models, body.model);
|
|
62
|
+
if (!model) return sendRenderError(reply, 500, "render_model_unavailable", "no image models are available");
|
|
63
|
+
const apiKey = await resolveImageApiKey(authStorage, model.provider);
|
|
64
|
+
if (!apiKey) return sendRenderError(reply, 400, "render_auth_unconfigured", `${model.provider} image rendering is not configured`);
|
|
65
|
+
try {
|
|
66
|
+
const mimeType = body.mimeType === "image/jpeg" ? "image/jpeg" : "image/png";
|
|
67
|
+
const generated = await generateImages(model, {
|
|
68
|
+
input: [
|
|
69
|
+
{ type: "text", text: renderPrompt(prompt) },
|
|
70
|
+
{ type: "image", mimeType, data: sketchPngBase64 }
|
|
71
|
+
]
|
|
72
|
+
}, {
|
|
73
|
+
apiKey,
|
|
74
|
+
timeoutMs: 12e4,
|
|
75
|
+
maxRetries: 1
|
|
76
|
+
});
|
|
77
|
+
if (generated.stopReason === "error") {
|
|
78
|
+
return sendRenderError(reply, 502, "render_generation_failed", generated.errorMessage || "image generation failed");
|
|
79
|
+
}
|
|
80
|
+
const image = generated.output.find((item) => item.type === "image");
|
|
81
|
+
if (!image || image.type !== "image") return sendRenderError(reply, 502, "render_output_missing", "image model returned no image");
|
|
82
|
+
const outputPath = renderTargetFor(typeof body.path === "string" ? body.path : "");
|
|
83
|
+
const metadata = {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
kind: "diagram.render",
|
|
86
|
+
sourcePath: typeof body.path === "string" ? body.path : "",
|
|
87
|
+
outputPath,
|
|
88
|
+
prompt: { text: prompt },
|
|
89
|
+
model: { provider: model.provider, id: model.id },
|
|
90
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
91
|
+
response: {
|
|
92
|
+
id: generated.responseId,
|
|
93
|
+
usage: generated.usage,
|
|
94
|
+
mimeType: image.mimeType
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const absoluteOutputPath = resolveWorkspaceFile(options.workspaceRoot, outputPath);
|
|
98
|
+
const imageBytes = Buffer.from(image.data, "base64");
|
|
99
|
+
await writeWorkspaceFileNoSymlink(options.workspaceRoot, absoluteOutputPath, withPngTextMetadata(imageBytes, {
|
|
100
|
+
"boring.diagram.schema": JSON.stringify({ schemaVersion: metadata.schemaVersion, kind: metadata.kind }),
|
|
101
|
+
"boring.diagram.prompt": prompt,
|
|
102
|
+
"boring.diagram.metadata": JSON.stringify(metadata)
|
|
103
|
+
}));
|
|
104
|
+
const metadataPath = absoluteOutputPath.replace(/\.png$/i, ".json");
|
|
105
|
+
await writeWorkspaceFileNoSymlink(options.workspaceRoot, metadataPath, JSON.stringify(metadata, null, 2));
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
path: outputPath,
|
|
109
|
+
model: model.id,
|
|
110
|
+
prompt,
|
|
111
|
+
mimeType: image.mimeType
|
|
112
|
+
};
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const message = err instanceof Error ? err.message : "image rendering failed";
|
|
115
|
+
const lower = message.toLowerCase();
|
|
116
|
+
if (lower.includes("path") || lower.includes("symlink")) {
|
|
117
|
+
return sendRenderError(reply, 400, "render_output_rejected", message);
|
|
118
|
+
}
|
|
119
|
+
if (lower.includes("image") || lower.includes("generation") || lower.includes("timeout")) {
|
|
120
|
+
return sendRenderError(reply, 502, "render_generation_failed", message);
|
|
121
|
+
}
|
|
122
|
+
return sendRenderError(reply, 500, "render_internal_error", message);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
return defineServerPlugin({
|
|
127
|
+
id: DIAGRAM_PLUGIN_ID,
|
|
128
|
+
label: "Diagram",
|
|
129
|
+
systemPrompt: "Diagram files can be opened in the workspace UI. Use the Render control in the Diagram pane to turn diagrams into generated images when requested.",
|
|
130
|
+
routes
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function sendRenderError(reply, statusCode, code, message) {
|
|
134
|
+
return reply.code(statusCode).send({ error: { code, message } });
|
|
135
|
+
}
|
|
136
|
+
function listRenderableImageModels() {
|
|
137
|
+
return getImageProviders().flatMap((provider) => getImageModels(provider).filter((model) => model.input.includes("image") && model.output.includes("image")));
|
|
138
|
+
}
|
|
139
|
+
function encodeModelKey(model) {
|
|
140
|
+
return `${model.provider}:${model.id}`;
|
|
141
|
+
}
|
|
142
|
+
function resolveDefaultModel(models) {
|
|
143
|
+
const configured = process.env.BORING_DIAGRAM_RENDER_MODEL ?? process.env.BORING_EXCALIDRAW_RENDER_MODEL;
|
|
144
|
+
if (configured) {
|
|
145
|
+
const match = models.find((model2) => encodeModelKey(model2) === configured || model2.id === configured);
|
|
146
|
+
if (match) return { provider: match.provider, id: match.id };
|
|
147
|
+
}
|
|
148
|
+
const preferred = models.find((model2) => encodeModelKey(model2) === DEFAULT_IMAGE_MODEL_KEY || model2.id === "openrouter/auto");
|
|
149
|
+
const model = preferred ?? models[0];
|
|
150
|
+
return model ? { provider: model.provider, id: model.id } : void 0;
|
|
151
|
+
}
|
|
152
|
+
function resolveRequestedModel(models, requested) {
|
|
153
|
+
const defaultModel = resolveDefaultModel(models);
|
|
154
|
+
const requestedKey = requested || (defaultModel ? `${defaultModel.provider}:${defaultModel.id}` : void 0);
|
|
155
|
+
return models.find((model) => requestedKey && (encodeModelKey(model) === requestedKey || model.id === requestedKey)) ?? models.find((model) => encodeModelKey(model) === DEFAULT_IMAGE_MODEL_KEY) ?? models[0];
|
|
156
|
+
}
|
|
157
|
+
async function resolveImageApiKey(authStorage, provider) {
|
|
158
|
+
const suffix = String(provider).toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
159
|
+
const envName = `BORING_DIAGRAM_${suffix}_API_KEY`;
|
|
160
|
+
const legacyEnvName = `BORING_EXCALIDRAW_${suffix}_API_KEY`;
|
|
161
|
+
return process.env[envName] || process.env[legacyEnvName] || await authStorage.getApiKey(provider) || getEnvApiKey(provider) || (provider === "openrouter" ? process.env.OPENROUTER_API_KEY : void 0);
|
|
162
|
+
}
|
|
163
|
+
function renderPrompt(prompt) {
|
|
164
|
+
return [
|
|
165
|
+
"Use the attached diagram sketch as the source composition.",
|
|
166
|
+
"Preserve the important spatial layout, labels, arrows, and relationships unless the prompt says otherwise.",
|
|
167
|
+
"Create a polished image from it.",
|
|
168
|
+
"",
|
|
169
|
+
`User prompt: ${prompt}`
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
function withPngTextMetadata(bytes, entries) {
|
|
173
|
+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
174
|
+
if (bytes.length < signature.length + 12 || !bytes.subarray(0, signature.length).equals(signature)) return bytes;
|
|
175
|
+
let offset = signature.length;
|
|
176
|
+
while (offset + 12 <= bytes.length) {
|
|
177
|
+
const length = bytes.readUInt32BE(offset);
|
|
178
|
+
const type = bytes.subarray(offset + 4, offset + 8).toString("ascii");
|
|
179
|
+
const next = offset + 12 + length;
|
|
180
|
+
if (next > bytes.length) return bytes;
|
|
181
|
+
if (type === "IEND") {
|
|
182
|
+
const chunks = Object.entries(entries).filter(([key, value]) => /^[\x20-\x7e]{1,79}$/.test(key) && !key.includes("\0") && value.length > 0).map(([key, value]) => pngChunk("iTXt", Buffer.concat([
|
|
183
|
+
Buffer.from(key, "ascii"),
|
|
184
|
+
Buffer.from([0, 0, 0, 0, 0]),
|
|
185
|
+
Buffer.from(value, "utf8")
|
|
186
|
+
])));
|
|
187
|
+
return Buffer.concat([bytes.subarray(0, offset), ...chunks, bytes.subarray(offset)]);
|
|
188
|
+
}
|
|
189
|
+
offset = next;
|
|
190
|
+
}
|
|
191
|
+
return bytes;
|
|
192
|
+
}
|
|
193
|
+
function pngChunk(type, data) {
|
|
194
|
+
const typeBytes = Buffer.from(type, "ascii");
|
|
195
|
+
const out = Buffer.allocUnsafe(12 + data.length);
|
|
196
|
+
out.writeUInt32BE(data.length, 0);
|
|
197
|
+
typeBytes.copy(out, 4);
|
|
198
|
+
data.copy(out, 8);
|
|
199
|
+
out.writeUInt32BE(crc32(Buffer.concat([typeBytes, data])), 8 + data.length);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
function crc32(bytes) {
|
|
203
|
+
let crc = 4294967295;
|
|
204
|
+
for (const byte of bytes) {
|
|
205
|
+
crc ^= byte;
|
|
206
|
+
for (let i = 0; i < 8; i += 1) crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
|
|
207
|
+
}
|
|
208
|
+
return (crc ^ 4294967295) >>> 0;
|
|
209
|
+
}
|
|
210
|
+
function resolveWorkspaceFile(workspaceRoot, relativePath) {
|
|
211
|
+
const normalized = relativePath.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
212
|
+
if (!normalized || normalized.includes("\0") || isAbsolute(normalized)) throw new Error("invalid output path");
|
|
213
|
+
const root = resolve(workspaceRoot);
|
|
214
|
+
const candidate = resolve(root, normalized);
|
|
215
|
+
if (candidate !== root && !candidate.startsWith(root.endsWith(sep) ? root : `${root}${sep}`)) {
|
|
216
|
+
throw new Error("output path escapes workspace");
|
|
217
|
+
}
|
|
218
|
+
return candidate;
|
|
219
|
+
}
|
|
220
|
+
async function writeWorkspaceFileNoSymlink(workspaceRoot, absolutePath, data) {
|
|
221
|
+
const root = resolve(workspaceRoot);
|
|
222
|
+
const relativePath = relative(root, absolutePath);
|
|
223
|
+
if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error("output path escapes workspace");
|
|
224
|
+
const parent = dirname(absolutePath);
|
|
225
|
+
await assertNoSymlinkAncestors(root, parent);
|
|
226
|
+
await assertTargetIsNotSymlink(absolutePath);
|
|
227
|
+
const handle = await open(absolutePath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 438);
|
|
228
|
+
try {
|
|
229
|
+
await handle.writeFile(data);
|
|
230
|
+
} finally {
|
|
231
|
+
await handle.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function assertNoSymlinkAncestors(root, targetDir) {
|
|
235
|
+
const relativeDir = relative(root, targetDir);
|
|
236
|
+
if (relativeDir.startsWith("..") || isAbsolute(relativeDir)) throw new Error("output path escapes workspace");
|
|
237
|
+
let current = root;
|
|
238
|
+
if ((await lstat(current)).isSymbolicLink()) throw new Error("workspace root symlink rejected for render output");
|
|
239
|
+
for (const part of relativeDir.split(/[\\/]+/).filter(Boolean)) {
|
|
240
|
+
current = resolve(current, part);
|
|
241
|
+
if ((await lstat(current)).isSymbolicLink()) throw new Error("render output parent symlink rejected");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function assertTargetIsNotSymlink(target) {
|
|
245
|
+
try {
|
|
246
|
+
if ((await lstat(target)).isSymbolicLink()) throw new Error("render output target symlink rejected");
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err?.code === "ENOENT") return;
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
createDiagramServerPlugin,
|
|
254
|
+
defaultDiagramServerPlugin as default
|
|
255
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const DIAGRAM_PLUGIN_ID = "diagram";
|
|
2
|
+
declare const DIAGRAM_PANEL_ID = "diagram.panel";
|
|
3
|
+
declare function isDiagramPath(path: string): boolean;
|
|
4
|
+
declare function titleForPath(path?: string): string;
|
|
5
|
+
declare function saveTargetFor(path: string): string;
|
|
6
|
+
declare function renderTargetFor(path: string): string;
|
|
7
|
+
|
|
8
|
+
export { DIAGRAM_PANEL_ID, DIAGRAM_PLUGIN_ID, isDiagramPath, renderTargetFor, saveTargetFor, titleForPath };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/shared/index.ts
|
|
2
|
+
var DIAGRAM_PLUGIN_ID = "diagram";
|
|
3
|
+
var DIAGRAM_PANEL_ID = "diagram.panel";
|
|
4
|
+
function isDiagramPath(path) {
|
|
5
|
+
const lower = path.toLowerCase();
|
|
6
|
+
return lower.endsWith(".excalidraw") || lower.endsWith(".excalidraw.png");
|
|
7
|
+
}
|
|
8
|
+
function titleForPath(path) {
|
|
9
|
+
if (!path) return "Diagram";
|
|
10
|
+
return path.split("/").pop() || path;
|
|
11
|
+
}
|
|
12
|
+
function saveTargetFor(path) {
|
|
13
|
+
if (path.toLowerCase().endsWith(".excalidraw.png")) return path.replace(/\.png$/i, "");
|
|
14
|
+
return path || "untitled.excalidraw";
|
|
15
|
+
}
|
|
16
|
+
function renderTargetFor(path) {
|
|
17
|
+
const source = saveTargetFor(path);
|
|
18
|
+
if (source.toLowerCase().endsWith(".excalidraw")) return source.replace(/\.excalidraw$/i, ".render.png");
|
|
19
|
+
return `${source}.render.png`;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
DIAGRAM_PANEL_ID,
|
|
23
|
+
DIAGRAM_PLUGIN_ID,
|
|
24
|
+
isDiagramPath,
|
|
25
|
+
renderTargetFor,
|
|
26
|
+
saveTargetFor,
|
|
27
|
+
titleForPath
|
|
28
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hachej/boring-diagram",
|
|
3
|
+
"version": "0.1.67",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "Diagram workspace plugin for viewing and editing .excalidraw files.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hachej/boring-ui"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/hachej/boring-ui",
|
|
13
|
+
"boring": {
|
|
14
|
+
"id": "diagram",
|
|
15
|
+
"label": "Diagram",
|
|
16
|
+
"front": "dist/front/index.js",
|
|
17
|
+
"server": "dist/server/index.js"
|
|
18
|
+
},
|
|
19
|
+
"pi": {
|
|
20
|
+
"systemPrompt": "Diagram plugin: open .excalidraw and .excalidraw.png workspace files through workspace.open.path. The editor autosaves .excalidraw JSON with optimistic conflict detection."
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/front/index.d.ts",
|
|
29
|
+
"import": "./dist/front/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./front": {
|
|
32
|
+
"types": "./dist/front/index.d.ts",
|
|
33
|
+
"import": "./dist/front/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./server": {
|
|
36
|
+
"types": "./dist/server/index.d.ts",
|
|
37
|
+
"import": "./dist/server/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./shared": {
|
|
40
|
+
"types": "./dist/shared/index.d.ts",
|
|
41
|
+
"import": "./dist/shared/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": [
|
|
46
|
+
"**/*.css"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run --passWithNoTests",
|
|
52
|
+
"lint": "pnpm run typecheck",
|
|
53
|
+
"clean": "rm -rf dist .tsbuildinfo"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@hachej/boring-workspace": "workspace:*",
|
|
57
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
58
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@earendil-works/pi-ai": "^0.79.1",
|
|
62
|
+
"@excalidraw/excalidraw": "^0.18.1",
|
|
63
|
+
"@hachej/boring-agent": "workspace:*",
|
|
64
|
+
"@hachej/boring-ui-kit": "workspace:*",
|
|
65
|
+
"@mariozechner/pi-coding-agent": "npm:@earendil-works/pi-coding-agent@0.75.5"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@hachej/boring-workspace": "workspace:*",
|
|
69
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
70
|
+
"@types/node": "^22.20.0",
|
|
71
|
+
"@types/react": "^19.2.17",
|
|
72
|
+
"@types/react-dom": "^19.0.0",
|
|
73
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
74
|
+
"jsdom": "^29.1.1",
|
|
75
|
+
"react": "^19.2.7",
|
|
76
|
+
"react-dom": "^19.2.7",
|
|
77
|
+
"tsup": "^8.4.0",
|
|
78
|
+
"typescript": "~5.9.3",
|
|
79
|
+
"vitest": "^4.1.9"
|
|
80
|
+
}
|
|
81
|
+
}
|