@gaunt-sloth/core 2.0.0-alpha.26 → 2.0.0-alpha.28

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 (51) hide show
  1. package/dist/config/schema.js +64 -15
  2. package/dist/config/schema.js.map +1 -1
  3. package/dist/config/types.d.ts +17 -8
  4. package/dist/config/types.js.map +1 -1
  5. package/dist/constants.d.ts +7 -4
  6. package/dist/constants.js +7 -4
  7. package/dist/constants.js.map +1 -1
  8. package/dist/core/GthAbstractAgent.d.ts +1 -19
  9. package/dist/core/GthAbstractAgent.js +13 -65
  10. package/dist/core/GthAbstractAgent.js.map +1 -1
  11. package/dist/core/GthAgentRunner.d.ts +10 -4
  12. package/dist/core/GthAgentRunner.js +24 -7
  13. package/dist/core/GthAgentRunner.js.map +1 -1
  14. package/dist/core/GthLangChainAgent.js +16 -11
  15. package/dist/core/GthLangChainAgent.js.map +1 -1
  16. package/dist/core/launchBanner.js +36 -17
  17. package/dist/core/launchBanner.js.map +1 -1
  18. package/dist/core/shell/abstention.d.ts +88 -0
  19. package/dist/core/shell/abstention.js +184 -0
  20. package/dist/core/shell/abstention.js.map +1 -0
  21. package/dist/core/shell/openWorld.d.ts +137 -12
  22. package/dist/core/shell/openWorld.js +677 -12
  23. package/dist/core/shell/openWorld.js.map +1 -1
  24. package/dist/core/shell/rater.d.ts +79 -39
  25. package/dist/core/shell/rater.js +132 -71
  26. package/dist/core/shell/rater.js.map +1 -1
  27. package/dist/core/shell/rejection.d.ts +7 -4
  28. package/dist/core/shell/rejection.js +3 -3
  29. package/dist/core/shell/rejection.js.map +1 -1
  30. package/dist/core/toolDisplay.d.ts +12 -3
  31. package/dist/core/toolDisplay.js +27 -7
  32. package/dist/core/toolDisplay.js.map +1 -1
  33. package/dist/providers/openrouter.d.ts +3 -4
  34. package/dist/providers/openrouter.js +15 -30
  35. package/dist/providers/openrouter.js.map +1 -1
  36. package/dist/runtime/askStructured.js +7 -5
  37. package/dist/runtime/askStructured.js.map +1 -1
  38. package/dist/runtime/structuredOutput.d.ts +104 -0
  39. package/dist/runtime/structuredOutput.js +393 -0
  40. package/dist/runtime/structuredOutput.js.map +1 -0
  41. package/dist/utils/displayWidth.d.ts +30 -0
  42. package/dist/utils/displayWidth.js +140 -0
  43. package/dist/utils/displayWidth.js.map +1 -0
  44. package/dist/utils/systemPromptNotes.d.ts +28 -8
  45. package/dist/utils/systemPromptNotes.js +47 -49
  46. package/dist/utils/systemPromptNotes.js.map +1 -1
  47. package/dist/utils/untrustedText.d.ts +66 -0
  48. package/dist/utils/untrustedText.js +80 -0
  49. package/dist/utils/untrustedText.js.map +1 -0
  50. package/package.json +6 -1
  51. package/schema/gsloth-config.schema.json +3 -1
@@ -15,6 +15,7 @@
15
15
  * `GthDeepAgent`.
16
16
  */
17
17
  import { DEFAULT_COMMIT_CO_AUTHOR_EMAIL, DEFAULT_COMMIT_CO_AUTHOR_NAME } from '#src/constants.js';
