talon-agent 1.47.1 → 1.47.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talon-agent",
3
- "version": "1.47.1",
3
+ "version": "1.47.2",
4
4
  "description": "Multi-frontend AI agent with full tool access, streaming, cron jobs, and plugin system",
5
5
  "author": "Dylan Neve",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@ import {
25
25
  writeFile,
26
26
  } from "node:fs/promises";
27
27
  import { tmpdir } from "node:os";
28
- import { dirname, join } from "node:path";
28
+ import { dirname, extname, join } from "node:path";
29
29
  import { getMeshService } from "../../mesh/index.js";
30
30
  import {
31
31
  clearTeleport,
@@ -39,7 +39,12 @@ import {
39
39
  } from "../../../util/exec-output.js";
40
40
  import type { SharedActionHandlers } from "./types.js";
41
41
 
42
- type Result = { ok: boolean; text: string };
42
+ type Result = {
43
+ ok: boolean;
44
+ text: string;
45
+ /** Set for image files so the tool result carries a viewable image block. */
46
+ image?: { data: string; mimeType: string };
47
+ };
43
48
 
44
49
  /**
45
50
  * ripgrep binary, resolved from PATH (env-overridable for tests). NOT a
@@ -70,6 +75,21 @@ const TELEPORT_TIMEOUT_HINT =
70
75
  "background it in-shell instead: `cmd > /tmp/out.log 2>&1 &`, then poll the log " +
71
76
  "with read, or bound the command itself: `logcat -d`, `timeout 30 …`, `head -n 200`.)";
72
77
  const MAX_READ_LINES = 2_000;
78
+ /**
79
+ * Image extensions the model can actually view, mapped to their MIME type.
80
+ * Reading one returns an image content block instead of the file's raw bytes
81
+ * decoded as (garbage) UTF-8 — so `read`ing a photo/screenshot/design shows
82
+ * the picture, not mojibake.
83
+ */
84
+ const IMAGE_MIME: Record<string, string> = {
85
+ ".png": "image/png",
86
+ ".jpg": "image/jpeg",
87
+ ".jpeg": "image/jpeg",
88
+ ".gif": "image/gif",
89
+ ".webp": "image/webp",
90
+ };
91
+ /** Anthropic caps a single image around 5MB; refuse larger with a downscale hint. */
92
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
73
93
  /** Caps for the pure-JS glob/search fallbacks (rg unavailable). */
74
94
  const MAX_JS_RESULTS = 2_000;
75
95
  const MAX_JS_FILE_BYTES = 2 * 1024 * 1024;
@@ -437,23 +457,58 @@ async function read(
437
457
  ): Promise<Result> {
438
458
  const p = str(path);
439
459
  if (!p) return { ok: false, text: "A file path is required." };
460
+ const active = await getTeleport(chatId);
461
+ const where = active ? active.deviceName : "local";
462
+
463
+ // Image files: return a viewable image block, not the raw bytes decoded as
464
+ // UTF-8. Without this, `read`ing a photo/screenshot/design hands the model
465
+ // mojibake and it can't see the picture at all.
466
+ const mime = IMAGE_MIME[extname(p).toLowerCase()];
467
+ if (mime) {
468
+ let bytes: Buffer;
469
+ if (active) {
470
+ const res = await getMeshService().readFileBytes(active.deviceId, p);
471
+ if ("error" in res) return { ok: false, text: res.error };
472
+ bytes = res.data;
473
+ } else {
474
+ try {
475
+ bytes = await readFile(p);
476
+ } catch (err) {
477
+ return {
478
+ ok: false,
479
+ text: `Cannot read ${p}: ${(err as Error).message}`,
480
+ };
481
+ }
482
+ }
483
+ if (bytes.length > MAX_IMAGE_BYTES) {
484
+ return {
485
+ ok: false,
486
+ text:
487
+ `${p} [${where}] is ${(bytes.length / 1_048_576).toFixed(1)}MB — over the ` +
488
+ `${MAX_IMAGE_BYTES / 1_048_576}MB image limit. Downscale it first ` +
489
+ `(e.g. \`convert '${p}' -resize 1568x /tmp/small.jpg\`) and read that.`,
490
+ };
491
+ }
492
+ return {
493
+ ok: true,
494
+ text: `${p} [${where}] — image (${mime}, ${bytes.length} bytes)`,
495
+ image: { data: bytes.toString("base64"), mimeType: mime },
496
+ };
497
+ }
498
+
440
499
  const start = num(offset) ?? 0;
441
500
  const max = Math.min(num(limit) ?? MAX_READ_LINES, MAX_READ_LINES);
442
- const active = await getTeleport(chatId);
443
501
  let content: string;
444
- let where: string;
445
502
  if (active) {
446
503
  const res = await getMeshService().readFileBytes(active.deviceId, p);
447
504
  if ("error" in res) return { ok: false, text: res.error };
448
505
  content = res.data.toString("utf8");
449
- where = active.deviceName;
450
506
  } else {
451
507
  try {
452
508
  content = await readFile(p, "utf8");
453
509
  } catch (err) {
454
510
  return { ok: false, text: `Cannot read ${p}: ${(err as Error).message}` };
455
511
  }
456
- where = "local";
457
512
  }
458
513
  const lines = content.split("\n");
459
514
  const slice = lines.slice(start, start + max);
@@ -125,14 +125,39 @@ export function createBridge(
125
125
 
126
126
  /** Wrap a bridge result into the MCP content format. */
127
127
  export function textResult(result: unknown): {
128
- content: Array<{ type: "text"; text: string }>;
128
+ content: Array<
129
+ | { type: "text"; text: string }
130
+ | { type: "image"; data: string; mimeType: string }
131
+ >;
129
132
  isError?: boolean;
130
133
  } {
131
- const r = result as { ok?: boolean; text?: string; error?: string };
134
+ const r = result as {
135
+ ok?: boolean;
136
+ text?: string;
137
+ error?: string;
138
+ image?: { data?: unknown; mimeType?: unknown };
139
+ };
140
+ const content: Array<
141
+ | { type: "text"; text: string }
142
+ | { type: "image"; data: string; mimeType: string }
143
+ > = [{ type: "text" as const, text: r.text ?? JSON.stringify(result) }];
144
+ // A result may carry an image (e.g. `read` on a photo) — surface it as an
145
+ // MCP image block so the model actually sees the picture, not base64 text.
146
+ if (
147
+ r &&
148
+ typeof r === "object" &&
149
+ r.image &&
150
+ typeof r.image.data === "string" &&
151
+ typeof r.image.mimeType === "string"
152
+ ) {
153
+ content.push({
154
+ type: "image" as const,
155
+ data: r.image.data,
156
+ mimeType: r.image.mimeType,
157
+ });
158
+ }
132
159
  return {
133
- content: [
134
- { type: "text" as const, text: r.text ?? JSON.stringify(result) },
135
- ],
160
+ content,
136
161
  // Gateway results carry ok:false on failure — mark those as tool errors
137
162
  // so the model treats the message as a failure, not a success payload.
138
163
  ...(r && typeof r === "object" && r.ok === false ? { isError: true } : {}),
package/src/core/types.ts CHANGED
@@ -233,6 +233,12 @@ export type ActionResult = {
233
233
  * tool calls (react/edit/delete) that target this message.
234
234
  */
235
235
  message_id?: number | string;
236
+ /**
237
+ * Set by tools that return a viewable image (e.g. `read` on a photo). The
238
+ * MCP layer turns this into an image content block so the model sees the
239
+ * picture instead of base64 text.
240
+ */
241
+ image?: { data: string; mimeType: string };
236
242
  [key: string]: unknown;
237
243
  };
238
244