sparkecoder 0.1.20 → 0.1.22

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.
Files changed (100) hide show
  1. package/dist/agent/index.d.ts +2 -2
  2. package/dist/agent/index.js +53 -3
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +397 -46
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -1
  7. package/dist/db/index.js.map +1 -1
  8. package/dist/{index-BzedNBK-.d.ts → index-CNwLFGiZ.d.ts} +24 -3
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +392 -41
  11. package/dist/index.js.map +1 -1
  12. package/dist/{schema-CkrIadxa.d.ts → schema-Df7MU3nM.d.ts} +26 -3
  13. package/dist/server/index.js +392 -41
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/tools/index.js.map +1 -1
  16. package/package.json +1 -1
  17. package/web/.next/BUILD_ID +1 -1
  18. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  19. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  20. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  21. package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
  22. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  23. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  24. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  26. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  38. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  39. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  41. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  42. package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
  43. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  44. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  45. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  46. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  48. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  49. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_1d78db71._.js → 2374f_387a1437._.js} +1 -1
  50. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_378282b1._.js → 2374f_5f58fd73._.js} +1 -1
  51. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_30f9df13._.js → 2374f_65fcfd95._.js} +1 -1
  52. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8825dcc9._.js → 2374f_741f6b67._.js} +1 -1
  53. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9bf3c7f3._.js → 2374f_814be2c9._.js} +2 -2
  54. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5de336d2._.js → 2374f_84859a94._.js} +1 -1
  55. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_bbc99511._.js → 2374f_cfd0137a._.js} +1 -1
  56. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d94c2b70._.js → 2374f_f1038f7c._.js} +1 -1
  57. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__a984d933._.js → [root-of-the-server]__3ec22171._.js} +2 -2
  58. package/web/.next/standalone/web/.next/server/chunks/ssr/web_96bca05b._.js +1 -1
  59. package/web/.next/standalone/web/.next/server/chunks/ssr/web_c7618534._.js +8 -0
  60. package/web/.next/standalone/web/.next/server/chunks/ssr/web_d7d3e40d._.js +1 -1
  61. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  62. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  63. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  64. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  65. package/web/.next/standalone/web/.next/static/chunks/{55705f91c6cfe307.js → 3bb454ca848ec78e.js} +3 -3
  66. package/web/.next/standalone/web/.next/static/chunks/{5ec82ce8f3aabaf0.js → 5e5b485d77ac0d8f.js} +1 -1
  67. package/web/.next/standalone/web/.next/static/chunks/{c9d8a4b9a763e232.js → beb9625c4a470042.js} +1 -1
  68. package/web/.next/standalone/web/.next/static/chunks/c81c1aec4369c77f.js +5 -0
  69. package/web/.next/standalone/web/.next/static/chunks/cb355fac10c6ad11.css +1 -0
  70. package/web/.next/standalone/web/.next/static/static/chunks/{55705f91c6cfe307.js → 3bb454ca848ec78e.js} +3 -3
  71. package/web/.next/standalone/web/.next/static/static/chunks/{5ec82ce8f3aabaf0.js → 5e5b485d77ac0d8f.js} +1 -1
  72. package/web/.next/standalone/web/.next/static/static/chunks/{c9d8a4b9a763e232.js → beb9625c4a470042.js} +1 -1
  73. package/web/.next/standalone/web/.next/static/static/chunks/c81c1aec4369c77f.js +5 -0
  74. package/web/.next/standalone/web/.next/static/static/chunks/cb355fac10c6ad11.css +1 -0
  75. package/web/.next/standalone/web/src/app/(main)/layout.tsx +2 -2
  76. package/web/.next/standalone/web/src/components/ai-elements/speech-input.tsx +89 -36
  77. package/web/.next/standalone/web/src/components/chat-interface.tsx +354 -38
  78. package/web/.next/standalone/web/src/components/ui/sidebar.tsx +2 -2
  79. package/web/.next/standalone/web/src/lib/api.ts +133 -2
  80. package/web/.next/static/chunks/{55705f91c6cfe307.js → 3bb454ca848ec78e.js} +3 -3
  81. package/web/.next/static/chunks/{5ec82ce8f3aabaf0.js → 5e5b485d77ac0d8f.js} +1 -1
  82. package/web/.next/static/chunks/{c9d8a4b9a763e232.js → beb9625c4a470042.js} +1 -1
  83. package/web/.next/static/chunks/c81c1aec4369c77f.js +5 -0
  84. package/web/.next/static/chunks/cb355fac10c6ad11.css +1 -0
  85. package/web/.next/standalone/web/.next/server/chunks/ssr/web_19b6934c._.js +0 -8
  86. package/web/.next/standalone/web/.next/static/chunks/61d61c75ce7cd4ba.js +0 -5
  87. package/web/.next/standalone/web/.next/static/chunks/d0a69c59b1c0d99c.css +0 -1
  88. package/web/.next/standalone/web/.next/static/static/chunks/61d61c75ce7cd4ba.js +0 -5
  89. package/web/.next/standalone/web/.next/static/static/chunks/d0a69c59b1c0d99c.css +0 -1
  90. package/web/.next/static/chunks/61d61c75ce7cd4ba.js +0 -5
  91. package/web/.next/static/chunks/d0a69c59b1c0d99c.css +0 -1
  92. /package/web/.next/standalone/web/.next/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_buildManifest.js +0 -0
  93. /package/web/.next/standalone/web/.next/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_clientMiddlewareManifest.json +0 -0
  94. /package/web/.next/standalone/web/.next/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_ssgManifest.js +0 -0
  95. /package/web/.next/standalone/web/.next/static/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_buildManifest.js +0 -0
  96. /package/web/.next/standalone/web/.next/static/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_clientMiddlewareManifest.json +0 -0
  97. /package/web/.next/standalone/web/.next/static/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_ssgManifest.js +0 -0
  98. /package/web/.next/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_buildManifest.js +0 -0
  99. /package/web/.next/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_clientMiddlewareManifest.json +0 -0
  100. /package/web/.next/static/{ogmHZngrFt0TlARPSetHj → n86r6x1RoUipFp6nLIk-R}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -18,8 +18,8 @@ import { Hono as Hono5 } from "hono";
