tinker-agent 1.3.0 → 1.5.0

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 (49) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +271 -72
  3. package/bin/tinker.js +75 -25
  4. package/package.json +12 -3
  5. package/src/agent/runtime-session.ts +113 -15
  6. package/src/cli/command-line.ts +291 -0
  7. package/src/cli/config.ts +158 -262
  8. package/src/cli/index.ts +33 -21
  9. package/src/cli/main.ts +213 -0
  10. package/src/cli/model-profiles.ts +226 -72
  11. package/src/cli/output.ts +113 -0
  12. package/src/cli/package-metadata.ts +36 -0
  13. package/src/cli/prompt-source.ts +229 -0
  14. package/src/cli/public-cli-contract.ts +69 -0
  15. package/src/cli/public-config-contract.ts +732 -0
  16. package/src/cli/run-runner.ts +17 -12
  17. package/src/cli/runner-dependencies.ts +108 -0
  18. package/src/cli/tui-memory.ts +67 -0
  19. package/src/cli/tui-runner.tsx +79 -49
  20. package/src/context/context-policy.ts +2 -2
  21. package/src/events/stdout-event-printer.ts +1 -0
  22. package/src/mcp/mcp-manager.ts +2 -19
  23. package/src/mcp/mcp-tool-executor.ts +3 -4
  24. package/src/memory/contracts.ts +148 -0
  25. package/src/memory/embedding-client.ts +105 -0
  26. package/src/memory/memory-coordinator.ts +556 -0
  27. package/src/memory/memory-extractor.ts +231 -0
  28. package/src/memory/memory-log.ts +88 -0
  29. package/src/memory/memory-search-tool.ts +100 -0
  30. package/src/memory/memory-store.ts +687 -0
  31. package/src/memory/vector.ts +153 -0
  32. package/src/model/fake-model-client.ts +971 -3
  33. package/src/model/model-context-profile.ts +0 -30
  34. package/src/observation/observation-builder.ts +20 -0
  35. package/src/session/session-store.ts +123 -0
  36. package/src/tools/bash.ts +8 -25
  37. package/src/tools/grep.ts +9 -1
  38. package/src/tools/registry.ts +19 -1
  39. package/src/tools/ripgrep.ts +24 -27
  40. package/src/tools/types.ts +16 -0
  41. package/src/tools/web-fetch/index.ts +2 -15
  42. package/src/tui/app.tsx +72 -2
  43. package/src/tui/clipboard.ts +22 -0
  44. package/src/tui/components/footer.tsx +9 -4
  45. package/src/tui/components/memory-browser.tsx +151 -0
  46. package/src/tui/components/prompt-input.tsx +6 -3
  47. package/src/tui/event-store.ts +9 -2
  48. package/src/tui/slash-commands.ts +88 -24
  49. package/src/tui/workspace-file-search.ts +78 -71
package/CHANGELOG.md CHANGED
@@ -5,6 +5,42 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.5.0] - 2026-07-27
9
+
10
+ ### Added
11
+
12
+ - Add optional global Memory for the interactive TUI. When configured, Tinker
13
+ extracts durable facts from completed turns, stores them locally, and exposes
14
+ relevant memories to the agent through `MemorySearch`.
15
+ - Add a read-only `/memory` browser for reviewing the memories stored in the
16
+ current global Memory database.
17
+ - Show live elapsed time in the TUI footer while a turn is running.
18
+
19
+ ### Changed
20
+
21
+ - Compact and retire context toward 30% input utilization instead of 60%, leaving
22
+ more headroom for continued work after automatic or manual maintenance.
23
+
24
+ ### Fixed
25
+
26
+ - Let the Tinker Chrome MCP server exit promptly when its client closes stdin,
27
+ avoiding the shutdown timeout during ordinary CLI exit.
28
+
29
+ ## [1.4.0] - 2026-07-24
30
+
31
+ ### Added
32
+
33
+ - Add stable `tinker --help`, `tinker --version`, and `tinker help run` command
34
+ surfaces with explicit usage errors and exit codes.
35
+ - Let one-shot runs read an exact Prompt from explicit stdin or a UTF-8 text file,
36
+ with bounded input and strict encoding validation.
37
+
38
+ ### Changed
39
+
40
+ - Require `tinker run` to receive exactly one shell-quoted Prompt argument,
41
+ `--stdin`, or `--file <path>`. The former unquoted variadic form is no longer
42
+ joined automatically; quote a short Prompt or use stdin/file input instead.
43
+
8
44
  ## [1.3.0] - 2026-07-22
