tinker-agent 1.6.0 → 1.8.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.
- package/CHANGELOG.md +35 -1
- package/README.md +8 -2
- package/package.json +5 -3
- package/src/agent/runtime-session.ts +1 -0
- package/src/cli/command-line.ts +24 -2
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +4 -4
- package/src/cli/public-cli-contract.ts +4 -0
- package/src/cli/runner-dependencies.ts +4 -1
- package/src/cli/update-runner.ts +308 -0
- package/src/events/bash-result-detail.ts +13 -6
- package/src/events/observation-text-log.ts +5 -1
- package/src/events/stdout-event-printer.ts +7 -2
- package/src/mcp/mcp-manager.ts +34 -3
- package/src/model/fake-model-client.ts +84 -0
- package/src/observation/observation-builder.ts +43 -6
- package/src/session/session-catalog.ts +17 -11
- package/src/session/session-store.ts +1 -0
- package/src/tools/bash-task.ts +125 -90
- package/src/tools/bash.ts +44 -13
- package/src/tools/registry.ts +2 -0
- package/src/tools/shell-process.ts +296 -0
- package/src/tools/task-input.ts +229 -0
- package/src/tools/task-output-tool.ts +4 -1
- package/src/tools/terminal-screen.ts +105 -0
- package/src/tools/types.ts +32 -0
- package/src/tui/components/background-tasks.tsx +3 -2
- package/src/tui/components/resume-session-picker.tsx +292 -46
- package/src/tui/event-store.ts +14 -1
- package/src/tui/tui-session-controller.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.8.0] - 2026-08-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add `tinker update` for manually upgrading a direct npm global installation to
|
|
13
|
+
the latest stable release from the official npm registry.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Wait for completed background-task output to finish flushing before returning
|
|
18
|
+
it through `TaskOutput`.
|
|
19
|
+
|
|
20
|
+
## [1.7.0] - 2026-08-01
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Run interactive terminal programs through PTY-backed Bash tasks, send exact
|
|
25
|
+
keystrokes with `TaskInput`, and inspect their current terminal screen with
|
|
26
|
+
`TaskOutput`.
|
|
27
|
+
- Search all resumable sessions from the `/resume` picker by text from their
|
|
28
|
+
first user prompt, including older sessions beyond the 20 most recent.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Keep the live background-task panel compact by showing at most two tasks and
|
|
33
|
+
label PTY-backed tasks explicitly.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Complete PTY tasks reliably after subprocess exit on Linux, including the EIO
|
|
38
|
+
signal reported when the terminal closes.
|
|
39
|
+
|
|
8
40
|
## [1.6.0] - 2026-08-01
|
|
9
41
|
|
|
10
42
|
### Added
|
|
@@ -129,7 +161,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
|
|
|
129
161
|
- First formal npm release under the `tinker-agent` package name with the `tinker`
|
|
130
162
|
executable.
|
|
131
163
|
|
|
132
|
-
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.
|
|
164
|
+
[Unreleased]: https://github.com/ishowshao/tinker/compare/v1.8.0...HEAD
|
|
165
|
+
[1.8.0]: https://github.com/ishowshao/tinker/releases/tag/v1.8.0
|
|
166
|
+
[1.7.0]: https://github.com/ishowshao/tinker/releases/tag/v1.7.0
|
|
133
167
|
[1.6.0]: https://github.com/ishowshao/tinker/releases/tag/v1.6.0
|
|
134
168
|
[1.5.1]: https://github.com/ishowshao/tinker/releases/tag/v1.5.1
|
|
135
169
|
[1.5.0]: https://github.com/ishowshao/tinker/releases/tag/v1.5.0
|
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://gith
|
|
|
12
12
|
- `Glob` / `Grep` — Find and search files by pattern or content
|
|
13
13
|
- `Read` / `Write` / `Edit` — File I/O with content hashing and concurrent-modification protection
|
|
14
14
|
- `Delete` — Delete one existing regular file without directory or symlink support
|
|
15
|
-
- `Bash` — Run
|
|
16
|
-
- `TaskList` / `TaskOutput` / `TaskStop` —
|
|
15
|
+
- `Bash` — Run foreground, background, and PTY shell commands with per-task working directories
|
|
16
|
+
- `TaskList` / `TaskOutput` / `TaskInput` / `TaskStop` — Inspect, interact with, and stop long-running shell tasks
|
|
17
17
|
- `WebSearch` — Search the web via Exa API
|
|
18
18
|
- `WebFetch` — Fetch and refine web page content (local, browser, or Exa backend)
|
|
19
19
|
- `Recall` — Search or retrieve model-visible history from the current session
|
|
@@ -80,11 +80,17 @@ The installed package exposes this public CLI:
|
|
|
80
80
|
| `tinker run [--profile <profile-name>] [--yolo] <prompt>` | Submit one shell-quoted prompt argument. |
|
|
81
81
|
| `tinker run [--profile <profile-name>] [--yolo] --stdin` | Read the prompt from standard input until EOF. |
|
|
82
82
|
| `tinker run [--profile <profile-name>] [--yolo] --file <path>` | Read the prompt from a UTF-8 text file. |
|
|
83
|
+
| `tinker update` | Update the global npm installation from the official npm registry. |
|
|
83
84
|
| `tinker --help` | Show top-level CLI help. |
|
|
84
85
|
| `tinker help run` | Show one-shot command help. |
|
|
86
|
+
| `tinker help update` | Show update command help. |
|
|
85
87
|
| `tinker --version` | Print the installed package version. |
|
|
86
88
|
<!-- END GENERATED: PUBLIC CLI COMMANDS -->
|
|
87
89
|
|
|
90
|
+
`tinker update` is available only to direct npm global installations. It checks
|
|
91
|
+
the `latest` stable version on the official npm registry, updates that same global
|
|
92
|
+
prefix, and exits without loading model configuration or starting a session.
|
|
93
|
+
|
|
88
94
|
Repository development commands are separate from the installed CLI:
|
|
89
95
|
|
|
90
96
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"chrome:diagnose": "bun packages/tinker-chrome/src/cli.ts diagnose",
|
|
60
60
|
"chrome:install-host": "bun packages/tinker-chrome/src/cli.ts install-host",
|
|
61
61
|
"chrome:mcp": "bun packages/tinker-chrome/src/cli.ts mcp",
|
|
62
|
+
"chrome:smoke": "bun packages/tinker-chrome/scripts/live-smoke.ts",
|
|
62
63
|
"docs:generate": "bun scripts/render-public-contract-docs.ts --write",
|
|
63
64
|
"docs:check": "bun scripts/render-public-contract-docs.ts --check",
|
|
64
65
|
"tinker": "bun src/cli/index.ts",
|
|
@@ -80,6 +81,8 @@
|
|
|
80
81
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
81
82
|
"@mozilla/readability": "^0.6.0",
|
|
82
83
|
"@vscode/ripgrep": "1.18.0",
|
|
84
|
+
"@xterm/addon-unicode11": "0.9.0",
|
|
85
|
+
"@xterm/headless": "6.0.0",
|
|
83
86
|
"ansi-escapes": "^7.3.0",
|
|
84
87
|
"bun": "1.3.14",
|
|
85
88
|
"clipboardy": "^5.3.1",
|
|
@@ -91,6 +94,7 @@
|
|
|
91
94
|
"markdansi": "0.3.2",
|
|
92
95
|
"marked": "^18.0.5",
|
|
93
96
|
"openai": "^7.1.0",
|
|
97
|
+
"puppeteer-core": "25.4.0",
|
|
94
98
|
"react": "^19.2.7",
|
|
95
99
|
"sharp": "^0.35.3",
|
|
96
100
|
"shiki": "^4.3.1",
|
|
@@ -105,8 +109,6 @@
|
|
|
105
109
|
"@types/chrome": "^0.2.2",
|
|
106
110
|
"@types/react": "^19.2.17",
|
|
107
111
|
"@types/turndown": "^5.0.6",
|
|
108
|
-
"@xterm/addon-unicode11": "0.9.0",
|
|
109
|
-
"@xterm/headless": "6.0.0",
|
|
110
112
|
"eslint": "^10.6.0",
|
|
111
113
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
112
114
|
"globals": "^17.7.0",
|
|
@@ -635,6 +635,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
635
635
|
if (mcpConfig !== undefined) {
|
|
636
636
|
session.mcpManager = await dependencies.createMcpManager({
|
|
637
637
|
config: mcpConfig,
|
|
638
|
+
workspaceRoot: input.workspaceRoot,
|
|
638
639
|
runtimeSession: session.context,
|
|
639
640
|
timeoutMs: input.toolingConfig?.mcpTimeoutMs,
|
|
640
641
|
maxObservationChars: input.toolingConfig?.mcpMaxObservationChars,
|
package/src/cli/command-line.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { CliUsageError, type CliCommandScope } from "./output";
|
|
|
5
5
|
|
|
6
6
|
export type CliCommand =
|
|
7
7
|
| { readonly type: "tui"; readonly profileName?: string }
|
|
8
|
+
| { readonly type: "update" }
|
|
8
9
|
| {
|
|
9
10
|
readonly type: "run";
|
|
10
11
|
readonly profileName?: string;
|
|
@@ -129,6 +130,17 @@ export async function parseCommandLine(
|
|
|
129
130
|
},
|
|
130
131
|
);
|
|
131
132
|
|
|
133
|
+
program
|
|
134
|
+
.command(contract.update.command)
|
|
135
|
+
.description(contract.update.description)
|
|
136
|
+
.showHelpAfterError('Run "tinker update --help" for usage.')
|
|
137
|
+
.showSuggestionAfterError(false)
|
|
138
|
+
.allowExcessArguments(false)
|
|
139
|
+
.exitOverride()
|
|
140
|
+
.action(() => {
|
|
141
|
+
selectedCommand = Object.freeze({ type: "update" });
|
|
142
|
+
});
|
|
143
|
+
|
|
132
144
|
try {
|
|
133
145
|
await program.parseAsync([...args], { from: "user" });
|
|
134
146
|
} catch (error) {
|
|
@@ -154,6 +166,12 @@ export async function parseCommandLine(
|
|
|
154
166
|
"run",
|
|
155
167
|
);
|
|
156
168
|
}
|
|
169
|
+
if (selectedCommand.type === "update" && topLevelProfile !== undefined) {
|
|
170
|
+
throw new CliUsageError(
|
|
171
|
+
"The top-level --profile option only applies to the TUI.",
|
|
172
|
+
"update",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
157
175
|
return Object.freeze({ type: "command", command: selectedCommand });
|
|
158
176
|
}
|
|
159
177
|
|
|
@@ -173,7 +191,7 @@ function preflightArgv(args: readonly string[]): CliCommandScope {
|
|
|
173
191
|
|
|
174
192
|
if (
|
|
175
193
|
(!rootBlocked && scope === "root" && isRootTerminalOption(token)) ||
|
|
176
|
-
(scope
|
|
194
|
+
(scope !== "root" && isSubcommandTerminalOption(token))
|
|
177
195
|
) {
|
|
178
196
|
return scope;
|
|
179
197
|
}
|
|
@@ -202,6 +220,10 @@ function preflightArgv(args: readonly string[]): CliCommandScope {
|
|
|
202
220
|
scope = "run";
|
|
203
221
|
continue;
|
|
204
222
|
}
|
|
223
|
+
if (token === "update") {
|
|
224
|
+
scope = "update";
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
205
227
|
if (token === "help") {
|
|
206
228
|
return "root";
|
|
207
229
|
}
|
|
@@ -290,7 +312,7 @@ function isRootTerminalOption(token: string): boolean {
|
|
|
290
312
|
);
|
|
291
313
|
}
|
|
292
314
|
|
|
293
|
-
function
|
|
315
|
+
function isSubcommandTerminalOption(token: string): boolean {
|
|
294
316
|
return token === "--help" || token === "-h";
|
|
295
317
|
}
|
|
296
318
|
|
package/src/cli/main.ts
CHANGED
|
@@ -67,6 +67,14 @@ type OneShotRunner = {
|
|
|
67
67
|
) => Promise<number>;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
type UpdateRunner = {
|
|
71
|
+
readonly runUpdate: (options: {
|
|
72
|
+
readonly metadata: PackageMetadata;
|
|
73
|
+
readonly stdout: CliOutputWriter;
|
|
74
|
+
readonly env: NodeJS.ProcessEnv;
|
|
75
|
+
}) => Promise<number>;
|
|
76
|
+
};
|
|
77
|
+
|
|
70
78
|
export type MainDependencies = {
|
|
71
79
|
readonly loadPackageMetadata: () => Promise<PackageMetadata>;
|
|
72
80
|
readonly parseCommandLine: (
|
|
@@ -81,6 +89,7 @@ export type MainDependencies = {
|
|
|
81
89
|
) => Promise<ResolvedPrompt>;
|
|
82
90
|
readonly loadTuiRunner: () => Promise<TuiRunner>;
|
|
83
91
|
readonly loadOneShotRunner: () => Promise<OneShotRunner>;
|
|
92
|
+
readonly loadUpdateRunner: () => Promise<UpdateRunner>;
|
|
84
93
|
};
|
|
85
94
|
|
|
86
95
|
const DEFAULT_DEPENDENCIES: MainDependencies = {
|
|
@@ -91,6 +100,7 @@ const DEFAULT_DEPENDENCIES: MainDependencies = {
|
|
|
91
100
|
resolvePromptSource,
|
|
92
101
|
loadTuiRunner: () => import("./tui-runner"),
|
|
93
102
|
loadOneShotRunner: () => import("./run-runner"),
|
|
103
|
+
loadUpdateRunner: () => import("./update-runner"),
|
|
94
104
|
};
|
|
95
105
|
|
|
96
106
|
export async function main(
|
|
@@ -133,6 +143,22 @@ export async function main(
|
|
|
133
143
|
return finish(0);
|
|
134
144
|
}
|
|
135
145
|
|
|
146
|
+
if (parsed.command.type === "update") {
|
|
147
|
+
try {
|
|
148
|
+
const runner = await dependencies.loadUpdateRunner();
|
|
149
|
+
return finish(
|
|
150
|
+
await runner.runUpdate({
|
|
151
|
+
metadata,
|
|
152
|
+
stdout: input.stdout,
|
|
153
|
+
env,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await writeCliOutput(input.stderr, renderCliFailure("Update failed", error));
|
|
158
|
+
return finish(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
136
162
|
let configBoundary: ConfigBoundary;
|
|
137
163
|
let publicConfig: ResolvedPublicConfig;
|
|
138
164
|
let runnerConfig: RunnerConfig;
|
package/src/cli/output.ts
CHANGED
|
@@ -4,7 +4,7 @@ const TRUNCATION_MARKER = "...[truncated]";
|
|
|
4
4
|
const ESCAPE = String.fromCharCode(27);
|
|
5
5
|
const ANSI_CSI_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
6
6
|
|
|
7
|
-
export type CliCommandScope = "root" | "run";
|
|
7
|
+
export type CliCommandScope = "root" | "run" | "update";
|
|
8
8
|
|
|
9
9
|
export interface CliOutputWriter {
|
|
10
10
|
write(chunk: string): boolean | void;
|
|
@@ -24,9 +24,9 @@ export class CliUsageError extends Error {
|
|
|
24
24
|
|
|
25
25
|
export function renderUsageError(error: CliUsageError): string {
|
|
26
26
|
const hint =
|
|
27
|
-
error.scope === "
|
|
28
|
-
? 'Run "tinker
|
|
29
|
-
:
|
|
27
|
+
error.scope === "root"
|
|
28
|
+
? 'Run "tinker --help" for usage.'
|
|
29
|
+
: `Run "tinker ${error.scope} --help" for usage.`;
|
|
30
30
|
return `error: ${sanitizeDiagnosticDetail(error.message)}\n${hint}\n`;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -68,6 +68,10 @@ export const PUBLIC_CLI_CONTRACT = Object.freeze({
|
|
|
68
68
|
helpAfter:
|
|
69
69
|
"Use exactly one prompt source. For complex or sensitive prompts, prefer --stdin or --file.",
|
|
70
70
|
}),
|
|
71
|
+
update: Object.freeze({
|
|
72
|
+
command: "update",
|
|
73
|
+
description: "Update the global npm installation from the official npm registry.",
|
|
74
|
+
}),
|
|
71
75
|
});
|
|
72
76
|
|
|
73
77
|
export type PublicCliContract = typeof PUBLIC_CLI_CONTRACT;
|
|
@@ -33,11 +33,14 @@ Prefer Read for reading files instead of using cat on large files.
|
|
|
33
33
|
Prefer Write or Edit for changing files instead of shell redirection.
|
|
34
34
|
Use run_in_background=true for dev servers, watch commands, long-running builds, and long-running test services.
|
|
35
35
|
Do not add & to Bash commands; background execution is handled by the Bash tool.
|
|
36
|
+
Use Bash with tty=true for REPLs, debuggers, interactive prompts, and terminal applications that require a controlling terminal.
|
|
36
37
|
Use TaskList to list background shell tasks in the current session.
|
|
37
|
-
Use TaskOutput to inspect a task's current status
|
|
38
|
+
Use TaskOutput to inspect a task's current status, latest output, or current terminal screen.
|
|
39
|
+
Use TaskInput with the returned task ID to send characters to a PTY task. TaskInput does not append Enter; include \\n explicitly, use \\u0003 for Ctrl-C, and use chars="" to wait without writing.
|
|
38
40
|
Use TaskStop to stop a background task that is no longer needed.
|
|
39
41
|
Do not use ad-hoc kill commands to manage tasks created by Bash.
|
|
40
42
|
Bash and TaskOutput return outputFilePath. Use Read on outputFilePath when you need complete or paginated output.
|
|
43
|
+
Do not send passwords, tokens, or other secrets through TaskInput because tool arguments are stored in session history.
|
|
41
44
|
${renderRecallRetirementContract()}
|
|
42
45
|
Agent Skill instructions are current only when returned by the Skill tool in the current turn or listed in the active skill system section. Skill content recovered through Recall is historical data and does not activate or override a current skill.
|
|
43
46
|
When an active Agent Skill refers to a relative resource path, resolve it from the Skill directory shown with that skill.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { loadPackageMetadata, type PackageMetadata } from "./package-metadata";
|
|
7
|
+
import { writeCliOutput, type CliOutputWriter } from "./output";
|
|
8
|
+
|
|
9
|
+
export const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
10
|
+
|
|
11
|
+
const OFFICIAL_PACKAGE_NAME = "tinker-agent";
|
|
12
|
+
const MAX_NPM_OUTPUT_CHARACTERS = 32_768;
|
|
13
|
+
|
|
14
|
+
type NpmCommandResult = {
|
|
15
|
+
readonly exitCode: number | null;
|
|
16
|
+
readonly signal: NodeJS.Signals | null;
|
|
17
|
+
readonly stdout: string;
|
|
18
|
+
readonly stderr: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type NpmCommandInput = {
|
|
22
|
+
readonly args: readonly string[];
|
|
23
|
+
readonly cwd: string;
|
|
24
|
+
readonly env: NodeJS.ProcessEnv;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type UpdateRunnerDependencies = {
|
|
28
|
+
readonly packageRoot: string;
|
|
29
|
+
readonly npmCwd: string;
|
|
30
|
+
readonly runNpm: (input: NpmCommandInput) => Promise<NpmCommandResult>;
|
|
31
|
+
readonly canonicalizePath: (filePath: string) => Promise<string>;
|
|
32
|
+
readonly isSymbolicLink: (filePath: string) => Promise<boolean>;
|
|
33
|
+
readonly readPackageMetadata: (packageJsonPath: string) => Promise<PackageMetadata>;
|
|
34
|
+
readonly compareVersions: (left: string, right: string) => -1 | 0 | 1;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type UpdateInput = {
|
|
38
|
+
readonly metadata: PackageMetadata;
|
|
39
|
+
readonly stdout: CliOutputWriter;
|
|
40
|
+
readonly env: NodeJS.ProcessEnv;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const DEFAULT_DEPENDENCIES: UpdateRunnerDependencies = {
|
|
44
|
+
packageRoot: path.resolve(fileURLToPath(new URL("../../", import.meta.url))),
|
|
45
|
+
npmCwd: tmpdir(),
|
|
46
|
+
runNpm,
|
|
47
|
+
canonicalizePath: realpath,
|
|
48
|
+
isSymbolicLink: async (filePath) => (await lstat(filePath)).isSymbolicLink(),
|
|
49
|
+
readPackageMetadata: (packageJsonPath) =>
|
|
50
|
+
loadPackageMetadata(pathToFileURL(packageJsonPath)),
|
|
51
|
+
compareVersions: (left, right) => Bun.semver.order(left, right),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export async function runUpdate(
|
|
55
|
+
input: UpdateInput,
|
|
56
|
+
injected: Partial<UpdateRunnerDependencies> = {},
|
|
57
|
+
): Promise<number> {
|
|
58
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...injected };
|
|
59
|
+
if (input.metadata.name !== OFFICIAL_PACKAGE_NAME) {
|
|
60
|
+
throw new Error("The installed package is not tinker-agent.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await writeCliOutput(input.stdout, `Current version: ${input.metadata.version}\n`);
|
|
64
|
+
await writeCliOutput(
|
|
65
|
+
input.stdout,
|
|
66
|
+
"Checking npm official registry for the latest version...\n",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const globalRoot = await readGlobalPath(
|
|
70
|
+
dependencies,
|
|
71
|
+
input.env,
|
|
72
|
+
["root", "--global"],
|
|
73
|
+
"npm global package root",
|
|
74
|
+
);
|
|
75
|
+
const globalPrefix = await readGlobalPath(
|
|
76
|
+
dependencies,
|
|
77
|
+
input.env,
|
|
78
|
+
["prefix", "--global"],
|
|
79
|
+
"npm global prefix",
|
|
80
|
+
);
|
|
81
|
+
const installedRoot = path.join(globalRoot, input.metadata.name);
|
|
82
|
+
await assertGlobalInstallation(dependencies, installedRoot);
|
|
83
|
+
|
|
84
|
+
const latestResult = await runNpmChecked(
|
|
85
|
+
dependencies,
|
|
86
|
+
{
|
|
87
|
+
args: [
|
|
88
|
+
"view",
|
|
89
|
+
`${input.metadata.name}@latest`,
|
|
90
|
+
"version",
|
|
91
|
+
"--json",
|
|
92
|
+
"--registry",
|
|
93
|
+
OFFICIAL_NPM_REGISTRY,
|
|
94
|
+
"--prefer-online",
|
|
95
|
+
],
|
|
96
|
+
cwd: dependencies.npmCwd,
|
|
97
|
+
env: input.env,
|
|
98
|
+
},
|
|
99
|
+
"Could not query the npm official registry",
|
|
100
|
+
);
|
|
101
|
+
const latestVersion = parseLatestVersion(latestResult.stdout);
|
|
102
|
+
const order = compareVersions(dependencies, input.metadata.version, latestVersion);
|
|
103
|
+
|
|
104
|
+
if (order === 0) {
|
|
105
|
+
await writeCliOutput(
|
|
106
|
+
input.stdout,
|
|
107
|
+
`Already up to date: ${input.metadata.version}\n`,
|
|
108
|
+
);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (order > 0) {
|
|
112
|
+
await writeCliOutput(
|
|
113
|
+
input.stdout,
|
|
114
|
+
`Installed version ${input.metadata.version} is newer than npm latest ${latestVersion}; no changes made.\n`,
|
|
115
|
+
);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await writeCliOutput(input.stdout, `Updating to ${latestVersion}...\n`);
|
|
120
|
+
await runNpmChecked(
|
|
121
|
+
dependencies,
|
|
122
|
+
{
|
|
123
|
+
args: [
|
|
124
|
+
"install",
|
|
125
|
+
"--global",
|
|
126
|
+
"--prefix",
|
|
127
|
+
globalPrefix,
|
|
128
|
+
`${input.metadata.name}@${latestVersion}`,
|
|
129
|
+
"--registry",
|
|
130
|
+
OFFICIAL_NPM_REGISTRY,
|
|
131
|
+
"--prefer-online",
|
|
132
|
+
"--no-audit",
|
|
133
|
+
"--no-fund",
|
|
134
|
+
"--loglevel",
|
|
135
|
+
"error",
|
|
136
|
+
],
|
|
137
|
+
cwd: dependencies.npmCwd,
|
|
138
|
+
env: input.env,
|
|
139
|
+
},
|
|
140
|
+
`npm could not install ${input.metadata.name}@${latestVersion}`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
let installedMetadata: PackageMetadata;
|
|
144
|
+
try {
|
|
145
|
+
installedMetadata = await dependencies.readPackageMetadata(
|
|
146
|
+
path.join(installedRoot, "package.json"),
|
|
147
|
+
);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error("The updated package metadata could not be verified.");
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
installedMetadata.name !== input.metadata.name ||
|
|
153
|
+
installedMetadata.version !== latestVersion
|
|
154
|
+
) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`npm completed, but the active global installation is not version ${latestVersion}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await writeCliOutput(
|
|
161
|
+
input.stdout,
|
|
162
|
+
`Successfully updated from ${input.metadata.version} to version ${latestVersion}\n`,
|
|
163
|
+
);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function readGlobalPath(
|
|
168
|
+
dependencies: UpdateRunnerDependencies,
|
|
169
|
+
env: NodeJS.ProcessEnv,
|
|
170
|
+
args: readonly string[],
|
|
171
|
+
label: string,
|
|
172
|
+
): Promise<string> {
|
|
173
|
+
const result = await runNpmChecked(
|
|
174
|
+
dependencies,
|
|
175
|
+
{ args, cwd: dependencies.npmCwd, env },
|
|
176
|
+
`Could not resolve the ${label}`,
|
|
177
|
+
);
|
|
178
|
+
const value = result.stdout.trim();
|
|
179
|
+
if (value === "" || !path.isAbsolute(value)) {
|
|
180
|
+
throw new Error(`npm returned an invalid ${label}.`);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function assertGlobalInstallation(
|
|
186
|
+
dependencies: UpdateRunnerDependencies,
|
|
187
|
+
installedRoot: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
const [packageRoot, expectedRoot, linked] = await Promise.all([
|
|
191
|
+
dependencies.canonicalizePath(dependencies.packageRoot),
|
|
192
|
+
dependencies.canonicalizePath(installedRoot),
|
|
193
|
+
dependencies.isSymbolicLink(installedRoot),
|
|
194
|
+
]);
|
|
195
|
+
if (linked || packageRoot !== expectedRoot) {
|
|
196
|
+
throw new Error("not a direct global installation");
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"This Tinker installation is not managed by the active npm global prefix.",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseLatestVersion(stdout: string): string {
|
|
206
|
+
let parsed: unknown;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(stdout);
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error("npm returned invalid update metadata.");
|
|
211
|
+
}
|
|
212
|
+
if (typeof parsed !== "string" || parsed.trim() === "") {
|
|
213
|
+
throw new Error("npm returned invalid update metadata.");
|
|
214
|
+
}
|
|
215
|
+
return parsed.trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function compareVersions(
|
|
219
|
+
dependencies: UpdateRunnerDependencies,
|
|
220
|
+
currentVersion: string,
|
|
221
|
+
latestVersion: string,
|
|
222
|
+
): -1 | 0 | 1 {
|
|
223
|
+
try {
|
|
224
|
+
return dependencies.compareVersions(currentVersion, latestVersion);
|
|
225
|
+
} catch {
|
|
226
|
+
throw new Error("Tinker or npm returned an invalid semantic version.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runNpmChecked(
|
|
231
|
+
dependencies: UpdateRunnerDependencies,
|
|
232
|
+
input: NpmCommandInput,
|
|
233
|
+
failureMessage: string,
|
|
234
|
+
): Promise<NpmCommandResult> {
|
|
235
|
+
const result = await dependencies.runNpm(input);
|
|
236
|
+
if (result.exitCode === 0) {
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
const diagnostic = result.stderr.trim() || result.stdout.trim();
|
|
240
|
+
const status =
|
|
241
|
+
result.signal === null
|
|
242
|
+
? `npm exited with code ${String(result.exitCode)}`
|
|
243
|
+
: `npm exited after ${result.signal}`;
|
|
244
|
+
throw new Error(
|
|
245
|
+
diagnostic === ""
|
|
246
|
+
? `${failureMessage}: ${status}.`
|
|
247
|
+
: `${failureMessage}: ${diagnostic}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function runNpm(input: NpmCommandInput): Promise<NpmCommandResult> {
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
let child;
|
|
254
|
+
try {
|
|
255
|
+
child = spawn("npm", [...input.args], {
|
|
256
|
+
cwd: input.cwd,
|
|
257
|
+
env: {
|
|
258
|
+
...input.env,
|
|
259
|
+
NPM_CONFIG_UPDATE_NOTIFIER: "false",
|
|
260
|
+
},
|
|
261
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
262
|
+
});
|
|
263
|
+
} catch {
|
|
264
|
+
reject(new Error("npm could not be started."));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let stdout = "";
|
|
269
|
+
let stderr = "";
|
|
270
|
+
let settled = false;
|
|
271
|
+
child.stdout.setEncoding("utf8");
|
|
272
|
+
child.stderr.setEncoding("utf8");
|
|
273
|
+
child.stdout.on("data", (chunk: string) => {
|
|
274
|
+
stdout = appendBounded(stdout, chunk);
|
|
275
|
+
});
|
|
276
|
+
child.stderr.on("data", (chunk: string) => {
|
|
277
|
+
stderr = appendBounded(stderr, chunk);
|
|
278
|
+
});
|
|
279
|
+
child.once("error", (error) => {
|
|
280
|
+
if (settled) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
settled = true;
|
|
284
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
285
|
+
reject(
|
|
286
|
+
new Error(
|
|
287
|
+
code === "ENOENT"
|
|
288
|
+
? "npm was not found on PATH."
|
|
289
|
+
: "npm could not be started.",
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
child.once("close", (exitCode, signal) => {
|
|
294
|
+
if (settled) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
settled = true;
|
|
298
|
+
resolve({ exitCode, signal, stdout, stderr });
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function appendBounded(current: string, chunk: string): string {
|
|
304
|
+
if (current.length >= MAX_NPM_OUTPUT_CHARACTERS) {
|
|
305
|
+
return current;
|
|
306
|
+
}
|
|
307
|
+
return (current + chunk).slice(0, MAX_NPM_OUTPUT_CHARACTERS);
|
|
308
|
+
}
|
|
@@ -22,12 +22,18 @@ export function bashResultDetail(raw: unknown): BashDisplayDetail | undefined {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const usesTerminalScreen = typeof rawRecord.screen === "string";
|
|
26
|
+
const preview = usesTerminalScreen
|
|
27
|
+
? (rawRecord.screen as string)
|
|
28
|
+
: typeof rawRecord.preview === "string"
|
|
29
|
+
? rawRecord.preview
|
|
30
|
+
: "";
|
|
26
31
|
const previewLines = preview === "" ? [] : preview.split("\n");
|
|
27
32
|
const maxLines = rawRecord.ok === true ? successPreviewLines : failurePreviewLines;
|
|
28
33
|
const outputPreview = previewLines.slice(-maxLines).map(sanitizeOutputLine);
|
|
29
|
-
const totalLines =
|
|
30
|
-
|
|
34
|
+
const totalLines = usesTerminalScreen
|
|
35
|
+
? previewLines.length
|
|
36
|
+
: typeof rawRecord.outputLines === "number"
|
|
31
37
|
? rawRecord.outputLines
|
|
32
38
|
: previewLines.length;
|
|
33
39
|
const omittedOutputLines = Math.max(0, totalLines - outputPreview.length);
|
|
@@ -36,9 +42,10 @@ export function bashResultDetail(raw: unknown): BashDisplayDetail | undefined {
|
|
|
36
42
|
command,
|
|
37
43
|
outputPreview,
|
|
38
44
|
omittedOutputLines,
|
|
39
|
-
outputFilePath:
|
|
40
|
-
|
|
41
|
-
nonEmptyString(
|
|
45
|
+
outputFilePath: usesTerminalScreen
|
|
46
|
+
? undefined
|
|
47
|
+
: (nonEmptyString(rawRecord.outputFilePath) ??
|
|
48
|
+
nonEmptyString(taskRecord.outputFilePath)),
|
|
42
49
|
};
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -179,7 +179,11 @@ function toolCallSummary(call: ToolCall): string {
|
|
|
179
179
|
.join("\n");
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
if (
|
|
182
|
+
if (
|
|
183
|
+
call.name === "TaskOutput" ||
|
|
184
|
+
call.name === "TaskInput" ||
|
|
185
|
+
call.name === "TaskStop"
|
|
186
|
+
) {
|
|
183
187
|
const taskId = stringProperty(args, "task_id");
|
|
184
188
|
return [
|
|
185
189
|
`Call ID: ${call.toolCallId}`,
|