talon-agent 3.0.3 → 3.0.4

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": "3.0.3",
3
+ "version": "3.0.4",
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",
@@ -52,6 +52,10 @@ async function probeSupportedModels(
52
52
  reject(new Error("aborted")),
53
53
  );
54
54
  });
55
+ // Unreachable — the promise above only ever rejects. The yield* both
56
+ // satisfies require-yield and documents that this stream produces
57
+ // nothing by design (streaming-input mode with no user message).
58
+ yield* [] as never[];
55
59
  };
56
60
 
57
61
  const q = query({
@@ -43,6 +43,10 @@ export async function warmSession(chatId: string): Promise<void> {
43
43
  reject(new Error("aborted")),
44
44
  );
45
45
  });
46
+ // Unreachable — the promise above only ever rejects. The yield* both
47
+ // satisfies require-yield and documents that this stream produces
48
+ // nothing by design (streaming-input mode with no user message).
49
+ yield* [] as never[];
46
50
  };
47
51
 
48
52
  const q = query({
@@ -133,7 +133,8 @@ export async function ensureChatMcpServer<TClient extends RemoteAgentClient>(
133
133
  // Rotate out other chat servers BEFORE registering this one. The
134
134
  // heartbeat sentinel server is exempt (it has no real chat to leak
135
135
  // into and stays connected for cron / trigger paths).
136
- for (const other of [...state.registeredMcpServers]) {
136
+ // Snapshot: rotation removes entries from the live set mid-iteration.
137
+ for (const other of Array.from(state.registeredMcpServers)) {
137
138
  if (
138
139
  !other.startsWith(`${TALON_MCP_SERVER_NAME}-`) ||
139
140
  other === serverName ||
@@ -672,6 +672,16 @@ async function edit(
672
672
  }
673
673
  }
674
674
 
675
+ // Binary guard: decoding binary as UTF-8 is lossy (invalid sequences
676
+ // become U+FFFD), so a read→replace→write round-trip would corrupt every
677
+ // non-text byte — even when old_string matches a clean region.
678
+ if (content.includes("\0")) {
679
+ return {
680
+ ok: false,
681
+ text: `${shown} looks binary — refusing a text edit that would corrupt it. Use bash for byte-level changes.`,
682
+ };
683
+ }
684
+
675
685
  const count = from ? content.split(from).length - 1 : 0;
676
686
  if (count === 0) {
677
687
  return {
@@ -693,13 +703,14 @@ async function edit(
693
703
  replaceAll === true
694
704
  ? content.split(from).join(to)
695
705
  : spliceOnce(content, from, to);
706
+ const firstLine = content.slice(0, content.indexOf(from)).split("\n").length;
696
707
 
697
708
  if (active) {
698
709
  const res = await svc.writeFileToDevice(active.deviceId, p, updated);
699
710
  return res.ok
700
711
  ? {
701
712
  ok: true,
702
- text: `Edited ${shown} [${active.deviceName}] (${count} replacement${count === 1 ? "" : "s"}).`,
713
+ text: `Edited ${shown} [${active.deviceName}] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
703
714
  }
704
715
  : res;
705
716
  }
@@ -713,7 +724,7 @@ async function edit(
713
724
  }
714
725
  return {
715
726
  ok: true,
716
- text: `Edited ${shown} [local] (${count} replacement${count === 1 ? "" : "s"}).`,
727
+ text: `Edited ${shown} [local] (${count} replacement${count === 1 ? "" : "s"}, first at line ${firstLine}).`,
717
728
  };
718
729
  }
719
730
 
@@ -733,8 +744,8 @@ async function glob(
733
744
  // -name, path patterns via -path). `command -v` gates the choice so a
734
745
  // no-match rg exit (1) isn't misread as "rg missing, run find too".
735
746
  const findExpr = pat.includes("/")
736
- ? `-path ${shellQuote(`*${pat}`)}`
737
- : `-name ${shellQuote(pat)}`;
747
+ ? `-type f -path ${shellQuote(`*${pat}`)}`
748
+ : `-type f -name ${shellQuote(pat)}`;
738
749
  const cmd =
739
750
  `if command -v rg >/dev/null 2>&1; ` +
740
751
  `then rg --files -g ${shellQuote(pat)} ${shellQuote(root)}; ` +
@@ -193,7 +193,9 @@ export class MeshService {
193
193
  // Stamp arrival against our own clock so waitForFreshLocation is immune
194
194
  // to device clock skew.
195
195
  this.receivedAt.set(loc.deviceId, Date.now());
196
- for (const notify of [...this.waiters]) notify();
196
+ // Snapshot: a notified waiter removes itself (and new waiters may be
197
+ // added) mid-iteration — iterate a copy, not the live set.
198
+ for (const notify of Array.from(this.waiters)) notify();
197
199
  return loc;
198
200
  }
199
201
 
@@ -372,11 +374,11 @@ export class MeshService {
372
374
 
373
375
  /** `ring_device`: make the device sound/vibrate so it can be found. */
374
376
  ringDevice(query?: unknown, message?: unknown): Promise<MeshToolResult> {
375
- return this.commandTool(query, "ring", {
376
- ...(typeof message === "string" && message.trim()
377
- ? { message: message.trim().slice(0, 200) }
378
- : {}),
379
- });
377
+ const note =
378
+ typeof message === "string" && message.trim()
379
+ ? message.trim().slice(0, 200)
380
+ : undefined;
381
+ return this.commandTool(query, "ring", note ? { message: note } : {});
380
382
  }
381
383
 
382
384
  /**
@@ -77,7 +77,7 @@ export function setPluginEnabled(
77
77
  if (!name) return { ok: false, error: "A plugin name is required." };
78
78
 
79
79
  if (isBuiltinPlugin(name)) {
80
- const section = { ...(builtinSection(config, name) ?? {}), enabled };
80
+ const section = { ...builtinSection(config, name), enabled };
81
81
  (config as unknown as Record<string, unknown>)[name] = section;
82
82
  return { ok: true, name, persist: { [name]: section } };
83
83
  }
@@ -48,7 +48,7 @@ export function pagerank(
48
48
  const idx = new Map(nodes.map((h, i) => [h, i]));
49
49
 
50
50
  // Weighted out-adjacency over coactivation edges (only among value nodes).
51
- const outWeight = new Array<number>(n).fill(0);
51
+ const outWeight = Array.from({ length: n }, () => 0);
52
52
  const edges: { from: number; to: number; w: number }[] = [];
53
53
  for (const from of nodes) {
54
54
  for (const e of dag.edgesFrom(from, "coactivation")) {
@@ -61,7 +61,7 @@ export function pagerank(
61
61
  }
62
62
 
63
63
  // Teleport / personalization vector.
64
- const teleport = new Array<number>(n).fill(1 / n);
64
+ const teleport = Array.from({ length: n }, () => 1 / n);
65
65
  if (opts.personalization && opts.personalization.size > 0) {
66
66
  let total = 0;
67
67
  for (const [, w] of opts.personalization) total += Math.max(0, w);
@@ -74,7 +74,7 @@ export function pagerank(
74
74
  }
75
75
  }
76
76
 
77
- let rank = new Array<number>(n).fill(1 / n);
77
+ let rank = Array.from({ length: n }, () => 1 / n);
78
78
  for (let it = 0; it < iterations; it++) {
79
79
  const next = teleport.map((t) => (1 - damping) * t);
80
80
 
@@ -66,7 +66,10 @@ const SYCOPHANCY = [
66
66
 
67
67
  // Emoji detection without a heavy dependency: pictographic + symbol ranges.
68
68
  const EMOJI_RE =
69
- /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2190}-\u{21FF}\u{2B00}-\u{2BFF}\u{FE00}-\u{FE0F}\u{1F000}-\u{1F0FF}]/gu;
69
+ // Variation selectors are combining marks — alternated separately so the
70
+ // character class holds only standalone pictographic ranges
71
+ // (no-misleading-character-class).
72
+ /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2190}-\u{21FF}\u{2B00}-\u{2BFF}\u{1F000}-\u{1F0FF}]|[\u{FE00}-\u{FE0F}]/gu;
70
73
 
71
74
  function countOccurrences(
72
75
  haystack: string,
@@ -63,7 +63,7 @@ export function cosineDistance(
63
63
  export function centroid(vectors: readonly (readonly number[])[]): number[] {
64
64
  if (vectors.length === 0) return [];
65
65
  const dim = vectors[0]!.length;
66
- const acc = new Array<number>(dim).fill(0);
66
+ const acc = Array.from({ length: dim }, () => 0);
67
67
  for (const v of vectors) {
68
68
  for (let i = 0; i < dim; i++) acc[i]! += v[i]!;
69
69
  }
@@ -110,7 +110,7 @@ export class HashingEmbedder implements Embedder {
110
110
  }
111
111
 
112
112
  private embedOne(text: string): number[] {
113
- const vec = new Array<number>(this.dim).fill(0);
113
+ const vec = Array.from({ length: this.dim }, () => 0);
114
114
  const lower = text.toLowerCase();
115
115
  const add = (feature: string): void => {
116
116
  const h = createHash("md5").update(feature).digest();
@@ -67,7 +67,7 @@ export class TalonEmbedder implements Embedder {
67
67
  }
68
68
  }
69
69
 
70
- const vec = new Array<number>(this.dim).fill(0);
70
+ const vec = Array.from({ length: this.dim }, () => 0);
71
71
  for (const [feature, count] of counts) {
72
72
  const h = createHash("md5").update(feature).digest();
73
73
  const idx = ((h[0]! << 16) | (h[1]! << 8) | h[2]!) % this.dim;
@@ -98,9 +98,9 @@ export function createGitHubPlugin(config: { token?: string }): TalonPlugin {
98
98
  },
99
99
 
100
100
  getEnvVars() {
101
- return {
102
- ...(token ? { GITHUB_PERSONAL_ACCESS_TOKEN: token } : {}),
103
- };
101
+ const vars: Record<string, string> = {};
102
+ if (token) vars.GITHUB_PERSONAL_ACCESS_TOKEN = token;
103
+ return vars;
104
104
  },
105
105
  };
106
106
  }