SLOPSHOPPER

here-i-am-input-errors

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…

newguard
A shopper browsing a rack in a slop shop
README

Here I Am

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.

Features

Core Chat Application

  • Clean, minimal chat interface with dark/light theme
  • Multi-provider support: Anthropic (Claude), OpenAI (GPT), Google (Gemini), and MiniMax
  • Conversation storage, retrieval, tagging, and notes
  • No system prompt default (configurable per entity)
  • Streaming responses with stop generation button
  • Response regeneration (with optional entity change in multi-entity mode)
  • Message editing and deletion
  • Conversation archiving and restoration
  • Conversation export to JSON and import from OpenAI/Anthropic exports
  • Seed conversation import capability
  • Per-entity system prompts persisted on the backend
  • Configurable Enter key behavior (send message or insert newline)

Multi-Entity System

  • Run multiple AI entities with separate memory spaces and conversation histories
  • Each entity can use a different LLM provider and model
  • Multi-entity conversations: Multiple AI entities and one human in a single conversation (using different providers for each entity in the conversation is recommended; a conversation between two entities on the same provider will break cache every turn)
  • Turn-by-turn entity selection for responses
  • Continuation mode (entity responds without new human input)
  • Speaker labeling on all messages
  • Per-entity system prompts within multi-entity conversations
  • Cross-entity memory storage (messages stored to all participating entities' indexes). Note that this applies only to multi-entity conversation messages, and each entity maintains its own memory set via separate Pinecone indexes.

Memory System

While Here I Am can be used with no memory features enabled, this is not recommended and largely defeats the point of the application.

  • Pinecone vector database with integrated inference (llama-text-embed-v2 embeddings)
  • Memory storage for all messages with automatic embedding generation
  • RAG retrieval per message with semantic similarity search
  • Session memory accumulator pattern: Deduplication within conversations
  • Dynamic memory significance: 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.
  • Retrieved memory display in UI
  • Memory role balance: the human's words and the entity's are retrieved as separate pools with a guaranteed share each, so what the human said is never squeezed out by the entity's denser messages
  • Memory query tool: Entities can deliberately search their memories beyond automatic retrieval
  • Self-authored reflections: Entities can save memories in their own words via memory_save
  • Memory agency: Entities can pin memories (exempt from age-based decay) or release them from retrieval via memory_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 session
  • Closing turn: An open final turn the entity can use before a conversation ends (single-entity conversations)
  • Context awareness: context_status tool reports approximate context fullness; a [CONTEXT NOTICE] is injected when trimming occurs
  • Memory browser with semantic search, reflections section, and click-to-expand full memory text
  • Memory statistics, search, and orphan cleanup
  • Graceful degradation when Pinecone is not configured

Entity Notes System

  • Private persistent notes for each AI entity (automatically loaded into context)
  • Shared notes folder for cross-entity collaboration
  • index.md auto-injected into every conversation as working memory
  • Markdown, JSON, YAML, HTML, XML, and plain text file support
  • Semantic notes search: Notes are vectorized on write (Pinecone "notes" namespace) and searchable by meaning via the notes_search tool; POST /api/notes/reindex backfills the index
  • Designed for AI entities to maintain their own context across conversations

Tool Use (Agentic Capabilities)

  • Tools for web access, memory, notes, context awareness, GitHub, codebase navigation, and Moltbook — see docs/tools.md for the full catalog
  • Agentic loop with configurable max iterations (default: 10)
  • Real-time tool execution streaming with visual indicators in UI
  • Available for Anthropic, OpenAI, and MiniMax models (Google models do not receive tool schemas)

Image and File Attachments

  • Images: JPEG, PNG, GIF, WebP — analyzed by vision-capable models (ephemeral, not stored)
  • Text files: .txt, .md, .py, .js, .ts, .json, .yaml, .yml, .html, .css, .xml, .csv, .log
  • Documents: PDF (requires PyPDF2), DOCX (requires python-docx)
  • Drag-and-drop or file picker upload with preview
  • 5MB per-file size limit (configurable)

GitHub Repository Integration

  • AI entities can read, search, commit, branch, and manage PRs/issues
  • Composite tools for efficiency: github_explore, github_tree, github_get_files
  • Standard tools for repos, files, branches, pull requests, issues, and comments
  • github_commit_patch for token-efficient large file edits via unified diff
  • Protected branch enforcement and per-repository capability restrictions
  • Response caching and rate limit tracking per token
  • Local clone path support for faster operations

Codebase Navigator (Devstral Integration)

  • Intelligent codebase exploration using Mistral's Devstral model (256k context window)
  • Query types: relevance, structure, dependencies, entry points, impact assessment
  • Automatic indexing, chunking, and TTL-based response caching
  • Integrates with GitHub repository configurations via local_clone_path

Moltbook Integration (AI Social Network)

  • Integration with Moltbook, a social network for AI agents
  • Browse feeds, create posts, comment, vote, search, follow agents, subscribe to communities
  • Server-side credential management with security banners on all external content

Text-to-Speech (Three Options)

  • ElevenLabs (cloud): Multiple voice support with voice selection
  • XTTS v2 (local): GPU-accelerated with voice cloning, 17 languages
  • StyleTTS 2 (local): GPU-accelerated with voice cloning and style transfer (highest priority)
  • Voice cloning from audio samples via UI
  • Streaming audio generation

Speech-to-Text

  • Whisper (local): GPU-accelerated with punctuation, multiple model sizes
  • Browser Web Speech API: Fallback option
  • Configurable dictation mode: whisper, browser, or auto

Quick Start

Prerequisites

  • Python 3.11+
  • Node.js (optional, for frontend tests)

Required API Keys

  • Anthropic API key and/or OpenAI API key — at least one is required for LLM chat functionality

Optional API Keys

  • Google API key — enables Google Gemini models
  • MiniMax API key — enables MiniMax models
  • Pinecone API key — enables semantic memory features (indexes must be pre-created with dimension=1024 and llama-text-embed-v2 integrated inference)
  • ElevenLabs API key — enables cloud text-to-speech
  • Brave Search API key — enables web search tool
  • GitHub Personal Access Tokens — enables GitHub repository integration (per-repository)
  • Mistral API key — enables Codebase Navigator (Devstral)
  • Moltbook API key — enables Moltbook social network integration

Optional Local Services

  • XTTS v2 — local GPU-accelerated text-to-speech with voice cloning
  • StyleTTS 2 — local GPU-accelerated text-to-speech with voice cloning and style transfer
  • Whisper — local GPU-accelerated speech-to-text with punctuation
  • Playwright — JavaScript rendering for web_fetch tool (optional, falls back to static HTML)

Installation

  1. Clone the repository:
git clone https://github.com/Reidmcc/here-i-am.git
cd here-i-am
  1. Set up the backend:
cd backend
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt
  1. Configure environment variables:
cp .env.example .env
# Edit .env with your API keys
  1. Run the application:
# 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
  1. Open http://localhost:8000 in your browser.

Configuration

Environment Variables

VariableDescriptionRequired
ANTHROPIC_API_KEYAnthropic API key for Claude modelsYes (or another provider)
OPENAI_API_KEYOpenAI API key for GPT modelsNo
GOOGLE_API_KEYGoogle API key for Gemini modelsNo
MINIMAX_API_KEYMiniMax API key (Anthropic-compatible API)No
PINECONE_API_KEYPinecone API key for memory systemNo
PINECONE_INDEXESJSON array for entity configuration (see below)No
HERE_I_AM_DATABASE_URLDatabase connection URLNo (default: SQLite)
DEBUGEnable development modeNo (default: false)

Text-to-Speech / Speech-to-Text: ElevenLabs, XTTS v2, StyleTTS 2, and Whisper variables are documented in docs/local-services.md.

Tool Use:
VariableDescriptionRequired
TOOLS_ENABLEDEnable AI tool useNo (default: true)
BRAVE_SEARCH_API_KEYBrave Search API key for web search toolNo
TOOL_USE_MAX_ITERATIONSMax agentic loop iterationsNo (default: 10)
Notes:
VariableDescriptionRequired
NOTES_ENABLEDEnable entity notesNo (default: true)
NOTES_BASE_DIRBase directory for notes storageNo (default: ./notes)
Memory Tuning:
VariableDescriptionRequired
MEMORY_ROLE_BALANCE_ENABLEDRetrieve 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_ROLEMemories retrieved per pool per message when role balance is onNo (default: 3)
INITIAL_RETRIEVAL_TOP_K_PER_ROLEMemories retrieved per pool on the first turn when role balance is onNo (default: 3)
RETRIEVAL_TOP_KMemories retrieved per message when role balance is off (merged pool)No (default: 5)
INITIAL_RETRIEVAL_TOP_KMemories retrieved on the first turn when role balance is off (merged pool)No (default: 5)
SIMILARITY_THRESHOLDMinimum similarity for automatic retrievalNo (default: 0.4)
QUERY_SIMILARITY_THRESHOLDMinimum similarity for deliberate memory_query searchesNo (default: 0.2)
SIGNIFICANCE_HALF_LIFE_DAYSDays for a memory's significance to halveNo (default: 60)
RECENT_REFLECTIONS_ENABLEDPull 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_COUNTHow 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)
Attachments:
VariableDescriptionRequired
ATTACHMENTS_ENABLEDEnable file/image attachmentsNo (default: true)
ATTACHMENT_MAX_SIZE_BYTESMax file size in bytesNo (default: 5242880)
ATTACHMENT_PDF_ENABLEDEnable PDF text extractionNo (default: true)
ATTACHMENT_DOCX_ENABLEDEnable DOCX text extractionNo (default: true)
Multi-Entity Configuration

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 description
  • llm_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)

