tinker-agent 1.3.0 → 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 +17 -1
- package/README.md +217 -72
- package/bin/tinker.js +75 -25
- package/package.json +8 -3
- 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/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/model/model-context-profile.ts +0 -30
- 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/slash-commands.ts +76 -24
- package/src/tui/workspace-file-search.ts +78 -71
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { PUBLIC_CLI_CONTRACT } from "./public-cli-contract";
|
|
3
|
+
import type { PromptSource } from "./prompt-source";
|
|
4
|
+
import { CliUsageError, type CliCommandScope } from "./output";
|
|
5
|
+
|
|
6
|
+
export type CliCommand =
|
|
7
|
+
| { readonly type: "tui"; readonly profileName?: string }
|
|
8
|
+
| {
|
|
9
|
+
readonly type: "run";
|
|
10
|
+
readonly profileName?: string;
|
|
11
|
+
readonly promptSource: PromptSource;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type CommandLineResult =
|
|
15
|
+
| { readonly type: "command"; readonly command: CliCommand }
|
|
16
|
+
| {
|
|
17
|
+
readonly type: "terminal";
|
|
18
|
+
readonly stdout: string;
|
|
19
|
+
readonly stderr: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SUCCESSFUL_TERMINAL_CODES = new Set([
|
|
23
|
+
"commander.help",
|
|
24
|
+
"commander.helpDisplayed",
|
|
25
|
+
"commander.version",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export async function parseCommandLine(
|
|
29
|
+
args: readonly string[],
|
|
30
|
+
packageVersion: string,
|
|
31
|
+
): Promise<CommandLineResult> {
|
|
32
|
+
const scopeHint = preflightArgv(args);
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
let selectedCommand: CliCommand | undefined;
|
|
36
|
+
const contract = PUBLIC_CLI_CONTRACT;
|
|
37
|
+
|
|
38
|
+
const program = new Command()
|
|
39
|
+
.name(contract.name)
|
|
40
|
+
.description(contract.description)
|
|
41
|
+
.helpOption(contract.helpFlags)
|
|
42
|
+
.version(packageVersion, contract.versionFlags)
|
|
43
|
+
.option(contract.tui.profileOption.flags, contract.tui.profileOption.description)
|
|
44
|
+
.helpCommand(contract.helpCommand.command, contract.helpCommand.description)
|
|
45
|
+
.showHelpAfterError('Run "tinker --help" for usage.')
|
|
46
|
+
.showSuggestionAfterError(false)
|
|
47
|
+
.allowExcessArguments(false)
|
|
48
|
+
.enablePositionalOptions()
|
|
49
|
+
.exitOverride()
|
|
50
|
+
.configureOutput({
|
|
51
|
+
writeOut: (value) => {
|
|
52
|
+
stdout += value;
|
|
53
|
+
},
|
|
54
|
+
writeErr: (value) => {
|
|
55
|
+
stderr += value;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program.action(() => {
|
|
60
|
+
const { profile } = program.opts<{ profile?: string }>();
|
|
61
|
+
selectedCommand = Object.freeze({
|
|
62
|
+
type: "tui",
|
|
63
|
+
...(profile === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { profileName: validateProfile(profile, "root") }),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command(contract.run.command)
|
|
71
|
+
.description(contract.run.description)
|
|
72
|
+
.option(contract.run.profileOption.flags, contract.run.profileOption.description)
|
|
73
|
+
.option(contract.run.stdinOption.flags, contract.run.stdinOption.description)
|
|
74
|
+
.option(contract.run.fileOption.flags, contract.run.fileOption.description)
|
|
75
|
+
.addHelpText("after", `\n${contract.run.helpAfter}\n`)
|
|
76
|
+
.showHelpAfterError('Run "tinker run --help" for usage.')
|
|
77
|
+
.showSuggestionAfterError(false)
|
|
78
|
+
.allowExcessArguments(false)
|
|
79
|
+
.exitOverride()
|
|
80
|
+
.action(
|
|
81
|
+
(
|
|
82
|
+
prompt: string | undefined,
|
|
83
|
+
options: { profile?: string; stdin?: boolean; file?: string },
|
|
84
|
+
) => {
|
|
85
|
+
const sources: PromptSource[] = [];
|
|
86
|
+
if (prompt !== undefined) {
|
|
87
|
+
sources.push({ kind: "argument", value: prompt });
|
|
88
|
+
}
|
|
89
|
+
if (options.stdin === true) {
|
|
90
|
+
sources.push({ kind: "stdin" });
|
|
91
|
+
}
|
|
92
|
+
if (options.file !== undefined) {
|
|
93
|
+
if (options.file.length === 0) {
|
|
94
|
+
throw new CliUsageError("--file requires a non-empty path.", "run");
|
|
95
|
+
}
|
|
96
|
+
sources.push({ kind: "file", filePath: options.file });
|
|
97
|
+
}
|
|
98
|
+
if (sources.length === 0) {
|
|
99
|
+
throw new CliUsageError(
|
|
100
|
+
"Exactly one prompt source is required: [prompt], --stdin, or --file <path>.",
|
|
101
|
+
"run",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (sources.length > 1) {
|
|
105
|
+
throw new CliUsageError(
|
|
106
|
+
"Prompt sources are mutually exclusive: use [prompt], --stdin, or --file <path>.",
|
|
107
|
+
"run",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const promptSource = sources[0];
|
|
111
|
+
if (promptSource === undefined) {
|
|
112
|
+
throw new CliUsageError("A prompt source is required.", "run");
|
|
113
|
+
}
|
|
114
|
+
selectedCommand = Object.freeze({
|
|
115
|
+
type: "run",
|
|
116
|
+
...(options.profile === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { profileName: validateProfile(options.profile, "run") }),
|
|
119
|
+
promptSource,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await program.parseAsync([...args], { from: "user" });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof CliUsageError) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof CommanderError) {
|
|
131
|
+
if (SUCCESSFUL_TERMINAL_CODES.has(error.code)) {
|
|
132
|
+
return Object.freeze({ type: "terminal", stdout, stderr });
|
|
133
|
+
}
|
|
134
|
+
throw new CliUsageError(commanderErrorDetail(error), scopeHint);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (selectedCommand === undefined) {
|
|
140
|
+
throw new Error("Commander completed without selecting a command.");
|
|
141
|
+
}
|
|
142
|
+
const topLevelProfile = program.opts<{ profile?: string }>().profile;
|
|
143
|
+
if (selectedCommand.type === "run" && topLevelProfile !== undefined) {
|
|
144
|
+
throw new CliUsageError(
|
|
145
|
+
"The top-level --profile option only applies to the TUI; place --profile after run.",
|
|
146
|
+
"run",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({ type: "command", command: selectedCommand });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function preflightArgv(args: readonly string[]): CliCommandScope {
|
|
153
|
+
let scope: CliCommandScope = "root";
|
|
154
|
+
let rootBlocked = false;
|
|
155
|
+
let topProfileOccurrences = 0;
|
|
156
|
+
let runProfileOccurrences = 0;
|
|
157
|
+
let stdinOccurrences = 0;
|
|
158
|
+
let fileOccurrences = 0;
|
|
159
|
+
|
|
160
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
161
|
+
const token = args[index];
|
|
162
|
+
if (token === undefined || token === "--") {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
(!rootBlocked && scope === "root" && isRootTerminalOption(token)) ||
|
|
168
|
+
(scope === "run" && isRunTerminalOption(token))
|
|
169
|
+
) {
|
|
170
|
+
return scope;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (scope === "root" && !rootBlocked) {
|
|
174
|
+
const profile = readOptionOccurrence(
|
|
175
|
+
args,
|
|
176
|
+
index,
|
|
177
|
+
token,
|
|
178
|
+
"--profile",
|
|
179
|
+
"root",
|
|
180
|
+
"-p",
|
|
181
|
+
);
|
|
182
|
+
if (profile !== undefined) {
|
|
183
|
+
topProfileOccurrences += 1;
|
|
184
|
+
assertSingleOccurrence("--profile", topProfileOccurrences, "root");
|
|
185
|
+
validateProfile(profile.value, "root");
|
|
186
|
+
index += profile.consumedNext ? 1 : 0;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (token.startsWith("-")) {
|
|
190
|
+
rootBlocked = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (token === "run") {
|
|
194
|
+
scope = "run";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (token === "help") {
|
|
198
|
+
return "root";
|
|
199
|
+
}
|
|
200
|
+
throw new CliUsageError(`unknown command '${token}'`, "root");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (scope !== "run") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const profile = readOptionOccurrence(args, index, token, "--profile", "run", "-p");
|
|
207
|
+
if (profile !== undefined) {
|
|
208
|
+
runProfileOccurrences += 1;
|
|
209
|
+
assertSingleOccurrence("--profile", runProfileOccurrences, "run");
|
|
210
|
+
validateProfile(profile.value, "run");
|
|
211
|
+
index += profile.consumedNext ? 1 : 0;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const file = readOptionOccurrence(args, index, token, "--file", "run");
|
|
215
|
+
if (file !== undefined) {
|
|
216
|
+
fileOccurrences += 1;
|
|
217
|
+
assertSingleOccurrence("--file", fileOccurrences, "run");
|
|
218
|
+
if (file.value.length === 0) {
|
|
219
|
+
throw new CliUsageError("--file requires a non-empty path.", "run");
|
|
220
|
+
}
|
|
221
|
+
index += file.consumedNext ? 1 : 0;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (token === "--stdin") {
|
|
225
|
+
stdinOccurrences += 1;
|
|
226
|
+
assertSingleOccurrence("--stdin", stdinOccurrences, "run");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return scope;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function readOptionOccurrence(
|
|
233
|
+
args: readonly string[],
|
|
234
|
+
index: number,
|
|
235
|
+
token: string,
|
|
236
|
+
longName: string,
|
|
237
|
+
scope: CliCommandScope,
|
|
238
|
+
shortName?: string,
|
|
239
|
+
): { readonly value: string; readonly consumedNext: boolean } | undefined {
|
|
240
|
+
if (token === longName || token === shortName) {
|
|
241
|
+
const next = args[index + 1];
|
|
242
|
+
if (next === undefined || next.startsWith("-")) {
|
|
243
|
+
throw new CliUsageError(`option '${token}' argument missing`, scope);
|
|
244
|
+
}
|
|
245
|
+
return { value: next, consumedNext: true };
|
|
246
|
+
}
|
|
247
|
+
const longPrefix = `${longName}=`;
|
|
248
|
+
if (token.startsWith(longPrefix)) {
|
|
249
|
+
return { value: token.slice(longPrefix.length), consumedNext: false };
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
shortName !== undefined &&
|
|
253
|
+
token.startsWith(shortName) &&
|
|
254
|
+
token !== shortName &&
|
|
255
|
+
!token.startsWith("--")
|
|
256
|
+
) {
|
|
257
|
+
return { value: token.slice(shortName.length), consumedNext: false };
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateProfile(value: string, scope: CliCommandScope): string {
|
|
263
|
+
if (value.trim() === "") {
|
|
264
|
+
throw new CliUsageError("--profile requires a non-empty value.", scope);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function assertSingleOccurrence(
|
|
270
|
+
option: string,
|
|
271
|
+
count: number,
|
|
272
|
+
scope: CliCommandScope,
|
|
273
|
+
): void {
|
|
274
|
+
if (count > 1) {
|
|
275
|
+
throw new CliUsageError(`${option} may only be specified once.`, scope);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isRootTerminalOption(token: string): boolean {
|
|
280
|
+
return (
|
|
281
|
+
token === "--help" || token === "-h" || token === "--version" || token === "-V"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isRunTerminalOption(token: string): boolean {
|
|
286
|
+
return token === "--help" || token === "-h";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function commanderErrorDetail(error: CommanderError): string {
|
|
290
|
+
return error.message.replace(/^error:\s*/u, "");
|
|
291
|
+
}
|