9
45
 
10
46
  ### Added
@@ -48,7 +84,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
48
84
  - First formal npm release under the `tinker-agent` package name with the `tinker`
49
85
  executable.
50
86
 
51
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.3.0...HEAD
87
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.5.0...HEAD
88
+ [1.5.0]: https://github.com/ishowshao/tinker/releases/tag/v1.5.0
89
+ [1.4.0]: https://github.com/ishowshao/tinker/releases/tag/v1.4.0
52
90
  [1.3.0]: https://github.com/ishowshao/tinker/releases/tag/v1.3.0
53
91
  [1.2.1]: https://github.com/ishowshao/tinker/releases/tag/v1.2.1
54
92
  [1.2.0]: https://github.com/ishowshao/tinker/releases/tag/v1.2.0
package/README.md CHANGED
@@ -29,7 +29,9 @@ Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://gith
29
29
  context revisions, Recall-addressable cold state, and qualified prefix retirement
30
30
  keep long-running sessions recoverable without pretending the model has infinite
31
31
  tokens. See the [technical design](docs/infinite-context-technical-design-a.md).
32
- - **Choice of models**: Supports any OpenAI-compatible API (defaults to DeepSeek). Configurable via environment variables.
32
+ - **Choice of models**: Uses an OpenAI-compatible Chat Completions transport with
33
+ explicit model and context limits. Actual provider support must be established
34
+ by a qualification matrix; transport compatibility alone is not a guarantee.
33
35
 
34
36
  ## Quick Start
35
37
 
@@ -41,31 +43,65 @@ tinker
41
43
 
42
44
  # Run a one-shot prompt
43
45
  tinker run "explain the project structure"