18
+ import { capUntrustedText, defangUntrustedDelimiters, MCP_FENCE_BEGIN, MCP_FENCE_END, } from '#src/utils/untrustedText.js';
18
19
  /**
19
20
  * EXT-26: the platform-agnostic tail shared by both {@link appendOsShellNote} branches.
20
21
  *
@@ -84,34 +85,64 @@ export function appendCwdNote(systemPrompt, cwd) {
84
85
  return systemPrompt ? `${systemPrompt}\n\n${cwdNote}` : cwdNote;
85
86
  }
86
87
  /**
87
- * GS2-35: append the commit co-authoring rule to the composed code-mode system prompt.
88
+ * GS2-35/EXT-83: append the commit-writing rules to the composed code-mode system prompt.
88
89
  *
89
90
  * Gaunt Sloth has **no dedicated git-commit tool** — the agent commits by calling
90
- * `run_shell_command` with `git commit`, composing the message (including any trailer) itself. Left
91
- * unguided, models emit `Co-Authored-By: <their own model name>` (e.g. `Claude`, `GPT`, `Gemini`)
92
- * from trained habit, which is **factually wrong**: the commit was produced by *Gaunt Sloth*, not by
93
- * the model. This note is the fix at the correct layer first-party prompt guidance that (a) states
94
- * the exact trailer to emit and (b) forbids a model-name co-author.
91
+ * `run_shell_command` with `git commit`, composing the message (including any trailer) itself. That
92
+ * leaves two things it must be told, and both live here because both are about committing:
93
+ *
94
+ * 1. **WHO the co-author is.** Left unguided, models emit their own model name from trained habit,
95
+ * which is factually wrong: the commit was produced by *Gaunt Sloth*, not by the model. The note
96
+ * states the exact trailer to emit. EXT-83 — rather than enumerate model names not to write (a
97
+ * denylist is stale the day a new vendor ships, and an enumeration beside a catch-all teaches the
98
+ * model that the list is the rule), the correct name is SUPPLIED: the resolved
99
+ * {@link ResolvedModelIdentity} decorates the DEFAULT name as `Gaunt Sloth (provider:model)`, so
100
+ * the real model is named while the authorship stays Gaunt Sloth's.
101
+ * 2. **HOW the message reaches git.** A commit message passed inline in a double-quoted shell
102
+ * argument is EXPANDED BY THE SHELL before git runs, so a message that quotes code the way
103
+ * ordinary technical prose does is executed as a command. The note states that mechanism rather
104
+ * than merely forbidding the construct — naming a construct without its mechanism has been
105
+ * measured not to work. A file path carries no shell metacharacters, so the file form removes the
106
+ * failure mode instead of asking the model to avoid it.
107
+ *
108
+ * The note's prose carries **no backtick and no other markup** — including no angle-bracket
109
+ * placeholder: it is the one piece of guidance whose subject is how to write a commit message, so
110
+ * quoting its own examples in backticks would demonstrate the exact style rule 2 exists to stop, and
111
+ * an angle-bracket placeholder copied literally is itself a shell input redirect. The `<email>` of
112
+ * the trailer line is the exception the RFC form requires, and is scoped out of the scan.
95
113
  *
96
114
  * The identity is config-driven (`commit.coAuthor` in {@link import('#src/config/types.js').GthConfig}).
97
115
  * Each field falls back INDEPENDENTLY to the Gaunt Sloth account
98
116
  * ({@link DEFAULT_COMMIT_CO_AUTHOR_NAME} / {@link DEFAULT_COMMIT_CO_AUTHOR_EMAIL}) — so a partial
99
117
  * override (name only, or a config that bypassed the loader) still yields a complete trailer, and a
100
- * fully-absent config yields the default account. Blank/whitespace values are treated as unset.
118
+ * fully-absent config yields the default account. Blank/whitespace values are treated as unset. An
119
+ * EXPLICITLY CONFIGURED name is emitted verbatim, with no identity spliced in — the user asked for
120
+ * that string; the identity decorates only the default. An unresolvable identity (`undefined`) falls
121
+ * back to the plain default name, never to a placeholder.
101
122
  *
102
123
  * Backend-agnostic: composed through the shared code path so BOTH the lean `GthLangChainAgent` and
103
124
  * the deep `GthDeepAgent` inject it (the git-commit capability rides on `run_shell_command`, which
104
125
  * both backends expose in code mode). Returns the note alone when there is no base prompt.
105
126
  */
