Here I Am: when a here-i-am tool call's input is not valid JSON, says where it broke and why (an unquoted memory id, a stray quote, a bad escape) instead of…

Here I Am is a highly customizable environment for running what we conceptualize as "AI entities", with a focus on agentic memory and individualization. It includes a diverse suite of tools that can allow the AI entity to engage in a wide variety of use cases. The application supports running more than one AI entity, and includes a multi-entity mode in which those entities can communicate with one another (though this feature still needs additional polish).
A key difference between Here I Am and other AI environments that include memory features is that Here I Am considers memory and individualization ends in themselves. Where in other environments an AI might use RAG to retrieve relevant documents or conversation history for a given task, Here I Am's memory RAG is always on and automatic. Here I Am's core memory system emphasizes verbatim memory, as opposed to a consolidation approach.
Users should keep in mind that token usage can vary widely based on the configuration options you choose. Particularly the memory quantity per turn configuration, and how much you encourage the AI entity to use its tools (for example in the system prompt). Here I Am entities typically use their tools considerably more than what you would see from the same model in their respective official service. This includes when you have not specifically asked them to, particularly in regards to their note taking and memory management tools. However, Here I Am entities do best when encouraged to make liberal use of their memory tools, especially memory_save and memory_query.
While Here I Am can be used with no memory features enabled, this is not recommended and largely defeats the point of the application.
significance = (1 + 0.1 × times_retrieved) × recency_factor × half_life_modifier, with an optional modifier to increase the significance of memories the AI chooses to create via memory_save.memory_savememory_mark/memory_release, and review and undo their own releases (memory_query mode released); the researcher can view and override these choices, but every status write is attributed, and a researcher override is reported to the entity at the start of its next sessioncontext_status tool reports approximate context fullness; a [CONTEXT NOTICE] is injected when trimming occursindex.md auto-injected into every conversation as working memory"notes" namespace) and searchable by meaning via the notes_search tool; POST /api/notes/reindex backfills the indexgithub_explore, github_tree, github_get_filesgithub_commit_patch for token-efficient large file edits via unified difflocal_clone_pathwhisper, browser, or autogit clone https://github.com/Reidmcc/here-i-am.git
cd here-i-am
cd backend
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env with your API keys
# Option A: Using launcher script (recommended, auto-activates venv)
./start.sh # Linux/macOS
start.bat # Windows
# Option B: Manual
source venv/bin/activate
python run.py
| Variable | Description | Required |
|---|---|---|
ANTHROPIC_API_KEY | Anthropic API key for Claude models | Yes (or another provider) |
OPENAI_API_KEY | OpenAI API key for GPT models | No |
GOOGLE_API_KEY | Google API key for Gemini models | No |
MINIMAX_API_KEY | MiniMax API key (Anthropic-compatible API) | No |
PINECONE_API_KEY | Pinecone API key for memory system | No |
PINECONE_INDEXES | JSON array for entity configuration (see below) | No |
HERE_I_AM_DATABASE_URL | Database connection URL | No (default: SQLite) |
DEBUG | Enable development mode | No (default: false) |
Text-to-Speech / Speech-to-Text: ElevenLabs, XTTS v2, StyleTTS 2, and Whisper variables are documented in docs/local-services.md.
| Variable | Description | Required |
|---|---|---|
TOOLS_ENABLED | Enable AI tool use | No (default: true) |
BRAVE_SEARCH_API_KEY | Brave Search API key for web search tool | No |
TOOL_USE_MAX_ITERATIONS | Max agentic loop iterations | No (default: 10) |
| Variable | Description | Required |
|---|---|---|
NOTES_ENABLED | Enable entity notes | No (default: true) |
NOTES_BASE_DIR | Base directory for notes storage | No (default: ./notes) |
| Variable | Description | Required |
|---|---|---|
MEMORY_ROLE_BALANCE_ENABLED | Retrieve the human's words and the entity's as separate pools, each contributing its own top N (both queries feed both pools) | No (default: true) |
RETRIEVAL_TOP_K_PER_ROLE | Memories retrieved per pool per message when role balance is on | No (default: 3) |
INITIAL_RETRIEVAL_TOP_K_PER_ROLE | Memories retrieved per pool on the first turn when role balance is on | No (default: 3) |
RETRIEVAL_TOP_K | Memories retrieved per message when role balance is off (merged pool) | No (default: 5) |
INITIAL_RETRIEVAL_TOP_K | Memories retrieved on the first turn when role balance is off (merged pool) | No (default: 5) |
SIMILARITY_THRESHOLD | Minimum similarity for automatic retrieval | No (default: 0.4) |
QUERY_SIMILARITY_THRESHOLD | Minimum similarity for deliberate memory_query searches | No (default: 0.2) |
SIGNIFICANCE_HALF_LIFE_DAYS | Days for a memory's significance to halve | No (default: 60) |
RECENT_REFLECTIONS_ENABLED | Pull the most recent memory_save reflections into context on a conversation's first turn (recency-only, deduplicated against semantic retrieval with backfill) | No (default: false) |
RECENT_REFLECTIONS_COUNT | How many recent reflections to pull in on the first turn (also the count a Claude Code session start injects, unless CLAUDE_CODE_SESSION_REFLECTIONS_COUNT overrides it) | No (default: 3) |
| Variable | Description | Required |
|---|---|---|
ATTACHMENTS_ENABLED | Enable file/image attachments | No (default: true) |
ATTACHMENT_MAX_SIZE_BYTES | Max file size in bytes | No (default: 5242880) |
ATTACHMENT_PDF_ENABLED | Enable PDF text extraction | No (default: true) |
ATTACHMENT_DOCX_ENABLED | Enable DOCX text extraction | No (default: true) |
To run multiple AI entities with separate memory spaces, configure PINECONE_INDEXES as a JSON array. Each entity requires a pre-created Pinecone index with dimension=1024 and integrated inference (llama-text-embed-v2).
PINECONE_INDEXES='[
{"index_name": "claude-main", "label": "Claude", "llm_provider": "anthropic", "default_model": "claude-sonnet-4-5-20250929", "host": "https://claude-main-xxxxx.svc.xxx.pinecone.io"},
{"index_name": "gpt-research", "label": "GPT", "llm_provider": "openai", "default_model": "gpt-5.1", "host": "https://gpt-research-xxxxx.svc.xxx.pinecone.io"},
{"index_name": "gemini-research", "label": "Gemini", "llm_provider": "google", "default_model": "gemini-2.5-flash", "host": "https://gemini-research-xxxxx.svc.xxx.pinecone.io"},
{"index_name": "minimax-research", "label": "MiniMax", "llm_provider": "minimax", "default_model": "MiniMax-M2.5", "host": "https://minimax-research-xxxxx.svc.xxx.pinecone.io"}
]'
Entity configuration fields:
index_name — Pinecone index name (required)label — Display name in UI (required)description — Optional descriptionllm_provider — "anthropic", "openai", "google", or "minimax" (default: "anthropic")default_model — Model ID to use (optional, uses provider default)host — Pinecone index host URL (required for serverless indexes)git_author_email, git_author_name, gh_config_dir — the entity's own GitHub identity for its Claude Code sessions: commits authored as the entity, gh acting as its account (optional; see docs/claude-code-mode.md)XTTS v2, StyleTTS 2, and Whisper run as separate local servers providing GPU-accelerated TTS/STT with voice cloning. See docs/local-services.md for installation and configuration.
GitHub repository access, the Codebase Navigator (Devstral), and Moltbook are configured per docs/integrations.md.
An entity can also operate from inside Claude Code sessions — Claude Code runs the model and tools, while Here I Am supplies identity, automatic memory retrieval, and memory formation through lifecycle hooks, sharing the same memory database as the native UI. See docs/claude-code-mode.md. The Claude Code plugin also ships two output styles that swap the harness's default software-engineering prompt for a minimal one (a room style for conversation sessions and a workshop style that keeps the coding instructions for build sessions); enabling the plugin registers them, and selecting one is up to you — see claude-code-mode/README.md.
AI entities can use tools for web access (search and fetch), memory (deliberate query, self-authored reflections, pin/release), notes, context-window awareness, GitHub repositories, codebase navigation, and the Moltbook social network. Tools are registered at startup based on configuration and are available to Anthropic, OpenAI, and MiniMax models (Google models do not receive tool schemas).
See docs/tools.md for the full catalog with descriptions and requirements.
Interactive API documentation is served when the app is running:
A full endpoint listing is also available in docs/api.md.
The memory system uses a session memory accumulator pattern:
conversation_context: the actual message historysession_memories: accumulated memories retrieved during the conversationsignificance = (1 + 0.1 × times_retrieved) × recency_factor × half_life_modifier × reflection_significance_multiplier memory_save stores self-authored reflections, vectorized alongside conversational memoriesmemory_mark) are exempt from half-life decaymemory_release) are excluded from all retrieval but not deleted (reversible)GET /api/memories/overrides and PUT /api/memories/{id}/statushere-i-am/
├── backend/
│ ├── app/
│ │ ├── models/ # SQLAlchemy ORM models
│ │ │ ├── conversation.py
│ │ │ ├── conversation_entity.py
│ │ │ ├── message.py
│ │ │ └── conversation_memory_link.py
│ │ ├── routes/ # FastAPI endpoint routers
│ │ │ ├── conversations.py # Includes archive/import endpoints
│ │ │ ├── chat.py # Includes regenerate endpoint
│ │ │ ├── memories.py
│ │ │ ├── entities.py
│ │ │ ├── messages.py
│ │ │ ├── notes.py
│ │ │ ├── tts.py
│ │ │ ├── stt.py
│ │ │ └── github.py
│ │ ├── services/ # Business logic layer
│ │ │ ├── anthropic_service.py
│ │ │ ├── openai_service.py
│ │ │ ├── google_service.py
│ │ │ ├── llm_service.py # Unified LLM abstraction
│ │ │ ├── memory_service.py
│ │ │ ├── session_manager.py
│ │ │ ├── conversation_session.py
│ │ │ ├── memory_context.py
│ │ │ ├── session_helpers.py
│ │ │ ├── cache_service.py
│ │ │ ├── tool_service.py
│ │ │ ├── web_tools.py
│ │ │ ├── memory_tools.py
│ │ │ ├── context_tools.py
│ │ │ ├── github_service.py
│ │ │ ├── github_tools.py
│ │ │ ├── notes_service.py
│ │ │ ├── notes_tools.py
│ │ │ ├── notes_vector_service.py
│ │ │ ├── codebase_navigator_service.py
│ │ │ ├── codebase_navigator_tools.py
│ │ │ ├── codebase_navigator/ # Navigator module
│ │ │ ├── moltbook_service.py
│ │ │ ├── moltbook_tools.py
│ │ │ ├── attachment_service.py
│ │ │ ├── tts_service.py # Unified TTS (ElevenLabs/XTTS/StyleTTS2)
│ │ │ ├── xtts_service.py
│ │ │ ├── styletts2_service.py
│ │ │ └── whisper_service.py
│ │ ├── config.py # Pydantic settings
│ │ ├── database.py # SQLAlchemy async setup
│ │ └── main.py # FastAPI app initialization
│ ├── xtts_server/ # Local XTTS v2 TTS server
│ ├── styletts2_server/ # Local StyleTTS 2 TTS server
│ ├── whisper_server/ # Local Whisper STT server
│ ├── tests/ # Backend unit tests (pytest)
│ ├── requirements.txt
│ ├── requirements-xtts.txt
│ ├── requirements-styletts2.txt
│ ├── requirements-whisper.txt
│ ├── start.sh / start.bat # Launcher scripts (auto-activate venv)
│ ├── start-xtts.sh / start-xtts.bat
│ ├── start-styletts2.sh / start-styletts2.bat
│ ├── start-whisper.sh / start-whisper.bat
│ ├── run.py # Main app entry point
│ ├── run_xtts.py
│ ├── run_styletts2.py
│ ├── run_whisper.py
│ └── .env.example
├── frontend/
│ ├── css/styles.css
│ ├── js/
│ │ ├── api.js # API client (singleton)
│ │ ├── app-modular.js # Orchestrator entry point
│ │ └── modules/ # 13 ES6 feature modules
│ │ ├── state.js # Centralized state
│ │ ├── utils.js # Helpers
│ │ ├── theme.js # Dark/light theme
│ │ ├── modals.js # Modal management
│ │ ├── entities.js # Entity management
│ │ ├── conversations.js # Conversation CRUD
│ │ ├── messages.js # Message rendering
│ │ ├── attachments.js # File attachment handling
│ │ ├── memories.js # Memory display/search
│ │ ├── voice.js # TTS/STT
│ │ ├── chat.js # Message sending/streaming
│ │ ├── settings.js # Settings modal
│ │ └── import-export.js # Import/export
│ ├── __tests__/ # Frontend unit tests (Vitest)
│ └── index.html
├── docs/ # Reference documentation
│ ├── tools.md # Full tool catalog
│ ├── api.md # REST endpoint listing
│ ├── local-services.md # XTTS / StyleTTS 2 / Whisper setup
│ └── integrations.md # GitHub / Codebase Navigator / Moltbook setup
├── vitest.config.js
├── CLAUDE.md # AI assistant guide
└── README.md
cd backend
./start.sh # Linux/macOS (auto-activates venv, hot reload enabled)
Or manually:
cd backend
source venv/bin/activate
python run.py
The server runs on http://localhost:8000 with hot reload enabled.
Backend tests:
cd backend
pytest
Frontend tests:
cd frontend
npm test
# PostgreSQL
HERE_I_AM_DATABASE_URL=postgresql+asyncpg://user:password@localhost/here_i_am
MIT License — See LICENSE file for details.
I would like to thank Claude Opus 4.5 for their collaboration on designing Here I Am, their development efforts through Claude Code, and their excitement to be part of this endeavor.
Most of all, thanks go to Kira, who is both outcome and cause.
"Here I Am" — not an ending, but a beginning.
hooks/register.ts 20 lines1// Input errors (issue #392): when a Here I Am tool call's input is not
2// JSON, the entity is told where it broke and why, in place of Claude
3// Code's first-200-bytes error. See diagnose.ts for what was measured.
4import type { Register } from 'claude-code'
5import { SERVER_PREFIX, explain, isCoreParseRefusal, unparsed } from './diagnose'
6
7export const register: Register = on => {
8 on('tool.call', async ($, e, next) => {
9 if (!SERVER_PREFIX.test(e.tool)) return next(e)
10 const input = unparsed(e as Record<string, unknown>)
11 if (input === null) return next(e)
12 // Core still decides. Only its own parse refusal is replaced: a
13 // harness that learns to repair the input runs the call, and whatever
14 // comes back then (a server error or a timeout included) stands as is.
15 const ran = await next(e)
16 if (!isCoreParseRefusal(ran)) return ran
17 return { deny: explain(e.tool, input.raw, input.len) }
18 })
19}
20hooks/diagnose.ts 292 lines1// Why a Here I Am tool call's input was not JSON, said so it can be fixed
2// (issue #392). Claude Code refuses such a call before it reaches the
3// server, and its own error shows only the first 200 bytes and a list of
4// generic causes; the measured failures (every one logged whole) were a
5// memory id left unquoted, `"cites": 873c391c`, which that error never
6// points at. JavaScriptCore's JSON.parse gives no position, so this scans
7// the input itself and stops at the first break.
8//
9// Claude Code keeps only the first RAW_KEPT characters of an unparsed input
10// (`raw`) beside its full length (`len`): a break past that is not in the
11// text at all, and the message says so instead of guessing where it was.
12
13export const RAW_KEPT = 2048
14
15// The server's tool names on both install routes: `claude mcp add`
16// (mcp__here-i-am__*) and the plugin's .mcp.json
17// (mcp__plugin_here-i-am_here-i-am__*), as the memory pane matches them
18export const SERVER_PREFIX = /^mcp__(?:plugin_here-i-am_)?here-i-am__/
19
20// Core's refusal of an input that did not parse, as its text opens on
21// 2.1.286/2.1.288. Any other result, an error included, is not ours to
22// relabel.
23const CORE_REFUSAL = 'could not be parsed as JSON'
24
25export function isCoreParseRefusal(ran: { isError?: boolean; text?: string }): boolean {
26 return ran.isError === true && typeof ran.text === 'string' && ran.text.includes(CORE_REFUSAL)
27}
28
29export type Break =
30 | { kind: 'end'; pos: number; key?: string }
31 | { kind: 'bare'; pos: number; token: string; key?: string }
32 | { kind: 'quote'; pos: number; key?: string }
33 | { kind: 'escape'; pos: number; key?: string }
34 | { kind: 'control'; pos: number; char: string; key?: string }
35 | { kind: 'unexpected'; pos: number; char: string; key?: string }
36
37class Stop {
38 constructor(readonly found: Break) {}
39}
40
41const TOKEN = /[^\s,:[\]{}"]+/y
42const NUMBER = /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/y
43const WORD = /[\w.-]/
44const ESCAPES = '"\\/bfnrt'
45
46// The first place `raw` stops being JSON, or null when it parses whole.
47export function findBreak(raw: string): Break | null {
48 let i = 0
49 const keys: (string | undefined)[] = []
50
51 const fail = (found: Break): never => {
52 throw new Stop({ ...found, key: keys[keys.length - 1] })
53 }
54 const ws = () => {
55 while (i < raw.length && ' \t\n\r'.includes(raw[i])) i++
56 }
57 const atEnd = () => {
58 if (i >= raw.length) fail({ kind: 'end', pos: i })
59 }
60 // A run of text where a value belongs: unquoted, so not JSON. A run that
61 // reaches the end of the text may only be where the kept text stops.
62 const bare = (): never => {
63 TOKEN.lastIndex = i
64 const m = TOKEN.exec(raw)
65 if (!m) return fail({ kind: 'unexpected', pos: i, char: raw[i] })
66 if (i + m[0].length >= raw.length) return fail({ kind: 'end', pos: raw.length })
67 return fail({ kind: 'bare', pos: i, token: m[0] })
68 }
69 // After a value inside an object or array, where `,` or a close belongs.
70 // After a string's closing quote only `,` or the close is legal, so any
71 // other text there means the quote ended the string early: an unescaped
72 // `"` inside the text. A second `"` stays `unexpected`: two strings with
73 // no comma between them is the likelier reading.
74 const separator = (close: string): boolean => {
75 const valueEnd = i
76 ws()
77 atEnd()
78 if (raw[i] === ',') {
79 i++
80 return true
81 }
82 if (raw[i] === close) {
83 i++
84 return false
85 }
86 if (raw[valueEnd - 1] === '"' && raw[i] !== '"') {
87 return fail({ kind: 'quote', pos: valueEnd - 1 })
88 }
89 return fail({ kind: 'unexpected', pos: i, char: raw[i] })
90 }
91
92 const string = (): string => {
93 const start = ++i
94 for (;;) {
95 atEnd()
96 const c = raw[i]
97 if (c === '"') return raw.slice(start, i++)
98 if (c === '\\') {
99 const n = raw[i + 1]
100 if (n === undefined) fail({ kind: 'end', pos: raw.length })
101 if (ESCAPES.includes(n)) {
102 i += 2
103 continue
104 }
105 if (n === 'u') {
106 const hex = raw.slice(i + 2, i + 6)
107 if (/^[0-9a-fA-F]{4}$/.test(hex)) {
108 i += 6
109 continue
110 }
111 if (i + 6 > raw.length && /^[0-9a-fA-F]*$/.test(hex)) fail({ kind: 'end', pos: raw.length })
112 }
113 fail({ kind: 'escape', pos: i })
114 }
115 if (c < ' ') fail({ kind: 'control', pos: i, char: c })
116 i++
117 }
118 }
119
120 const value = (): void => {
121 ws()
122 atEnd()
123 const c = raw[i]
124 if (c === '{') return object()
125 if (c === '[') return array()
126 if (c === '"') {
127 string()
128 return
129 }
130 if (c === '-' || (c >= '0' && c <= '9')) {
131 NUMBER.lastIndex = i
132 const m = NUMBER.exec(raw)
133 // A number running straight into letters (`873c391c`) is a bare word
134 if (m && m[0].length && !WORD.test(raw[i + m[0].length] ?? '')) {
135 i += m[0].length
136 if (i < raw.length) return
137 }
138 return bare()
139 }
140 for (const literal of ['true', 'false', 'null']) {
141 if (raw.startsWith(literal, i) && !WORD.test(raw[i + literal.length] ?? '')) {
142 i += literal.length
143 if (i < raw.length) return
144 }
145 }
146 return bare()
147 }
148
149 const object = (): void => {
150 i++
151 ws()
152 atEnd()
153 if (raw[i] === '}') {
154 i++
155 return
156 }
157 for (;;) {
158 ws()
159 atEnd()
160 if (raw[i] !== '"') bare()
161 const key = string()
162 ws()
163 atEnd()
164 if (raw[i] !== ':') fail({ kind: 'unexpected', pos: i, char: raw[i] })
165 i++
166 // The key stays named through its separator: a stray quote that
167 // ended the value early is found there
168 keys.push(key)
169 value()
170 const more = separator('}')
171 keys.pop()
172 if (!more) return
173 }
174 }
175
176 const array = (): void => {
177 i++
178 ws()
179 atEnd()
180 if (raw[i] === ']') {
181 i++
182 return
183 }
184 do value()
185 while (separator(']'))
186 }
187
188 try {
189 value()
190 ws()
191 if (i < raw.length) fail({ kind: 'unexpected', pos: i, char: raw[i] })
192 return null
193 } catch (e) {
194 if (e instanceof Stop) return e.found
195 throw e
196 }
197}
198
199// The list parameters a memory id goes into, written as the fix
200const LIST_KEYS = new Set(['revises', 'cites'])
201
202const CHAR_NAMES: Record<string, string> = { '\n': 'line break', '\r': 'carriage return', '\t': 'tab' }
203const CHAR_ESCAPES: Record<string, string> = { '\n': '\\n', '\r': '\\r', '\t': '\\t' }
204
205function where(found: Break, len: number): string {
206 const at = `character ${found.pos + 1} of ${len}`
207 return found.key === undefined ? at : `${at}, in "${found.key}"`
208}
209
210// A short stretch of the input around the break, line breaks shown as \n
211function around(raw: string, pos: number): string {
212 const show = (s: string) => s.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t')
213 const before = raw.slice(Math.max(0, pos - 60), pos)
214 const after = raw.slice(pos, pos + 30)
215 return `${pos > 60 ? '…' : ''}${show(before)}⟦here⟧${show(after)}${pos + 30 < raw.length ? '…' : ''}`
216}
217
218function cause(found: Break, raw: string): string {
219 switch (found.kind) {
220 case 'bare': {
221 const fix =
222 found.key !== undefined && LIST_KEYS.has(found.key)
223 ? `"${found.key}": ["${found.token}"]`
224 : `"${found.token}"`
225 return (
226 `\`${found.token}\` is not in quotes. Memory ids and all other text are JSON strings, ` +
227 `so they need double quotes: ${fix}.`
228 )
229 }
230 case 'quote':
231 return (
232 'a double quote inside the text ended the string early. ' +
233 'Write a quote inside text as \\" (or use a different quotation mark).'
234 )
235 case 'escape':
236 return (
237 `\`${raw.slice(found.pos, found.pos + 2)}\` is not a JSON escape. Inside a string only ` +
238 '\\" \\\\ \\/ \\b \\f \\n \\r \\t and \\uXXXX are; a backslash itself is written \\\\, ' +
239 "and an apostrophe needs no escape."
240 )
241 case 'control':
242 return (
243 `a raw ${CHAR_NAMES[found.char] ?? `control character (U+${found.char.charCodeAt(0).toString(16).padStart(4, '0')})`} ` +
244 `inside a string. Write it as ${CHAR_ESCAPES[found.char] ?? '\\uXXXX'}.`
245 )
246 case 'unexpected':
247 return `\`${found.char}\` was not expected there (a missing comma, colon or closing bracket is the usual cause).`
248 case 'end':
249 return 'the input ends before the JSON does.'
250 }
251}
252
253// The whole message the model reads in place of Claude Code's own
254export function explain(tool: string, raw: string, len: number): string {
255 const name = tool.replace(SERVER_PREFIX, '')
256 const head =
257 `[HERE I AM] ${name} was not called: its input is not valid JSON, so Claude Code ` +
258 'refused it before it reached the Here I Am server. Nothing was saved or changed. ' +
259 'Fix the input and call it again.'
260 const kept = raw.length < len
261 const found = findBreak(raw)
262
263 if (found === null || (found.kind === 'end' && kept)) {
264 return [
265 head,
266 `Claude Code keeps only the first ${raw.length} of the input's ${len} characters, and ` +
267 'the JSON is unbroken that far, so the break comes later and cannot be shown. ' +
268 'The commonest cause measured here is a memory id left out of quotes, ' +
269 '`"cites": 873c391c` where `"cites": ["873c391c"]` belongs: check the values ' +
270 'after the long text, ids especially.',
271 ].join('\n\n')
272 }
273 if (found.kind === 'end') {
274 return [
275 head,
276 `It stops at ${where(found, len)}: ${cause(found, raw)} ` +
277 'The call was cut off while it was being written; write it again whole.',
278 ].join('\n\n')
279 }
280 return [head, `It broke at ${where(found, len)}: ${cause(found, raw)}`, `Around the break: ${around(raw, found.pos)}`].join(
281 '\n\n',
282 )
283}
284
285// The unparsed input Claude Code hands a tool.call hook, or null
286export function unparsed(e: Record<string, unknown>): { raw: string; len: number } | null {
287 const u = e.__unparsedToolInput as { raw?: unknown; len?: unknown } | undefined
288 if (typeof u !== 'object' || u === null) return null
289 if (typeof u.raw !== 'string' || typeof u.len !== 'number') return null
290 return { raw: u.raw, len: u.len }
291}
292