tinker-agent 1.7.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 CHANGED
@@ -5,6 +5,18 @@ 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
+
8
20
  ## [1.7.0] - 2026-08-01
9
21
 
10
22
  ### Added
@@ -149,7 +161,8 @@ All notable user-facing changes to Tinker are documented here. The project follo
149
161
  - First formal npm release under the `tinker-agent` package name with the `tinker`
150
162
  executable.
151
163
 
152
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v1.7.0...HEAD
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
153
166
  [1.7.0]: https://github.com/ishowshao/tinker/releases/tag/v1.7.0
154
167
  [1.6.0]: https://github.com/ishowshao/tinker/releases/tag/v1.6.0
155
168
  [1.5.1]: https://github.com/ishowshao/tinker/releases/tag/v1.5.1
package/README.md CHANGED
@@ -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.7.0",
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",
@@ -93,6 +94,7 @@
93
94
  "markdansi": "0.3.2",
94
95
  "marked": "^18.0.5",
95
96
  "openai": "^7.1.0",
97
+ "puppeteer-core": "25.4.0",
96
98
  "react": "^19.2.7",
97
99
  "sharp": "^0.35.3",
98
100
  "shiki": "^4.3.1",
@@ -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,
@@ -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 === "run" && isRunTerminalOption(token))
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 isRunTerminalOption(token: string): boolean {
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 === "run"
28
- ? 'Run "tinker run --help" for usage.'
29
- : 'Run "tinker --help" for usage.';
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;
@@ -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
+ }
@@ -1,15 +1,24 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { pathToFileURL } from "node:url";
2
6
  import {
3
7
  StdioClientTransport,
4
8
  getDefaultEnvironment,
5
9
  } from "@modelcontextprotocol/sdk/client/stdio.js";
6
10
  import type { RuntimeSessionContext } from "../agent/runtime-session";
11
+ import { ListRootsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
12
  import type { ToolExecutor } from "../tools/types";
8
13
  import type { McpConfig, McpServerConfig } from "./mcp-config";
9
14
  import { createMcpToolExecutor } from "./mcp-tool-executor";
10
15
 
11
16
  const STDERR_TAIL_MAX_CHARS = 2_000;
12
17
 
18
+ function temporaryDirectoryEnvironment(): Record<string, string> {
19
+ return process.platform === "win32" ? { TEMP: tmpdir() } : { TMPDIR: tmpdir() };
20
+ }
21
+
13
22
  export type McpClientConnection = {
14
23
  client: Client;
15
24
  close(): Promise<void>;
@@ -18,6 +27,7 @@ export type McpClientConnection = {
18
27
  export type McpClientFactory = (
19
28
  serverName: string,
20
29
  serverConfig: McpServerConfig,
30
+ workspaceRoot: string,
21
31
  ) => Promise<McpClientConnection>;
22
32
 
23
33
  export type McpManager = {
@@ -37,6 +47,7 @@ export type McpServerInventory = {
37
47
 
38
48
  export type CreateMcpManagerOptions = {
39
49
  config: McpConfig;
50
+ workspaceRoot: string;
40
51
  runtimeSession: RuntimeSessionContext;
41
52
  clientFactory?: McpClientFactory;
42
53
  timeoutMs?: number;
@@ -70,7 +81,11 @@ export async function createMcpManager(
70
81
  let tools;
71
82
 
72
83
  try {
73
- connection = await clientFactory(serverName, serverConfig);
84
+ connection = await clientFactory(
85
+ serverName,
86
+ serverConfig,
87
+ options.workspaceRoot,
88
+ );
74
89
  } catch (error) {
75
90
  await options.runtimeSession.append({
76
91
  type: "mcp.server.failed",
@@ -227,11 +242,16 @@ async function closeConnections(
227
242
  async function stdioClientFactory(
228
243
  serverName: string,
229
244
  serverConfig: McpServerConfig,
245
+ workspaceRoot: string,
230
246
  ): Promise<McpClientConnection> {
231
247
  const transport = new StdioClientTransport({
232
248
  command: serverConfig.command,
233
249
  args: serverConfig.args,
234
- env: { ...getDefaultEnvironment(), ...serverConfig.env },
250
+ env: {
251
+ ...getDefaultEnvironment(),
252
+ ...temporaryDirectoryEnvironment(),
253
+ ...serverConfig.env,
254
+ },
235
255
  cwd: serverConfig.cwd,
236
256
  stderr: "pipe",
237
257
  });
@@ -241,7 +261,18 @@ async function stdioClientFactory(
241
261
  stderrTail = (stderrTail + chunk.toString("utf8")).slice(-STDERR_TAIL_MAX_CHARS);
242
262
  });
243
263
 
244
- const client = new Client({ name: "tinker", version: "0.1.0" });
264
+ const client = new Client(
265
+ { name: "tinker", version: "0.1.0" },
266
+ { capabilities: { roots: { listChanged: false } } },
267
+ );
268
+ client.setRequestHandler(ListRootsRequestSchema, () => ({
269
+ roots: [
270
+ {
271
+ uri: pathToFileURL(workspaceRoot).href,
272
+ name: path.basename(workspaceRoot),
273
+ },
274
+ ],
275
+ }));
245
276
 
246
277
  try {
247
278
  await client.connect(transport);
@@ -264,11 +264,7 @@ export class ShellTaskManager {
264
264
  }
265
265
 
266
266
  this.synchronizeTerminalState(task);
267
- if (
268
- task.mode === "pty" &&
269
- isTerminalStatus(task.status) &&
270
- task.finalScreen === undefined
271
- ) {
267
+ if (isTerminalStatus(task.status)) {
272
268
  await task.completion;
273
269
  } else {
274
270
  await task.terminalScreen?.flush();