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.
- package/CHANGELOG.md +26 -1
- package/README.md +217 -72
- package/bin/tinker.js +75 -25
- package/package.json +8 -3
- package/src/agent/loop.ts +115 -11
- package/src/agent/runtime-session.ts +34 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +131 -264
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +143 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +650 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +100 -0
- package/src/cli/tui-runner.tsx +52 -49
- package/src/events/observation-text-log.ts +2 -0
- package/src/events/stdout-event-printer.ts +7 -1
- package/src/events/types.ts +27 -3
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/model/model-client.ts +29 -0
- package/src/model/model-context-profile.ts +0 -30
- package/src/model/openai-chat-mapping.ts +54 -15
- package/src/model/openai-chat-model-client.ts +46 -27
- package/src/model/openai-chat-stream.ts +6 -2
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +15 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/event-store.ts +24 -7
- package/src/tui/slash-commands.ts +76 -24
- 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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ name: "
|
|
20
|
-
{
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
if (error === null) {
|
|
57
|
+
resolve(splitPaths(stdout));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
const execError = error as Error & {
|
|
62
|
+
code?: number | string;
|
|
63
|
+
killed?: boolean;
|
|
64
|
+
signal?: string | null;
|
|
65
|
+
};
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
if (execError.code === 1 && stderr.trim() === "") {
|
|
68
|
+
resolve(splitPaths(stdout));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
if (execError.code === "ENOENT") {
|
|
73
|
+
reject(new Error(RIPGREP_MISSING_ERROR));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
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
|
-
|
|
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
|