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,650 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
3
|
+
|
|
4
|
+
export type PublicConfigValueKind = "non-empty-string" | "positive-integer" | "boolean";
|
|
5
|
+
|
|
6
|
+
export type PublicConfigField = {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly valueKind: PublicConfigValueKind;
|
|
9
|
+
readonly requiredIn: "always" | "env-mode" | "never";
|
|
10
|
+
readonly appliesIn: "always" | "env-mode";
|
|
11
|
+
readonly defaultValue?: string | number | boolean;
|
|
12
|
+
readonly defaultSource?: "process-cwd" | "bundled-ripgrep";
|
|
13
|
+
readonly secret: boolean;
|
|
14
|
+
readonly section: "model" | "workspace" | "tooling";
|
|
15
|
+
readonly description: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function publicField<const T extends PublicConfigField>(field: T): Readonly<T> {
|
|
19
|
+
return Object.freeze(field);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
23
|
+
publicField({
|
|
24
|
+
name: "TINKER_MODELS",
|
|
25
|
+
valueKind: "non-empty-string",
|
|
26
|
+
requiredIn: "never",
|
|
27
|
+
appliesIn: "always",
|
|
28
|
+
secret: false,
|
|
29
|
+
section: "model",
|
|
30
|
+
description:
|
|
31
|
+
"Optional model profiles JSON path. Relative paths resolve from the process cwd.",
|
|
32
|
+
}),
|
|
33
|
+
publicField({
|
|
34
|
+
name: "TINKER_MODEL",
|
|
35
|
+
valueKind: "non-empty-string",
|
|
36
|
+
requiredIn: "env-mode",
|
|
37
|
+
appliesIn: "env-mode",
|
|
38
|
+
secret: false,
|
|
39
|
+
section: "model",
|
|
40
|
+
description: "Model name used when model profiles are not configured.",
|
|
41
|
+
}),
|
|
42
|
+
publicField({
|
|
43
|
+
name: "TINKER_BASE_URL",
|
|
44
|
+
valueKind: "non-empty-string",
|
|
45
|
+
requiredIn: "env-mode",
|
|
46
|
+
appliesIn: "env-mode",
|
|
47
|
+
secret: false,
|
|
48
|
+
section: "model",
|
|
49
|
+
description: "OpenAI-compatible Chat Completions API base URL.",
|
|
50
|
+
}),
|
|
51
|
+
publicField({
|
|
52
|
+
name: "TINKER_API_KEY",
|
|
53
|
+
valueKind: "non-empty-string",
|
|
54
|
+
requiredIn: "env-mode",
|
|
55
|
+
appliesIn: "env-mode",
|
|
56
|
+
secret: true,
|
|
57
|
+
section: "model",
|
|
58
|
+
description: "API credential for the configured model endpoint.",
|
|
59
|
+
}),
|
|
60
|
+
publicField({
|
|
61
|
+
name: "TINKER_CONTEXT_WINDOW_TOKENS",
|
|
62
|
+
valueKind: "positive-integer",
|
|
63
|
+
requiredIn: "env-mode",
|
|
64
|
+
appliesIn: "env-mode",
|
|
65
|
+
secret: false,
|
|
66
|
+
section: "model",
|
|
67
|
+
description: "Model context-window size in tokens.",
|
|
68
|
+
}),
|
|
69
|
+
publicField({
|
|
70
|
+
name: "TINKER_MAX_SUPPORTED_OUTPUT_TOKENS",
|
|
71
|
+
valueKind: "positive-integer",
|
|
72
|
+
requiredIn: "env-mode",
|
|
73
|
+
appliesIn: "env-mode",
|
|
74
|
+
secret: false,
|
|
75
|
+
section: "model",
|
|
76
|
+
description:
|
|
77
|
+
"Maximum output-token count supported by the model; must not exceed the context window.",
|
|
78
|
+
}),
|
|
79
|
+
publicField({
|
|
80
|
+
name: "TINKER_INCLUDE_REASONING_CONTENT",
|
|
81
|
+
valueKind: "boolean",
|
|
82
|
+
requiredIn: "never",
|
|
83
|
+
appliesIn: "env-mode",
|
|
84
|
+
defaultValue: false,
|
|
85
|
+
secret: false,
|
|
86
|
+
section: "model",
|
|
87
|
+
description: "Include provider reasoning content in the model response mapping.",
|
|
88
|
+
}),
|
|
89
|
+
publicField({
|
|
90
|
+
name: "TINKER_STREAM",
|
|
91
|
+
valueKind: "boolean",
|
|
92
|
+
requiredIn: "never",
|
|
93
|
+
appliesIn: "env-mode",
|
|
94
|
+
defaultValue: true,
|
|
95
|
+
secret: false,
|
|
96
|
+
section: "model",
|
|
97
|
+
description: "Use streaming Chat Completions transport.",
|
|
98
|
+
}),
|
|
99
|
+
publicField({
|
|
100
|
+
name: "TINKER_WEBFETCH_REFINE_MODEL",
|
|
101
|
+
valueKind: "non-empty-string",
|
|
102
|
+
requiredIn: "never",
|
|
103
|
+
appliesIn: "env-mode",
|
|
104
|
+
secret: false,
|
|
105
|
+
section: "model",
|
|
106
|
+
description: "Optional WebFetch refiner model; currently must match TINKER_MODEL.",
|
|
107
|
+
}),
|
|
108
|
+
publicField({
|
|
109
|
+
name: "TINKER_WORKSPACE",
|
|
110
|
+
valueKind: "non-empty-string",
|
|
111
|
+
requiredIn: "never",
|
|
112
|
+
appliesIn: "always",
|
|
113
|
+
defaultSource: "process-cwd",
|
|
114
|
+
secret: false,
|
|
115
|
+
section: "workspace",
|
|
116
|
+
description: "Workspace path. Relative paths resolve from the process cwd.",
|
|
117
|
+
}),
|
|
118
|
+
publicField({
|
|
119
|
+
name: "TINKER_MAX_ITERATIONS",
|
|
120
|
+
valueKind: "positive-integer",
|
|
121
|
+
requiredIn: "never",
|
|
122
|
+
appliesIn: "always",
|
|
123
|
+
defaultValue: 512,
|
|
124
|
+
secret: false,
|
|
125
|
+
section: "workspace",
|
|
126
|
+
description: "Maximum agent-loop iterations per turn.",
|
|
127
|
+
}),
|
|
128
|
+
publicField({
|
|
129
|
+
name: "EXA_API_KEY",
|
|
130
|
+
valueKind: "non-empty-string",
|
|
131
|
+
requiredIn: "never",
|
|
132
|
+
appliesIn: "always",
|
|
133
|
+
secret: true,
|
|
134
|
+
section: "tooling",
|
|
135
|
+
description: "Enables WebSearch and the Exa WebFetch backend when set.",
|
|
136
|
+
}),
|
|
137
|
+
publicField({
|
|
138
|
+
name: "TINKER_MCP_TIMEOUT_MS",
|
|
139
|
+
valueKind: "positive-integer",
|
|
140
|
+
requiredIn: "never",
|
|
141
|
+
appliesIn: "always",
|
|
142
|
+
defaultValue: 60_000,
|
|
143
|
+
secret: false,
|
|
144
|
+
section: "tooling",
|
|
145
|
+
description: "MCP tool-call timeout in milliseconds.",
|
|
146
|
+
}),
|
|
147
|
+
publicField({
|
|
148
|
+
name: "TINKER_MCP_MAX_OBSERVATION_CHARS",
|
|
149
|
+
valueKind: "positive-integer",
|
|
150
|
+
requiredIn: "never",
|
|
151
|
+
appliesIn: "always",
|
|
152
|
+
defaultValue: 40_000,
|
|
153
|
+
secret: false,
|
|
154
|
+
section: "tooling",
|
|
155
|
+
description: "Maximum model-visible characters in one MCP result.",
|
|
156
|
+
}),
|
|
157
|
+
publicField({
|
|
158
|
+
name: "TINKER_BASH_DEFAULT_TIMEOUT_MS",
|
|
159
|
+
valueKind: "positive-integer",
|
|
160
|
+
requiredIn: "never",
|
|
161
|
+
appliesIn: "always",
|
|
162
|
+
defaultValue: 5_000,
|
|
163
|
+
secret: false,
|
|
164
|
+
section: "tooling",
|
|
165
|
+
description: "Default Bash foreground timeout in milliseconds.",
|
|
166
|
+
}),
|
|
167
|
+
publicField({
|
|
168
|
+
name: "TINKER_BASH_MAX_TIMEOUT_MS",
|
|
169
|
+
valueKind: "positive-integer",
|
|
170
|
+
requiredIn: "never",
|
|
171
|
+
appliesIn: "always",
|
|
172
|
+
defaultValue: 600_000,
|
|
173
|
+
secret: false,
|
|
174
|
+
section: "tooling",
|
|
175
|
+
description: "Maximum Bash foreground timeout in milliseconds.",
|
|
176
|
+
}),
|
|
177
|
+
publicField({
|
|
178
|
+
name: "TINKER_GREP_TIMEOUT_MS",
|
|
179
|
+
valueKind: "positive-integer",
|
|
180
|
+
requiredIn: "never",
|
|
181
|
+
appliesIn: "always",
|
|
182
|
+
defaultValue: 20_000,
|
|
183
|
+
secret: false,
|
|
184
|
+
section: "tooling",
|
|
185
|
+
description: "Bundled ripgrep invocation timeout in milliseconds.",
|
|
186
|
+
}),
|
|
187
|
+
publicField({
|
|
188
|
+
name: "TINKER_GREP_MAX_BUFFER_BYTES",
|
|
189
|
+
valueKind: "positive-integer",
|
|
190
|
+
requiredIn: "never",
|
|
191
|
+
appliesIn: "always",
|
|
192
|
+
defaultValue: 20_000_000,
|
|
193
|
+
secret: false,
|
|
194
|
+
section: "tooling",
|
|
195
|
+
description: "Maximum buffered output from one ripgrep invocation.",
|
|
196
|
+
}),
|
|
197
|
+
publicField({
|
|
198
|
+
name: "TINKER_WEBFETCH_REFINE_THRESHOLD",
|
|
199
|
+
valueKind: "positive-integer",
|
|
200
|
+
requiredIn: "never",
|
|
201
|
+
appliesIn: "always",
|
|
202
|
+
defaultValue: 2_000,
|
|
203
|
+
secret: false,
|
|
204
|
+
section: "tooling",
|
|
205
|
+
description: "Content-length threshold that enables WebFetch refinement.",
|
|
206
|
+
}),
|
|
207
|
+
publicField({
|
|
208
|
+
name: "TINKER_RIPGREP_PATH",
|
|
209
|
+
valueKind: "non-empty-string",
|
|
210
|
+
requiredIn: "never",
|
|
211
|
+
appliesIn: "always",
|
|
212
|
+
defaultSource: "bundled-ripgrep",
|
|
213
|
+
secret: false,
|
|
214
|
+
section: "tooling",
|
|
215
|
+
description: "Explicit diagnostic override for the bundled ripgrep executable.",
|
|
216
|
+
}),
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
export type ModelProfileField = {
|
|
220
|
+
readonly name: string;
|
|
221
|
+
readonly valueKind: PublicConfigValueKind | "input-modalities" | "token-estimator";
|
|
222
|
+
readonly required: boolean;
|
|
223
|
+
readonly defaultValue?: string | number | boolean | readonly string[];
|
|
224
|
+
readonly secret: boolean;
|
|
225
|
+
readonly description: string;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
function profileField<const T extends ModelProfileField>(field: T): Readonly<T> {
|
|
229
|
+
return Object.freeze(field);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const MODEL_PROFILE_FIELDS = Object.freeze([
|
|
233
|
+
profileField({
|
|
234
|
+
name: "model",
|
|
235
|
+
valueKind: "non-empty-string",
|
|
236
|
+
required: true,
|
|
237
|
+
secret: false,
|
|
238
|
+
description: "Provider model name.",
|
|
239
|
+
}),
|
|
240
|
+
profileField({
|
|
241
|
+
name: "apiBase",
|
|
242
|
+
valueKind: "non-empty-string",
|
|
243
|
+
required: true,
|
|
244
|
+
secret: false,
|
|
245
|
+
description: "OpenAI-compatible API base URL.",
|
|
246
|
+
}),
|
|
247
|
+
profileField({
|
|
248
|
+
name: "apiKey",
|
|
249
|
+
valueKind: "non-empty-string",
|
|
250
|
+
required: true,
|
|
251
|
+
secret: true,
|
|
252
|
+
description: "API credential for this profile.",
|
|
253
|
+
}),
|
|
254
|
+
profileField({
|
|
255
|
+
name: "contextWindowTokens",
|
|
256
|
+
valueKind: "positive-integer",
|
|
257
|
+
required: true,
|
|
258
|
+
secret: false,
|
|
259
|
+
description: "Model context-window size in tokens.",
|
|
260
|
+
}),
|
|
261
|
+
profileField({
|
|
262
|
+
name: "maxSupportedOutputTokens",
|
|
263
|
+
valueKind: "positive-integer",
|
|
264
|
+
required: true,
|
|
265
|
+
secret: false,
|
|
266
|
+
description:
|
|
267
|
+
"Maximum output-token count supported by the model; must not exceed contextWindowTokens.",
|
|
268
|
+
}),
|
|
269
|
+
profileField({
|
|
270
|
+
name: "includeReasoningContent",
|
|
271
|
+
valueKind: "boolean",
|
|
272
|
+
required: false,
|
|
273
|
+
defaultValue: false,
|
|
274
|
+
secret: false,
|
|
275
|
+
description: "Include provider reasoning content in response mapping.",
|
|
276
|
+
}),
|
|
277
|
+
profileField({
|
|
278
|
+
name: "stream",
|
|
279
|
+
valueKind: "boolean",
|
|
280
|
+
required: false,
|
|
281
|
+
defaultValue: true,
|
|
282
|
+
secret: false,
|
|
283
|
+
description: "Use streaming Chat Completions transport.",
|
|
284
|
+
}),
|
|
285
|
+
profileField({
|
|
286
|
+
name: "inputModalities",
|
|
287
|
+
valueKind: "input-modalities",
|
|
288
|
+
required: false,
|
|
289
|
+
defaultValue: Object.freeze(["text"] as const),
|
|
290
|
+
secret: false,
|
|
291
|
+
description:
|
|
292
|
+
'Accepted model input modalities; normalizes to ["text"] or ["text", "image"].',
|
|
293
|
+
}),
|
|
294
|
+
profileField({
|
|
295
|
+
name: "tokenEstimator",
|
|
296
|
+
valueKind: "token-estimator",
|
|
297
|
+
required: false,
|
|
298
|
+
secret: true,
|
|
299
|
+
description: "Independent token estimator required for image profiles.",
|
|
300
|
+
}),
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
export type ModelTokenEstimatorField = {
|
|
304
|
+
readonly name: string;
|
|
305
|
+
readonly valueKind: PublicConfigValueKind | "literal-string" | "literal-number";
|
|
306
|
+
readonly required: true;
|
|
307
|
+
readonly secret: boolean;
|
|
308
|
+
readonly literalValue?: string | number;
|
|
309
|
+
readonly minimum?: number;
|
|
310
|
+
readonly maximum?: number;
|
|
311
|
+
readonly description: string;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
function estimatorField<const T extends ModelTokenEstimatorField>(
|
|
315
|
+
field: T,
|
|
316
|
+
): Readonly<T> {
|
|
317
|
+
return Object.freeze(field);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const MODEL_TOKEN_ESTIMATOR_FIELDS = Object.freeze([
|
|
321
|
+
estimatorField({
|
|
322
|
+
name: "kind",
|
|
323
|
+
valueKind: "literal-string",
|
|
324
|
+
required: true,
|
|
325
|
+
secret: false,
|
|
326
|
+
literalValue: "moonshot-estimate-token-count-v1",
|
|
327
|
+
description: "Estimator protocol discriminator.",
|
|
328
|
+
}),
|
|
329
|
+
estimatorField({
|
|
330
|
+
name: "model",
|
|
331
|
+
valueKind: "non-empty-string",
|
|
332
|
+
required: true,
|
|
333
|
+
secret: false,
|
|
334
|
+
description: "Estimator model name.",
|
|
335
|
+
}),
|
|
336
|
+
estimatorField({
|
|
337
|
+
name: "apiBase",
|
|
338
|
+
valueKind: "non-empty-string",
|
|
339
|
+
required: true,
|
|
340
|
+
secret: false,
|
|
341
|
+
description: "Estimator API base URL.",
|
|
342
|
+
}),
|
|
343
|
+
estimatorField({
|
|
344
|
+
name: "apiKey",
|
|
345
|
+
valueKind: "non-empty-string",
|
|
346
|
+
required: true,
|
|
347
|
+
secret: true,
|
|
348
|
+
description: "Estimator API credential.",
|
|
349
|
+
}),
|
|
350
|
+
estimatorField({
|
|
351
|
+
name: "timeoutMs",
|
|
352
|
+
valueKind: "positive-integer",
|
|
353
|
+
required: true,
|
|
354
|
+
secret: false,
|
|
355
|
+
minimum: 1_000,
|
|
356
|
+
maximum: 60_000,
|
|
357
|
+
description: "Estimator request timeout in milliseconds.",
|
|
358
|
+
}),
|
|
359
|
+
estimatorField({
|
|
360
|
+
name: "maxRetries",
|
|
361
|
+
valueKind: "literal-number",
|
|
362
|
+
required: true,
|
|
363
|
+
secret: false,
|
|
364
|
+
literalValue: 0,
|
|
365
|
+
description: "Estimator retry count; retries are disabled.",
|
|
366
|
+
}),
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
export type ModelTokenEstimatorKind = Extract<
|
|
370
|
+
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number],
|
|
371
|
+
{ readonly name: "kind" }
|
|
372
|
+
>["literalValue"];
|
|
373
|
+
|
|
374
|
+
export type ModelTokenEstimatorMaxRetries = Extract<
|
|
375
|
+
(typeof MODEL_TOKEN_ESTIMATOR_FIELDS)[number],
|
|
376
|
+
{ readonly name: "maxRetries" }
|
|
377
|
+
>["literalValue"];
|
|
378
|
+
|
|
379
|
+
export const MODEL_PROFILES_DOCUMENT_FIELDS = Object.freeze([
|
|
380
|
+
Object.freeze({ name: "default", valueKind: "non-empty-string" as const }),
|
|
381
|
+
Object.freeze({ name: "profiles", valueKind: "profiles-map" as const }),
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
export type PublicToolingConfig = {
|
|
385
|
+
readonly exaApiKey?: string;
|
|
386
|
+
readonly mcpTimeoutMs: number;
|
|
387
|
+
readonly mcpMaxObservationChars: number;
|
|
388
|
+
readonly bashDefaultTimeoutMs: number;
|
|
389
|
+
readonly bashMaxTimeoutMs: number;
|
|
390
|
+
readonly grepTimeoutMs: number;
|
|
391
|
+
readonly grepMaxBufferBytes: number;
|
|
392
|
+
readonly webFetchRefineThreshold: number;
|
|
393
|
+
readonly ripgrepPath: string;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
type ParsedCommonEnvironment = {
|
|
397
|
+
readonly workspaceRoot: string;
|
|
398
|
+
readonly maxIterations: number;
|
|
399
|
+
readonly tooling: PublicToolingConfig;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export type ParsedPublicEnvironment =
|
|
403
|
+
| (ParsedCommonEnvironment & {
|
|
404
|
+
readonly mode: "profile";
|
|
405
|
+
readonly modelsPath: string;
|
|
406
|
+
})
|
|
407
|
+
| (ParsedCommonEnvironment & {
|
|
408
|
+
readonly mode: "env";
|
|
409
|
+
readonly modelName: string;
|
|
410
|
+
readonly apiBase: string;
|
|
411
|
+
readonly apiKey: string;
|
|
412
|
+
readonly contextWindowTokens: number;
|
|
413
|
+
readonly maxSupportedOutputTokens: number;
|
|
414
|
+
readonly includeReasoningContent: boolean;
|
|
415
|
+
readonly stream: boolean;
|
|
416
|
+
readonly webFetchRefineModel?: string;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
type PublicConfigFieldName = (typeof PUBLIC_CONFIG_FIELDS)[number]["name"];
|
|
420
|
+
type ParsedPrimitive = string | number | boolean | undefined;
|
|
421
|
+
|
|
422
|
+
const PUBLIC_CONFIG_FIELD_MAP = new Map<string, PublicConfigField>(
|
|
423
|
+
PUBLIC_CONFIG_FIELDS.map((field) => [field.name, field]),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
export const DEFAULT_PUBLIC_TOOLING_CONFIG: PublicToolingConfig = Object.freeze({
|
|
427
|
+
mcpTimeoutMs: defaultNumber("TINKER_MCP_TIMEOUT_MS"),
|
|
428
|
+
mcpMaxObservationChars: defaultNumber("TINKER_MCP_MAX_OBSERVATION_CHARS"),
|
|
429
|
+
bashDefaultTimeoutMs: defaultNumber("TINKER_BASH_DEFAULT_TIMEOUT_MS"),
|
|
430
|
+
bashMaxTimeoutMs: defaultNumber("TINKER_BASH_MAX_TIMEOUT_MS"),
|
|
431
|
+
grepTimeoutMs: defaultNumber("TINKER_GREP_TIMEOUT_MS"),
|
|
432
|
+
grepMaxBufferBytes: defaultNumber("TINKER_GREP_MAX_BUFFER_BYTES"),
|
|
433
|
+
webFetchRefineThreshold: defaultNumber("TINKER_WEBFETCH_REFINE_THRESHOLD"),
|
|
434
|
+
ripgrepPath: rgPath,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
export function parsePublicEnvironment(
|
|
438
|
+
env: NodeJS.ProcessEnv,
|
|
439
|
+
cwd: string,
|
|
440
|
+
): ParsedPublicEnvironment {
|
|
441
|
+
const configuredModelsPath = optionalRawString(env.TINKER_MODELS);
|
|
442
|
+
const mode = configuredModelsPath === undefined ? "env" : "profile";
|
|
443
|
+
const values = {} as Record<PublicConfigFieldName, ParsedPrimitive>;
|
|
444
|
+
|
|
445
|
+
for (const field of PUBLIC_CONFIG_FIELDS) {
|
|
446
|
+
if (field.appliesIn === "env-mode" && mode !== "env") {
|
|
447
|
+
values[field.name] = undefined;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
values[field.name] = parseEnvironmentField(field, env[field.name], mode, cwd);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const workspaceRoot = path.resolve(
|
|
454
|
+
cwd,
|
|
455
|
+
requiredStringValue(values, "TINKER_WORKSPACE"),
|
|
456
|
+
);
|
|
457
|
+
const maxIterations = requiredNumberValue(values, "TINKER_MAX_ITERATIONS");
|
|
458
|
+
const exaApiKey = optionalStringValue(values, "EXA_API_KEY");
|
|
459
|
+
const tooling = Object.freeze({
|
|
460
|
+
...(exaApiKey === undefined ? {} : { exaApiKey }),
|
|
461
|
+
mcpTimeoutMs: requiredNumberValue(values, "TINKER_MCP_TIMEOUT_MS"),
|
|
462
|
+
mcpMaxObservationChars: requiredNumberValue(
|
|
463
|
+
values,
|
|
464
|
+
"TINKER_MCP_MAX_OBSERVATION_CHARS",
|
|
465
|
+
),
|
|
466
|
+
bashDefaultTimeoutMs: requiredNumberValue(values, "TINKER_BASH_DEFAULT_TIMEOUT_MS"),
|
|
467
|
+
bashMaxTimeoutMs: requiredNumberValue(values, "TINKER_BASH_MAX_TIMEOUT_MS"),
|
|
468
|
+
grepTimeoutMs: requiredNumberValue(values, "TINKER_GREP_TIMEOUT_MS"),
|
|
469
|
+
grepMaxBufferBytes: requiredNumberValue(values, "TINKER_GREP_MAX_BUFFER_BYTES"),
|
|
470
|
+
webFetchRefineThreshold: requiredNumberValue(
|
|
471
|
+
values,
|
|
472
|
+
"TINKER_WEBFETCH_REFINE_THRESHOLD",
|
|
473
|
+
),
|
|
474
|
+
ripgrepPath: requiredStringValue(values, "TINKER_RIPGREP_PATH"),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (tooling.bashDefaultTimeoutMs > tooling.bashMaxTimeoutMs) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`TINKER_BASH_DEFAULT_TIMEOUT_MS must not exceed TINKER_BASH_MAX_TIMEOUT_MS; received ${tooling.bashDefaultTimeoutMs} > ${tooling.bashMaxTimeoutMs}.`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const common = { workspaceRoot, maxIterations, tooling };
|
|
484
|
+
if (mode === "profile") {
|
|
485
|
+
if (configuredModelsPath === undefined) {
|
|
486
|
+
throw new Error("Profile mode requires TINKER_MODELS.");
|
|
487
|
+
}
|
|
488
|
+
return Object.freeze({
|
|
489
|
+
...common,
|
|
490
|
+
mode,
|
|
491
|
+
modelsPath: path.isAbsolute(configuredModelsPath)
|
|
492
|
+
? configuredModelsPath
|
|
493
|
+
: path.resolve(cwd, configuredModelsPath),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const modelName = requiredStringValue(values, "TINKER_MODEL");
|
|
498
|
+
const webFetchRefineModel = optionalStringValue(
|
|
499
|
+
values,
|
|
500
|
+
"TINKER_WEBFETCH_REFINE_MODEL",
|
|
501
|
+
);
|
|
502
|
+
if (webFetchRefineModel !== undefined && webFetchRefineModel !== modelName) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`TINKER_WEBFETCH_REFINE_MODEL must match TINKER_MODEL in F2; received ${JSON.stringify(webFetchRefineModel)} for main model ${JSON.stringify(modelName)}.`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return Object.freeze({
|
|
509
|
+
...common,
|
|
510
|
+
mode,
|
|
511
|
+
modelName,
|
|
512
|
+
apiBase: requiredStringValue(values, "TINKER_BASE_URL"),
|
|
513
|
+
apiKey: requiredStringValue(values, "TINKER_API_KEY"),
|
|
514
|
+
contextWindowTokens: requiredNumberValue(values, "TINKER_CONTEXT_WINDOW_TOKENS"),
|
|
515
|
+
maxSupportedOutputTokens: requiredNumberValue(
|
|
516
|
+
values,
|
|
517
|
+
"TINKER_MAX_SUPPORTED_OUTPUT_TOKENS",
|
|
518
|
+
),
|
|
519
|
+
includeReasoningContent: requiredBooleanValue(
|
|
520
|
+
values,
|
|
521
|
+
"TINKER_INCLUDE_REASONING_CONTENT",
|
|
522
|
+
),
|
|
523
|
+
stream: requiredBooleanValue(values, "TINKER_STREAM"),
|
|
524
|
+
...(webFetchRefineModel === undefined ? {} : { webFetchRefineModel }),
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseEnvironmentField(
|
|
529
|
+
field: PublicConfigField,
|
|
530
|
+
rawValue: string | undefined,
|
|
531
|
+
mode: "env" | "profile",
|
|
532
|
+
cwd: string,
|
|
533
|
+
): ParsedPrimitive {
|
|
534
|
+
const normalized = rawValue?.trim();
|
|
535
|
+
const missing = normalized === undefined || normalized === "";
|
|
536
|
+
if (missing) {
|
|
537
|
+
if (
|
|
538
|
+
field.requiredIn === "always" ||
|
|
539
|
+
(field.requiredIn === "env-mode" && mode === "env")
|
|
540
|
+
) {
|
|
541
|
+
throw new Error(`${field.name} is required.`);
|
|
542
|
+
}
|
|
543
|
+
if (field.defaultValue !== undefined) {
|
|
544
|
+
return field.defaultValue;
|
|
545
|
+
}
|
|
546
|
+
if (field.defaultSource === "process-cwd") {
|
|
547
|
+
return cwd;
|
|
548
|
+
}
|
|
549
|
+
if (field.defaultSource === "bundled-ripgrep") {
|
|
550
|
+
return rgPath;
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (field.valueKind === "non-empty-string") {
|
|
556
|
+
return normalized;
|
|
557
|
+
}
|
|
558
|
+
if (field.valueKind === "positive-integer") {
|
|
559
|
+
if (!/^\d+$/.test(normalized)) {
|
|
560
|
+
throw positiveIntegerError(field.name, rawValue);
|
|
561
|
+
}
|
|
562
|
+
const parsed = Number(normalized);
|
|
563
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
564
|
+
throw positiveIntegerError(field.name, rawValue);
|
|
565
|
+
}
|
|
566
|
+
return parsed;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const booleanValue = parseBooleanValue(normalized);
|
|
570
|
+
if (booleanValue === undefined) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`${field.name} must be one of true/false, 1/0, yes/no, or on/off; received ${rawValue}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return booleanValue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function defaultNumber(name: PublicConfigFieldName): number {
|
|
579
|
+
const field = PUBLIC_CONFIG_FIELD_MAP.get(name);
|
|
580
|
+
if (field === undefined || typeof field.defaultValue !== "number") {
|
|
581
|
+
throw new Error(`Public config field ${name} does not declare a numeric default.`);
|
|
582
|
+
}
|
|
583
|
+
return field.defaultValue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function optionalRawString(value: string | undefined): string | undefined {
|
|
587
|
+
const normalized = value?.trim();
|
|
588
|
+
return normalized === undefined || normalized === "" ? undefined : normalized;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function optionalStringValue(
|
|
592
|
+
values: Record<PublicConfigFieldName, ParsedPrimitive>,
|
|
593
|
+
name: PublicConfigFieldName,
|
|
594
|
+
): string | undefined {
|
|
595
|
+
const value = values[name];
|
|
596
|
+
if (value === undefined || typeof value === "string") {
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
throw new Error(`Public config field ${name} did not resolve to a string.`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function requiredStringValue(
|
|
603
|
+
values: Record<PublicConfigFieldName, ParsedPrimitive>,
|
|
604
|
+
name: PublicConfigFieldName,
|
|
605
|
+
): string {
|
|
606
|
+
const value = optionalStringValue(values, name);
|
|
607
|
+
if (value === undefined) {
|
|
608
|
+
throw new Error(`Public config field ${name} did not resolve to a string.`);
|
|
609
|
+
}
|
|
610
|
+
return value;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function requiredNumberValue(
|
|
614
|
+
values: Record<PublicConfigFieldName, ParsedPrimitive>,
|
|
615
|
+
name: PublicConfigFieldName,
|
|
616
|
+
): number {
|
|
617
|
+
const value = values[name];
|
|
618
|
+
if (typeof value !== "number") {
|
|
619
|
+
throw new Error(`Public config field ${name} did not resolve to a number.`);
|
|
620
|
+
}
|
|
621
|
+
return value;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function requiredBooleanValue(
|
|
625
|
+
values: Record<PublicConfigFieldName, ParsedPrimitive>,
|
|
626
|
+
name: PublicConfigFieldName,
|
|
627
|
+
): boolean {
|
|
628
|
+
const value = values[name];
|
|
629
|
+
if (typeof value !== "boolean") {
|
|
630
|
+
throw new Error(`Public config field ${name} did not resolve to a boolean.`);
|
|
631
|
+
}
|
|
632
|
+
return value;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function parseBooleanValue(value: string): boolean | undefined {
|
|
636
|
+
const normalized = value.toLowerCase();
|
|
637
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function positiveIntegerError(name: string, value: string | undefined): Error {
|
|
647
|
+
return new Error(
|
|
648
|
+
`${name} must be a positive safe integer; received ${value ?? "undefined"}`,
|
|
649
|
+
);
|
|
650
|
+
}
|