tinker-agent 1.2.1 → 1.4.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 (39) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +217 -72
  3. package/bin/tinker.js +75 -25
  4. package/package.json +8 -3
  5. package/src/agent/loop.ts +115 -11
  6. package/src/agent/runtime-session.ts +34 -15
  7. package/src/cli/command-line.ts +291 -0
  8. package/src/cli/config.ts +131 -264
  9. package/src/cli/index.ts +33 -21
  10. package/src/cli/main.ts +213 -0
  11. package/src/cli/model-profiles.ts +143 -72
  12. package/src/cli/output.ts +113 -0
  13. package/src/cli/package-metadata.ts +36 -0
  14. package/src/cli/prompt-source.ts +229 -0
  15. package/src/cli/public-cli-contract.ts +69 -0
  16. package/src/cli/public-config-contract.ts +650 -0
  17. package/src/cli/run-runner.ts +17 -12
  18. package/src/cli/runner-dependencies.ts +100 -0
  19. package/src/cli/tui-runner.tsx +52 -49
  20. package/src/events/observation-text-log.ts +2 -0
  21. package/src/events/stdout-event-printer.ts +7 -1
  22. package/src/events/types.ts +27 -3
  23. package/src/mcp/mcp-manager.ts +2 -19
  24. package/src/mcp/mcp-tool-executor.ts +3 -4
  25. package/src/model/model-client.ts +29 -0
  26. package/src/model/model-context-profile.ts +0 -30
  27. package/src/model/openai-chat-mapping.ts +54 -15
  28. package/src/model/openai-chat-model-client.ts +46 -27
  29. package/src/model/openai-chat-stream.ts +6 -2
  30. package/src/tools/bash.ts +8 -25
  31. package/src/tools/grep.ts +9 -1
  32. package/src/tools/registry.ts +15 -1
  33. package/src/tools/ripgrep.ts +24 -27
  34. package/src/tools/web-fetch/index.ts +2 -15
  35. package/src/tui/app.tsx +3 -0
  36. package/src/tui/components/prompt-input.tsx +6 -3
  37. package/src/tui/event-store.ts +24 -7
  38. package/src/tui/slash-commands.ts +76 -24
  39. package/src/tui/workspace-file-search.ts +78 -71
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.4.0] - 2026-07-24
9
+
10
+ ### Added
11
+
12
+ - Add stable `tinker --help`, `tinker --version`, and `tinker help run` command
13
+ surfaces with explicit usage errors and exit codes.
14
+ - Let one-shot runs read an exact Prompt from explicit stdin or a UTF-8 text file,
15
+ with bounded input and strict encoding validation.
16
+
17
+ ### Changed
18
+
19
+ - Require `tinker run` to receive exactly one shell-quoted Prompt argument,
20
+ `--stdin`, or `--file <path>`. The former unquoted variadic form is no longer
21
+ joined automatically; quote a short Prompt or use stdin/file input instead.
22
+
23
+ ## [1.3.0] - 2026-07-22
24
+
25
+ ### Added
26
+
27
+ - Recover automatically when an OpenAI-compatible provider completes a response
28
+ with reasoning only and no final answer or tool call: Tinker retries that exact
29
+ request once without adding the invalid response to session history.
30
+
8
31
  ## [1.2.1] - 2026-07-22
9
32
 
10
33
  ### Fixed
@@ -40,7 +63,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
40
63
  - First formal npm release under the `tinker-agent` package name with the `tinker`
41
64
  executable.
42
65
 
43
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.2.1...HEAD
66
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.4.0...HEAD
67
+ [1.4.0]: https://github.com/ishowshao/tinker/releases/tag/v1.4.0
68
+ [1.3.0]: https://github.com/ishowshao/tinker/releases/tag/v1.3.0
44
69
  [1.2.1]: https://github.com/ishowshao/tinker/releases/tag/v1.2.1
45
70
  [1.2.0]: https://github.com/ishowshao/tinker/releases/tag/v1.2.0
46
71
  [1.1.0]: https://www.npmjs.com/package/tinker-agent/v/1.1.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,133 @@ 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