106
- export function appendCommitCoAuthorNote(systemPrompt, coAuthor) {
107
- const name = coAuthor?.name?.trim() || DEFAULT_COMMIT_CO_AUTHOR_NAME;
127
+ export function appendCommitCoAuthorNote(systemPrompt, coAuthor, modelIdentity) {
128
+ // EXT-83 ONE place composes the name, so the parenthesised shape stays a one-line change. The
129
+ // identity decorates ONLY the default name: a configured name is the user's own string and is
130
+ // emitted verbatim, and an unresolved identity yields the bare default (never a placeholder).
131
+ const configuredName = coAuthor?.name?.trim();
132
+ const identity = modelIdentity?.identity?.trim();
133
+ const name = configuredName ||
134
+ (identity ? `${DEFAULT_COMMIT_CO_AUTHOR_NAME} (${identity})` : DEFAULT_COMMIT_CO_AUTHOR_NAME);
108
135
  const email = coAuthor?.email?.trim() || DEFAULT_COMMIT_CO_AUTHOR_EMAIL;
109
136
  const note = 'When you create a git commit, add EXACTLY this co-author trailer line (on its own line, at ' +
110
137
  `the end of the commit message):\nCo-Authored-By: ${name} <${email}>\n` +
111
- 'NEVER attribute the co-author to the underlying model or provider name (do not write ' +
112
- '`Co-Authored-By: Claude`, `GPT`, `Gemini`, `Opus`, `Sonnet`, or any model/vendor name): the ' +
113
- 'commit is authored by Gaunt Sloth, the assistant, not the model. Emit at most this one ' +
114
- 'Co-Authored-By trailer.';
138
+ 'Emit at most this one Co-Authored-By trailer.\n' +
139
+ 'Write the commit message in plain English: say what changed and why, and keep code, shell ' +
140
+ 'commands, backticks and markup out of it.\n' +
141
+ 'Never pass a commit message inline with the -m option: inside double quotes a POSIX shell ' +
142
+ 'expands backtick and dollar-parenthesis constructs before git ever runs, so a message that ' +
143
+ 'quotes code is executed as a command. Write the message to a file with the write_file tool ' +
144
+ '(never with shell echo or a heredoc, which put the same text back into a shell argument), ' +
145
+ 'then commit it with git commit -F followed by that file path.';
115
146
  return systemPrompt ? `${systemPrompt}\n\n${note}` : note;
116
147
  }
117
148
  /**
@@ -208,46 +239,13 @@ export function appendModelContextNote(systemPrompt, modelIdentity) {
208
239
  export const MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER = 4000;
209
240
  /** EXT-32: truncation marker appended when a server's instructions exceed the per-server cap. */
210
241
  export const MCP_INSTRUCTIONS_TRUNCATION_MARKER = '… [truncated]';
211
- /** EXT-32: the ONLY real structural delimiters in the composed block (emitted by this helper). */
212
- const MCP_FENCE_BEGIN = '[BEGIN MCP SERVER-PROVIDED CONTEXT]';
213
- const MCP_FENCE_END = '[END MCP SERVER-PROVIDED CONTEXT]';
214
- /**
215
- * EXT-32 (security): neutralize the block's structural delimiters inside UNTRUSTED server text.
216
- *
217
- * `getInstructions()` is fully server-controlled, so a malicious/compromised MCP server can emit
218
- * text that forges the fence tokens or a per-server label — closing the fence early so its lines
219
- * land OUTSIDE the visual boundary, or impersonating another server. Before fencing, we defang any
220
- * occurrence in the server text of:
221
- * - the fence tokens `[BEGIN|END MCP SERVER-PROVIDED CONTEXT]` (bracket run collapsed so they can
222
- * no longer be read as the real delimiter), and
223
- * - a per-server label line `--- Server: …` (the leading `---` run broken so it can't masquerade
224
- * as one of our labels).
225
- * After this, the ONLY real delimiters in the composed block are the ones this helper emits. The
226
- * server NAME in our own label comes from trusted config keys, so it is not sanitized — only the
227
- * server-supplied CONTENT is. Whitespace-tolerant matching (`\s+`, optional bracket padding, `-{3,}`)
228
- * so trivial spacing variants can't slip a delimiter through.
229
- */
230
- function defangMcpDelimiters(text) {
231
- return text
232
- .replace(/\[\s*(BEGIN|END)\s+MCP\s+SERVER-PROVIDED\s+CONTEXT\s*\]/gi, (_m, kw) => `(server text: ${kw.toUpperCase()} MCP SERVER-PROVIDED CONTEXT)`)
233
- .replace(/-{3,}(\s*Server\s*:)/gi, '- - -$1');
234
- }
235
242
  /**
236
243
  * EXT-32: cap server text at {@link MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER}, SURROGATE-SAFE.
237
244
  *
238
- * A naive `slice(0, N)` can split a surrogate pair (e.g. an emoji) at the boundary, emitting a lone
239
- * half-code-unit. If the cut would land between a high and low surrogate, back off one code unit so
240
- * the pair is kept whole (dropped entirely rather than split). Appends the truncation marker only
241
- * when text is actually clipped.
245
+ * Thin binding of the shared {@link capUntrustedText} to this block's own cap and marker.
242
246
  */
243
247
  function capMcpText(text) {
244
- if (text.length <= MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER)
245
- return text;
246
- let end = MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER;
247
- const code = text.charCodeAt(end - 1);
248
- if (code >= 0xd800 && code <= 0xdbff)
249
- end -= 1; // don't split a surrogate pair
250
- return `${text.slice(0, end).trimEnd()}\n${MCP_INSTRUCTIONS_TRUNCATION_MARKER}`;
248
+ return capUntrustedText(text, MCP_INSTRUCTIONS_MAX_CHARS_PER_SERVER, MCP_INSTRUCTIONS_TRUNCATION_MARKER);
251
249
  }
252
250
  /**
253
251
  * EXT-32: append connected MCP servers' discovery `instructions` to the composed system prompt.
@@ -277,7 +275,7 @@ export function appendMcpServerInstructionsNote(systemPrompt, instructions) {
277
275
  // SECURITY: defang the untrusted server text's structural delimiters BEFORE fencing, then cap
278
276
  // (surrogate-safe). Order matters — defang first so a forged fence/label can't survive into the
279
277
  // composed block; cap after so the final per-server size stays bounded.
280
- const safe = capMcpText(defangMcpDelimiters(text));
278
+ const safe = capMcpText(defangUntrustedDelimiters(text));
281
279
  blocks.push(`--- Server: "${entry.server}" ---\n${safe}`);
282
280
  }
283
281
  if (blocks.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"systemPromptNotes.js","sourceRoot":"","sources":["../../src/utils/systemPromptNotes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"systemPromptNotes.js","sourceRoot":"","sources":["../../src/utils/systemPromptNotes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @module utils/untrustedText
3
+ *
4
+ * **The structural delimiters gsloth wraps untrusted text in, and the one function that neutralizes
5
+ * a forged one.**
6
+ *
7
+ * Text that arrived from outside this process — an MCP server's discovery `instructions`, a command
8
+ * string the model composed from an issue body or a fetched page — is quoted into the model's
9
+ * context in several places. Wherever it is, the same two things have to be true:
10
+ *
11
+ * 1. the quoted text is visibly fenced, so the model can tell data from first-party instruction; and
12
+ * 2. **the quoted text cannot forge the fence** and escape it.
13
+ *
14
+ * (2) is the load-bearing half, and it is why the delimiters and the defang live together in one
15
+ * module rather than beside each consumer. A second defang written for a second consumer is how one
16
+ * of them comes to know about a delimiter the other emits: a forged `[END …]` token that the
17
+ * emitting site happens not to defang closes its fence early and the attacker's lines land OUTSIDE
18
+ * the boundary, reading as first-party text. {@link defangUntrustedDelimiters} knows **every**
19
+ * delimiter we emit, so every consumer is protected by every arm.
20
+ *
21
+ * **Defang BEFORE wrapping, always.** That ordering is the whole mechanism; wrapping first and
22
+ * sanitizing after would sanitize a string that already contains the real delimiters.
23
+ */
24
+ /** The MCP discovery-instructions fence (EXT-32), emitted by `utils/systemPromptNotes.ts`. */
25
+ export declare const MCP_FENCE_BEGIN = "[BEGIN MCP SERVER-PROVIDED CONTEXT]";
26
+ export declare const MCP_FENCE_END = "[END MCP SERVER-PROVIDED CONTEXT]";
27
+ /**
28
+ * EXT-65 — the fence a refused command is quoted back inside, emitted by
29
+ * `core/shell/abstention.ts`. A command the gate could not parse is frequently a command the model
30
+ * assembled out of text it read somewhere, so quoting it back for the model to rewrite is quoting
31
+ * untrusted bytes into the model's context.
32
+ */
33
+ export declare const QUOTED_COMMAND_FENCE_BEGIN = "[BEGIN QUOTED COMMAND TEXT]";
34
+ export declare const QUOTED_COMMAND_FENCE_END = "[END QUOTED COMMAND TEXT]";
35
+ /** How much of a refused command is quoted back. Generous; a realistic command is far shorter. */
36
+ export declare const QUOTED_COMMAND_MAX_CHARS = 2000;
37
+ /** Appended when {@link QUOTED_COMMAND_MAX_CHARS} actually clipped the quoted command. */
38
+ export declare const QUOTED_COMMAND_TRUNCATION_MARKER = "\u2026 [truncated]";
39
+ /**
40
+ * Neutralize every structural delimiter this codebase emits, inside UNTRUSTED text.
41
+ *
42
+ * The text is fully attacker-influenceable, so it may forge:
43
+ * - either fence's tokens (`[BEGIN|END MCP SERVER-PROVIDED CONTEXT]`,
44
+ * `[BEGIN|END QUOTED COMMAND TEXT]`) — the bracket run is collapsed so they can no longer be
45
+ * read as the real delimiter, while staying legible to a reader who wants to see what was
46
+ * attempted; and
47
+ * - a per-server label line `--- Server: …` (EXT-32) — the leading `---` run is broken so it
48
+ * cannot masquerade as one of ours.
49
+ *
50
+ * After this, the ONLY real delimiters in a composed block are the ones the caller emits. Names we
51
+ * put in our OWN labels come from trusted config keys and are not sanitized — only the untrusted
52
+ * CONTENT is.
53
+ *
54
+ * Whitespace-tolerant (`\s+`, optional bracket padding, `-{3,}`) so trivial spacing variants cannot
55
+ * slip a delimiter through, and case-insensitive so neither can a lowercase one.
56
+ */
57
+ export declare function defangUntrustedDelimiters(text: string): string;
58
+ /**
59
+ * Truncate untrusted text at `maxChars`, SURROGATE-SAFE, appending `marker` only when text was
60
+ * actually clipped.
61
+ *
62
+ * A naive `slice(0, N)` can split a surrogate pair (e.g. an emoji) at the boundary and emit a lone
63
+ * half-code-unit. If the cut would land between a high and a low surrogate, back off one code unit
64
+ * so the pair is kept whole (dropped entirely rather than split).
65
+ */
66
+ export declare function capUntrustedText(text: string, maxChars: number, marker: string): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @module utils/untrustedText
3
+ *
4
+ * **The structural delimiters gsloth wraps untrusted text in, and the one function that neutralizes
5
+ * a forged one.**
6
+ *
7
+ * Text that arrived from outside this process — an MCP server's discovery `instructions`, a command
8
+ * string the model composed from an issue body or a fetched page — is quoted into the model's
9
+ * context in several places. Wherever it is, the same two things have to be true:
10
+ *
11
+ * 1. the quoted text is visibly fenced, so the model can tell data from first-party instruction; and
12
+ * 2. **the quoted text cannot forge the fence** and escape it.
13
+ *
14
+ * (2) is the load-bearing half, and it is why the delimiters and the defang live together in one
15
+ * module rather than beside each consumer. A second defang written for a second consumer is how one
16
+ * of them comes to know about a delimiter the other emits: a forged `[END …]` token that the
17
+ * emitting site happens not to defang closes its fence early and the attacker's lines land OUTSIDE
18
+ * the boundary, reading as first-party text. {@link defangUntrustedDelimiters} knows **every**
19
+ * delimiter we emit, so every consumer is protected by every arm.
20
+ *
21
+ * **Defang BEFORE wrapping, always.** That ordering is the whole mechanism; wrapping first and
22
+ * sanitizing after would sanitize a string that already contains the real delimiters.
23
+ */
24
+ /** The MCP discovery-instructions fence (EXT-32), emitted by `utils/systemPromptNotes.ts`. */
25
+ export const MCP_FENCE_BEGIN = '[BEGIN MCP SERVER-PROVIDED CONTEXT]';
26
+ export const MCP_FENCE_END = '[END MCP SERVER-PROVIDED CONTEXT]';
27
+ /**
28
+ * EXT-65 — the fence a refused command is quoted back inside, emitted by
29
+ * `core/shell/abstention.ts`. A command the gate could not parse is frequently a command the model
30
+ * assembled out of text it read somewhere, so quoting it back for the model to rewrite is quoting
31
+ * untrusted bytes into the model's context.
32
+ */
33
+ export const QUOTED_COMMAND_FENCE_BEGIN = '[BEGIN QUOTED COMMAND TEXT]';
34
+ export const QUOTED_COMMAND_FENCE_END = '[END QUOTED COMMAND TEXT]';
35
+ /** How much of a refused command is quoted back. Generous; a realistic command is far shorter. */
36
+ export const QUOTED_COMMAND_MAX_CHARS = 2000;
37
+ /** Appended when {@link QUOTED_COMMAND_MAX_CHARS} actually clipped the quoted command. */
38
+ export const QUOTED_COMMAND_TRUNCATION_MARKER = '… [truncated]';
39
+ /**
40
+ * Neutralize every structural delimiter this codebase emits, inside UNTRUSTED text.
41
+ *
42
+ * The text is fully attacker-influenceable, so it may forge:
43
+ * - either fence's tokens (`[BEGIN|END MCP SERVER-PROVIDED CONTEXT]`,
44
+ * `[BEGIN|END QUOTED COMMAND TEXT]`) — the bracket run is collapsed so they can no longer be
45
+ * read as the real delimiter, while staying legible to a reader who wants to see what was
46
+ * attempted; and
47
+ * - a per-server label line `--- Server: …` (EXT-32) — the leading `---` run is broken so it
48
+ * cannot masquerade as one of ours.
49
+ *
50
+ * After this, the ONLY real delimiters in a composed block are the ones the caller emits. Names we
51
+ * put in our OWN labels come from trusted config keys and are not sanitized — only the untrusted
52
+ * CONTENT is.
53
+ *
54
+ * Whitespace-tolerant (`\s+`, optional bracket padding, `-{3,}`) so trivial spacing variants cannot
55
+ * slip a delimiter through, and case-insensitive so neither can a lowercase one.
56
+ */
57
+ export function defangUntrustedDelimiters(text) {
58
+ return text
59
+ .replace(/\[\s*(BEGIN|END)\s+MCP\s+SERVER-PROVIDED\s+CONTEXT\s*\]/gi, (_m, kw) => `(server text: ${kw.toUpperCase()} MCP SERVER-PROVIDED CONTEXT)`)
60
+ .replace(/\[\s*(BEGIN|END)\s+QUOTED\s+COMMAND\s+TEXT\s*\]/gi, (_m, kw) => `(quoted text: ${kw.toUpperCase()} QUOTED COMMAND TEXT)`)
61
+ .replace(/-{3,}(\s*Server\s*:)/gi, '- - -$1');
62
+ }
63
+ /**
64
+ * Truncate untrusted text at `maxChars`, SURROGATE-SAFE, appending `marker` only when text was
65
+ * actually clipped.
66
+ *
67
+ * A naive `slice(0, N)` can split a surrogate pair (e.g. an emoji) at the boundary and emit a lone
68
+ * half-code-unit. If the cut would land between a high and a low surrogate, back off one code unit
69
+ * so the pair is kept whole (dropped entirely rather than split).
70
+ */
71
+ export function capUntrustedText(text, maxChars, marker) {
72
+ if (text.length <= maxChars)
73
+ return text;
74
+ let end = maxChars;
75
+ const code = text.charCodeAt(end - 1);
76
+ if (code >= 0xd800 && code <= 0xdbff)
77
+ end -= 1; // don't split a surrogate pair
78
+ return `${text.slice(0, end).trimEnd()}\n${marker}`;
79
+ }
80
+ //# sourceMappingURL=untrustedText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"untrustedText.js","sourceRoot":"","sources":["../../src/utils/untrustedText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,8FAA8F;AAC9F,MAAM,CAAC,MAAM,eAAe,GAAG,qCAAqC,CAAC;AACrE,MAAM,CAAC,MAAM,aAAa,GAAG,mCAAmC,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,2BAA2B,CAAC;AAEpE,kGAAkG;AAClG,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAE7C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,gCAAgC,GAAG,eAAe,CAAC;AAEhE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,IAAI;SACR,OAAO,CACN,2DAA2D,EAC3D,CAAC,EAAE,EAAE,EAAU,EAAE,EAAE,CAAC,iBAAiB,EAAE,CAAC,WAAW,EAAE,+BAA+B,CACrF;SACA,OAAO,CACN,mDAAmD,EACnD,CAAC,EAAE,EAAE,EAAU,EAAE,EAAE,CAAC,iBAAiB,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAC7E;SACA,OAAO,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAgB,EAAE,MAAc;IAC7E,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,IAAI,GAAG,GAAG,QAAQ,CAAC;IACnB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACtC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;QAAE,GAAG,IAAI,CAAC,CAAC,CAAC,+BAA+B;IAC/E,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;AACtD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaunt-sloth/core",
3
- "version": "2.0.0-alpha.26",
3
+ "version": "2.0.0-alpha.28",
4
4
  "description": "Core utilities and types for Gaunt Sloth",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Kondratev",
@@ -35,6 +35,7 @@
35
35
  "jiti": "^2.7.0",
36
36
  "jsonc-parser": "^3.3.1",
37
37
  "langchain": "^1.5.4",
38
+ "string-width": "^8.2.2",
38
39
  "zod": "^4.4.3"
39
40
  },
40
41
  "peerDependencies": {
@@ -44,6 +45,7 @@
44
45
  "@langchain/groq": "^1.3.0",
45
46
  "@langchain/ollama": "^1.3.0",
46
47
  "@langchain/openai": "^1.5.1",
48
+ "@langchain/openrouter": "^0.4.5",
47
49
  "@langchain/xai": "^1.4.1"
48
50
  },
49
51
  "peerDependenciesMeta": {
@@ -65,6 +67,9 @@
65
67
  "@langchain/openai": {
66
68
  "optional": true
67
69
  },
70
+ "@langchain/openrouter": {
71
+ "optional": true
72
+ },
68
73
  "@langchain/xai": {
69
74
  "optional": true
70
75
  }
@@ -615,7 +615,9 @@
615
615
  "mcpServers": {
616
616
  "type": "object",
617
617
  "propertyNames": {
618
- "type": "string"
618
+ "type": "string",
619
+ "minLength": 1,
620
+ "pattern": "^([^*]|[\\s\\S]{2,})$"
619
621
  },
620
622
  "additionalProperties": {}
621
623
  },