18
18
  import { serve } from "@hono/node-server";
19
19
  import { cors } from "hono/cors";
20
20
  import { logger } from "hono/logger";
21
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
22
- import { resolve as resolve8, dirname as dirname6, join as join3 } from "path";
21
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
22
+ import { resolve as resolve8, dirname as dirname6, join as join5 } from "path";
23
23
  import { spawn as spawn2 } from "child_process";
24
24
  import { createServer as createNetServer } from "net";
25
25
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -28,6 +28,9 @@ import { fileURLToPath as fileURLToPath2 } from "url";
28
28
  import { Hono } from "hono";
29
29
  import { zValidator } from "@hono/zod-validator";
30
30
  import { z as z9 } from "zod";
31
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readdirSync, statSync, unlinkSync } from "fs";
32
+ import { join as join3, basename as basename2, extname as extname5 } from "path";
33
+ import { nanoid as nanoid4 } from "nanoid";
31
34
 
32
35
  // src/db/index.ts
33
36
  import Database from "better-sqlite3";
@@ -3295,11 +3298,12 @@ ${this.summary}`
3295
3298
  }
3296
3299
  /**
3297
3300
  * Add a user message to the context
3301
+ * Content can be a string or an array of content parts (for messages with images/files)
3298
3302
  */
3299
- addUserMessage(text2) {
3303
+ addUserMessage(content) {
3300
3304
  const userMessage = {
3301
3305
  role: "user",
3302
- content: text2
3306
+ content
3303
3307
  };
3304
3308
  messageQueries.create(this.sessionId, userMessage);
3305
3309
  }
@@ -3399,13 +3403,62 @@ var Agent = class _Agent {
3399
3403
  getSession() {
3400
3404
  return this.session;
3401
3405
  }
3406
+ /**
3407
+ * Build user message content from prompt and attachments
3408
+ */
3409
+ buildUserMessageContent(prompt, attachments) {
3410
+ if (!attachments || attachments.length === 0) {
3411
+ return prompt;
3412
+ }
3413
+ const contentParts = [];
3414
+ const attachmentDescriptions = attachments.map((a, i) => {
3415
+ const name = a.filename || `attachment_${i + 1}`;
3416
+ const typeLabel = a.type === "image" ? "Image" : "File";
3417
+ const location = a.savedPath || "(path unknown)";
3418
+ return `${i + 1}. ${typeLabel}: "${name}" saved at: ${location}`;
3419
+ }).join("\n");
3420
+ contentParts.push({
3421
+ type: "text",
3422
+ text: `[FILE ATTACHMENTS - The user has attached the following files which are saved on disk]
3423
+ ${attachmentDescriptions}
3424
+
3425
+ You can reference these files by their paths above. The file contents are also shown inline below.`
3426
+ });
3427
+ if (prompt) {
3428
+ contentParts.push({ type: "text", text: `
3429
+ [USER MESSAGE]
3430
+ ${prompt}` });
3431
+ }
3432
+ for (const attachment of attachments) {
3433
+ if (attachment.type === "image") {
3434
+ contentParts.push({
3435
+ type: "image",
3436
+ image: attachment.data,
3437
+ // base64 data URL or raw base64
3438
+ mediaType: attachment.mediaType,
3439
+ filename: attachment.filename,
3440
+ savedPath: attachment.savedPath
3441
+ });
3442
+ } else {
3443
+ contentParts.push({
3444
+ type: "file",
3445
+ data: attachment.data,
3446
+ mediaType: attachment.mediaType || "application/octet-stream",
3447
+ filename: attachment.filename,
3448
+ savedPath: attachment.savedPath
3449
+ });
3450
+ }
3451
+ }
3452
+ return contentParts;
3453
+ }
3402
3454
  /**
3403
3455
  * Run the agent with a prompt (streaming)
3404
3456
  */
3405
3457
  async stream(options) {
3406
3458
  const config = getConfig();
3459
+ const userContent = this.buildUserMessageContent(options.prompt, options.attachments);
3407
3460
  if (!options.skipSaveUserMessage) {
3408
- this.context.addUserMessage(options.prompt);
3461
+ this.context.addUserMessage(userContent);
3409
3462
  }
3410
3463
  sessionQueries.updateStatus(this.session.id, "active");
3411
3464
  const systemPrompt = await buildSystemPrompt({
@@ -3965,11 +4018,138 @@ sessions2.get("/:id/diff/:filePath", async (c) => {
3965
4018
  currentContent: fileDiff.currentContent
3966
4019
  });
3967
4020
  });
4021
+ function getAttachmentsDir(sessionId) {
4022
+ const appDataDir = getAppDataDirectory();
4023
+ return join3(appDataDir, "attachments", sessionId);
4024
+ }
4025
+ function ensureAttachmentsDir(sessionId) {
4026
+ const dir = getAttachmentsDir(sessionId);
4027
+ if (!existsSync10(dir)) {
4028
+ mkdirSync3(dir, { recursive: true });
4029
+ }
4030
+ return dir;
4031
+ }
4032
+ sessions2.get("/:id/attachments", async (c) => {
4033
+ const sessionId = c.req.param("id");
4034
+ const session = sessionQueries.getById(sessionId);
4035
+ if (!session) {
4036
+ return c.json({ error: "Session not found" }, 404);
4037
+ }
4038
+ const dir = getAttachmentsDir(sessionId);
4039
+ if (!existsSync10(dir)) {
4040
+ return c.json({ sessionId, attachments: [], count: 0 });
4041
+ }
4042
+ const files = readdirSync(dir);
4043
+ const attachments = files.map((filename) => {
4044
+ const filePath = join3(dir, filename);
4045
+ const stats = statSync(filePath);
4046
+ return {
4047
+ id: filename.split("_")[0],
4048
+ // Extract the nanoid prefix
4049
+ filename,
4050
+ path: filePath,
4051
+ size: stats.size,
4052
+ createdAt: stats.birthtime.toISOString()
4053
+ };
4054
+ });
4055
+ return c.json({
4056
+ sessionId,
4057
+ attachments,
4058
+ count: attachments.length
4059
+ });
4060
+ });
4061
+ sessions2.post("/:id/attachments", async (c) => {
4062
+ const sessionId = c.req.param("id");
4063
+ const session = sessionQueries.getById(sessionId);
4064
+ if (!session) {
4065
+ return c.json({ error: "Session not found" }, 404);
4066
+ }
4067
+ const contentType = c.req.header("content-type") || "";
4068
+ if (contentType.includes("multipart/form-data")) {
4069
+ try {
4070
+ const formData = await c.req.formData();
4071
+ const file = formData.get("file");
4072
+ if (!file || !(file instanceof File)) {
4073
+ return c.json({ error: "No file provided" }, 400);
4074
+ }
4075
+ const dir = ensureAttachmentsDir(sessionId);
4076
+ const id = nanoid4(10);
4077
+ const ext = extname5(file.name) || "";
4078
+ const safeFilename = `${id}_${basename2(file.name).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
4079
+ const filePath = join3(dir, safeFilename);
4080
+ const arrayBuffer = await file.arrayBuffer();
4081
+ writeFileSync2(filePath, Buffer.from(arrayBuffer));
4082
+ return c.json({
4083
+ id,
4084
+ filename: file.name,
4085
+ storedAs: safeFilename,
4086
+ path: filePath,
4087
+ size: file.size,
4088
+ mediaType: file.type,
4089
+ sessionId
4090
+ }, 201);
4091
+ } catch (err) {
4092
+ console.error("Failed to upload attachment:", err);
4093
+ return c.json({ error: "Failed to upload file" }, 500);
4094
+ }
4095
+ }
4096
+ try {
4097
+ const body = await c.req.json();
4098
+ if (!body.filename || !body.data) {
4099
+ return c.json({ error: "Missing filename or data" }, 400);
4100
+ }
4101
+ const dir = ensureAttachmentsDir(sessionId);
4102
+ const id = nanoid4(10);
4103
+ const ext = extname5(body.filename) || "";
4104
+ const safeFilename = `${id}_${basename2(body.filename).replace(/[^a-zA-Z0-9._-]/g, "_")}`;
4105
+ const filePath = join3(dir, safeFilename);
4106
+ let base64Data = body.data;
4107
+ if (base64Data.includes(",")) {
4108
+ base64Data = base64Data.split(",")[1];
4109
+ }
4110
+ const buffer = Buffer.from(base64Data, "base64");
4111
+ writeFileSync2(filePath, buffer);
4112
+ return c.json({
4113
+ id,
4114
+ filename: body.filename,
4115
+ storedAs: safeFilename,
4116
+ path: filePath,
4117
+ size: buffer.length,
4118
+ mediaType: body.mediaType,
4119
+ sessionId
4120
+ }, 201);
4121
+ } catch (err) {
4122
+ console.error("Failed to upload attachment:", err);
4123
+ return c.json({ error: "Failed to upload file" }, 500);
4124
+ }
4125
+ });
4126
+ sessions2.delete("/:id/attachments/:attachmentId", async (c) => {
4127
+ const sessionId = c.req.param("id");
4128
+ const attachmentId = c.req.param("attachmentId");
4129
+ const session = sessionQueries.getById(sessionId);
4130
+ if (!session) {
4131
+ return c.json({ error: "Session not found" }, 404);
4132
+ }
4133
+ const dir = getAttachmentsDir(sessionId);
4134
+ if (!existsSync10(dir)) {
4135
+ return c.json({ error: "Attachment not found" }, 404);
4136
+ }
4137
+ const files = readdirSync(dir);
4138
+ const file = files.find((f) => f.startsWith(attachmentId + "_"));
4139
+ if (!file) {
4140
+ return c.json({ error: "Attachment not found" }, 404);
4141
+ }
4142
+ const filePath = join3(dir, file);
4143
+ unlinkSync(filePath);
4144
+ return c.json({ success: true, id: attachmentId });
4145
+ });
3968
4146
 