+
118
196
  ```json
119
197
  {
120
- "default": "deepseek",
198
+ "default": "text",
121
199
  "profiles": {
122
- "deepseek": {
123
- "model": "deepseek-chat",
124
- "apiBase": "https://api.deepseek.com/v1",
125
- "apiKey": "sk-xxx",
200
+ "text": {
201
+ "model": "example-text-model",
202
+ "apiBase": "https://api.example.com/v1",
203
+ "apiKey": "your-model-api-key",
126
204
  "contextWindowTokens": 128000,
127
- "maxSupportedOutputTokens": 8192
128
- },
129
- "gpt-4o": {
130
- "model": "gpt-4o",
131
- "apiBase": "https://api.openai.com/v1",
132
- "apiKey": "sk-yyy",
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
+
218
+ ```json
219
+ {
220
+ "default": "image",
221
+ "profiles": {
222
+ "image": {
223
+ "model": "example-vision-model",
224
+ "apiBase": "https://api.example.com/v1",
225
+ "apiKey": "your-model-api-key",
133
226
  "contextWindowTokens": 128000,
134
- "maxSupportedOutputTokens": 16384,
135
- "includeReasoningContent": true
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
+ }
136
242
  }
137
243
  }
138
244
  }
139
245
  ```
246
+ <!-- END GENERATED: MODEL PROFILE FIELDS -->
140
247
 
141
- You can also start TUI with a specific profile:
248
+ You can also select profiles explicitly for the TUI or one-shot command:
142
249
 
143
250
  ```bash
144
- bun run tinker --profile gpt-4o
251
+ tinker --profile text
252
+ tinker run --profile text "explain the project structure"
145
253
  ```
146
254
 
147
255
  Switching models creates a new session. The previous session is preserved and
@@ -151,6 +259,46 @@ longer present or its runtime contract has changed. Older sessions without a
151
259
  stored profile name can resume only when their model name uniquely matches one
152
260
  configured profile.
153
261
 
262
+ ### Image Input
263
+
264
+ Image attachment is enabled only for a profile whose `inputModalities` explicitly
265
+ includes `image` and which supplies a valid `tokenEstimator`. In the interactive
266
+ TUI, type `@` and select a file that is inside the workspace and visible to the
267
+ workspace search rules. One-shot commands, clipboard image bytes, remote URLs, and
268
+ files outside or ignored by the workspace search are not supported.
269
+
270
+ Tinker accepts PNG (not APNG), JPEG, and static WebP. It rejects GIF, animated
271
+ WebP, and other formats. A message and provider request may contain at most eight
272
+ images; each image may be at most 20 MiB, 4096 pixels on either edge, and 8,847,360
273
+ pixels in total. See the
274
+ [`multimodal image input design`](docs/multimodal-image-input-design.md) for the
275
+ complete fixed policy and persistence contract.
276
+
277
+ ### Built-in Slash Commands
278
+
279
+ <!-- BEGIN GENERATED: BUILT-IN SLASH COMMANDS -->
280
+ | Command | Description |
281
+ | --- | --- |
282
+ | `/status` | Show session and context details |
283
+ | `/skills` | Show available and active Agent Skills |
284
+ | `/mcp` | Show MCP servers and runtime tools |
285
+ | `/compact [retire]` | Swap tool output or retire a cold history prefix |
286
+ | `/clear` | Start a new session and clear conversation |
287
+ | `/fork` | Clone the current session |
288
+ | `/view <path>` | View a local UTF-8 text file |
289
+ | `/copy` | Copy the last response as Markdown |
290
+ | `/model [profile-name]` | Switch model profile (new session) |
291
+ | `/resume [session-id]` | Choose or resume a session |
292
+ | `/session delete <session-id> --confirm` | Manage stored sessions |
293
+ | `/quit` | Exit the TUI |
294
+ <!-- END GENERATED: BUILT-IN SLASH COMMANDS -->
295
+
296
+ `/view` opens a readable UTF-8 text file in a full-window viewer. Relative paths
297
+ must remain inside the workspace; absolute paths may point outside it. `/compact`
298
+ swaps eligible historical tool output, while `/compact retire` retires a complete
299
+ cold prefix whose original history remains available through `Recall`. `/copy`
300
+ copies the last completed assistant response as raw Markdown.
301
+
154
302
  ### Project Custom Slash Commands
155
303
 
156
304
  The TUI loads optional project-scoped prompt aliases from `.tinker.json` in the
@@ -178,24 +326,20 @@ one-shot `tinker run` command. See
178
326
  [`docs/project-custom-slash-commands-design.md`](docs/project-custom-slash-commands-design.md)
179
327
  for the full contract.
180
328
 
329
+ ### Workspace-Local Runtime Data
330
+
331
+ Tinker keeps private runtime state under `<workspace>/.tinker/`: each session has
332
+ its own SQLite database, event log, and observation log, while image assets and
333
+ Prompt history are stored at the workspace level. These locations are fixed and
334
+ have no public path-override environment variables. MCP server configuration is
335
+ loaded from `<workspace>/.mcp.json`; project slash commands are loaded from
336
+ `<workspace>/.tinker.json`.
337
+
181
338
  `Read` has a fixed 262144-byte (256 KiB) content limit per call. A successful
182
339
  call always returns the complete requested line range. Use `offset` and `limit`
183
340
  to page through larger files; oversized requests fail instead of returning
184
341
  truncated content.
185
342
 
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
343
  ## Project Structure
200
344
 
201
345
  ```
