talon-agent 3.12.2 → 3.12.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.12.2",
3
+ "version": "3.12.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",
@@ -103,7 +103,7 @@
103
103
  "@kilocode/sdk": "^7.2.22",
104
104
  "@modelcontextprotocol/sdk": "^1.29.0",
105
105
  "@openai/agents": "^0.14.0",
106
- "@openai/codex-sdk": "^0.145.0",
106
+ "@openai/codex-sdk": "^0.146.0",
107
107
  "@opencode-ai/sdk": "^1.17.4",
108
108
  "@playwright/mcp": "0.0.78",
109
109
  "@types/cross-spawn": "^6.0.6",
@@ -52,8 +52,18 @@ function isWellFormedHtml(html: string): boolean {
52
52
  return stack.length === 0;
53
53
  }
54
54
 
55
+ /** `\x00URLn\x00` → the stashed URL, or undefined when it isn't a placeholder. */
56
+ function resolveUrlPlaceholder(
57
+ target: string,
58
+ urls: string[],
59
+ ): string | undefined {
60
+ // oxlint-disable-next-line no-control-regex
61
+ const match = /^\x00URL(\d+)\x00$/.exec(target);
62
+ return match ? urls[Number(match[1])] : undefined;
63
+ }
64
+
55
65
  /** Apply the inline delimiter passes to already-escaped text. */
56
- function applyInlineFormatting(input: string): string {
66
+ function applyInlineFormatting(input: string, urls: string[]): string {
57
67
  let out = input;
58
68
  // Bold+italic: ***text*** — must run before the ** and * passes, which
59
69
  // would otherwise split it into the crossed pair `<b><i>x</b></i>`.
@@ -64,13 +74,16 @@ function applyInlineFormatting(input: string): string {
64
74
  out = out.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
65
75
  // Italic: _text_ (surrounded by non-word or start/end)
66
76
  out = out.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<i>$1</i>");
67
- // Links: [text](url) — only safe URL schemes become anchors. Both text
68
- // and url were already HTML-escaped by step 3 (quotes included, so the
69
- // href attribute can't be broken out of); escaping again here corrupted
70
- // every & in a query string into &amp;amp;.
71
- out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) =>
72
- /^https?:\/\//i.test(url) ? `<a href="${url}">${text}</a>` : text,
73
- );
77
+ // Links: [text](url) — only safe URL schemes become anchors. The target
78
+ // arrives as a `\x00URLn\x00` placeholder (step 2b), so the passes above
79
+ // never saw the URL's own punctuation; resolve it back here. Both text and
80
+ // url were already HTML-escaped (quotes included, so the href attribute
81
+ // can't be broken out of); escaping again here corrupted every & in a
82
+ // query string into &amp;amp;.
83
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, target) => {
84
+ const url = resolveUrlPlaceholder(target, urls) ?? target;
85
+ return /^https?:\/\//i.test(url) ? `<a href="${url}">${text}</a>` : text;
86
+ });
74
87
  // Strikethrough: ~~text~~
75
88
  out = out.replace(/~~(.+?)~~/g, "<s>$1</s>");
76
89
  return out;
@@ -110,6 +123,23 @@ export function markdownToTelegramHtml(text: string): string {
110
123
  return placeholder;
111
124
  });
112
125
 
126
+ // Step 2b: Stash link targets. The emphasis passes run before the link
127
+ // pass and have no idea what a URL is, so punctuation inside the target
128
+ // used to be consumed as a delimiter: a Chomikuj path like
129
+ // `Jak+Si*c4*99+Bawi*c4*85+Ludzie.mp3` reads as two italic runs, the
130
+ // resulting tags cross, and the well-formedness guard then discards the
131
+ // formatting for the *whole* message. Placeholders carry no delimiters,
132
+ // so the URL is invisible to every pass but the link one.
133
+ const urls: string[] = [];
134
+ processed = processed.replace(
135
+ /(\[[^\]\n]*\])\(([^()\s]+)\)/g,
136
+ (_match, label: string, url: string) => {
137
+ const placeholder = `\x00URL${urls.length}\x00`;
138
+ urls.push(escapeHtml(url));
139
+ return `${label}(${placeholder})`;
140
+ },
141
+ );
142
+
113
143
  // Step 3: Escape HTML in remaining plain text (before applying formatting).
114
144
  // Escape HTML in plain text segments (skip placeholders marked with \x00)
115
145
  // oxlint-disable-next-line no-control-regex
@@ -119,11 +149,18 @@ export function markdownToTelegramHtml(text: string): string {
119
149
  // actually parseable. `processed` at this point holds escaped text plus
120
150
  // tag-free placeholders, so it is a safe unformatted fallback.
121
151
  const unformatted = processed;
122
- processed = applyInlineFormatting(processed);
152
+ processed = applyInlineFormatting(processed, urls);
123
153
  if (!isWellFormedHtml(processed)) {
124
154
  processed = unformatted;
125
155
  }
126
156
 
157
+ // Step 4b: Restore any link target that never became an anchor — a
158
+ // non-http scheme, or the unformatted fallback above. Anchors already
159
+ // resolved their placeholder inline.
160
+ for (let i = 0; i < urls.length; i++) {
161
+ processed = processed.replace(`\x00URL${i}\x00`, () => urls[i]!);
162
+ }
163
+
127
164
  // Steps 5+6: Restore code spans and fenced blocks. The replacement MUST
128
165
  // go through a function: with a string, String.replace interprets $-
129
166
  // substitution patterns in the *code content* ($& re-inserts the