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
@@ -1,24 +1,66 @@
1
1
  export type SlashCommand = {
2
- name: string;
3
- description: string;
2
+ readonly name: string;
3
+ readonly description: string;
4
+ readonly usage?: string;
4
5
  };
5
6
 
6
- export const SLASH_COMMANDS: readonly SlashCommand[] = [
7
- { name: "status", description: "Show session and context details" },
8
- { name: "skills", description: "Show available and active Agent Skills" },
9
- { name: "mcp", description: "Show MCP servers and runtime tools" },
7
+ export type BuiltInSlashCommand = SlashCommand & {
8
+ readonly usage: string;
9
+ };
10
+
11
+ export const SLASH_COMMANDS: readonly BuiltInSlashCommand[] = [
12
+ {
13
+ name: "status",
14
+ usage: "/status",
15
+ description: "Show session and context details",
16
+ },
17
+ {
18
+ name: "skills",
19
+ usage: "/skills",
20
+ description: "Show available and active Agent Skills",
21
+ },
22
+ {
23
+ name: "mcp",
24
+ usage: "/mcp",
25
+ description: "Show MCP servers and runtime tools",
26
+ },
10
27
  {
11
28
  name: "compact",
29
+ usage: "/compact [retire]",
12
30
  description: "Swap tool output or retire a cold history prefix",
13
31
  },
14
- { name: "clear", description: "Start a new session and clear conversation" },
15
- { name: "fork", description: "Clone the current session" },
16
- { name: "view", description: "View a local UTF-8 text file" },
17
- { name: "copy", description: "Copy the last response as Markdown" },
18
- { name: "model", description: "Switch model profile (new session)" },
19
- { name: "resume", description: "Choose or resume a session" },
20
- { name: "session", description: "Manage stored sessions" },
21
- { name: "quit", description: "Exit the TUI" },
32
+ {
33
+ name: "clear",
34
+ usage: "/clear",
35
+ description: "Start a new session and clear conversation",
36
+ },
37
+ { name: "fork", usage: "/fork", description: "Clone the current session" },
38
+ {
39
+ name: "view",
40
+ usage: "/view <path>",
41
+ description: "View a local UTF-8 text file",
42
+ },
43
+ {
44
+ name: "copy",
45
+ usage: "/copy",
46
+ description: "Copy the last response as Markdown",
47
+ },
48
+ {
49
+ name: "model",
50
+ usage: "/model [profile-name]",
51
+ description: "Switch model profile (new session)",
52
+ },
53
+ {
54
+ name: "resume",
55
+ usage: "/resume [session-id]",
56
+ description: "Choose or resume a session",
57
+ },
58
+ {
59
+ name: "session",
60
+ usage: "/session delete <session-id> --confirm",
61
+ description: "Manage stored sessions",
62
+ },
63
+ { name: "quit", usage: "/quit", description: "Exit the TUI" },
22
64
  ];
23
65
 
24
66
  export type ParsedSlashCommand =
@@ -48,12 +90,12 @@ export class SlashCommandError extends Error {
48
90
  export function parseSlashCommand(input: string): ParsedSlashCommand {
49
91
  const trimmed = input.trim();
50
92
  if (trimmed === "/view") {
51
- throw new SlashCommandError("Usage: /view <path>");
93
+ throw slashCommandUsageError("view");
52
94
  }
53
95
  if (trimmed.startsWith("/view ") || trimmed.startsWith("/view\t")) {
54
96
  const filePath = trimmed.slice(5).trim();
55
97
  if (filePath === "") {
56
- throw new SlashCommandError("Usage: /view <path>");
98
+ throw slashCommandUsageError("view");
57
99
  }
58
100
  return { type: "view", filePath };
59
101
  }
@@ -70,7 +112,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
70
112
  if (tokens.length === 1) {
71
113
  return { type: "mcp" };
72
114
  }
73
- throw new SlashCommandError("Usage: /mcp");
115
+ throw slashCommandUsageError("mcp");
74
116
  }
75
117
  if (command === "/compact") {
76
118
  if (tokens.length === 1) {
@@ -79,25 +121,25 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
79
121
  if (tokens.length === 2 && tokens[1] === "retire") {
80
122
  return { type: "compact_retire" };
81
123
  }
82
- throw new SlashCommandError("Usage: /compact [retire]");
124
+ throw slashCommandUsageError("compact");
83
125
  }
84
126
  if (command === "/clear") {
85
127
  if (tokens.length === 1) {
86
128
  return { type: "clear" };
87
129
  }
88
- throw new SlashCommandError("Usage: /clear");
130
+ throw slashCommandUsageError("clear");
89
131
  }
90
132
  if (command === "/fork") {
91
133
  if (tokens.length === 1) {
92
134
  return { type: "fork" };
93
135
  }
94
- throw new SlashCommandError("Usage: /fork");
136
+ throw slashCommandUsageError("fork");
95
137
  }
96
138
  if (command === "/copy") {
97
139
  if (tokens.length === 1) {
98
140
  return { type: "copy" };
99
141
  }
100
- throw new SlashCommandError("Usage: /copy");
142
+ throw slashCommandUsageError("copy");
101
143
  }
102
144
  if (command === "/quit" && tokens.length === 1) {
103
145
  return { type: "quit" };
@@ -109,7 +151,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
109
151
  if (tokens.length === 2) {
110
152
  return { type: "model_switch", profileName: tokens[1] };
111
153
  }
112
- throw new SlashCommandError("Usage: /model [profile-name]");
154
+ throw slashCommandUsageError("model");
113
155
  }
114
156
  if (command === "/resume") {
115
157
  if (tokens.length === 1) {
@@ -118,7 +160,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
118
160
  if (tokens.length === 2) {
119
161
  return { type: "resume", sessionId: parsePublicSessionId(tokens[1]) };
120
162
  }
121
- throw new SlashCommandError("Usage: /resume [session-id]");
163
+ throw slashCommandUsageError("resume");
122
164
  }
123
165
  if (command === "/session") {
124
166
  if (tokens.length === 4 && tokens[1] === "delete" && tokens[3] === "--confirm") {
@@ -127,7 +169,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
127
169
  sessionId: parsePublicSessionId(tokens[2]),
128
170
  };
129
171
  }
130
- throw new SlashCommandError("Usage: /session delete <session-id> --confirm");
172
+ throw slashCommandUsageError("session");
131
173
  }
132
174
  throw new SlashCommandError(`Unknown command: ${trimmed}`);
133
175
  }
@@ -167,4 +209,14 @@ function parsePublicSessionId(value: string): SessionId {
167
209
  throw new SlashCommandError(`Invalid session ID: ${value}`);
168
210
  }
169
211
  }
212
+
213
+ function slashCommandUsageError(
214
+ name: (typeof SLASH_COMMANDS)[number]["name"],
215
+ ): SlashCommandError {
216
+ const command = SLASH_COMMANDS.find((candidate) => candidate.name === name);
217
+ if (command === undefined) {
218
+ throw new Error(`Missing built-in slash command declaration for ${name}.`);
219
+ }
220
+ return new SlashCommandError(`Usage: ${command.usage}`);
221
+ }
170
222
  import { parseSessionId, type SessionId } from "../ids/runtime-id";
@@ -1,90 +1,97 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { RIPGREP_MISSING_ERROR, findRipgrepCommand } from "../tools/ripgrep";
3
-
4
- const FILE_SEARCH_TIMEOUT_MS = 20_000;
5
- const FILE_SEARCH_MAX_BUFFER_BYTES = 20_000_000;
3
+ import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
6
4
 
7
5
  export type WorkspaceFileLister = (
8
6
  workspaceRoot: string,
9
7
  signal: AbortSignal,
10
8
  ) => Promise<readonly string[]>;
11
9
 
12
- export const listWorkspaceFiles: WorkspaceFileLister = (workspaceRoot, signal) =>
13
- new Promise((resolve, reject) => {
14
- if (signal.aborted) {
15
- reject(new Error("Workspace file search was cancelled."));
16
- return;
17
- }
10
+ export type WorkspaceFileListerOptions = {
11
+ readonly command?: string;
12
+ readonly timeoutMs?: number;
13
+ readonly maxBufferBytes?: number;
14
+ };
15
+
16
+ export function createWorkspaceFileLister(
17
+ options: WorkspaceFileListerOptions = {},
18
+ ): WorkspaceFileLister {
19
+ const command = findRipgrepCommand(options.command);
20
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepTimeoutMs;
21
+ const maxBufferBytes =
22
+ options.maxBufferBytes ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepMaxBufferBytes;
23
+
24
+ return (workspaceRoot, signal) =>
25
+ new Promise((resolve, reject) => {
26
+ if (signal.aborted) {
27
+ reject(new Error("Workspace file search was cancelled."));
28
+ return;
29
+ }
18
30
 
19
- execFile(
20
- findRipgrepCommand(),
21
- [
22
- "--files",
23
- "--hidden",
24
- "--glob",
25
- "!**/node_modules/**",
26
- "--glob",
27
- "!**/.git/**",
28
- "--glob",
29
- "!**/.tinker/**",
30
- ],
31
- {
32
- cwd: workspaceRoot,
33
- encoding: "utf8",
34
- maxBuffer: FILE_SEARCH_MAX_BUFFER_BYTES,
35
- signal,
36
- timeout: FILE_SEARCH_TIMEOUT_MS,
37
- },
38
- (error, stdout, stderr) => {
39
- if (signal.aborted) {
40
- reject(error ?? new Error("Workspace file search was cancelled."));
41
- return;
42
- }
31
+ execFile(
32
+ command,
33
+ [
34
+ "--files",
35
+ "--hidden",
36
+ "--glob",
37
+ "!**/node_modules/**",
38
+ "--glob",
39
+ "!**/.git/**",
40
+ "--glob",
41
+ "!**/.tinker/**",
42
+ ],
43
+ {
44
+ cwd: workspaceRoot,
45
+ encoding: "utf8",
46
+ maxBuffer: maxBufferBytes,
47
+ signal,
48
+ timeout: timeoutMs,
49
+ },
50
+ (error, stdout, stderr) => {
51
+ if (signal.aborted) {
52
+ reject(error ?? new Error("Workspace file search was cancelled."));
53
+ return;
54
+ }
43
55
 
44
- if (error === null) {
45
- resolve(splitPaths(stdout));
46
- return;
47
- }
56
+ if (error === null) {
57
+ resolve(splitPaths(stdout));
58
+ return;
59
+ }
48
60
 
49
- const execError = error as Error & {
50
- code?: number | string;
51
- killed?: boolean;
52
- signal?: string | null;
53
- };
61
+ const execError = error as Error & {
62
+ code?: number | string;
63
+ killed?: boolean;
64
+ signal?: string | null;
65
+ };
54
66
 
55
- if (execError.code === 1 && stderr.trim() === "") {
56
- resolve(splitPaths(stdout));
57
- return;
58
- }
67
+ if (execError.code === 1 && stderr.trim() === "") {
68
+ resolve(splitPaths(stdout));
69
+ return;
70
+ }
59
71
 
60
- if (execError.code === "ENOENT") {
61
- reject(new Error(RIPGREP_MISSING_ERROR));
62
- return;
63
- }
72
+ if (execError.code === "ENOENT") {
73
+ reject(new Error(RIPGREP_MISSING_ERROR));
74
+ return;
75
+ }
64
76
 
65
- if (execError.killed === true || typeof execError.signal === "string") {
66
- reject(
67
- new Error(
68
- `Workspace file search timed out after ${FILE_SEARCH_TIMEOUT_MS}ms.`,
69
- ),
70
- );
71
- return;
72
- }
77
+ if (execError.killed === true || typeof execError.signal === "string") {
78
+ reject(new Error(`Workspace file search timed out after ${timeoutMs}ms.`));
79
+ return;
80
+ }
73
81
 
74
- if (execError.message.includes("maxBuffer")) {
75
- reject(
76
- new Error(
77
- `Workspace file list exceeded ${FILE_SEARCH_MAX_BUFFER_BYTES} bytes.`,
78
- ),
79
- );
80
- return;
81
- }
82
+ if (execError.message.includes("maxBuffer")) {
83
+ reject(new Error(`Workspace file list exceeded ${maxBufferBytes} bytes.`));
84
+ return;
85
+ }
86
+
87
+ const detail = stderr.trim() === "" ? execError.message : stderr.trim();
88
+ reject(new Error(`Workspace file search failed: ${detail}`));
89
+ },
90
+ );
91
+ });
92
+ }
82
93
 
83
- const detail = stderr.trim() === "" ? execError.message : stderr.trim();
84
- reject(new Error(`Workspace file search failed: ${detail}`));
85
- },
86
- );
87
- });
94
+ export const listWorkspaceFiles = createWorkspaceFileLister();
88
95
 
89
96
  function splitPaths(stdout: string): string[] {
90
97
  return stdout