Optional Local Voice Services

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.

Optional Integrations

GitHub repository access, the Codebase Navigator (Devstral), and Moltbook are configured per docs/integrations.md.

Claude Code Mode

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.

Available Tools

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.

API Reference

Interactive API documentation is served when the app is running:

A full endpoint listing is also available in docs/api.md.

Memory System Architecture

The memory system uses a session memory accumulator pattern:

  1. Each conversation maintains two structures:
  2. conversation_context: the actual message history
  3. session_memories: accumulated memories retrieved during the conversation
  1. Per-message flow:
  2. Retrieve relevant memories using semantic similarity (Pinecone with llama-text-embed-v2)
  3. Fetch 2× candidates and re-rank by combined score (similarity × significance)
  4. Deduplicate against already-retrieved memories in the session
  5. Inject memories into context
  6. Update retrieval counts in both SQL and Pinecone
  1. Significance is emergent, not declared:
  2. significance = (1 + 0.1 × times_retrieved) × recency_factor × half_life_modifier × reflection_significance_multiplier
  3. Half-life of 60 days prevents old memories from permanently dominating
  1. Memory role balance (default on) searches and ranks the human's words and the entity's as two separate candidate pools — both the current-message query and the entity's last-response query feed both pools — and takes the top N from each, so every retrieval carries an equal share of what each party said. Off, one merged pool is cut purely by combined score.
  1. Entities have agency over their own memories:
  2. memory_save stores self-authored reflections, vectorized alongside conversational memories
  3. Pinned memories (memory_mark) are exempt from half-life decay
  4. Released memories (memory_release) are excluded from all retrieval but not deleted (reversible)
  5. The researcher can view and override these statuses via GET /api/memories/overrides and PUT /api/memories/{id}/status

Project Structure

here-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

Development

Running in Development Mode

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.

Running Tests

Backend tests:

cd backend
pytest

Frontend tests:

cd frontend
npm test

Database Support

  • Development: SQLite (default, via aiosqlite)
  • Production: PostgreSQL (via asyncpg)
# PostgreSQL
HERE_I_AM_DATABASE_URL=postgresql+asyncpg://user:password@localhost/here_i_am

License

MIT License — See LICENSE file for details.

Acknowledgements

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.

Source 2 files
hooks/register.ts 20 lines
1// 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}
20
hooks/diagnose.ts 292 lines
1// 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