@jay-framework/aiditor 0.16.2

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/dist/index.js ADDED
@@ -0,0 +1,464 @@
1
+ import { makeJayQuery, makeJayStream, makeJayStackComponent, phaseOutput, RenderPipeline } from "@jay-framework/fullstack-component";
2
+ import { DEV_SERVER_SERVICE } from "@jay-framework/dev-server";
3
+ import { readFile } from "fs/promises";
4
+ import path, { extname } from "path";
5
+ import { query } from "@anthropic-ai/claude-agent-sdk";
6
+ import fs from "fs";
7
+ import { setActionCallerOptions } from "@jay-framework/stack-client-runtime";
8
+ const getAiditorBootstrap = makeJayQuery(
9
+ "aiditor.getAiditorBootstrap"
10
+ ).withHandler(async (input) => {
11
+ return {
12
+ projectDir: process.cwd(),
13
+ httpBaseUrl: input.origin.trim()
14
+ };
15
+ });
16
+ const getProjectInfoAction = makeJayQuery("aditor.getProjectInfo").withServices(DEV_SERVER_SERVICE).withHandler(async (input, devServer) => {
17
+ const routes = devServer.listRoutes();
18
+ return { routes, httpBaseUrl: input.jayDevUrl };
19
+ });
20
+ const getPageParamsAction = makeJayStream("aditor.getPageParams").withServices(DEV_SERVER_SERVICE).withHandler(async function* (input, devServer) {
21
+ for await (const batch of devServer.loadRouteParams(input.route)) {
22
+ yield batch;
23
+ }
24
+ });
25
+ const listFreezesAction = makeJayQuery("aditor.listFreezes").withServices(DEV_SERVER_SERVICE).withHandler(async (input, devServer) => {
26
+ const store = devServer.freezeStore;
27
+ if (!store) return { freezes: [] };
28
+ const freezes = await store.list(input.route);
29
+ return {
30
+ freezes: freezes.map((f) => ({
31
+ id: f.id,
32
+ name: f.name,
33
+ route: f.route,
34
+ createdAt: f.createdAt
35
+ }))
36
+ };
37
+ });
38
+ const IMAGE_EXTS = /* @__PURE__ */ new Set([
39
+ ".png",
40
+ ".jpg",
41
+ ".jpeg",
42
+ ".gif",
43
+ ".webp",
44
+ ".svg",
45
+ ".bmp",
46
+ ".ico"
47
+ ]);
48
+ const readFileAction = makeJayQuery("aiditor.readFile").withHandler(
49
+ async (input) => {
50
+ const ext = extname(input.filePath).toLowerCase();
51
+ const isImage = IMAGE_EXTS.has(ext);
52
+ const buf = await readFile(input.filePath);
53
+ if (isImage) {
54
+ const mime = ext === ".svg" ? "image/svg+xml" : `image/${ext.slice(1).replace("jpg", "jpeg")}`;
55
+ return {
56
+ type: "image",
57
+ mime,
58
+ data: buf.toString("base64"),
59
+ filePath: input.filePath
60
+ };
61
+ }
62
+ return {
63
+ type: "text",
64
+ data: buf.toString("utf-8"),
65
+ filePath: input.filePath
66
+ };
67
+ }
68
+ );
69
+ function formatToolDetail(name, input) {
70
+ switch (name) {
71
+ case "Read":
72
+ return String(input.file_path ?? "");
73
+ case "Edit": {
74
+ const file = String(input.file_path ?? "");
75
+ const old_str = String(input.old_string ?? "");
76
+ const new_str = String(input.new_string ?? "");
77
+ const preview = old_str.length > 60 ? old_str.slice(0, 60) + "…" : old_str;
78
+ const replaceAll = input.replace_all ? " (all)" : "";
79
+ return `${file}${replaceAll}
80
+ - ${JSON.stringify(preview)} → ${JSON.stringify(new_str.length > 60 ? new_str.slice(0, 60) + "…" : new_str)}`;
81
+ }
82
+ case "Write":
83
+ return String(input.file_path ?? "");
84
+ case "Glob":
85
+ return `${input.pattern ?? ""}${input.path ? ` in ${input.path}` : ""}`;
86
+ case "Grep":
87
+ return `${input.pattern ?? ""}${input.path ? ` in ${input.path}` : ""}`;
88
+ case "Bash":
89
+ return String(input.command ?? "");
90
+ default:
91
+ return String(
92
+ input.file_path ?? input.pattern ?? input.command ?? input.query ?? ""
93
+ );
94
+ }
95
+ }
96
+ function* transformSDKMessage(message) {
97
+ if (message.type === "assistant") {
98
+ const content = message.message?.content;
99
+ if (!Array.isArray(content)) return;
100
+ for (const block of content) {
101
+ if (block.type === "thinking" && block.thinking) {
102
+ yield { type: "thinking", text: block.thinking };
103
+ } else if (block.type === "text" && block.text) {
104
+ yield { type: "chunk", text: block.text };
105
+ }
106
+ if (block.name) {
107
+ const detail = block.input ? formatToolDetail(block.name, block.input) : "";
108
+ yield { type: "tool", name: block.name, text: detail };
109
+ }
110
+ }
111
+ } else if (message.type === "result") {
112
+ const msg = message;
113
+ yield {
114
+ type: "result",
115
+ result: msg.result ?? "",
116
+ cost: msg.total_cost_usd ?? 0,
117
+ duration: msg.duration_ms ?? 0
118
+ };
119
+ }
120
+ }
121
+ function buildNonVisualPrompt(config, notes, imagePath) {
122
+ const lines = [
123
+ `Project directory: ${config.projectDir}`,
124
+ null,
125
+ "",
126
+ "Task:",
127
+ notes
128
+ ];
129
+ return lines.filter((l) => l !== null).join("\n");
130
+ }
131
+ const ANNOTATION_TARGETING_RULES = `Annotation targeting (follow strictly):
132
+ - The only authoritative edit target is the element indicated by the annotation marker (dot, arrow, or highlighted region). Do not choose another element because it is colorful, prominent, or a common control (e.g. buttons, links) unless the marker clearly overlaps that element.
133
+ - If several elements could match, prefer the text or content under/near the marker (e.g. a heading) over unrelated controls elsewhere on the page.
134
+ - If you cannot confidently map the marker to a single target in source, do not change a plausible but unmarked control. Briefly say what is ambiguous and list candidate elements or ask for clarification.`;
135
+ function parseNotesField(notes) {
136
+ const empty = () => ({
137
+ structured: null,
138
+ structuredVideo: null,
139
+ plain: notes
140
+ });
141
+ const t = notes.trim();
142
+ if (!t.startsWith("{")) {
143
+ return empty();
144
+ }
145
+ try {
146
+ const j = JSON.parse(t);
147
+ if (j.version === 2 && j.taskKind === "video" && Array.isArray(j.annotations) && j.annotations.every(
148
+ (a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.instruction === "string" && typeof a.mode === "string" && typeof a.timeSec === "number" && Number.isFinite(a.timeSec)
149
+ )) {
150
+ let previewNavLog;
151
+ if (Array.isArray(j.previewNavLog) && j.previewNavLog.length > 0) {
152
+ const parsed = [];
153
+ for (const e of j.previewNavLog) {
154
+ if (!e || typeof e !== "object") continue;
155
+ const o = e;
156
+ if (typeof o.atRecordingSec === "number" && Number.isFinite(o.atRecordingSec) && typeof o.href === "string" && typeof o.locationPath === "string") {
157
+ parsed.push({
158
+ atRecordingSec: o.atRecordingSec,
159
+ href: o.href,
160
+ locationPath: o.locationPath
161
+ });
162
+ }
163
+ }
164
+ if (parsed.length > 0) previewNavLog = parsed;
165
+ }
166
+ return {
167
+ structured: null,
168
+ structuredVideo: {
169
+ annotations: j.annotations,
170
+ previewNavLog
171
+ },
172
+ plain: ""
173
+ };
174
+ }
175
+ if (j.version === 1 && Array.isArray(j.annotations) && j.annotations.length > 0 && j.annotations.every(
176
+ (a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.instruction === "string" && typeof a.mode === "string"
177
+ )) {
178
+ return {
179
+ structured: { annotations: j.annotations },
180
+ structuredVideo: null,
181
+ plain: ""
182
+ };
183
+ }
184
+ } catch {
185
+ }
186
+ return empty();
187
+ }
188
+ function buildVisualPromptTriple(config, pageRoute, renderedUrl, triple, parsed, attachmentPathsByAnnotationId) {
189
+ const preamble = [
190
+ `Project directory: ${config.projectDir}`,
191
+ "",
192
+ `Implement the requested changes on page route ${pageRoute}. The captures correspond to this rendered URL: ${renderedUrl}.`,
193
+ "",
194
+ "Multimodal reference images (use together):",
195
+ `- Screenshot 1 — Full annotations (markers and on-image messages): ${triple.full}`,
196
+ `- Screenshot 2 — Numbered markers only (use with Pin list; ignore overlay text from Screenshot 1 for saliency): ${triple.markersOnly}`,
197
+ `- Screenshot 3 — Clean page (no markers; use to see underlying layout and align pin positions from Screenshot 2): ${triple.clean}`,
198
+ "",
199
+ "How to use these screenshots:",
200
+ "- Use Screenshot 1 for what the user wrote on the page.",
201
+ "- Use Screenshot 2 to decide which UI region each Pin N refers to.",
202
+ "- Use Screenshot 3 to understand unmarked typography and structure when overlays obscure content.",
203
+ "",
204
+ ANNOTATION_TARGETING_RULES,
205
+ ""
206
+ ];
207
+ const body = [];
208
+ if (parsed.structured) {
209
+ body.push("Annotations (apply in order):", "");
210
+ const sorted = [...parsed.structured.annotations].sort(
211
+ (a, b) => Number.parseInt(a.id, 10) - Number.parseInt(b.id, 10)
212
+ );
213
+ for (const ann of sorted) {
214
+ const pin = ann.id;
215
+ const paths = attachmentPathsByAnnotationId.get(pin) ?? [];
216
+ body.push(`Annotation ${pin} (${ann.mode})`);
217
+ body.push(`Instruction: ${ann.instruction.trim()}`);
218
+ if (paths.length > 0) {
219
+ body.push(
220
+ "Attachments (read these files; they belong to this pin only):"
221
+ );
222
+ for (const p of paths) {
223
+ body.push(`- ${p}`);
224
+ }
225
+ } else {
226
+ body.push("Attachments: (none)");
227
+ }
228
+ body.push("");
229
+ }
230
+ } else {
231
+ body.push("Instructions:", parsed.plain.trim());
232
+ }
233
+ return [...preamble, ...body].join("\n");
234
+ }
235
+ function pinOrder(annotations) {
236
+ return [...annotations].sort((a, b) => {
237
+ if (a.timeSec !== b.timeSec) return a.timeSec - b.timeSec;
238
+ return a.id.localeCompare(b.id, void 0, { sensitivity: "base" });
239
+ });
240
+ }
241
+ function buildVisualPromptVideo(config, pageRoute, renderedUrl, videoPath, frames, parsed, attachmentPathsByPin) {
242
+ const sv = parsed.structuredVideo;
243
+ const navLines = [];
244
+ if (sv?.previewNavLog && sv.previewNavLog.length > 0) {
245
+ navLines.push(
246
+ "Preview URL history during recording (live iframe navigations while capturing; SPA route changes appear here):",
247
+ ...sv.previewNavLog.map(
248
+ (e) => `- t≈${e.atRecordingSec.toFixed(2)}s → ${e.href} (path: ${e.locationPath})`
249
+ ),
250
+ ""
251
+ );
252
+ }
253
+ const preamble = [
254
+ `Project directory: ${config.projectDir}`,
255
+ "",
256
+ `Implement the requested changes on page route ${pageRoute}. The recording corresponds to this rendered URL: ${renderedUrl}.`,
257
+ "",
258
+ `Original screen recording (no overlays): ${videoPath}`,
259
+ "",
260
+ ...navLines,
261
+ ANNOTATION_TARGETING_RULES,
262
+ ""
263
+ ];
264
+ if (!sv || sv.annotations.length === 0) {
265
+ return [
266
+ ...preamble,
267
+ "No per-frame stills were submitted (video-only task). Use the recording to reproduce the flow.",
268
+ ""
269
+ ].join("\n");
270
+ }
271
+ const pinById = /* @__PURE__ */ new Map();
272
+ for (const [i, a] of pinOrder(sv.annotations).entries()) {
273
+ pinById.set(a.id, String(i + 1));
274
+ }
275
+ const body = ["Per-moment captures (triple screenshots):", ""];
276
+ for (const fr of frames) {
277
+ body.push(
278
+ `Moment ${fr.frameIndex + 1} — t=${fr.timeSec.toFixed(2)}s`,
279
+ `- Full (markers + on-image messages): ${fr.full}`,
280
+ `- Markers only (numbered pins): ${fr.markersOnly}`,
281
+ `- Clean (no markers): ${fr.clean}`,
282
+ ""
283
+ );
284
+ const atT = sv.annotations.filter((a) => a.timeSec === fr.timeSec);
285
+ body.push("Pins anchored at this timestamp:");
286
+ for (const ann of pinOrder(atT)) {
287
+ const pinNum = pinById.get(ann.id) ?? "?";
288
+ const paths = attachmentPathsByPin.get(pinNum) ?? [];
289
+ body.push(`- Pin ${pinNum} (id ${ann.id}) — ${ann.mode}`);
290
+ body.push(` Instruction: ${ann.instruction.trim()}`);
291
+ if (ann.previewUrlAtTime) {
292
+ body.push(
293
+ ` Preview URL at this pin (~${ann.timeSec.toFixed(2)}s in the recording): ${ann.previewUrlAtTime}`
294
+ );
295
+ }
296
+ if (ann.pagePathAtTime) {
297
+ body.push(` Page path at this pin: ${ann.pagePathAtTime}`);
298
+ }
299
+ if (paths.length > 0) {
300
+ body.push(
301
+ " Attachments (read these files; they belong to this pin only):"
302
+ );
303
+ for (const p of paths) body.push(` - ${p}`);
304
+ } else {
305
+ body.push(" Attachments: (none)");
306
+ }
307
+ body.push("");
308
+ }
309
+ }
310
+ return [...preamble, ...body].join("\n");
311
+ }
312
+ function persistFile(file, destDir, fileName) {
313
+ fs.mkdirSync(destDir, { recursive: true });
314
+ const dest = path.join(destDir, fileName ?? file.name);
315
+ fs.copyFileSync(file.path, dest);
316
+ return dest;
317
+ }
318
+ const submitTaskAction = makeJayStream("aiditor.submitTask").withServices(DEV_SERVER_SERVICE).withFiles({ maxFileSize: 2e7, maxFiles: 300 }).withHandler(async function* (input, devServer) {
319
+ const projectDir = process.cwd();
320
+ const buildFolder = devServer.buildFolder ?? path.join(projectDir, "build");
321
+ const taskId = Math.random().toString(36).slice(2, 10);
322
+ const taskDir = path.join(buildFolder, "aditor", taskId);
323
+ fs.mkdirSync(taskDir, { recursive: true });
324
+ yield { type: "status", message: "Preparing task..." };
325
+ const config = { projectDir };
326
+ const parsed = parseNotesField(input.notes);
327
+ const isVideo = input.visualTask === "5";
328
+ const isVisual = !!input.visualTask;
329
+ let promptContent;
330
+ if (isVideo) {
331
+ let videoPath = "";
332
+ if (input.video) {
333
+ const videoDir = path.join(taskDir, "video");
334
+ videoPath = persistFile(input.video, videoDir, "recording.webm");
335
+ }
336
+ const extra = input.extraFiles ?? {};
337
+ const frames = [];
338
+ for (let i = 0; ; i++) {
339
+ const full = extra[`frame_${i}_full`];
340
+ const markers = extra[`frame_${i}_markers_only`];
341
+ const clean = extra[`frame_${i}_clean`];
342
+ if (!full) break;
343
+ const frameDir = path.join(taskDir, "frames", String(i));
344
+ const timeSec = parsed.structuredVideo?.annotations.find(
345
+ (a, idx) => idx === i || a.timeSec !== void 0
346
+ )?.timeSec ?? i;
347
+ frames.push({
348
+ timeSec,
349
+ frameIndex: i,
350
+ full: persistFile(full, frameDir, "full.png"),
351
+ markersOnly: markers ? persistFile(markers, frameDir, "markers-only.png") : "",
352
+ clean: clean ? persistFile(clean, frameDir, "clean.png") : ""
353
+ });
354
+ }
355
+ const attachmentPathsByPin = /* @__PURE__ */ new Map();
356
+ for (const [key, file] of Object.entries(extra)) {
357
+ if (!key.startsWith("attachment_")) continue;
358
+ const pinId = key.split("_")[1];
359
+ const attDir = path.join(taskDir, `annotation-${pinId}`);
360
+ const filePath = persistFile(file, attDir);
361
+ const existing = attachmentPathsByPin.get(pinId) ?? [];
362
+ existing.push(filePath);
363
+ attachmentPathsByPin.set(pinId, existing);
364
+ }
365
+ promptContent = buildVisualPromptVideo(
366
+ config,
367
+ input.pageRoute ?? "/",
368
+ input.renderedUrl ?? "",
369
+ videoPath,
370
+ frames,
371
+ parsed,
372
+ attachmentPathsByPin
373
+ );
374
+ } else if (isVisual && input.screenshot_full) {
375
+ const ssDir = path.join(taskDir, "screenshots");
376
+ const triple = {
377
+ full: persistFile(input.screenshot_full, ssDir, "full.png"),
378
+ markersOnly: input.screenshot_markers_only ? persistFile(
379
+ input.screenshot_markers_only,
380
+ ssDir,
381
+ "markers-only.png"
382
+ ) : "",
383
+ clean: input.screenshot_clean ? persistFile(input.screenshot_clean, ssDir, "clean.png") : ""
384
+ };
385
+ const extraTriple = input.extraFiles ?? {};
386
+ const attachmentPathsByAnnotationId = /* @__PURE__ */ new Map();
387
+ for (const [key, file] of Object.entries(extraTriple)) {
388
+ if (!key.startsWith("attachment_")) continue;
389
+ const pinId = key.split("_")[1];
390
+ const attDir = path.join(taskDir, `annotation-${pinId}`);
391
+ const filePath = persistFile(file, attDir);
392
+ const existing = attachmentPathsByAnnotationId.get(pinId) ?? [];
393
+ existing.push(filePath);
394
+ attachmentPathsByAnnotationId.set(pinId, existing);
395
+ }
396
+ promptContent = buildVisualPromptTriple(
397
+ config,
398
+ input.pageRoute ?? "/",
399
+ input.renderedUrl ?? "",
400
+ triple,
401
+ parsed,
402
+ attachmentPathsByAnnotationId
403
+ );
404
+ } else {
405
+ promptContent = buildNonVisualPrompt(config, input.notes);
406
+ }
407
+ yield { type: "status", message: "Running agent..." };
408
+ let systemPrompt = "";
409
+ const agentKitPath = path.join(
410
+ projectDir,
411
+ "agent-kit",
412
+ "designer",
413
+ "INSTRUCTIONS.md"
414
+ );
415
+ if (fs.existsSync(agentKitPath)) {
416
+ systemPrompt = fs.readFileSync(agentKitPath, "utf-8");
417
+ }
418
+ try {
419
+ for await (const message of query({
420
+ prompt: promptContent,
421
+ options: {
422
+ cwd: projectDir,
423
+ tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
424
+ allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
425
+ maxTurns: 30,
426
+ persistSession: false,
427
+ systemPrompt
428
+ }
429
+ })) {
430
+ for (const chunk of transformSDKMessage(message)) {
431
+ yield chunk;
432
+ }
433
+ }
434
+ } catch (err) {
435
+ yield {
436
+ type: "error",
437
+ message: err instanceof Error ? err.message : String(err)
438
+ };
439
+ }
440
+ yield { type: "done" };
441
+ });
442
+ setActionCallerOptions({ timeout: 12e4 });
443
+ const page = makeJayStackComponent().withProps();
444
+ const aiditorShell = makeJayStackComponent().withProps().withSlowlyRender(async () => {
445
+ return phaseOutput({}, { headline: "AIditor" });
446
+ }).withFastRender(async (_props, carryForward) => {
447
+ const Pipeline = RenderPipeline.for();
448
+ return Pipeline.ok({}).toPhaseOutput(() => ({
449
+ viewState: {
450
+ headline: `${carryForward.headline}`
451
+ },
452
+ carryForward
453
+ }));
454
+ });
455
+ export {
456
+ page as aiditorPage,
457
+ aiditorShell,
458
+ getAiditorBootstrap,
459
+ getPageParamsAction,
460
+ getProjectInfoAction,
461
+ listFreezesAction,
462
+ readFileAction,
463
+ submitTaskAction
464
+ };
@@ -0,0 +1 @@
1
+ /* AIditor page: styles live inline in page.jay-html for POC parity with aditor-poc. */
@@ -0,0 +1,12 @@
1
+ name: Page
2
+ tags:
3
+ # Mirrors aiditor-shell.jay-contract (local plugin). Inlined so static validation sees shell.headline.
4
+ - tag: shell
5
+ type: sub-contract
6
+ phase: fast
7
+ tags:
8
+ - tag: headline
9
+ type: data
10
+ dataType: string
11
+ phase: fast
12
+ description: Visible title for the AIditor POC shell
@@ -0,0 +1,24 @@
1
+ import {JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export interface ShellOfPageViewState {
5
+ headline: string
6
+ }
7
+
8
+ export interface PageViewState {
9
+ shell: ShellOfPageViewState
10
+ }
11
+
12
+ export type PageSlowViewState = {};
13
+
14
+ export type PageFastViewState = {
15
+ shell: PageViewState['shell'];
16
+ };
17
+
18
+ export type PageInteractiveViewState = {};
19
+
20
+ export interface PageRefs {}
21
+
22
+ export interface PageRepeatedRefs {}
23
+
24
+ export type PageContract = JayContract<PageViewState, PageRefs, PageSlowViewState, PageFastViewState, PageInteractiveViewState>