3969
4147
  // src/server/routes/agents.ts
3970
4148
  import { Hono as Hono2 } from "hono";
3971
4149
  import { zValidator as zValidator2 } from "@hono/zod-validator";
3972
4150
  import { z as z10 } from "zod";
4151
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
4152
+ import { join as join4 } from "path";
3973
4153
 
3974
4154
  // src/server/resumable-stream.ts
3975
4155
  import { createResumableStreamContext } from "resumable-stream/generic";
@@ -4044,11 +4224,23 @@ var streamContext = createResumableStreamContext({
4044
4224
  });
4045
4225
 
4046
4226
  // src/server/routes/agents.ts
4047
- import { nanoid as nanoid4 } from "nanoid";
4227
+ import { nanoid as nanoid5 } from "nanoid";
4048
4228
  var agents = new Hono2();
4049
- var runPromptSchema = z10.object({
4050
- prompt: z10.string().min(1)
4229
+ var attachmentSchema = z10.object({
4230
+ type: z10.enum(["image", "file"]),
4231
+ data: z10.string(),
4232
+ // base64 data URL or raw base64
4233
+ mediaType: z10.string().optional(),
4234
+ filename: z10.string().optional()
4051
4235
  });
4236
+ var runPromptSchema = z10.object({
4237
+ prompt: z10.string(),
4238
+ // Can be empty if attachments are provided
4239
+ attachments: z10.array(attachmentSchema).optional()
4240
+ }).refine(
4241
+ (data) => data.prompt.trim().length > 0 || data.attachments && data.attachments.length > 0,
4242
+ { message: "Either prompt or attachments must be provided" }
4243
+ );
4052
4244
  var quickStartSchema = z10.object({
4053
4245
  prompt: z10.string().min(1),
4054
4246
  name: z10.string().optional(),
@@ -4060,7 +4252,53 @@ var rejectSchema = z10.object({
4060
4252
  reason: z10.string().optional()
4061
4253
  }).optional();
4062
4254
  var streamAbortControllers = /* @__PURE__ */ new Map();
4063
- function createAgentStreamProducer(sessionId, prompt, streamId) {
4255
+ function getAttachmentsDirectory(sessionId) {
4256
+ const appDataDir = getAppDataDirectory();
4257
+ return join4(appDataDir, "attachments", sessionId);
4258
+ }
4259
+ function saveAttachmentToDisk(sessionId, attachment, index) {
4260
+ const attachmentsDir = getAttachmentsDirectory(sessionId);
4261
+ if (!existsSync11(attachmentsDir)) {
4262
+ mkdirSync4(attachmentsDir, { recursive: true });
4263
+ }
4264
+ let filename = attachment.filename;
4265
+ if (!filename) {
4266
+ const ext = getExtensionFromMediaType(attachment.mediaType, attachment.type);
4267
+ filename = `attachment_${index + 1}${ext}`;
4268
+ }
4269
+ let base64Data = attachment.data;
4270
+ if (base64Data.includes(",")) {
4271
+ base64Data = base64Data.split(",")[1];
4272
+ }
4273
+ const filePath = join4(attachmentsDir, filename);
4274
+ const buffer = Buffer.from(base64Data, "base64");
4275
+ writeFileSync3(filePath, buffer);
4276
+ return filePath;
4277
+ }
4278
+ function getExtensionFromMediaType(mediaType, type) {
4279
+ if (!mediaType) {
4280
+ return type === "image" ? ".png" : ".bin";
4281
+ }
4282
+ const mimeToExt = {
4283
+ "image/png": ".png",
4284
+ "image/jpeg": ".jpg",
4285
+ "image/jpg": ".jpg",
4286
+ "image/gif": ".gif",
4287
+ "image/webp": ".webp",
4288
+ "image/svg+xml": ".svg",
4289
+ "application/pdf": ".pdf",
4290
+ "text/plain": ".txt",
4291
+ "text/markdown": ".md",
4292
+ "application/json": ".json",
4293
+ "application/javascript": ".js",
4294
+ "text/javascript": ".js",
4295
+ "text/typescript": ".ts",
4296
+ "text/html": ".html",
4297
+ "text/css": ".css"
4298
+ };
4299
+ return mimeToExt[mediaType] || ".bin";
4300
+ }
4301
+ function createAgentStreamProducer(sessionId, prompt, streamId, attachments) {
4064
4302
  return () => {
4065
4303
  const { readable, writable } = new TransformStream();
4066
4304
  const writer = writable.getWriter();
@@ -4093,9 +4331,53 @@ function createAgentStreamProducer(sessionId, prompt, streamId) {
4093
4331
  try {
4094
4332
  const agent = await Agent.create({ sessionId });
4095
4333
  await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
4334
+ let broadcastContent;
4335
+ if (attachments && attachments.length > 0) {
4336
+ const contentParts = [];
4337
+ const attachmentDescriptions = attachments.map((a, i) => {
4338
+ const name = a.filename || `attachment_${i + 1}`;
4339
+ const typeLabel = a.type === "image" ? "Image" : "File";
4340
+ const location = a.savedPath || "(path unknown)";
4341
+ return `${i + 1}. ${typeLabel}: "${name}" saved at: ${location}`;
4342
+ }).join("\n");
4343
+ contentParts.push({
4344
+ type: "text",
4345
+ text: `[FILE ATTACHMENTS - The user has attached the following files which are saved on disk]
4346
+ ${attachmentDescriptions}
4347
+
4348
+ You can reference these files by their paths above. The file contents are also shown inline below.`
4349
+ });
4350
+ if (prompt) {
4351
+ contentParts.push({ type: "text", text: `
4352
+ [USER MESSAGE]
4353
+ ${prompt}` });
4354
+ }
4355
+ for (const attachment of attachments) {
4356
+ if (attachment.type === "image") {
4357
+ contentParts.push({
4358
+ type: "image",
4359
+ image: attachment.data,
4360
+ mediaType: attachment.mediaType,
4361
+ filename: attachment.filename,
4362
+ savedPath: attachment.savedPath
4363
+ });
4364
+ } else {
4365
+ contentParts.push({
4366
+ type: "file",
4367
+ data: attachment.data,
4368
+ mediaType: attachment.mediaType || "application/octet-stream",
4369
+ filename: attachment.filename,
4370
+ savedPath: attachment.savedPath
4371
+ });
4372
+ }
4373
+ }
4374
+ broadcastContent = contentParts;
4375
+ } else {
4376
+ broadcastContent = prompt;
4377
+ }
4096
4378
  await writeSSE(JSON.stringify({
4097
4379
  type: "data-user-message",
4098
- data: { id: `user_${Date.now()}`, content: prompt }
4380
+ data: { id: `user_${Date.now()}`, content: broadcastContent }
4099
4381
  }));
4100
4382
  const messageId = `msg_${Date.now()}`;
4101
4383
  await writeSSE(JSON.stringify({ type: "start", messageId }));
@@ -4103,6 +4385,8 @@ function createAgentStreamProducer(sessionId, prompt, streamId) {
4103
4385
  let textStarted = false;
4104
4386
  const result = await agent.stream({
4105
4387
  prompt,
4388
+ attachments,
4389
+ // Pass attachments to agent
4106
4390
  abortSignal: abortController.signal,
4107
4391
  // Use our managed abort controller, NOT client signal
4108
4392
  skipSaveUserMessage: true,
@@ -4212,14 +4496,20 @@ function createAgentStreamProducer(sessionId, prompt, streamId) {
4212
4496
  } else {
4213
4497
  await writeSSE(JSON.stringify({ type: "finish" }));
4214
4498
  }
4215
- activeStreamQueries.finish(streamId);
4499
+ try {
4500
+ activeStreamQueries.finish(streamId);
4501
+ } catch {
4502
+ }
4216
4503
  } catch (error) {
4217
4504
  if (error.name === "AbortError" || error.message?.includes("aborted")) {
4218
4505
  await writeSSE(JSON.stringify({ type: "abort" }));
4219
4506
  } else {
4220
4507
  console.error("Agent error:", error);
4221
4508
  await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
4222
- activeStreamQueries.markError(streamId);
4509
+ try {
4510
+ activeStreamQueries.markError(streamId);
4511
+ } catch {
4512
+ }
4223
4513
  }
4224
4514
  } finally {
4225
4515
  cleanupAbortController();
@@ -4235,19 +4525,74 @@ agents.post(
4235
4525
  zValidator2("json", runPromptSchema),
4236
4526
  async (c) => {
4237
4527
  const id = c.req.param("id");
4238
- const { prompt } = c.req.valid("json");
4528
+ const { prompt, attachments } = c.req.valid("json");
4239
4529
  const session = sessionQueries.getById(id);
4240
4530
  if (!session) {
4241
4531
  return c.json({ error: "Session not found" }, 404);
4242
4532
  }
4243
4533
  const nextSequence = messageQueries.getNextSequence(id);
4244
4534
  await createCheckpoint(id, session.workingDirectory, nextSequence);
4245
- messageQueries.create(id, { role: "user", content: prompt });
4246
- const streamId = `stream_${id}_${nanoid4(10)}`;
4535
+ let userMessageContent;
4536
+ const streamAttachments = attachments;
4537
+ if (streamAttachments && streamAttachments.length > 0) {
4538
+ for (let i = 0; i < streamAttachments.length; i++) {
4539
+ const attachment = streamAttachments[i];
4540
+ try {
4541
+ const savedPath = saveAttachmentToDisk(id, attachment, i);
4542
+ attachment.savedPath = savedPath;
4543
+ } catch (err) {
4544
+ console.error(`Failed to save attachment ${i}:`, err);
4545
+ }
4546
+ }
4547
+ const contentParts = [];
4548
+ const attachmentDescriptions = streamAttachments.map((a, i) => {
4549
+ const name = a.filename || `attachment_${i + 1}`;
4550
+ const typeLabel = a.type === "image" ? "Image" : "File";
4551
+ const location = a.savedPath || "(path unknown)";
4552
+ return `${i + 1}. ${typeLabel}: "${name}" saved at: ${location}`;
4553
+ }).join("\n");
4554
+ contentParts.push({
4555
+ type: "text",
4556
+ text: `[FILE ATTACHMENTS - The user has attached the following files which are saved on disk]
4557
+ ${attachmentDescriptions}
4558
+
4559
+ You can reference these files by their paths above. The file contents are also shown inline below.`
4560
+ });
4561
+ if (prompt) {
4562
+ contentParts.push({ type: "text", text: `
4563
+ [USER MESSAGE]
4564
+ ${prompt}` });
4565
+ }
4566
+ for (const attachment of streamAttachments) {
4567
+ if (attachment.type === "image") {
4568
+ contentParts.push({
4569
+ type: "image",
4570
+ image: attachment.data,
4571
+ // base64 data URL or raw base64
4572
+ mediaType: attachment.mediaType,
4573
+ filename: attachment.filename,
4574
+ savedPath: attachment.savedPath
4575
+ });
4576
+ } else {
4577
+ contentParts.push({
4578
+ type: "file",
4579
+ data: attachment.data,
4580
+ mediaType: attachment.mediaType || "application/octet-stream",
4581
+ filename: attachment.filename,
4582
+ savedPath: attachment.savedPath
4583
+ });
4584
+ }
4585
+ }
4586
+ userMessageContent = contentParts;
4587
+ } else {
4588
+ userMessageContent = prompt;
4589
+ }
4590
+ messageQueries.create(id, { role: "user", content: userMessageContent });
4591
+ const streamId = `stream_${id}_${nanoid5(10)}`;
4247
4592
  activeStreamQueries.create(id, streamId);
4248
4593
  const stream = await streamContext.resumableStream(
4249
4594
  streamId,
4250
- createAgentStreamProducer(id, prompt, streamId)
4595
+ createAgentStreamProducer(id, prompt, streamId, streamAttachments)
4251
4596
  );
4252
4597
  if (!stream) {
4253
4598
  return c.json({ error: "Failed to create stream" }, 500);
@@ -4440,7 +4785,7 @@ agents.post(
4440
4785
  sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
4441
4786
  });
4442
4787
  const session = agent.getSession();
4443
- const streamId = `stream_${session.id}_${nanoid4(10)}`;
4788
+ const streamId = `stream_${session.id}_${nanoid5(10)}`;
4444
4789
  await createCheckpoint(session.id, session.workingDirectory, 0);
4445
4790
  activeStreamQueries.create(session.id, streamId);
4446
4791
  const createQuickStreamProducer = () => {
@@ -5154,11 +5499,11 @@ function getWebDirectory() {
5154
5499
  try {
5155
5500
  const currentDir = dirname6(fileURLToPath2(import.meta.url));
5156
5501
  const webDir = resolve8(currentDir, "..", "web");
5157
- if (existsSync10(webDir) && existsSync10(join3(webDir, "package.json"))) {
5502
+ if (existsSync12(webDir) && existsSync12(join5(webDir, "package.json"))) {
5158
5503
  return webDir;
5159
5504
  }
5160
5505
  const altWebDir = resolve8(currentDir, "..", "..", "web");
5161
- if (existsSync10(altWebDir) && existsSync10(join3(altWebDir, "package.json"))) {
5506
+ if (existsSync12(altWebDir) && existsSync12(join5(altWebDir, "package.json"))) {
5162
5507
  return altWebDir;
5163
5508
  }
5164
5509
  return null;
@@ -5216,23 +5561,23 @@ async function findWebPort(preferredPort) {
5216
5561
  return { port: preferredPort, alreadyRunning: false };
5217
5562
  }
5218
5563
  function hasProductionBuild(webDir) {
5219
- const buildIdPath = join3(webDir, ".next", "BUILD_ID");
5220
- return existsSync10(buildIdPath);
5564
+ const buildIdPath = join5(webDir, ".next", "BUILD_ID");
5565
+ return existsSync12(buildIdPath);
5221
5566
  }
5222
5567
  function hasSourceFiles(webDir) {
5223
- const appDir = join3(webDir, "src", "app");
5224
- const pagesDir = join3(webDir, "src", "pages");
5225
- const rootAppDir = join3(webDir, "app");
5226
- const rootPagesDir = join3(webDir, "pages");
5227
- return existsSync10(appDir) || existsSync10(pagesDir) || existsSync10(rootAppDir) || existsSync10(rootPagesDir);
5568
+ const appDir = join5(webDir, "src", "app");
5569
+ const pagesDir = join5(webDir, "src", "pages");
5570
+ const rootAppDir = join5(webDir, "app");
5571
+ const rootPagesDir = join5(webDir, "pages");
5572
+ return existsSync12(appDir) || existsSync12(pagesDir) || existsSync12(rootAppDir) || existsSync12(rootPagesDir);
5228
5573
  }
5229
5574
  function getStandaloneServerPath(webDir) {
5230
5575
  const possiblePaths = [
5231
- join3(webDir, ".next", "standalone", "server.js"),
5232
- join3(webDir, ".next", "standalone", "web", "server.js")
5576
+ join5(webDir, ".next", "standalone", "server.js"),
5577
+ join5(webDir, ".next", "standalone", "web", "server.js")
5233
5578
  ];
5234
5579
  for (const serverPath of possiblePaths) {
5235
- if (existsSync10(serverPath)) {
5580
+ if (existsSync12(serverPath)) {
5236
5581
  return serverPath;
5237
5582
  }
5238
5583
  }
@@ -5272,15 +5617,15 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
5272
5617
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
5273
5618
  return { process: null, port: actualPort };
5274
5619
  }
5275
- const usePnpm = existsSync10(join3(webDir, "pnpm-lock.yaml"));
5276
- const useNpm = !usePnpm && existsSync10(join3(webDir, "package-lock.json"));
5620
+ const usePnpm = existsSync12(join5(webDir, "pnpm-lock.yaml"));
5621
+ const useNpm = !usePnpm && existsSync12(join5(webDir, "package-lock.json"));
5277
5622
  const pkgManager = usePnpm ? "pnpm" : useNpm ? "npm" : "npx";
5278
5623
  const { NODE_OPTIONS, TSX_TSCONFIG_PATH, ...cleanEnv } = process.env;
5279
5624
  const apiUrl = publicUrl || `http://127.0.0.1:${apiPort}`;
5280
5625
  const runtimeConfig = { apiBaseUrl: apiUrl };
5281
- const runtimeConfigPath = join3(webDir, "runtime-config.json");
5626
+ const runtimeConfigPath = join5(webDir, "runtime-config.json");
5282
5627
  try {
5283
- writeFileSync2(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
5628
+ writeFileSync4(runtimeConfigPath, JSON.stringify(runtimeConfig, null, 2));
5284
5629
  if (!quiet) console.log(` \u{1F4DD} Runtime config written to ${runtimeConfigPath}`);
5285
5630
  } catch (err) {
5286
5631
  if (!quiet) console.warn(` \u26A0 Could not write runtime config: ${err}`);
@@ -5306,7 +5651,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
5306
5651
  if (!quiet) console.log(" \u{1F4E6} Starting Web UI from standalone build...");
5307
5652
  } else if (hasBuild && (isProduction || !hasSource)) {
5308
5653
  command = pkgManager;
5309
- args = pkgManager === "npx" ? ["next", "start", "-p", String(actualPort)] : ["run", "start", "--", "-p", String(actualPort)];
5654
+ args = pkgManager === "npx" ? ["next", "start", "-p", String(actualPort)] : ["run", "start"];
5310
5655
  } else if (hasSource) {
5311
5656
  if (isProduction && !hasBuild) {
5312
5657
  if (!quiet) console.log(" \u{1F4E6} Building Web UI for production...");
@@ -5318,10 +5663,10 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
5318
5663
  }
5319
5664
  if (!quiet) console.log(" \u2713 Web UI build complete");
5320
5665
  command = pkgManager;
5321
- args = pkgManager === "npx" ? ["next", "start", "-p", String(actualPort)] : ["run", "start", "--", "-p", String(actualPort)];
5666
+ args = pkgManager === "npx" ? ["next", "start", "-p", String(actualPort)] : ["run", "start"];
5322
5667
  } else {
5323
5668
  command = pkgManager;
5324
- args = pkgManager === "npx" ? ["next", "dev", "-p", String(actualPort)] : ["run", "dev", "--", "-p", String(actualPort)];
5669
+ args = pkgManager === "npx" ? ["next", "dev", "-p", String(actualPort)] : ["run", "dev"];
5325
5670
  }
5326
5671
  } else {
5327
5672
  if (!quiet) {
@@ -5349,6 +5694,12 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
5349
5694
  }, startupTimeout);
5350
5695
  child.stdout?.on("data", (data) => {
5351
5696
  const output = data.toString();
5697
+ if (!quiet) {
5698
+ const lines = output.trim().split("\n").filter((l) => l.trim());
5699
+ for (const line of lines) {
5700
+ console.log(` Web UI: ${line}`);
5701
+ }
5702
+ }
5352
5703
  if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
5353
5704
  started = true;
5354
5705
  clearTimeout(timeout);
@@ -5356,9 +5707,9 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false, pu
5356
5707
  }
5357
5708
  });
5358
5709
  child.stderr?.on("data", (data) => {
5359
- const output = data.toString();
5360
- if (output.toLowerCase().includes("error")) {
5361
- if (!quiet) console.error(` Web UI error: ${output.trim().slice(0, 200)}`);
5710
+ const output = data.toString().trim();
5711
+ if (!quiet && output) {
5712
+ console.error(` Web UI: ${output.slice(0, 500)}`);
5362
5713
  }
5363
5714
  });
5364
5715
  child.on("error", (err) => {
@@ -5464,8 +5815,8 @@ async function startServer(options = {}) {
5464
5815
  if (options.workingDirectory) {
5465
5816
  config.resolvedWorkingDirectory = options.workingDirectory;
5466
5817
  }
5467
- if (!existsSync10(config.resolvedWorkingDirectory)) {
5468
- mkdirSync3(config.resolvedWorkingDirectory, { recursive: true });
5818
+ if (!existsSync12(config.resolvedWorkingDirectory)) {
5819
+ mkdirSync5(config.resolvedWorkingDirectory, { recursive: true });
5469
5820
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
5470
5821
  }
5471
5822
  initDatabase(config.resolvedDatabasePath);
@@ -5968,8 +6319,8 @@ function generateOpenAPISpec() {
5968
6319
  }
5969
6320
 
5970
6321
  // src/cli.ts
5971
- import { writeFileSync as writeFileSync3, existsSync as existsSync11 } from "fs";
5972
- import { resolve as resolve9, join as join4 } from "path";
6322
+ import { writeFileSync as writeFileSync5, existsSync as existsSync13 } from "fs";
6323
+ import { resolve as resolve9, join as join6 } from "path";
5973
6324
  async function apiRequest(baseUrl, path, options = {}) {
5974
6325
  const url = `${baseUrl}${path}`;
5975
6326
  const init = {
@@ -6526,19 +6877,19 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
6526
6877
  let configLocation;
6527
6878
  if (options.global) {
6528
6879
  const appDataDir = ensureAppDataDirectory();
6529
- configPath = join4(appDataDir, "sparkecoder.config.json");
6880
+ configPath = join6(appDataDir, "sparkecoder.config.json");
6530
6881
  configLocation = "global";
6531
6882
  } else {
6532
6883
  configPath = resolve9(process.cwd(), "sparkecoder.config.json");
6533
6884
  configLocation = "local";
6534
6885
  }
6535
- if (existsSync11(configPath) && !options.force) {
6886
+ if (existsSync13(configPath) && !options.force) {
6536
6887
  console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
6537
6888
  console.log(chalk.dim(` ${configPath}`));
6538
6889
  return;
6539
6890
  }
6540
6891
  const config = createDefaultConfig();
6541
- writeFileSync3(configPath, JSON.stringify(config, null, 2));
6892
+ writeFileSync5(configPath, JSON.stringify(config, null, 2));
6542
6893
  console.log(chalk.green(`\u2713 Created ${configLocation} config`));
6543
6894
  console.log(chalk.dim(` ${configPath}`));
6544
6895
  console.log(chalk.dim("Set AI_GATEWAY_API_KEY and run sparkecoder to start"));