@lakitu/sdk 0.1.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 +166 -0
- package/convex/_generated/api.d.ts +45 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +58 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/cloud/CLAUDE.md +238 -0
- package/convex/cloud/_generated/api.ts +84 -0
- package/convex/cloud/_generated/component.ts +861 -0
- package/convex/cloud/_generated/dataModel.ts +60 -0
- package/convex/cloud/_generated/server.ts +156 -0
- package/convex/cloud/convex.config.ts +16 -0
- package/convex/cloud/index.ts +29 -0
- package/convex/cloud/intentSchema/generate.ts +447 -0
- package/convex/cloud/intentSchema/index.ts +16 -0
- package/convex/cloud/intentSchema/types.ts +418 -0
- package/convex/cloud/ksaPolicy.ts +554 -0
- package/convex/cloud/mail.ts +92 -0
- package/convex/cloud/schema.ts +322 -0
- package/convex/cloud/utils/kanbanContext.ts +229 -0
- package/convex/cloud/workflows/agentBoard.ts +451 -0
- package/convex/cloud/workflows/agentPrompt.ts +272 -0
- package/convex/cloud/workflows/agentThread.ts +374 -0
- package/convex/cloud/workflows/compileSandbox.ts +146 -0
- package/convex/cloud/workflows/crudBoard.ts +217 -0
- package/convex/cloud/workflows/crudKSAs.ts +262 -0
- package/convex/cloud/workflows/crudLorobeads.ts +371 -0
- package/convex/cloud/workflows/crudSkills.ts +205 -0
- package/convex/cloud/workflows/crudThreads.ts +708 -0
- package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
- package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
- package/convex/sandbox/README.md +90 -0
- package/convex/sandbox/_generated/api.d.ts +2934 -0
- package/convex/sandbox/_generated/api.js +23 -0
- package/convex/sandbox/_generated/dataModel.d.ts +60 -0
- package/convex/sandbox/_generated/server.d.ts +143 -0
- package/convex/sandbox/_generated/server.js +93 -0
- package/convex/sandbox/actions/bash.ts +130 -0
- package/convex/sandbox/actions/browser.ts +282 -0
- package/convex/sandbox/actions/file.ts +336 -0
- package/convex/sandbox/actions/lsp.ts +325 -0
- package/convex/sandbox/actions/pdf.ts +119 -0
- package/convex/sandbox/agent/codeExecLoop.ts +535 -0
- package/convex/sandbox/agent/decisions.ts +284 -0
- package/convex/sandbox/agent/index.ts +515 -0
- package/convex/sandbox/agent/subagents.ts +651 -0
- package/convex/sandbox/brandResearch/index.ts +417 -0
- package/convex/sandbox/context/index.ts +7 -0
- package/convex/sandbox/context/session.ts +402 -0
- package/convex/sandbox/convex.config.ts +17 -0
- package/convex/sandbox/index.ts +51 -0
- package/convex/sandbox/nodeActions/codeExec.ts +130 -0
- package/convex/sandbox/planning/beads.ts +187 -0
- package/convex/sandbox/planning/index.ts +8 -0
- package/convex/sandbox/planning/sync.ts +194 -0
- package/convex/sandbox/prompts/codeExec.ts +852 -0
- package/convex/sandbox/prompts/modes.ts +231 -0
- package/convex/sandbox/prompts/system.ts +142 -0
- package/convex/sandbox/schema.ts +510 -0
- package/convex/sandbox/state/artifacts.ts +99 -0
- package/convex/sandbox/state/checkpoints.ts +341 -0
- package/convex/sandbox/state/files.ts +383 -0
- package/convex/sandbox/state/index.ts +10 -0
- package/convex/sandbox/state/verification.actions.ts +268 -0
- package/convex/sandbox/state/verification.ts +101 -0
- package/convex/sandbox/tsconfig.json +25 -0
- package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
- package/dist/cli/commands/build.d.ts +19 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +223 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +148 -0
- package/dist/cli/commands/publish.d.ts +12 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/publish.js +33 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +40 -0
- package/dist/sdk/builders.d.ts +104 -0
- package/dist/sdk/builders.d.ts.map +1 -0
- package/dist/sdk/builders.js +214 -0
- package/dist/sdk/index.d.ts +29 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +38 -0
- package/dist/sdk/types.d.ts +107 -0
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/types.js +6 -0
- package/ksa/README.md +263 -0
- package/ksa/_generated/REFERENCE.md +2954 -0
- package/ksa/_generated/registry.ts +257 -0
- package/ksa/_shared/configReader.ts +302 -0
- package/ksa/_shared/configSchemas.ts +649 -0
- package/ksa/_shared/gateway.ts +175 -0
- package/ksa/_shared/ksaBehaviors.ts +411 -0
- package/ksa/_shared/ksaProxy.ts +248 -0
- package/ksa/_shared/localDb.ts +302 -0
- package/ksa/index.ts +134 -0
- package/package.json +93 -0
- package/runtime/browser/agent-browser.ts +330 -0
- package/runtime/entrypoint.ts +194 -0
- package/runtime/lsp/manager.ts +366 -0
- package/runtime/pdf/pdf-generator.ts +50 -0
- package/runtime/pdf/renderer.ts +357 -0
- package/runtime/pdf/schema.ts +97 -0
- package/runtime/services/file-watcher.ts +191 -0
- package/template/build.ts +307 -0
- package/template/e2b/Dockerfile +69 -0
- package/template/e2b/e2b.toml +13 -0
- package/template/e2b/prebuild.sh +68 -0
- package/template/e2b/start.sh +14 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF Renderer - Converts DocNode schema to PDFKit drawing calls
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import PDFDocument from 'pdfkit';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import type { DocNode, TextVariant } from './schema';
|
|
8
|
+
import { tokens } from './schema';
|
|
9
|
+
|
|
10
|
+
export interface RenderContext {
|
|
11
|
+
doc: PDFKit.PDFDocument;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TextStyle {
|
|
18
|
+
size: number;
|
|
19
|
+
font: string;
|
|
20
|
+
lineGap: number;
|
|
21
|
+
color: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getTextStyle(variant?: TextVariant): TextStyle {
|
|
25
|
+
const base = tokens.typography[variant || 'body'];
|
|
26
|
+
const colorMap: Record<TextVariant, string> = {
|
|
27
|
+
title: tokens.colors.text,
|
|
28
|
+
subtitle: tokens.colors.textMuted,
|
|
29
|
+
body: tokens.colors.text,
|
|
30
|
+
caption: tokens.colors.textCaption,
|
|
31
|
+
};
|
|
32
|
+
return { ...base, color: colorMap[variant || 'body'] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function drawRoundedRect(
|
|
36
|
+
doc: PDFKit.PDFDocument,
|
|
37
|
+
x: number, y: number, w: number, h: number,
|
|
38
|
+
r: number, color: string
|
|
39
|
+
) {
|
|
40
|
+
doc.save().fillColor(color).roundedRect(x, y, w, h, r).fill().restore();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function measureText(doc: PDFKit.PDFDocument, text: string, width: number, style: TextStyle): number {
|
|
44
|
+
doc.fontSize(style.size).font(style.font);
|
|
45
|
+
return doc.heightOfString(text, { width }) + style.lineGap;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function measureNode(doc: PDFKit.PDFDocument, node: DocNode, width: number): number {
|
|
49
|
+
switch (node.type) {
|
|
50
|
+
case 'text': {
|
|
51
|
+
const style = getTextStyle(node.variant);
|
|
52
|
+
return measureText(doc, node.text, width, style);
|
|
53
|
+
}
|
|
54
|
+
case 'spacer':
|
|
55
|
+
return node.size;
|
|
56
|
+
case 'divider':
|
|
57
|
+
return 12;
|
|
58
|
+
case 'card': {
|
|
59
|
+
const padding = node.padding ?? tokens.spacing.card;
|
|
60
|
+
const innerWidth = width - padding * 2;
|
|
61
|
+
let h = padding * 2;
|
|
62
|
+
for (const child of node.children) {
|
|
63
|
+
h += measureNode(doc, child, innerWidth);
|
|
64
|
+
}
|
|
65
|
+
return h + tokens.spacing.gap;
|
|
66
|
+
}
|
|
67
|
+
case 'stack': {
|
|
68
|
+
if (node.direction === 'column') {
|
|
69
|
+
let h = 0;
|
|
70
|
+
for (const child of node.children) {
|
|
71
|
+
h += measureNode(doc, child, width);
|
|
72
|
+
}
|
|
73
|
+
return h;
|
|
74
|
+
} else {
|
|
75
|
+
const gap = node.gap ?? tokens.spacing.gap;
|
|
76
|
+
const colWidth = (width - gap * (node.children.length - 1)) / node.children.length;
|
|
77
|
+
let maxH = 0;
|
|
78
|
+
for (const child of node.children) {
|
|
79
|
+
maxH = Math.max(maxH, measureNode(doc, child, colWidth));
|
|
80
|
+
}
|
|
81
|
+
return maxH + tokens.spacing.gap;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
case 'table': {
|
|
85
|
+
const rowHeight = 22;
|
|
86
|
+
return rowHeight * (1 + node.rows.length) + tokens.spacing.gap;
|
|
87
|
+
}
|
|
88
|
+
case 'image':
|
|
89
|
+
return (node.height ?? 100) + tokens.spacing.gap;
|
|
90
|
+
default:
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderNode(node: DocNode, ctx: RenderContext): number {
|
|
96
|
+
const { doc, x, y, width } = ctx;
|
|
97
|
+
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case 'page': {
|
|
100
|
+
doc.addPage();
|
|
101
|
+
if (node.backgroundColor) {
|
|
102
|
+
doc.save()
|
|
103
|
+
.rect(0, 0, doc.page.width, doc.page.height)
|
|
104
|
+
.fill(node.backgroundColor)
|
|
105
|
+
.restore();
|
|
106
|
+
}
|
|
107
|
+
let currentY = tokens.spacing.page;
|
|
108
|
+
for (const child of node.children) {
|
|
109
|
+
currentY += renderNode(child, {
|
|
110
|
+
doc, x: tokens.spacing.page, y: currentY,
|
|
111
|
+
width: doc.page.width - tokens.spacing.page * 2
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case 'text': {
|
|
118
|
+
const style = getTextStyle(node.variant);
|
|
119
|
+
doc.fontSize(style.size).fillColor(node.color ?? style.color).font(style.font);
|
|
120
|
+
const height = doc.heightOfString(node.text, { width, align: node.align ?? 'left' });
|
|
121
|
+
doc.text(node.text, x, y, { width, align: node.align ?? 'left' });
|
|
122
|
+
return height + style.lineGap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'spacer':
|
|
126
|
+
return node.size;
|
|
127
|
+
|
|
128
|
+
case 'divider': {
|
|
129
|
+
const color = node.color ?? tokens.colors.divider;
|
|
130
|
+
doc.save().strokeColor(color).lineWidth(1).moveTo(x, y + 6).lineTo(x + width, y + 6).stroke().restore();
|
|
131
|
+
return 12;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'card': {
|
|
135
|
+
const padding = node.padding ?? tokens.spacing.card;
|
|
136
|
+
const radius = node.radius ?? tokens.radius.card;
|
|
137
|
+
const innerWidth = width - padding * 2;
|
|
138
|
+
|
|
139
|
+
let cardHeight = padding * 2;
|
|
140
|
+
for (const child of node.children) {
|
|
141
|
+
cardHeight += measureNode(doc, child, innerWidth);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (node.backgroundColor) {
|
|
145
|
+
drawRoundedRect(doc, x, y, width, cardHeight, radius, node.backgroundColor);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let currentY = y + padding;
|
|
149
|
+
for (const child of node.children) {
|
|
150
|
+
currentY += renderNode(child, { doc, x: x + padding, y: currentY, width: innerWidth });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return cardHeight + tokens.spacing.gap;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'stack': {
|
|
157
|
+
const gap = node.gap ?? tokens.spacing.gap;
|
|
158
|
+
if (node.direction === 'column') {
|
|
159
|
+
let currentY = y;
|
|
160
|
+
for (const child of node.children) {
|
|
161
|
+
currentY += renderNode(child, { doc, x, y: currentY, width });
|
|
162
|
+
}
|
|
163
|
+
return currentY - y;
|
|
164
|
+
} else {
|
|
165
|
+
const colWidth = (width - gap * (node.children.length - 1)) / node.children.length;
|
|
166
|
+
let maxH = 0;
|
|
167
|
+
let currentX = x;
|
|
168
|
+
for (const child of node.children) {
|
|
169
|
+
const h = renderNode(child, { doc, x: currentX, y, width: colWidth });
|
|
170
|
+
maxH = Math.max(maxH, h);
|
|
171
|
+
currentX += colWidth + gap;
|
|
172
|
+
}
|
|
173
|
+
return maxH + tokens.spacing.gap;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'table': {
|
|
178
|
+
const rowHeight = 22;
|
|
179
|
+
const colWidth = width / node.headers.length;
|
|
180
|
+
const headerBg = node.headerBg ?? tokens.colors.primary;
|
|
181
|
+
|
|
182
|
+
doc.save().fillColor(headerBg).rect(x, y, width, rowHeight).fill().restore();
|
|
183
|
+
|
|
184
|
+
doc.fillColor(tokens.colors.white).fontSize(10).font('Helvetica-Bold');
|
|
185
|
+
node.headers.forEach((header, i) => {
|
|
186
|
+
doc.text(header, x + i * colWidth + 6, y + 6, { width: colWidth - 12 });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
let currentY = y + rowHeight;
|
|
190
|
+
node.rows.forEach((row, rowIndex) => {
|
|
191
|
+
if (rowIndex % 2 === 0) {
|
|
192
|
+
doc.save().fillColor(tokens.colors.background).rect(x, currentY, width, rowHeight).fill().restore();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
doc.fillColor(tokens.colors.text).fontSize(10).font('Helvetica');
|
|
196
|
+
row.forEach((cell, i) => {
|
|
197
|
+
doc.text(cell, x + i * colWidth + 6, currentY + 6, { width: colWidth - 12 });
|
|
198
|
+
});
|
|
199
|
+
currentY += rowHeight;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return currentY - y + tokens.spacing.gap;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'image': {
|
|
206
|
+
try {
|
|
207
|
+
const imgWidth = node.width ?? width;
|
|
208
|
+
const imgHeight = node.height ?? 100;
|
|
209
|
+
doc.image(node.src, x, y, { width: imgWidth, height: imgHeight });
|
|
210
|
+
return imgHeight + tokens.spacing.gap;
|
|
211
|
+
} catch {
|
|
212
|
+
doc.save().fillColor(tokens.colors.background).rect(x, y, width, 100).fill().restore();
|
|
213
|
+
doc.fillColor(tokens.colors.textMuted).fontSize(10).text('Image not available', x, y + 40, { width, align: 'center' });
|
|
214
|
+
return 100 + tokens.spacing.gap;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface RenderOptions {
|
|
224
|
+
format?: 'a4' | 'letter';
|
|
225
|
+
outputPath: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function renderPdf(spec: DocNode, options: RenderOptions): Promise<string> {
|
|
229
|
+
const doc = new PDFDocument({
|
|
230
|
+
size: options.format === 'letter' ? 'LETTER' : 'A4',
|
|
231
|
+
margin: tokens.spacing.page,
|
|
232
|
+
autoFirstPage: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const writeStream = fs.createWriteStream(options.outputPath);
|
|
236
|
+
doc.pipe(writeStream);
|
|
237
|
+
|
|
238
|
+
if (spec.type === 'page') {
|
|
239
|
+
let currentY = tokens.spacing.page;
|
|
240
|
+
for (const child of (spec as any).children || []) {
|
|
241
|
+
currentY += renderNode(child, {
|
|
242
|
+
doc,
|
|
243
|
+
x: tokens.spacing.page,
|
|
244
|
+
y: currentY,
|
|
245
|
+
width: doc.page.width - tokens.spacing.page * 2,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
renderNode(spec, {
|
|
250
|
+
doc,
|
|
251
|
+
x: tokens.spacing.page,
|
|
252
|
+
y: tokens.spacing.page,
|
|
253
|
+
width: doc.page.width - tokens.spacing.page * 2,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
doc.end();
|
|
258
|
+
await new Promise<void>((resolve) => writeStream.on('finish', resolve));
|
|
259
|
+
|
|
260
|
+
return options.outputPath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Convert markdown to DocNode for PDF rendering
|
|
265
|
+
*/
|
|
266
|
+
export function markdownToDocNode(md: string, title?: string): DocNode {
|
|
267
|
+
const children: DocNode[] = [];
|
|
268
|
+
const lines = md.split('\n');
|
|
269
|
+
let i = 0;
|
|
270
|
+
|
|
271
|
+
let firstHeading: string | null = null;
|
|
272
|
+
for (let j = 0; j < lines.length; j++) {
|
|
273
|
+
const l = lines[j].trim();
|
|
274
|
+
if (!l) continue;
|
|
275
|
+
if (l.startsWith('# ')) {
|
|
276
|
+
firstHeading = l.slice(2);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const docTitle = title || firstHeading;
|
|
283
|
+
// Skip the first heading if we're using it as the document title (either from title arg or from content)
|
|
284
|
+
const skipFirstHeading = firstHeading && (title ? firstHeading === title : true);
|
|
285
|
+
|
|
286
|
+
if (docTitle) {
|
|
287
|
+
children.push({ type: 'text', variant: 'title', text: docTitle });
|
|
288
|
+
children.push({ type: 'spacer', size: 8 });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
while (i < lines.length) {
|
|
292
|
+
const line = lines[i].trim();
|
|
293
|
+
|
|
294
|
+
if (!line) { i++; continue; }
|
|
295
|
+
|
|
296
|
+
if (line.startsWith('### ')) {
|
|
297
|
+
children.push({ type: 'spacer', size: 8 });
|
|
298
|
+
children.push({ type: 'text', variant: 'subtitle', text: line.slice(4) });
|
|
299
|
+
i++; continue;
|
|
300
|
+
}
|
|
301
|
+
if (line.startsWith('## ')) {
|
|
302
|
+
children.push({ type: 'spacer', size: 12 });
|
|
303
|
+
children.push({ type: 'text', variant: 'subtitle', text: line.slice(3) });
|
|
304
|
+
i++; continue;
|
|
305
|
+
}
|
|
306
|
+
if (line.startsWith('# ')) {
|
|
307
|
+
if (skipFirstHeading && line.slice(2) === firstHeading) {
|
|
308
|
+
i++; continue;
|
|
309
|
+
}
|
|
310
|
+
children.push({ type: 'spacer', size: 16 });
|
|
311
|
+
children.push({ type: 'text', variant: 'title', text: line.slice(2) });
|
|
312
|
+
i++; continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (line === '---' || line === '***') {
|
|
316
|
+
children.push({ type: 'divider' });
|
|
317
|
+
i++; continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (line.includes('|') && lines[i + 1]?.includes('---')) {
|
|
321
|
+
const headers = line.split('|').map(h => h.trim()).filter(Boolean);
|
|
322
|
+
i += 2;
|
|
323
|
+
const rows: string[][] = [];
|
|
324
|
+
while (i < lines.length && lines[i].includes('|')) {
|
|
325
|
+
const row = lines[i].split('|').map(c => c.trim()).filter(Boolean);
|
|
326
|
+
rows.push(row);
|
|
327
|
+
i++;
|
|
328
|
+
}
|
|
329
|
+
children.push({ type: 'table', headers, rows });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (line.startsWith('- ') || line.startsWith('* ') || /^\d+\.\s/.test(line)) {
|
|
334
|
+
let listText = '';
|
|
335
|
+
while (i < lines.length) {
|
|
336
|
+
const l = lines[i].trim();
|
|
337
|
+
if (l.startsWith('- ') || l.startsWith('* ') || /^\d+\.\s/.test(l)) {
|
|
338
|
+
listText += '• ' + l.replace(/^[-*]\s|^\d+\.\s/, '') + '\n';
|
|
339
|
+
i++;
|
|
340
|
+
} else break;
|
|
341
|
+
}
|
|
342
|
+
children.push({ type: 'text', variant: 'body', text: listText.trim() });
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let para = line;
|
|
347
|
+
i++;
|
|
348
|
+
while (i < lines.length && lines[i].trim() && !lines[i].startsWith('#') && !lines[i].includes('|')) {
|
|
349
|
+
para += ' ' + lines[i].trim();
|
|
350
|
+
i++;
|
|
351
|
+
}
|
|
352
|
+
para = para.replace(/\*\*(.+?)\*\*/g, '$1').replace(/\*(.+?)\*/g, '$1').replace(/`(.+?)`/g, '$1');
|
|
353
|
+
children.push({ type: 'text', variant: 'body', text: para });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { type: 'page', children };
|
|
357
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocNode Schema - Layout primitives for PDF generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type TextVariant = 'title' | 'subtitle' | 'body' | 'caption';
|
|
6
|
+
export type Alignment = 'left' | 'center' | 'right';
|
|
7
|
+
export type StackDirection = 'row' | 'column';
|
|
8
|
+
|
|
9
|
+
export type DocNode =
|
|
10
|
+
| PageNode
|
|
11
|
+
| StackNode
|
|
12
|
+
| CardNode
|
|
13
|
+
| TextNode
|
|
14
|
+
| TableNode
|
|
15
|
+
| SpacerNode
|
|
16
|
+
| DividerNode
|
|
17
|
+
| ImageNode;
|
|
18
|
+
|
|
19
|
+
export interface PageNode {
|
|
20
|
+
type: 'page';
|
|
21
|
+
children: DocNode[];
|
|
22
|
+
backgroundColor?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StackNode {
|
|
26
|
+
type: 'stack';
|
|
27
|
+
direction: StackDirection;
|
|
28
|
+
gap?: number;
|
|
29
|
+
children: DocNode[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CardNode {
|
|
33
|
+
type: 'card';
|
|
34
|
+
padding?: number;
|
|
35
|
+
radius?: number;
|
|
36
|
+
backgroundColor?: string;
|
|
37
|
+
children: DocNode[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TextNode {
|
|
41
|
+
type: 'text';
|
|
42
|
+
text: string;
|
|
43
|
+
variant?: TextVariant;
|
|
44
|
+
color?: string;
|
|
45
|
+
align?: Alignment;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TableNode {
|
|
49
|
+
type: 'table';
|
|
50
|
+
headers: string[];
|
|
51
|
+
rows: string[][];
|
|
52
|
+
headerBg?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SpacerNode {
|
|
56
|
+
type: 'spacer';
|
|
57
|
+
size: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DividerNode {
|
|
61
|
+
type: 'divider';
|
|
62
|
+
color?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ImageNode {
|
|
66
|
+
type: 'image';
|
|
67
|
+
src: string;
|
|
68
|
+
width?: number;
|
|
69
|
+
height?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const tokens = {
|
|
73
|
+
colors: {
|
|
74
|
+
primary: '#1D4ED8',
|
|
75
|
+
primaryLight: '#EFF6FF',
|
|
76
|
+
background: '#F3F4F6',
|
|
77
|
+
text: '#111827',
|
|
78
|
+
textMuted: '#4B5563',
|
|
79
|
+
textCaption: '#9CA3AF',
|
|
80
|
+
white: '#FFFFFF',
|
|
81
|
+
divider: '#E5E7EB',
|
|
82
|
+
},
|
|
83
|
+
spacing: {
|
|
84
|
+
page: 40,
|
|
85
|
+
card: 16,
|
|
86
|
+
gap: 12,
|
|
87
|
+
},
|
|
88
|
+
radius: {
|
|
89
|
+
card: 8,
|
|
90
|
+
},
|
|
91
|
+
typography: {
|
|
92
|
+
title: { size: 22, font: 'Helvetica-Bold', lineGap: 10 },
|
|
93
|
+
subtitle: { size: 14, font: 'Helvetica-Bold', lineGap: 6 },
|
|
94
|
+
body: { size: 11, font: 'Helvetica', lineGap: 6 },
|
|
95
|
+
caption: { size: 9, font: 'Helvetica', lineGap: 4 },
|
|
96
|
+
},
|
|
97
|
+
} as const;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* File Watcher Service
|
|
4
|
+
*
|
|
5
|
+
* Watches the workspace directory for changes and forwards
|
|
6
|
+
* events to the Convex backend for state tracking.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { watch, type FSWatcher } from "fs";
|
|
10
|
+
import { readdir, stat } from "fs/promises";
|
|
11
|
+
import { join, relative } from "path";
|
|
12
|
+
|
|
13
|
+
const WORKSPACE_PATH = "/home/user/workspace";
|
|
14
|
+
const CONVEX_URL = process.env.CONVEX_URL || "http://localhost:3210";
|
|
15
|
+
|
|
16
|
+
interface FileEvent {
|
|
17
|
+
type: "create" | "change" | "delete";
|
|
18
|
+
path: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class FileWatcherService {
|
|
23
|
+
private watcher: FSWatcher | null = null;
|
|
24
|
+
private eventQueue: FileEvent[] = [];
|
|
25
|
+
private flushInterval: NodeJS.Timer | null = null;
|
|
26
|
+
private debounceTimers: Map<string, NodeJS.Timer> = new Map();
|
|
27
|
+
|
|
28
|
+
async start(): Promise<void> {
|
|
29
|
+
console.log(`[file-watcher] Watching: ${WORKSPACE_PATH}`);
|
|
30
|
+
|
|
31
|
+
// Initial scan
|
|
32
|
+
await this.scanDirectory(WORKSPACE_PATH);
|
|
33
|
+
|
|
34
|
+
// Watch for changes
|
|
35
|
+
this.watcher = watch(
|
|
36
|
+
WORKSPACE_PATH,
|
|
37
|
+
{ recursive: true },
|
|
38
|
+
(eventType, filename) => {
|
|
39
|
+
if (filename) {
|
|
40
|
+
this.handleChange(eventType, filename);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
this.watcher.on("error", (err) => {
|
|
46
|
+
console.error("[file-watcher] Error:", err.message);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Flush events periodically
|
|
50
|
+
this.flushInterval = setInterval(() => this.flushEvents(), 1000);
|
|
51
|
+
|
|
52
|
+
console.log("[file-watcher] Started");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
stop(): void {
|
|
56
|
+
if (this.watcher) {
|
|
57
|
+
this.watcher.close();
|
|
58
|
+
this.watcher = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.flushInterval) {
|
|
62
|
+
clearInterval(this.flushInterval);
|
|
63
|
+
this.flushInterval = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const timer of this.debounceTimers.values()) {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
this.debounceTimers.clear();
|
|
70
|
+
|
|
71
|
+
console.log("[file-watcher] Stopped");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async scanDirectory(dir: string): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = join(dir, entry.name);
|
|
80
|
+
|
|
81
|
+
// Skip hidden files and common excludes
|
|
82
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
await this.scanDirectory(fullPath);
|
|
88
|
+
} else if (entry.isFile()) {
|
|
89
|
+
this.queueEvent({
|
|
90
|
+
type: "create",
|
|
91
|
+
path: relative(WORKSPACE_PATH, fullPath),
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
console.error(`[file-watcher] Scan error for ${dir}:`, error.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private handleChange(eventType: string, filename: string): void {
|
|
102
|
+
// Debounce events for the same file
|
|
103
|
+
const existingTimer = this.debounceTimers.get(filename);
|
|
104
|
+
if (existingTimer) {
|
|
105
|
+
clearTimeout(existingTimer);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.debounceTimers.set(
|
|
109
|
+
filename,
|
|
110
|
+
setTimeout(async () => {
|
|
111
|
+
this.debounceTimers.delete(filename);
|
|
112
|
+
|
|
113
|
+
const fullPath = join(WORKSPACE_PATH, filename);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const stats = await stat(fullPath);
|
|
117
|
+
this.queueEvent({
|
|
118
|
+
type: eventType === "rename" ? "create" : "change",
|
|
119
|
+
path: filename,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
// File was deleted
|
|
124
|
+
this.queueEvent({
|
|
125
|
+
type: "delete",
|
|
126
|
+
path: filename,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}, 100)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private queueEvent(event: FileEvent): void {
|
|
135
|
+
// Skip certain paths
|
|
136
|
+
if (
|
|
137
|
+
event.path.includes("node_modules") ||
|
|
138
|
+
event.path.includes(".git") ||
|
|
139
|
+
event.path.startsWith(".")
|
|
140
|
+
) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.eventQueue.push(event);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async flushEvents(): Promise<void> {
|
|
148
|
+
if (this.eventQueue.length === 0) return;
|
|
149
|
+
|
|
150
|
+
const events = [...this.eventQueue];
|
|
151
|
+
this.eventQueue = [];
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Send events to Convex
|
|
155
|
+
// In a real implementation, this would call a Convex mutation
|
|
156
|
+
// For now, just log them
|
|
157
|
+
for (const event of events) {
|
|
158
|
+
console.log(`[file-watcher] ${event.type}: ${event.path}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// TODO: Call Convex mutation to track file changes
|
|
162
|
+
// await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
163
|
+
// method: "POST",
|
|
164
|
+
// headers: { "Content-Type": "application/json" },
|
|
165
|
+
// body: JSON.stringify({
|
|
166
|
+
// path: "state.files.trackBatch",
|
|
167
|
+
// args: { events },
|
|
168
|
+
// }),
|
|
169
|
+
// });
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
// Re-queue events on failure
|
|
172
|
+
this.eventQueue.unshift(...events);
|
|
173
|
+
console.error("[file-watcher] Flush failed:", error.message);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Start the service
|
|
179
|
+
const service = new FileWatcherService();
|
|
180
|
+
service.start();
|
|
181
|
+
|
|
182
|
+
// Handle shutdown
|
|
183
|
+
process.on("SIGTERM", () => {
|
|
184
|
+
service.stop();
|
|
185
|
+
process.exit(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
process.on("SIGINT", () => {
|
|
189
|
+
service.stop();
|
|
190
|
+
process.exit(0);
|
|
191
|
+
});
|