46
+
47
+ # Inspect the installed command surface
48
+ tinker --help
49
+ tinker --version
44
50
  ```
45
51
 
46
52
  The npm package includes its own Bun runtime. To run Tinker from a source checkout,
47
- install Bun 1.3 or later, then use `bun install` followed by `bun run tinker`.
48
-
49
- ### Slash Commands
50
-
51
- - `/view <path>` Open a readable UTF-8 text file in a full-window viewer. Relative
52
- paths must remain inside the workspace; absolute paths may point outside it. Use
53
- the keyboard or mouse wheel to scroll and press `Esc` to close the viewer.
54
- - `/copy` Copy the current session's last completed assistant response to the
55
- system clipboard as raw Markdown.
56
- - `/status` — Show session and context details.
57
- - `/skills` — Show available skills, their scope and active state, plus user skills
58
- shadowed by a project skill.
59
- - `/compact` Deterministically compact eligible historical tool output while the
60
- session is idle.
61
- - `/compact retire` Retire a complete cold history prefix from the active request;
62
- the original history remains available through `Recall`.
63
- - `/model [profile-name]` — Choose a model profile for a new session.
64
- - `/resume [session-id]` — Choose or directly resume a stored session.
65
- - `/session delete <session-id> --confirm` — Delete a stored session.
66
- - `/quit` Exit the TUI.
67
-
68
- ### Agent Skills
53
+ install Bun 1.3.14 or later, then use `bun install` followed by `bun run tinker`.
54
+
55
+ `run` accepts exactly one Prompt source: one shell-quoted argument, explicit stdin,
56
+ or a UTF-8 text file. Use stdin for multiline code, private patches, or Prompt text
57
+ containing secrets so it does not become a command-line argument:
58
+
59
+ ```bash
60
+ printf '%s\n' 'Review `$HOME`, *.ts, and "quotes" literally.' | tinker run --stdin
61
+ tinker run --file prompts/review.md
62
+ ```
63
+
64
+ Tinker does not reconstruct shell expansions or join multiple positional arguments.
65
+ Use `tinker run "one quoted prompt"`; the former unquoted variadic form now fails
66
+ with a usage error. File paths are resolved from the current directory and may point
67
+ outside the configured workspace. You are responsible for Prompt-file permissions
68
+ and cleanup.
69
+
70
+ ## Commands
71
+
72
+ The installed package exposes this public CLI:
73
+
74
+ <!-- BEGIN GENERATED: PUBLIC CLI COMMANDS -->
75
+ | Command | Description |
76
+ | --- | --- |
77
+ | `tinker` | Start the interactive terminal interface. |
78
+ | `tinker --profile <profile-name>` | Start the TUI with a selected model profile. |
79
+ | `tinker run [--profile <profile-name>] <prompt>` | Submit one shell-quoted prompt argument. |
80
+ | `tinker run [--profile <profile-name>] --stdin` | Read the prompt from standard input until EOF. |
81
+ | `tinker run [--profile <profile-name>] --file <path>` | Read the prompt from a UTF-8 text file. |
82
+ | `tinker --help` | Show top-level CLI help. |
83
+ | `tinker help run` | Show one-shot command help. |
84
+ | `tinker --version` | Print the installed package version. |
85
+ <!-- END GENERATED: PUBLIC CLI COMMANDS -->
86
+
87
+ Repository development commands are separate from the installed CLI:
88
+
89
+ ```bash
90
+ bun install # Install dependencies
91
+ bun run tinker # Start the interactive TUI
92
+ bun test # Run test suite
93
+ bun run typecheck # TypeScript type checking (tsc --noEmit)
94
+ bun run lint # ESLint (zero warnings required)
95
+ bun run format # Biome code formatting
96
+ bun run check # Full repository quality gate
97
+ ```
98
+
99
+ `bun run check` runs type checking, formatting verification, linting, the README
100
+ contract check, the full test suite, and the benchmark smoke check. Documentation
101
+ checking is read-only; use `bun run docs:generate` explicitly after changing a
102
+ public declaration.
103
+
104
+ ## Agent Skills
69
105
 
70
106
  Tinker scans `<workspace>/.agents/skills/` and `~/.agents/skills/` once when a new
71
107
  or resumed runtime starts. Each direct child skill must contain a strictly valid
@@ -87,61 +123,186 @@ read-only and does not rescan, install, activate, or remove skills.
87
123
 
88
124
  ## Configuration
89
125
 
90
- Tinker is configured via environment variables:
91
-
92
- | Variable | Default | Description |
93
- |---|---|---|
94
- | `TINKER_MODEL` | | Required model name when `TINKER_MODELS` is not set |
95
- | `TINKER_BASE_URL` | | Required API base URL |
96
- | `TINKER_API_KEY` | | Required API key |
97
- | `TINKER_MODELS` | — | Path to a multi-model profiles JSON file (see below) |
98
- | `TINKER_WORKSPACE` | `process.cwd()` | Workspace root |
99
- | `TINKER_MAX_ITERATIONS` | `512` | Max agent loop iterations per turn |
100
- | `EXA_API_KEY` | — | Enables WebSearch tool |
101
- | `TINKER_MCP_CONFIG` | — | Path to MCP server config JSON |
102
- | `TINKER_EVENT_LOG` | `$TINKER_DIR/events.jsonl` | Event log path |
103
- | `TINKER_SESSION_DIR` | `$TINKER_DIR/sessions` | Session storage directory |
104
- | `TINKER_INCLUDE_REASONING` | `false` | Include model reasoning in output |
105
- | `TINKER_TASK_STOP_GRACE_MS` | `5000` | Grace period before SIGKILL |
106
- | `TINKER_MCP_TIMEOUT_MS` | `30000` | MCP tool timeout |
107
- | `TINKER_CONTEXT_BUDGET_TOKENS` | `128000` | Context budget in tokens |
126
+ Set `TINKER_MODELS` to use a profile file. Without it, the env-mode model fields
127
+ are required. Boolean environment values accept case-insensitive `true/false`,
128
+ `1/0`, `yes/no`, and `on/off`.
129
+
130
+ <!-- BEGIN GENERATED: PUBLIC ENVIRONMENT VARIABLES -->
131
+ | Variable | Area | Applies | Required | Type | Default | Secret | Description |
132
+ | --- | --- | --- | --- | --- | --- | --- | --- |
133
+ | `TINKER_MODELS` | Model | All modes | No | Non-empty string | — | No | Optional model profiles JSON path. Relative paths resolve from the process cwd. |
134
+ | `TINKER_MODEL` | Model | Env mode | Env mode | Non-empty string | — | No | Model name used when model profiles are not configured. |
135
+ | `TINKER_BASE_URL` | Model | Env mode | Env mode | Non-empty string | — | No | OpenAI-compatible Chat Completions API base URL. |
136
+ | `TINKER_API_KEY` | Model | Env mode | Env mode | Non-empty string | — | Yes | API credential for the configured model endpoint. |
137
+ | `TINKER_CONTEXT_WINDOW_TOKENS` | Model | Env mode | Env mode | Positive integer | — | No | Model context-window size in tokens. |
138
+ | `TINKER_MAX_SUPPORTED_OUTPUT_TOKENS` | Model | Env mode | Env mode | Positive integer | — | No | Maximum output-token count supported by the model; must not exceed the context window. |
139
+ | `TINKER_INCLUDE_REASONING_CONTENT` | Model | Env mode | No | Boolean | `false` | No | Include provider reasoning content in the model response mapping. |
140
+ | `TINKER_STREAM` | Model | Env mode | No | Boolean | `true` | No | Use streaming Chat Completions transport. |
141
+ | `TINKER_WEBFETCH_REFINE_MODEL` | Model | Env mode | No | Non-empty string | — | No | Optional WebFetch refiner model; currently must match TINKER_MODEL. |
142
+ | `TINKER_WORKSPACE` | Workspace | All modes | No | Non-empty string | Process cwd | No | Workspace path. Relative paths resolve from the process cwd. |
143
+ | `TINKER_MAX_ITERATIONS` | Workspace | All modes | No | Positive integer | `512` | No | Maximum agent-loop iterations per turn. |
144
+ | `EXA_API_KEY` | Tooling | All modes | No | Non-empty string | — | Yes | Enables WebSearch and the Exa WebFetch backend when set. |
145
+ | `TINKER_MCP_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `60000` | No | MCP tool-call timeout in milliseconds. |
146
+ | `TINKER_MCP_MAX_OBSERVATION_CHARS` | Tooling | All modes | No | Positive integer | `40000` | No | Maximum model-visible characters in one MCP result. |
147
+ | `TINKER_BASH_DEFAULT_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `5000` | No | Default Bash foreground timeout in milliseconds. |
148
+ | `TINKER_BASH_MAX_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `600000` | No | Maximum Bash foreground timeout in milliseconds. |
149
+ | `TINKER_GREP_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `20000` | No | Bundled ripgrep invocation timeout in milliseconds. |
150
+ | `TINKER_GREP_MAX_BUFFER_BYTES` | Tooling | All modes | No | Positive integer | `20000000` | No | Maximum buffered output from one ripgrep invocation. |
151
+ | `TINKER_WEBFETCH_REFINE_THRESHOLD` | Tooling | All modes | No | Positive integer | `2000` | No | Content-length threshold that enables WebFetch refinement. |
152
+ | `TINKER_RIPGREP_PATH` | Tooling | All modes | No | Non-empty string | Bundled ripgrep | No | Explicit diagnostic override for the bundled ripgrep executable. |
153
+ <!-- END GENERATED: PUBLIC ENVIRONMENT VARIABLES -->
154
+
155
+ Model and estimator API keys are sent to their configured external endpoints.
156
+ `EXA_API_KEY` is sent to Exa when WebSearch or the Exa WebFetch backend is used.
157
+ Keep configuration files private and review each service's data-handling policy.
108
158
 
109
159
  ### Multi-Model Profiles
110
160
 
111
- Set `TINKER_MODELS` in `.env` to point to a JSON file with multiple named model
161
+ Set `TINKER_MODELS` to point to a JSON file with multiple named model
112
162
  profiles. When set, the profile's `default` field selects the startup model, and
113
163
  the `/model` slash command (available in new sessions before any turns) lets you
114
164
  switch to another profile. If `TINKER_MODELS` is not set, Tinker falls back to
115
165
  the individual `TINKER_*` environment variables. A configured profiles file must
116
166
  exist and be valid; Tinker does not silently fall back when it cannot be loaded.
117
167
 
168
+ <!-- BEGIN GENERATED: MODEL PROFILE FIELDS -->
169
+ Profile fields:
170
+
171
+ | Field | Required | Type / constraint | Default | Secret | Description |
172
+ | --- | --- | --- | --- | --- | --- |
173
+ | `model` | Yes | Non-empty string | — | No | Provider model name. |
174
+ | `apiBase` | Yes | Non-empty string | — | No | OpenAI-compatible API base URL. |
175
+ | `apiKey` | Yes | Non-empty string | — | Yes | API credential for this profile. |
176
+ | `contextWindowTokens` | Yes | Positive integer | — | No | Model context-window size in tokens. |
177
+ | `maxSupportedOutputTokens` | Yes | Positive integer | — | No | Maximum output-token count supported by the model; must not exceed contextWindowTokens. |
178
+ | `includeReasoningContent` | No | JSON boolean | `false` | No | Include provider reasoning content in response mapping. |
179
+ | `stream` | No | JSON boolean | `true` | No | Use streaming Chat Completions transport. |
180
+ | `inputModalities` | No | Normalized modality array | `["text"]` | No | Accepted model input modalities; normalizes to ["text"] or ["text", "image"]. |
181
+ | `tokenEstimator` | With image | Object | — | Yes | Independent token estimator required for image profiles. |
182
+
183
+ `tokenEstimator` fields:
184
+
185
+ | Field | Type / constraint | Secret | Description |
186
+ | --- | --- | --- | --- |
187
+ | `kind` | Literal `"moonshot-estimate-token-count-v1"` | No | Estimator protocol discriminator. |
188
+ | `model` | Non-empty string | No | Estimator model name. |
189
+ | `apiBase` | Non-empty string | No | Estimator API base URL. |
190
+ | `apiKey` | Non-empty string | Yes | Estimator API credential. |
191
+ | `timeoutMs` | Integer 1000–60000 | No | Estimator request timeout in milliseconds. |
192
+ | `maxRetries` | Literal `0` | No | Estimator retry count; retries are disabled. |
193
+
194
+ Text-only profile example:
195
+
196
+ ```json
197
+ {
198
+ "default": "text",
199
+ "profiles": {
200
+ "text": {
201
+ "model": "example-text-model",
202
+ "apiBase": "https://api.example.com/v1",
203
+ "apiKey": "your-model-api-key",
204
+ "contextWindowTokens": 128000,
205
+ "maxSupportedOutputTokens": 8192,
206
+ "includeReasoningContent": false,
207
+ "stream": true,
208
+ "inputModalities": [
209
+ "text"
210
+ ]
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ Image-capable profile example:
217
+
118
218
  ```json
119
219
  {
120
- "default": "deepseek",
220
+ "default": "image",
121
221
  "profiles": {
122
- "deepseek": {
123
- "model": "deepseek-chat",
124
- "apiBase": "https://api.deepseek.com/v1",
125
- "apiKey": "sk-xxx",
222
+ "image": {
223
+ "model": "example-vision-model",
224
+ "apiBase": "https://api.example.com/v1",
225
+ "apiKey": "your-model-api-key",
126
226
  "contextWindowTokens": 128000,
127
- "maxSupportedOutputTokens": 8192
128
- },
129
- "gpt-4o": {
130
- "model": "gpt-4o",
131
- "apiBase": "https://api.openai.com/v1",
132
- "apiKey": "sk-yyy",
227
+ "maxSupportedOutputTokens": 8192,
228
+ "includeReasoningContent": false,
229
+ "stream": true,
230
+ "inputModalities": [
231
+ "text",
232
+ "image"
233
+ ],
234
+ "tokenEstimator": {
235
+ "kind": "moonshot-estimate-token-count-v1",
236
+ "model": "example-token-estimator",
237
+ "apiBase": "https://estimator.example.com/v1",
238
+ "apiKey": "your-estimator-api-key",
239
+ "timeoutMs": 30000,
240
+ "maxRetries": 0
241
+ }
242
+ }
243
+ }
244
+ }
245
+ ```
246
+
247
+ The top-level `memory` object is optional. When present, every field below is required. It enables completed-turn extraction and `MemorySearch` only in the TUI; one-shot runs do not load memory.
248
+
249
+ | Field | Required | Type / constraint | Secret | Description |
250
+ | --- | --- | --- | --- | --- |
251
+ | `profile` | Yes | Non-empty string | No | Existing model profile used for completed-turn atomic-memory extraction. |
252
+ | `embedding` | Yes | Object | Yes | Single embedding profile for the global memory database. |
253
+
254
+ `memory.embedding` fields:
255
+
256
+ | Field | Required | Type / constraint | Secret | Description |
257
+ | --- | --- | --- | --- | --- |
258
+ | `name` | Yes | Non-empty string | No | Stable identity for the embedding space. |
259
+ | `kind` | Yes | Literal `"openai-compatible"` | No | Embedding transport kind. |
260
+ | `model` | Yes | Non-empty string | No | Embedding provider model name. |
261
+ | `apiBase` | Yes | Non-empty string | No | OpenAI-compatible API base URL. |
262
+ | `apiKey` | Yes | Non-empty string | Yes | Embedding provider credential. |
263
+ | `dimensions` | Yes | Positive integer | No | Fixed vector dimensions for the global memory database. |
264
+
265
+ Enabling memory sends completed-turn text (not image bytes) to `memory.profile`, and sends extracted candidates plus search queries to the embedding endpoint. Derived memories are stored in `~/.tinker/memory/memory.sqlite`; newly inserted memory text is appended to the private development log `~/.tinker/memory/extracted-memories.log`.
266
+
267
+ Atomic-memory profile example:
268
+
269
+ ```json
270
+ {
271
+ "default": "text",
272
+ "profiles": {
273
+ "text": {
274
+ "model": "example-text-model",
275
+ "apiBase": "https://api.example.com/v1",
276
+ "apiKey": "your-model-api-key",
133
277
  "contextWindowTokens": 128000,
134
- "maxSupportedOutputTokens": 16384,
135
- "includeReasoningContent": true
278
+ "maxSupportedOutputTokens": 8192,
279
+ "includeReasoningContent": false,
280
+ "stream": true,
281
+ "inputModalities": [
282
+ "text"
283
+ ]
284
+ }
285
+ },
286
+ "memory": {
287
+ "profile": "text",
288
+ "embedding": {
289
+ "name": "example-embedding-space",
290
+ "kind": "openai-compatible",
291
+ "model": "example-embedding-model",
292
+ "apiBase": "https://embeddings.example.com/v1",
293
+ "apiKey": "your-embedding-api-key",
294
+ "dimensions": 1024
136
295
  }
137
296
  }
138
297
  }
139
298
  ```
299
+ <!-- END GENERATED: MODEL PROFILE FIELDS -->
140
300
 
141
- You can also start TUI with a specific profile:
301
+ You can also select profiles explicitly for the TUI or one-shot command:
142
302
 
143
303
  ```bash
144
- bun run tinker --profile gpt-4o
304
+ tinker --profile text
305
+ tinker run --profile text "explain the project structure"
145
306
  ```
146
307
 
147
308
  Switching models creates a new session. The previous session is preserved and
@@ -151,6 +312,47 @@ longer present or its runtime contract has changed. Older sessions without a
151
312
  stored profile name can resume only when their model name uniquely matches one
152
313
  configured profile.
153
314
 
315
+ ### Image Input
316
+
317
+ Image attachment is enabled only for a profile whose `inputModalities` explicitly
318
+ includes `image` and which supplies a valid `tokenEstimator`. In the interactive
319
+ TUI, type `@` and select a file that is inside the workspace and visible to the
320
+ workspace search rules. One-shot commands, clipboard image bytes, remote URLs, and
321
+ files outside or ignored by the workspace search are not supported.
322
+
323
+ Tinker accepts PNG (not APNG), JPEG, and static WebP. It rejects GIF, animated
324
+ WebP, and other formats. A message and provider request may contain at most eight
325
+ images; each image may be at most 20 MiB, 4096 pixels on either edge, and 8,847,360
326
+ pixels in total. See the
327
+ [`multimodal image input design`](docs/multimodal-image-input-design.md) for the
328
+ complete fixed policy and persistence contract.
329
+
330
+ ### Built-in Slash Commands
331
+
332
+ <!-- BEGIN GENERATED: BUILT-IN SLASH COMMANDS -->
333
+ | Command | Description |
334
+ | --- | --- |
335
+ | `/status` | Show session and context details |
336
+ | `/skills` | Show available and active Agent Skills |
337
+ | `/mcp` | Show MCP servers and runtime tools |
338
+ | `/memory` | Browse stored global memories |
339
+ | `/compact [retire]` | Swap tool output or retire a cold history prefix |
340
+ | `/clear` | Start a new session and clear conversation |
341
+ | `/fork` | Clone the current session |
342
+ | `/view <path>` | View a local UTF-8 text file |
343
+ | `/copy` | Copy the last response as Markdown |
344
+ | `/model [profile-name]` | Switch model profile (new session) |
345
+ | `/resume [session-id]` | Choose or resume a session |
346
+ | `/session delete <session-id> --confirm` | Manage stored sessions |
347
+ | `/quit` | Exit the TUI |
348
+ <!-- END GENERATED: BUILT-IN SLASH COMMANDS -->
349
+
350
+ `/view` opens a readable UTF-8 text file in a full-window viewer. Relative paths
351
+ must remain inside the workspace; absolute paths may point outside it. `/compact`
352
+ swaps eligible historical tool output, while `/compact retire` retires a complete
353
+ cold prefix whose original history remains available through `Recall`. `/copy`
354
+ copies the last completed assistant response as raw Markdown.
355
+
154
356
  ### Project Custom Slash Commands
155
357
 
156
358
  The TUI loads optional project-scoped prompt aliases from `.tinker.json` in the
@@ -178,24 +380,20 @@ one-shot `tinker run` command. See
178
380
  [`docs/project-custom-slash-commands-design.md`](docs/project-custom-slash-commands-design.md)
179
381
  for the full contract.
180
382
 
383
+ ### Workspace-Local Runtime Data
384
+
385
+ Tinker keeps private runtime state under `<workspace>/.tinker/`: each session has
386
+ its own SQLite database, event log, and observation log, while image assets and
387
+ Prompt history are stored at the workspace level. These locations are fixed and
388
+ have no public path-override environment variables. MCP server configuration is
389
+ loaded from `<workspace>/.mcp.json`; project slash commands are loaded from
390
+ `<workspace>/.tinker.json`.
391
+
181
392
  `Read` has a fixed 262144-byte (256 KiB) content limit per call. A successful
182
393
  call always returns the complete requested line range. Use `offset` and `limit`
183
394
  to page through larger files; oversized requests fail instead of returning
184
395
  truncated content.
185
396
 
186
- ## Commands
187
-
188
- ```bash
189
- bun install # Install dependencies
190
- bun run tinker # Start the interactive TUI
191
- bun run tinker run "..." # Run a one-shot CLI prompt
192
- bun test # Run test suite
193
- bun run typecheck # TypeScript type checking (tsc --noEmit)
194
- bun run lint # ESLint (zero warnings required)
195
- bun run format # Biome code formatting
196
- bun run check # Full check: typecheck + format + lint + test
197
- ```
198
-
199
397
  ## Project Structure
200
398
 
201
399
  ```
@@ -228,8 +426,9 @@ tinker/
228
426
  ## Requirements
229
427
 
230
428
  - Node.js 20 or later and npm for the global package installation
231
- - [Bun](https://bun.sh) 1.3 or later for development from source
232
- - A compatible LLM API (defaults to DeepSeek; any OpenAI-compatible API works)
429
+ - [Bun](https://bun.sh) 1.3.14 or later for development from source
430
+ - A configured OpenAI-compatible Chat Completions endpoint; transport compatibility
431
+ alone does not establish provider qualification
233
432
 
234
433
  ## Security
235
434
 
package/bin/tinker.js CHANGED
@@ -1,39 +1,89 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from "node:child_process";
3
+ import { spawn } from "node:child_process";
4
+ import { realpathSync } from "node:fs";
4
5
  import { createRequire } from "node:module";
5
6
  import { dirname, join } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
8
9
  const require = createRequire(import.meta.url);
10
+ const START_FAILURE_MESSAGE =
11
+ "Tinker failed to start. Reinstall tinker-agent.\n";
9
12
 
10
- let bunPackageJson;
11
- try {
12
- bunPackageJson = require.resolve("bun/package.json");
13
- } catch {
14
- process.stderr.write(
15
- "Tinker could not find its bundled Bun runtime. Reinstall tinker-agent.\n",
13
+ export async function launchTinker(
14
+ input = {
15
+ args: process.argv.slice(2),
16
+ stderr: process.stderr,
17
+ },
18
+ injected = {},
19
+ ) {
20
+ const resolveBundledBun =
21
+ injected.resolveBundledBun ?? (() => require.resolve("bun/package.json"));
22
+ const startChild = injected.spawn ?? spawn;
23
+ const forwardSignal =
24
+ injected.forwardSignal ?? ((signal) => process.kill(process.pid, signal));
25
+
26
+ let bunPackageJson;
27
+ try {
28
+ bunPackageJson = resolveBundledBun();
29
+ } catch {
30
+ input.stderr.write(START_FAILURE_MESSAGE);
31
+ return 1;
32
+ }
33
+
34
+ const bunExecutable = join(dirname(bunPackageJson), "bin", "bun.exe");
35
+ const tinkerEntryPoint = fileURLToPath(
36
+ new URL("../src/cli/index.ts", import.meta.url),
16
37
  );
17
- process.exit(1);
18
- }
38
+ let child;
39
+ try {
40
+ child = startChild(
41
+ bunExecutable,
42
+ [tinkerEntryPoint, ...input.args],
43
+ { stdio: "inherit" },
44
+ );
45
+ } catch {
46
+ input.stderr.write(START_FAILURE_MESSAGE);
47
+ return 1;
48
+ }
19
49
 
20
- const bunExecutable = join(dirname(bunPackageJson), "bin", "bun.exe");
21
- const tinkerEntryPoint = fileURLToPath(
22
- new URL("../src/cli/index.ts", import.meta.url),
23
- );
24
- const result = spawnSync(
25
- bunExecutable,
26
- [tinkerEntryPoint, ...process.argv.slice(2)],
27
- { stdio: "inherit" },
28
- );
29
-
30
- if (result.error !== undefined) {
31
- process.stderr.write(`Tinker failed to start Bun: ${result.error.message}\n`);
32
- process.exit(1);
50
+ return new Promise((resolve) => {
51
+ let settled = false;
52
+ child.once("error", () => {
53
+ if (settled) {
54
+ return;
55
+ }
56
+ settled = true;
57
+ input.stderr.write(START_FAILURE_MESSAGE);
58
+ resolve(1);
59
+ });
60
+ child.once("exit", (code, signal) => {
61
+ if (settled) {
62
+ return;
63
+ }
64
+ settled = true;
65
+ if (signal !== null) {
66
+ forwardSignal(signal);
67
+ resolve(1);
68
+ return;
69
+ }
70
+ resolve(code ?? 1);
71
+ });
72
+ });
33
73
  }
34
74
 
35
- if (result.signal !== null) {
36
- process.kill(process.pid, result.signal);
75
+ function isExecutableEntryPoint() {
76
+ const entryPoint = process.argv[1];
77
+ if (entryPoint === undefined) {
78
+ return false;
79
+ }
80
+ try {
81
+ return realpathSync(entryPoint) === realpathSync(fileURLToPath(import.meta.url));
82
+ } catch {
83
+ return false;
84
+ }
37
85
  }
38
86
 
39
- process.exit(result.status ?? 1);
87
+ if (isExecutableEntryPoint()) {
88
+ process.exitCode = await launchTinker();
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A personal coding agent with an interactive TUI and one-shot CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,6 +20,7 @@
20
20
  "src/image",
21
21
  "src/instructions",
22
22
  "src/mcp",
23
+ "src/memory",
23
24
  "src/model",
24
25
  "src/observation",
25
26
  "src/session",
@@ -58,13 +59,18 @@
58
59
  "chrome:diagnose": "bun packages/tinker-chrome/src/cli.ts diagnose",
59
60
  "chrome:install-host": "bun packages/tinker-chrome/src/cli.ts install-host",
60
61
  "chrome:mcp": "bun packages/tinker-chrome/src/cli.ts mcp",
62
+ "docs:generate": "bun scripts/render-public-contract-docs.ts --write",
63
+ "docs:check": "bun scripts/render-public-contract-docs.ts --check",
61
64
  "tinker": "bun src/cli/index.ts",
62
- "check": "bun run typecheck && bun run format:check && bun run lint && bun test && bun run bench:smoke",
65
+ "check": "bun run typecheck && bun run format:check && bun run lint && bun run docs:check && bun run test && bun run bench:smoke",
66
+ "check:fast": "bun run typecheck && bun run format:check && bun run lint && bun run test:fast",
63
67
  "format": "biome format --write .",
64
68
  "format:check": "biome format .",
65
69
  "lint": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --max-warnings=0",
66
70
  "lint:fix": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --fix",
67
- "test": "bun test",
71
+ "test": "FORCE_COLOR=0 bun test",
72
+ "test:fast": "FORCE_COLOR=0 bun test --path-ignore-patterns='**/*pty*.test.ts' --path-ignore-patterns='**/native-host-integration.test.ts'",
73
+ "test:e2e": "FORCE_COLOR=0 bun test src/__tests__/cli-pty src/__tests__/pty-tui-harness packages/tinker-chrome/__tests__/native-host-integration",
68
74
  "typecheck": "tsc --noEmit"
69
75
  },
70
76
  "dependencies": {
@@ -76,6 +82,7 @@
76
82
  "@vscode/ripgrep": "1.18.0",
77
83
  "bun": "1.3.14",
78
84
  "clipboardy": "^5.3.1",
85
+ "commander": "^14.0.3",
79
86
  "diff": "^9.0.0",
80
87
  "glob": "^13.0.6",
81
88
  "ink": "^7.1.0",
@@ -96,6 +103,8 @@
96
103
  "@types/chrome": "^0.2.2",
97
104
  "@types/react": "^19.2.17",
98
105
  "@types/turndown": "^5.0.6",
106
+ "@xterm/addon-unicode11": "0.9.0",
107
+ "@xterm/headless": "6.0.0",
99
108
  "eslint": "^10.6.0",
100
109
  "eslint-plugin-react-hooks": "^7.1.1",
101
110
  "globals": "^17.7.0",