@link-assistant/agent 0.22.9 → 0.23.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/README.md +8 -5
- package/package.json +3 -3
- package/src/cli/argv.ts +1 -1
- package/src/cli/continuous-mode.js +117 -28
- package/src/cli/defaults.ts +1 -62
- package/src/cli/input-queue.js +169 -5
- package/src/cli/model-config.js +70 -34
- package/src/cli/output.ts +1 -1
- package/src/cli/run-options.js +50 -12
- package/src/config/defaults.ts +170 -0
- package/src/index.js +84 -24
- package/src/provider/opencode-zen.ts +138 -0
- package/src/provider/provider.ts +63 -12
- package/src/tool/task.ts +4 -3
package/src/cli/model-config.js
CHANGED
|
@@ -8,9 +8,11 @@ import { Log } from '../util/log.ts';
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_PROVIDER_ID,
|
|
10
10
|
DEFAULT_MODEL_ID,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
getDefaultModel,
|
|
12
|
+
getDefaultModelParts,
|
|
13
|
+
getDefaultCompactionModel,
|
|
14
|
+
getDefaultCompactionModels,
|
|
15
|
+
getDefaultCompactionSafetyMarginPercent,
|
|
14
16
|
} from './defaults.ts';
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -20,12 +22,21 @@ import {
|
|
|
20
22
|
* @param {function} outputStatus - Function to output status messages
|
|
21
23
|
* @returns {Promise<{providerID: string, modelID: string}>}
|
|
22
24
|
*/
|
|
23
|
-
export async function parseModelConfig(
|
|
25
|
+
export async function parseModelConfig(
|
|
26
|
+
argv,
|
|
27
|
+
outputError,
|
|
28
|
+
outputStatus,
|
|
29
|
+
defaultOptions = {}
|
|
30
|
+
) {
|
|
31
|
+
const defaultModel = getDefaultModel(defaultOptions);
|
|
32
|
+
const { providerID: defaultProviderID, modelID: defaultModelID } =
|
|
33
|
+
getDefaultModelParts(defaultOptions);
|
|
34
|
+
|
|
24
35
|
// Safeguard: validate argv.model against process.argv to detect yargs/cache mismatch (#192, #196, #239)
|
|
25
36
|
// This is critical because yargs under Bun may fail to parse --model correctly,
|
|
26
37
|
// returning the default value instead of the user's CLI argument.
|
|
27
38
|
const cliModelArg = getModelFromProcessArgv();
|
|
28
|
-
let modelArg = argv.model;
|
|
39
|
+
let modelArg = argv.model ?? defaultModel;
|
|
29
40
|
|
|
30
41
|
// Diagnostic logging: always log raw argv sources when debugging model resolution (#239)
|
|
31
42
|
// Bun global installs may have different process.argv structure (oven-sh/bun#22157)
|
|
@@ -54,7 +65,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
54
65
|
// Always use CLI value when available, even if it matches yargs
|
|
55
66
|
// This ensures we use the actual CLI argument, not a cached/default yargs value
|
|
56
67
|
modelArg = cliModelArg;
|
|
57
|
-
} else if (modelArg ===
|
|
68
|
+
} else if (modelArg === defaultModel) {
|
|
58
69
|
// cliModelArg is null AND yargs returned the default — check if process.argv
|
|
59
70
|
// actually contains --model to detect silent yargs/Bun mismatch (#239)
|
|
60
71
|
const rawArgvStr = process.argv.join(' ');
|
|
@@ -74,7 +85,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
74
85
|
typeof globalThis.Bun !== 'undefined' && globalThis.Bun.argv
|
|
75
86
|
? globalThis.Bun.argv
|
|
76
87
|
: '(not available)',
|
|
77
|
-
defaultModel
|
|
88
|
+
defaultModel,
|
|
78
89
|
}));
|
|
79
90
|
}
|
|
80
91
|
}
|
|
@@ -111,38 +122,54 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
111
122
|
// fail immediately instead of silently falling back to a different model.
|
|
112
123
|
// However, if the model is the default (no --model CLI flag), warn but proceed (#239).
|
|
113
124
|
// The models.dev API may lag behind the provider's actual model availability.
|
|
114
|
-
const isDefaultModel = !cliModelArg;
|
|
125
|
+
const isDefaultModel = !cliModelArg && modelArg === defaultModel;
|
|
115
126
|
try {
|
|
116
127
|
const { Provider } = await import('../provider/provider.ts');
|
|
117
128
|
const s = await Provider.state();
|
|
118
129
|
const provider = s.providers[providerID];
|
|
119
130
|
if (provider && !provider.info.models[modelID]) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
const liveInfo = await Provider.refreshLiveModelInfo(
|
|
132
|
+
providerID,
|
|
133
|
+
modelID
|
|
134
|
+
);
|
|
135
|
+
if (liveInfo) {
|
|
136
|
+
Log.Default.info(() => ({
|
|
125
137
|
message:
|
|
126
|
-
'
|
|
138
|
+
'model not found in models.dev catalog but found in provider live endpoint',
|
|
127
139
|
providerID,
|
|
128
140
|
modelID,
|
|
129
|
-
availableModels,
|
|
130
141
|
}));
|
|
131
142
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
message:
|
|
136
|
-
'model not found in provider — refusing to proceed with explicit provider/model',
|
|
137
|
-
providerID,
|
|
138
|
-
modelID,
|
|
139
|
-
availableModels,
|
|
140
|
-
}));
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Model "${modelID}" not found in provider "${providerID}". ` +
|
|
143
|
-
`Available models include: ${availableModels.join(', ')}. ` +
|
|
144
|
-
`Use --model ${providerID}/<model-id> with a valid model, or omit the provider prefix for auto-resolution.`
|
|
143
|
+
const availableModels = Object.keys(provider.info.models).slice(
|
|
144
|
+
0,
|
|
145
|
+
10
|
|
145
146
|
);
|
|
147
|
+
if (isDefaultModel) {
|
|
148
|
+
// Default model not in models.dev catalog — warn but proceed (#239)
|
|
149
|
+
// The provider may still accept it; models.dev can lag behind actual availability.
|
|
150
|
+
Log.Default.warn(() => ({
|
|
151
|
+
message:
|
|
152
|
+
'default model not found in models.dev catalog — proceeding anyway',
|
|
153
|
+
providerID,
|
|
154
|
+
modelID,
|
|
155
|
+
availableModels,
|
|
156
|
+
}));
|
|
157
|
+
} else {
|
|
158
|
+
// User explicitly specified provider/model — fail with a clear error (#231)
|
|
159
|
+
// Silent fallback caused kimi-k2.5-free to be routed to minimax-m2.5-free
|
|
160
|
+
Log.Default.error(() => ({
|
|
161
|
+
message:
|
|
162
|
+
'model not found in provider — refusing to proceed with explicit provider/model',
|
|
163
|
+
providerID,
|
|
164
|
+
modelID,
|
|
165
|
+
availableModels,
|
|
166
|
+
}));
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Model "${modelID}" not found in provider "${providerID}". ` +
|
|
169
|
+
`Available models include: ${availableModels.join(', ')}. ` +
|
|
170
|
+
`Use --model ${providerID}/<model-id> with a valid model, or omit the provider prefix for auto-resolution.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
146
173
|
}
|
|
147
174
|
}
|
|
148
175
|
} catch (validationError) {
|
|
@@ -177,7 +204,8 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
177
204
|
const compactionModelResult = await parseCompactionModelConfig(
|
|
178
205
|
argv,
|
|
179
206
|
providerID,
|
|
180
|
-
modelID
|
|
207
|
+
modelID,
|
|
208
|
+
defaultOptions
|
|
181
209
|
);
|
|
182
210
|
|
|
183
211
|
// Handle --use-existing-claude-oauth option
|
|
@@ -206,7 +234,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
206
234
|
|
|
207
235
|
// If user specified the default model (DEFAULT_MODEL), switch to claude-oauth
|
|
208
236
|
// If user explicitly specified kilo or another provider, warn but respect their choice
|
|
209
|
-
if (providerID ===
|
|
237
|
+
if (providerID === defaultProviderID && modelID === defaultModelID) {
|
|
210
238
|
providerID = 'claude-oauth';
|
|
211
239
|
modelID = 'claude-sonnet-4-5';
|
|
212
240
|
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
|
@@ -290,20 +318,28 @@ async function resolveCompactionModelEntry(
|
|
|
290
318
|
* @see https://github.com/link-assistant/agent/issues/219
|
|
291
319
|
* @see https://github.com/link-assistant/agent/issues/232
|
|
292
320
|
*/
|
|
293
|
-
async function parseCompactionModelConfig(
|
|
321
|
+
async function parseCompactionModelConfig(
|
|
322
|
+
argv,
|
|
323
|
+
baseProviderID,
|
|
324
|
+
baseModelID,
|
|
325
|
+
defaultOptions = {}
|
|
326
|
+
) {
|
|
327
|
+
const defaultCompactionSafetyMarginPercent =
|
|
328
|
+
getDefaultCompactionSafetyMarginPercent(defaultOptions);
|
|
329
|
+
|
|
294
330
|
// Get safety margin from CLI
|
|
295
331
|
const cliSafetyMarginArg = getCompactionSafetyMarginFromProcessArgv();
|
|
296
332
|
const compactionSafetyMarginPercent = cliSafetyMarginArg
|
|
297
333
|
? parseInt(cliSafetyMarginArg, 10)
|
|
298
334
|
: (argv['compaction-safety-margin'] ??
|
|
299
|
-
|
|
335
|
+
defaultCompactionSafetyMarginPercent);
|
|
300
336
|
|
|
301
337
|
// Check for --compaction-models (cascade) first — it overrides --compaction-model
|
|
302
338
|
const cliCompactionModelsArg = getCompactionModelsFromProcessArgv();
|
|
303
339
|
const compactionModelsArg =
|
|
304
340
|
cliCompactionModelsArg ??
|
|
305
341
|
argv['compaction-models'] ??
|
|
306
|
-
|
|
342
|
+
getDefaultCompactionModels(defaultOptions);
|
|
307
343
|
|
|
308
344
|
// Parse the links notation sequence into an array of model names
|
|
309
345
|
const modelNames = parseLinksNotationSequence(compactionModelsArg);
|
|
@@ -363,7 +399,7 @@ async function parseCompactionModelConfig(argv, baseProviderID, baseModelID) {
|
|
|
363
399
|
const compactionModelArg =
|
|
364
400
|
cliCompactionModelArg ??
|
|
365
401
|
argv['compaction-model'] ??
|
|
366
|
-
|
|
402
|
+
getDefaultCompactionModel(defaultOptions);
|
|
367
403
|
|
|
368
404
|
const resolved = await resolveCompactionModelEntry(
|
|
369
405
|
compactionModelArg,
|
package/src/cli/output.ts
CHANGED
package/src/cli/run-options.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
getDefaultModel,
|
|
3
|
+
getDefaultCompactionModel,
|
|
4
|
+
getDefaultCompactionModels,
|
|
5
|
+
getDefaultCompactionSafetyMarginPercent,
|
|
6
6
|
} from './defaults.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Yargs builder for the default `run` command options.
|
|
10
10
|
* Extracted from index.js to keep file size under 1000 lines.
|
|
11
11
|
*/
|
|
12
|
-
export function buildRunOptions(yargs) {
|
|
13
|
-
|
|
12
|
+
export function buildRunOptions(yargs, defaultOptions = {}) {
|
|
13
|
+
const defaultModel = getDefaultModel(defaultOptions);
|
|
14
|
+
const defaultCompactionModel = getDefaultCompactionModel(defaultOptions);
|
|
15
|
+
const defaultCompactionModels = getDefaultCompactionModels(defaultOptions);
|
|
16
|
+
const defaultCompactionSafetyMarginPercent =
|
|
17
|
+
getDefaultCompactionSafetyMarginPercent(defaultOptions);
|
|
18
|
+
|
|
19
|
+
const parser = yargs
|
|
14
20
|
.option('model', {
|
|
15
21
|
type: 'string',
|
|
16
22
|
description: 'Model to use in format providerID/modelID',
|
|
17
|
-
default:
|
|
23
|
+
default: defaultModel,
|
|
18
24
|
})
|
|
19
25
|
.option('json-standard', {
|
|
20
26
|
type: 'string',
|
|
@@ -23,6 +29,19 @@ export function buildRunOptions(yargs) {
|
|
|
23
29
|
default: 'opencode',
|
|
24
30
|
choices: ['opencode', 'claude'],
|
|
25
31
|
})
|
|
32
|
+
.option('output-format', {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description:
|
|
35
|
+
'Claude-compatible output format alias: "json" (OpenCode JSON) or "stream-json" (Claude NDJSON)',
|
|
36
|
+
choices: ['json', 'stream-json'],
|
|
37
|
+
})
|
|
38
|
+
.option('input-format', {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description:
|
|
41
|
+
'Input format: "text" (default) or Claude-compatible "stream-json" JSONL frames',
|
|
42
|
+
default: 'text',
|
|
43
|
+
choices: ['text', 'stream-json'],
|
|
44
|
+
})
|
|
26
45
|
.option('system-message', {
|
|
27
46
|
type: 'string',
|
|
28
47
|
description: 'Full override of the system message',
|
|
@@ -120,7 +139,26 @@ export function buildRunOptions(yargs) {
|
|
|
120
139
|
description:
|
|
121
140
|
'When used with --resume or --continue, continue in the same session without forking to a new UUID.',
|
|
122
141
|
default: false,
|
|
123
|
-
})
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const normalizedParser =
|
|
145
|
+
typeof parser.middleware === 'function'
|
|
146
|
+
? parser.middleware((argv) => {
|
|
147
|
+
if (argv.fork === false) {
|
|
148
|
+
argv['no-fork'] = true;
|
|
149
|
+
argv.noFork = true;
|
|
150
|
+
}
|
|
151
|
+
if (argv.outputFormat === 'stream-json') {
|
|
152
|
+
argv['json-standard'] = 'claude';
|
|
153
|
+
argv.jsonStandard = 'claude';
|
|
154
|
+
} else if (argv.outputFormat === 'json') {
|
|
155
|
+
argv['json-standard'] = 'opencode';
|
|
156
|
+
argv.jsonStandard = 'opencode';
|
|
157
|
+
}
|
|
158
|
+
}, true)
|
|
159
|
+
: parser;
|
|
160
|
+
|
|
161
|
+
return normalizedParser
|
|
124
162
|
.option('generate-title', {
|
|
125
163
|
type: 'boolean',
|
|
126
164
|
description:
|
|
@@ -153,7 +191,7 @@ export function buildRunOptions(yargs) {
|
|
|
153
191
|
type: 'string',
|
|
154
192
|
description:
|
|
155
193
|
'Model to use for context compaction in format providerID/modelID. Use "same" to use the base model. Default: opencode/gpt-5-nano (free, 400K context). Overridden by --compaction-models if both are specified.',
|
|
156
|
-
default:
|
|
194
|
+
default: defaultCompactionModel,
|
|
157
195
|
})
|
|
158
196
|
.option('compaction-models', {
|
|
159
197
|
type: 'string',
|
|
@@ -161,13 +199,13 @@ export function buildRunOptions(yargs) {
|
|
|
161
199
|
'Ordered cascade of compaction models in links notation sequence format: "(model1 model2 ... same)". ' +
|
|
162
200
|
"Models are tried from smallest/cheapest context to largest. If used context exceeds a model's limit or its rate limit is reached, the next model is tried. " +
|
|
163
201
|
'The special value "same" uses the base model. Overrides --compaction-model when specified.',
|
|
164
|
-
default:
|
|
202
|
+
default: defaultCompactionModels,
|
|
165
203
|
})
|
|
166
204
|
.option('compaction-safety-margin', {
|
|
167
205
|
type: 'number',
|
|
168
206
|
description:
|
|
169
|
-
'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default:
|
|
170
|
-
default:
|
|
207
|
+
'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default: 25.',
|
|
208
|
+
default: defaultCompactionSafetyMarginPercent,
|
|
171
209
|
})
|
|
172
210
|
.option('temperature', {
|
|
173
211
|
type: 'number',
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global default model configuration shared by runtime code and tests.
|
|
3
|
+
*
|
|
4
|
+
* Keep the hard-coded defaults here. Runtime helpers allow test runs and
|
|
5
|
+
* local automation to override those defaults without editing source files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type EnvLike = Record<string, string | undefined>;
|
|
9
|
+
|
|
10
|
+
export interface DefaultConfigOptions {
|
|
11
|
+
env?: EnvLike;
|
|
12
|
+
defaultModel?: string | null;
|
|
13
|
+
defaultCompactionModel?: string | null;
|
|
14
|
+
defaultCompactionModels?: string | null;
|
|
15
|
+
defaultCompactionSafetyMarginPercent?: number | string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Default model used when no `--model` CLI argument is provided. */
|
|
19
|
+
export const DEFAULT_MODEL = 'opencode/minimax-m2.5-free';
|
|
20
|
+
|
|
21
|
+
/** Env var for overriding the default model in test runs and automation. */
|
|
22
|
+
export const DEFAULT_MODEL_ENV = 'LINK_ASSISTANT_AGENT_DEFAULT_MODEL';
|
|
23
|
+
|
|
24
|
+
/** Default compaction model used when no `--compaction-model` CLI argument is provided. */
|
|
25
|
+
export const DEFAULT_COMPACTION_MODEL = 'opencode/gpt-5-nano';
|
|
26
|
+
|
|
27
|
+
/** Env var for overriding the default compaction model in test runs and automation. */
|
|
28
|
+
export const DEFAULT_COMPACTION_MODEL_ENV =
|
|
29
|
+
'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODEL';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default compaction models cascade, ordered from smallest/cheapest context to largest.
|
|
33
|
+
* During compaction, the system tries each model in order. If the used context exceeds
|
|
34
|
+
* a model's context limit, it skips to the next larger model. If a model's rate limit
|
|
35
|
+
* is reached, it also skips to the next model.
|
|
36
|
+
* The special value "same" means use the same model as `--model`.
|
|
37
|
+
*
|
|
38
|
+
* Parsed as links notation references sequence (single anonymous link):
|
|
39
|
+
* "(big-pickle minimax-m2.5-free nemotron-3-super-free hy3-preview-free ling-2.6-flash-free gpt-5-nano same)"
|
|
40
|
+
*
|
|
41
|
+
* Context limits (approximate):
|
|
42
|
+
* big-pickle: ~200K
|
|
43
|
+
* minimax-m2.5-free: ~204K
|
|
44
|
+
* nemotron-3-super-free: ~204K
|
|
45
|
+
* hy3-preview-free: ~256K
|
|
46
|
+
* ling-2.6-flash-free: ~262K
|
|
47
|
+
* gpt-5-nano: ~400K
|
|
48
|
+
* same: (base model's context)
|
|
49
|
+
*
|
|
50
|
+
* Note: qwen3.6-plus-free was removed because the free promotion ended in April 2026.
|
|
51
|
+
* Note: minimax-m2.5-free is the default model again as of issue #266.
|
|
52
|
+
* @see https://github.com/link-assistant/agent/issues/266
|
|
53
|
+
* @see https://github.com/link-assistant/agent/issues/242
|
|
54
|
+
* @see https://github.com/link-assistant/agent/issues/232
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_COMPACTION_MODELS =
|
|
57
|
+
'(big-pickle minimax-m2.5-free nemotron-3-super-free hy3-preview-free ling-2.6-flash-free gpt-5-nano same)';
|
|
58
|
+
|
|
59
|
+
/** Env var for overriding the default compaction cascade in test runs and automation. */
|
|
60
|
+
export const DEFAULT_COMPACTION_MODELS_ENV =
|
|
61
|
+
'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODELS';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Default compaction safety margin as a percentage of usable context window.
|
|
65
|
+
* Applied only when the compaction model has a context window equal to or smaller
|
|
66
|
+
* than the base model. When the compaction model has a larger context, the margin
|
|
67
|
+
* is automatically set to 0 (allowing 100% context usage).
|
|
68
|
+
*
|
|
69
|
+
* Increased from 15% to 25% to reduce probability of context overflow errors,
|
|
70
|
+
* especially when providers return inaccurate or zero token counts.
|
|
71
|
+
* Matches OpenCode upstream's 75% threshold (25% margin).
|
|
72
|
+
* @see https://github.com/link-assistant/agent/issues/219
|
|
73
|
+
* @see https://github.com/link-assistant/agent/issues/249
|
|
74
|
+
*/
|
|
75
|
+
export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT = 25;
|
|
76
|
+
|
|
77
|
+
/** Env var for overriding the default compaction safety margin in test runs and automation. */
|
|
78
|
+
export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT_ENV =
|
|
79
|
+
'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT';
|
|
80
|
+
|
|
81
|
+
function clean(value: string | null | undefined): string | undefined {
|
|
82
|
+
const trimmed = value?.trim();
|
|
83
|
+
return trimmed ? trimmed : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function optionString(value: string | null | undefined): string | undefined {
|
|
87
|
+
return clean(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function envString(env: EnvLike, key: string): string | undefined {
|
|
91
|
+
return clean(env[key]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function optionNumber(
|
|
95
|
+
value: number | string | null | undefined
|
|
96
|
+
): number | undefined {
|
|
97
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
100
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Resolve the effective default model. CLI `--model` still takes precedence later. */
|
|
106
|
+
export function getDefaultModel(options: DefaultConfigOptions = {}): string {
|
|
107
|
+
const env = options.env ?? process.env;
|
|
108
|
+
return (
|
|
109
|
+
optionString(options.defaultModel) ??
|
|
110
|
+
envString(env, DEFAULT_MODEL_ENV) ??
|
|
111
|
+
DEFAULT_MODEL
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getDefaultCompactionModel(
|
|
116
|
+
options: DefaultConfigOptions = {}
|
|
117
|
+
): string {
|
|
118
|
+
const env = options.env ?? process.env;
|
|
119
|
+
return (
|
|
120
|
+
optionString(options.defaultCompactionModel) ??
|
|
121
|
+
envString(env, DEFAULT_COMPACTION_MODEL_ENV) ??
|
|
122
|
+
DEFAULT_COMPACTION_MODEL
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getDefaultCompactionModels(
|
|
127
|
+
options: DefaultConfigOptions = {}
|
|
128
|
+
): string {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
return (
|
|
131
|
+
optionString(options.defaultCompactionModels) ??
|
|
132
|
+
envString(env, DEFAULT_COMPACTION_MODELS_ENV) ??
|
|
133
|
+
DEFAULT_COMPACTION_MODELS
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getDefaultCompactionSafetyMarginPercent(
|
|
138
|
+
options: DefaultConfigOptions = {}
|
|
139
|
+
): number {
|
|
140
|
+
const env = options.env ?? process.env;
|
|
141
|
+
return (
|
|
142
|
+
optionNumber(options.defaultCompactionSafetyMarginPercent) ??
|
|
143
|
+
optionNumber(env[DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT_ENV]) ??
|
|
144
|
+
DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function parseModelParts(model: string): {
|
|
149
|
+
providerID: string;
|
|
150
|
+
modelID: string;
|
|
151
|
+
} {
|
|
152
|
+
const [providerID, ...modelParts] = model.split('/');
|
|
153
|
+
return {
|
|
154
|
+
providerID,
|
|
155
|
+
modelID: modelParts.join('/'),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getDefaultModelParts(options: DefaultConfigOptions = {}): {
|
|
160
|
+
providerID: string;
|
|
161
|
+
modelID: string;
|
|
162
|
+
} {
|
|
163
|
+
return parseModelParts(getDefaultModel(options));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Default provider ID extracted from DEFAULT_MODEL. */
|
|
167
|
+
export const DEFAULT_PROVIDER_ID = parseModelParts(DEFAULT_MODEL).providerID;
|
|
168
|
+
|
|
169
|
+
/** Default model ID extracted from DEFAULT_MODEL. */
|
|
170
|
+
export const DEFAULT_MODEL_ID = parseModelParts(DEFAULT_MODEL).modelID;
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runContinuousDirectMode,
|
|
37
37
|
resolveResumeSession,
|
|
38
38
|
} from './cli/continuous-mode.js';
|
|
39
|
+
import { parseStreamJsonInput } from './cli/input-queue.js';
|
|
39
40
|
import { createBusEventSubscription } from './cli/event-handler.js';
|
|
40
41
|
import {
|
|
41
42
|
outputStatus,
|
|
@@ -259,6 +260,19 @@ async function readSystemMessages(argv) {
|
|
|
259
260
|
return { systemMessage, appendSystemMessage };
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
function getInputFormat(argv) {
|
|
264
|
+
return argv['input-format'] || argv.inputFormat || 'text';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getAcceptedInputFormats(inputFormat, isInteractive) {
|
|
268
|
+
if (inputFormat === 'stream-json') {
|
|
269
|
+
return ['Claude stream-json user frames'];
|
|
270
|
+
}
|
|
271
|
+
return isInteractive
|
|
272
|
+
? ['JSON object with "message" field', 'Plain text']
|
|
273
|
+
: ['JSON object with "message" field'];
|
|
274
|
+
}
|
|
275
|
+
|
|
262
276
|
async function runAgentMode(argv, request) {
|
|
263
277
|
// Log version and command info in verbose mode using lazy logging
|
|
264
278
|
Log.Default.info(() => ({
|
|
@@ -488,7 +502,7 @@ async function runServerMode(
|
|
|
488
502
|
});
|
|
489
503
|
unsub = eventUnsub;
|
|
490
504
|
|
|
491
|
-
// Send message to session with specified model
|
|
505
|
+
// Send message to session with specified model
|
|
492
506
|
const message = request.message || 'hi';
|
|
493
507
|
const parts = [{ type: 'text', text: message }];
|
|
494
508
|
|
|
@@ -663,8 +677,15 @@ async function main() {
|
|
|
663
677
|
// Check if stdin is a TTY (interactive terminal)
|
|
664
678
|
if (process.stdin.isTTY) {
|
|
665
679
|
// Enter interactive terminal mode with continuous listening
|
|
666
|
-
const
|
|
667
|
-
const
|
|
680
|
+
const inputFormat = getInputFormat(argv);
|
|
681
|
+
const isInteractive =
|
|
682
|
+
inputFormat === 'stream-json'
|
|
683
|
+
? false
|
|
684
|
+
: argv.interactive !== false;
|
|
685
|
+
const autoMerge =
|
|
686
|
+
inputFormat === 'stream-json'
|
|
687
|
+
? false
|
|
688
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
668
689
|
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
669
690
|
|
|
670
691
|
// Exit if --no-always-accept-stdin is set (single message mode not supported in TTY)
|
|
@@ -688,10 +709,12 @@ async function main() {
|
|
|
688
709
|
message:
|
|
689
710
|
'Agent CLI in interactive terminal mode. Type your message and press Enter.',
|
|
690
711
|
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
691
|
-
acceptedFormats:
|
|
692
|
-
|
|
693
|
-
|
|
712
|
+
acceptedFormats: getAcceptedInputFormats(
|
|
713
|
+
inputFormat,
|
|
714
|
+
isInteractive
|
|
715
|
+
),
|
|
694
716
|
options: {
|
|
717
|
+
inputFormat,
|
|
695
718
|
interactive: isInteractive,
|
|
696
719
|
autoMergeQueuedMessages: autoMerge,
|
|
697
720
|
alwaysAcceptStdin,
|
|
@@ -707,8 +730,13 @@ async function main() {
|
|
|
707
730
|
}
|
|
708
731
|
|
|
709
732
|
// stdin is piped - enter stdin listening mode
|
|
710
|
-
const
|
|
711
|
-
const
|
|
733
|
+
const inputFormat = getInputFormat(argv);
|
|
734
|
+
const isInteractive =
|
|
735
|
+
inputFormat === 'stream-json' ? false : argv.interactive !== false;
|
|
736
|
+
const autoMerge =
|
|
737
|
+
inputFormat === 'stream-json'
|
|
738
|
+
? false
|
|
739
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
712
740
|
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
713
741
|
|
|
714
742
|
outputStatus(
|
|
@@ -719,10 +747,12 @@ async function main() {
|
|
|
719
747
|
? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
|
|
720
748
|
: 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
|
|
721
749
|
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
722
|
-
acceptedFormats:
|
|
723
|
-
|
|
724
|
-
|
|
750
|
+
acceptedFormats: getAcceptedInputFormats(
|
|
751
|
+
inputFormat,
|
|
752
|
+
isInteractive
|
|
753
|
+
),
|
|
725
754
|
options: {
|
|
755
|
+
inputFormat,
|
|
726
756
|
interactive: isInteractive,
|
|
727
757
|
autoMergeQueuedMessages: autoMerge,
|
|
728
758
|
alwaysAcceptStdin,
|
|
@@ -757,27 +787,52 @@ async function main() {
|
|
|
757
787
|
|
|
758
788
|
// Try to parse as JSON, if it fails treat it as plain text message
|
|
759
789
|
let request;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
790
|
+
if (inputFormat === 'stream-json') {
|
|
791
|
+
try {
|
|
792
|
+
const firstFrame = trimmedInput
|
|
793
|
+
.split(/\r?\n/)
|
|
794
|
+
.find((line) => line.trim());
|
|
795
|
+
request = parseStreamJsonInput(firstFrame || '');
|
|
796
|
+
if (request.kind !== 'message') {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Expected stream-json user frame, received ${request.kind}`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
766
802
|
outputError(
|
|
767
803
|
{
|
|
768
804
|
errorType: 'ValidationError',
|
|
769
805
|
message:
|
|
770
|
-
|
|
771
|
-
hint: '
|
|
806
|
+
error instanceof Error ? error.message : String(error),
|
|
807
|
+
hint: 'Provide one Claude stream-json user frame per line.',
|
|
772
808
|
},
|
|
773
809
|
compactJson
|
|
774
810
|
);
|
|
775
811
|
process.exit(1);
|
|
776
812
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
813
|
+
} else {
|
|
814
|
+
try {
|
|
815
|
+
request = JSON.parse(trimmedInput);
|
|
816
|
+
} catch (_e) {
|
|
817
|
+
// Not JSON
|
|
818
|
+
if (!isInteractive) {
|
|
819
|
+
// In non-interactive mode, only accept JSON
|
|
820
|
+
outputError(
|
|
821
|
+
{
|
|
822
|
+
errorType: 'ValidationError',
|
|
823
|
+
message:
|
|
824
|
+
'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
|
|
825
|
+
hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
|
|
826
|
+
},
|
|
827
|
+
compactJson
|
|
828
|
+
);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
// In interactive mode, treat as plain text message
|
|
832
|
+
request = {
|
|
833
|
+
message: trimmedInput,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
781
836
|
}
|
|
782
837
|
|
|
783
838
|
// Output input confirmation in JSON format
|
|
@@ -785,7 +840,12 @@ async function main() {
|
|
|
785
840
|
{
|
|
786
841
|
raw: trimmedInput,
|
|
787
842
|
parsed: request,
|
|
788
|
-
format:
|
|
843
|
+
format:
|
|
844
|
+
inputFormat === 'stream-json'
|
|
845
|
+
? 'stream-json'
|
|
846
|
+
: isInteractive
|
|
847
|
+
? 'text'
|
|
848
|
+
: 'json',
|
|
789
849
|
},
|
|
790
850
|
compactJson
|
|
791
851
|
);
|