@@ -228,8 +372,9 @@ tinker/
228
372
  ## Requirements
229
373
 
230
374
  - 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)
375
+ - [Bun](https://bun.sh) 1.3.14 or later for development from source
376
+ - A configured OpenAI-compatible Chat Completions endpoint; transport compatibility
377
+ alone does not establish provider qualification
233
378
 
234
379
  ## Security
235
380
 
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.2.1",
3
+ "version": "1.4.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",
@@ -58,13 +58,15 @@
58
58
  "chrome:diagnose": "bun packages/tinker-chrome/src/cli.ts diagnose",
59
59
  "chrome:install-host": "bun packages/tinker-chrome/src/cli.ts install-host",
60
60
  "chrome:mcp": "bun packages/tinker-chrome/src/cli.ts mcp",
61
+ "docs:generate": "bun scripts/render-public-contract-docs.ts --write",
62
+ "docs:check": "bun scripts/render-public-contract-docs.ts --check",
61
63
  "tinker": "bun src/cli/index.ts",
62
- "check": "bun run typecheck && bun run format:check && bun run lint && bun test && bun run bench:smoke",
64
+ "check": "bun run typecheck && bun run format:check && bun run lint && bun run docs:check && bun run test && bun run bench:smoke",
63
65
  "format": "biome format --write .",
64
66
  "format:check": "biome format .",
65
67
  "lint": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --max-warnings=0",
66
68
  "lint:fix": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --fix",
67
- "test": "bun test",
69
+ "test": "FORCE_COLOR=0 bun test",
68
70
  "typecheck": "tsc --noEmit"
69
71
  },
70
72
  "dependencies": {
@@ -76,6 +78,7 @@
76
78
  "@vscode/ripgrep": "1.18.0",
77
79
  "bun": "1.3.14",
78
80
  "clipboardy": "^5.3.1",
81
+ "commander": "^14.0.3",
79
82
  "diff": "^9.0.0",
80
83
  "glob": "^13.0.6",
81
84
  "ink": "^7.1.0",
@@ -96,6 +99,8 @@
96
99
  "@types/chrome": "^0.2.2",
97
100
  "@types/react": "^19.2.17",
98
101
  "@types/turndown": "^5.0.6",
102
+ "@xterm/addon-unicode11": "0.9.0",
103
+ "@xterm/headless": "6.0.0",
99
104
  "eslint": "^10.6.0",
100
105
  "eslint-plugin-react-hooks": "^7.1.1",
101
106
  "globals": "^17.7.0",