flashlite 0.0.0-stage → 0.1.1
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 +583 -2
- package/dist/adapters/anthropic.js +341 -0
- package/dist/adapters/openai.js +241 -0
- package/dist/approval.js +28 -0
- package/dist/boostworkflow.js +15 -0
- package/dist/boundedfs.js +214 -0
- package/dist/chatviewport.js +123 -0
- package/dist/checkpoint.js +100 -0
- package/dist/client.js +463 -0
- package/dist/clipboard.js +72 -0
- package/dist/commandpolicy.js +544 -0
- package/dist/commands.js +29 -0
- package/dist/compaction.js +66 -0
- package/dist/config.js +206 -0
- package/dist/contextbudget.js +201 -0
- package/dist/customcommands.js +105 -0
- package/dist/effort.js +79 -0
- package/dist/hooks.js +86 -0
- package/dist/images.js +84 -0
- package/dist/index.js +459 -0
- package/dist/lineinput.js +982 -0
- package/dist/loop.js +442 -0
- package/dist/markdown.js +426 -0
- package/dist/memory.js +55 -0
- package/dist/pathcomplete.js +92 -0
- package/dist/projectinstructions.js +77 -0
- package/dist/repl.js +1659 -0
- package/dist/researchweb.js +87 -0
- package/dist/sanitize.js +89 -0
- package/dist/sessions.js +209 -0
- package/dist/sse.js +80 -0
- package/dist/subagents.js +182 -0
- package/dist/systemprompt.js +35 -0
- package/dist/terminal.js +235 -0
- package/dist/todo.js +34 -0
- package/dist/token.js +17 -0
- package/dist/tokenprofiles.js +25 -0
- package/dist/toolcache.js +50 -0
- package/dist/toolpreview.js +253 -0
- package/dist/toolresultview.js +62 -0
- package/dist/tools.js +889 -0
- package/dist/types.js +3 -0
- package/dist/ui.js +1630 -0
- package/package.json +28 -4
package/dist/images.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Explicit user-selected local attachments, separate from the agent's confined file tools.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
7
|
+
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
export const MAX_ATTACHED_IMAGES = 4;
|
|
9
|
+
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
10
|
+
function mediaType(bytes) {
|
|
11
|
+
if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
12
|
+
return 'image/png';
|
|
13
|
+
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255)
|
|
14
|
+
return 'image/jpeg';
|
|
15
|
+
if (['GIF87a', 'GIF89a'].includes(bytes.subarray(0, 6).toString('ascii')))
|
|
16
|
+
return 'image/gif';
|
|
17
|
+
if (bytes.subarray(0, 4).toString('ascii') === 'RIFF' && bytes.subarray(8, 12).toString('ascii') === 'WEBP')
|
|
18
|
+
return 'image/webp';
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
export function attachmentPath(value, cwd) {
|
|
22
|
+
let file = value.trim();
|
|
23
|
+
if (file.length >= 2 && (file[0] === '"' && file.at(-1) === '"' || file[0] === "'" && file.at(-1) === "'"))
|
|
24
|
+
file = file.slice(1, -1);
|
|
25
|
+
else
|
|
26
|
+
file = file.replace(/\\([\\ '"()])/g, '$1'); // macOS drag-and-drop shell escaping
|
|
27
|
+
if (!file || file.includes('\0'))
|
|
28
|
+
throw new Error('provide a local image path');
|
|
29
|
+
if (file === '~' || file.startsWith('~/'))
|
|
30
|
+
file = path.join(os.homedir(), file.slice(1));
|
|
31
|
+
return path.resolve(cwd, file);
|
|
32
|
+
}
|
|
33
|
+
export function imageFromBytes(bytes, name) {
|
|
34
|
+
if (bytes.length > MAX_IMAGE_BYTES)
|
|
35
|
+
throw new Error('image exceeds the 10 MiB attachment limit');
|
|
36
|
+
const type = mediaType(bytes);
|
|
37
|
+
if (!type)
|
|
38
|
+
throw new Error('unsupported image — use PNG, JPEG, WebP, or GIF (HEIC is not supported)');
|
|
39
|
+
return { name: stripVTControlCharacters(path.basename(name)).replace(/[\x00-\x1f\x7f]/g, ' '), mediaType: type, data: bytes.toString('base64') };
|
|
40
|
+
}
|
|
41
|
+
export async function loadImage(value, cwd) {
|
|
42
|
+
const file = attachmentPath(value, cwd);
|
|
43
|
+
let handle;
|
|
44
|
+
try {
|
|
45
|
+
handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error('could not open that image — check the path and file permissions');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const stat = await handle.stat();
|
|
52
|
+
if (!stat.isFile())
|
|
53
|
+
throw new Error('attachments must be regular image files');
|
|
54
|
+
if (stat.size > MAX_IMAGE_BYTES)
|
|
55
|
+
throw new Error('image exceeds the 10 MiB attachment limit');
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let size = 0;
|
|
58
|
+
while (true) {
|
|
59
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, MAX_IMAGE_BYTES + 1 - size));
|
|
60
|
+
const { bytesRead } = await handle.read(buffer);
|
|
61
|
+
if (!bytesRead)
|
|
62
|
+
break;
|
|
63
|
+
size += bytesRead;
|
|
64
|
+
if (size > MAX_IMAGE_BYTES)
|
|
65
|
+
throw new Error('image exceeds the 10 MiB attachment limit');
|
|
66
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
67
|
+
}
|
|
68
|
+
return imageFromBytes(Buffer.concat(chunks, size), file);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await handle.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function imageBytes(image) {
|
|
75
|
+
return Buffer.byteLength(image.data, 'base64');
|
|
76
|
+
}
|
|
77
|
+
export function addImages(current, added) {
|
|
78
|
+
const images = [...current, ...added];
|
|
79
|
+
if (images.length > MAX_ATTACHED_IMAGES)
|
|
80
|
+
throw new Error('attach at most 4 images per prompt — Ctrl+X clears pending images');
|
|
81
|
+
if (images.reduce((total, image) => total + imageBytes(image), 0) > MAX_ATTACHMENT_BYTES)
|
|
82
|
+
throw new Error('attached images exceed the 20 MiB total limit — Ctrl+X clears pending images');
|
|
83
|
+
return images;
|
|
84
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// flashlite — a provider-agnostic agentic coding CLI.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { RouterClient, errorMessage, dialectFor } from "./client.js";
|
|
7
|
+
import { configPath, migrateConfigFile, normalizeBaseUrl, resolveConfig, saveConfigFile } from "./config.js";
|
|
8
|
+
import { tokenProfile } from "./tokenprofiles.js";
|
|
9
|
+
import { parseApprovalMode } from "./approval.js";
|
|
10
|
+
import { startRepl, runHeadlessAgent } from "./repl.js";
|
|
11
|
+
import { renderMarkdown } from "./markdown.js";
|
|
12
|
+
import { EFFORT_LEVELS, parseEffort } from "./effort.js";
|
|
13
|
+
import { loadImage, addImages } from "./images.js";
|
|
14
|
+
import { listSessions, loadSession, sessionWorkspaceIssue } from "./sessions.js";
|
|
15
|
+
import { textOnlyPrompt } from "./systemprompt.js";
|
|
16
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
function version() {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8'));
|
|
20
|
+
return pkg.version ?? '0.0.0';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '0.1.0';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function printHelp() {
|
|
27
|
+
console.log(`flashlite — agentic coding CLI
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
flash start the REPL
|
|
31
|
+
flash -p "prompt" text-only one-shot (no tools or workspace access)
|
|
32
|
+
flash --agent -p "task" noninteractive tool workflow; read-only unless explicitly approved
|
|
33
|
+
flash --check connectivity + model catalog diagnostic
|
|
34
|
+
flash --base-url <url> use another OpenAI-compatible endpoint for this run only
|
|
35
|
+
flash --set-base-url <url> save the endpoint to config; --reset-base-url clears it
|
|
36
|
+
flash --resume <id|latest> continue only in its recorded workspace
|
|
37
|
+
flash --acknowledge-legacy-workspace acknowledge unknown workspace for legacy --resume
|
|
38
|
+
flash --model <id> set the session model
|
|
39
|
+
flash --effort <level> auto, low, medium, high, xhigh, max
|
|
40
|
+
flash --image <path> attach an image (repeat for more; requires a vision model)
|
|
41
|
+
flash --absolute unguarded auto: whole drive + every command; no gate
|
|
42
|
+
flash --auto guarded auto: arbitrary commands run; network/installs/sudo/
|
|
43
|
+
outside paths/destructive operations still ask
|
|
44
|
+
with --agent: denied commands never prompt; not sandboxed
|
|
45
|
+
flash --auto-approve allow file changes only; terminal commands still ask/deny
|
|
46
|
+
flash --mode <m> start in ask, files, auto, or unguarded
|
|
47
|
+
flash --debug print transport metadata only (no raw frames or message content)
|
|
48
|
+
flash --no-color plain output
|
|
49
|
+
flash --version print the version
|
|
50
|
+
|
|
51
|
+
Endpoint: set it on first interactive launch, or with --base-url / --set-base-url.
|
|
52
|
+
Precedence: --base-url > FLASHLITE_BASE_URL > base_url in config.json.
|
|
53
|
+
API key: enter it on first interactive launch; it is saved locally to
|
|
54
|
+
~/.config/flashlite/config.json (chmod 600).
|
|
55
|
+
Your saved API key is sent to whichever endpoint is active.`);
|
|
56
|
+
}
|
|
57
|
+
function parseArgs(argv) {
|
|
58
|
+
const flags = { check: false, showVersion: false, showHelp: false, images: [], debug: false };
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const arg = argv[i];
|
|
61
|
+
if (arg === '--check')
|
|
62
|
+
flags.check = true;
|
|
63
|
+
else if (arg === '--version' || arg === '-v')
|
|
64
|
+
flags.showVersion = true;
|
|
65
|
+
else if (arg === '--help' || arg === '-h')
|
|
66
|
+
flags.showHelp = true;
|
|
67
|
+
else if (arg === '--debug')
|
|
68
|
+
flags.debug = true;
|
|
69
|
+
else if (arg === '--agent')
|
|
70
|
+
flags.agent = true;
|
|
71
|
+
else if (arg === '--acknowledge-legacy-workspace')
|
|
72
|
+
flags.acknowledgeLegacyWorkspace = true;
|
|
73
|
+
else if (arg === '--absolute')
|
|
74
|
+
flags.absoluteMode = true;
|
|
75
|
+
else if (arg === '--auto')
|
|
76
|
+
flags.autoMode = true;
|
|
77
|
+
else if (arg === '--auto-approve')
|
|
78
|
+
flags.autoApprove = true;
|
|
79
|
+
else if (arg === '--mode') {
|
|
80
|
+
const parsed = parseApprovalMode(argv[++i] ?? '');
|
|
81
|
+
if (!parsed) {
|
|
82
|
+
console.error('--mode requires ask, files, auto or unguarded.');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
flags.mode = parsed;
|
|
86
|
+
}
|
|
87
|
+
else if (arg === '--base-url' || arg === '--set-base-url') {
|
|
88
|
+
const url = normalizeBaseUrl(argv[++i]);
|
|
89
|
+
if (!url) {
|
|
90
|
+
console.error(`${arg} requires an http(s) URL without credentials, query or fragment (e.g. https://api.deepseek.com/v1).`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (arg === '--base-url')
|
|
94
|
+
flags.baseUrl = url;
|
|
95
|
+
else
|
|
96
|
+
flags.setBaseUrl = url;
|
|
97
|
+
}
|
|
98
|
+
else if (arg === '--reset-base-url')
|
|
99
|
+
flags.resetBaseUrl = true;
|
|
100
|
+
else if (arg === '--no-color')
|
|
101
|
+
process.env.NO_COLOR = '1';
|
|
102
|
+
else if (arg === '--resume') {
|
|
103
|
+
const id = argv[++i];
|
|
104
|
+
if (!id || id.startsWith('--')) {
|
|
105
|
+
console.error('--resume requires a session ID or `latest`');
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
flags.resume = id;
|
|
109
|
+
}
|
|
110
|
+
else if ((arg === '--model' || arg === '-m') && i + 1 < argv.length)
|
|
111
|
+
flags.model = argv[++i];
|
|
112
|
+
else if (arg === '--effort') {
|
|
113
|
+
const effort = parseEffort(argv[++i]);
|
|
114
|
+
if (!effort) {
|
|
115
|
+
console.error(`--effort requires one of: ${EFFORT_LEVELS.join(', ')}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
flags.effort = effort;
|
|
119
|
+
}
|
|
120
|
+
else if (arg === '--image') {
|
|
121
|
+
const file = argv[++i];
|
|
122
|
+
if (!file || file.startsWith('--')) {
|
|
123
|
+
console.error('--image requires a local image path');
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
flags.images.push(file);
|
|
127
|
+
}
|
|
128
|
+
else if ((arg === '--prompt' || arg === '-p') && i + 1 < argv.length)
|
|
129
|
+
flags.prompt = argv[++i];
|
|
130
|
+
else {
|
|
131
|
+
if (['--model', '-m', '--prompt', '-p'].includes(arg))
|
|
132
|
+
console.error(`${arg} requires a value.`);
|
|
133
|
+
else
|
|
134
|
+
console.error(`Unknown argument: ${arg}`);
|
|
135
|
+
console.error('Run `flash --help` for usage.');
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if ([flags.absoluteMode, flags.autoMode, flags.autoApprove, flags.mode].filter(Boolean).length > 1) {
|
|
140
|
+
console.error('Choose --absolute, --auto, --auto-approve or --mode — not both or multiple.');
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
if ([flags.baseUrl !== undefined, flags.setBaseUrl !== undefined, flags.resetBaseUrl].filter(Boolean).length > 1) {
|
|
144
|
+
console.error('Choose one of --base-url, --set-base-url or --reset-base-url.');
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return flags;
|
|
148
|
+
}
|
|
149
|
+
function say(line) {
|
|
150
|
+
process.stdout.write(line + '\n');
|
|
151
|
+
}
|
|
152
|
+
/** Raw-mode line input for first-run setup; masked for secrets, plain for URLs. */
|
|
153
|
+
async function setupLine(prompt, mask, validate) {
|
|
154
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
155
|
+
return null;
|
|
156
|
+
const input = process.stdin;
|
|
157
|
+
const wasRaw = input.isRaw;
|
|
158
|
+
process.stdout.write(prompt);
|
|
159
|
+
input.setRawMode(true);
|
|
160
|
+
input.resume();
|
|
161
|
+
process.stdout.write('\x1b[?25l');
|
|
162
|
+
return await new Promise((resolve) => {
|
|
163
|
+
let value = '';
|
|
164
|
+
const finish = (result) => {
|
|
165
|
+
input.off('data', onData);
|
|
166
|
+
input.setRawMode(wasRaw ?? false);
|
|
167
|
+
process.stdout.write('\x1b[?25h\n');
|
|
168
|
+
resolve(result);
|
|
169
|
+
};
|
|
170
|
+
const onData = (chunk) => {
|
|
171
|
+
for (const char of chunk.toString('utf8')) {
|
|
172
|
+
if (char === '\u0003' || char === '\u001b') {
|
|
173
|
+
finish(null);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (char === '\r' || char === '\n') {
|
|
177
|
+
const problem = validate(value);
|
|
178
|
+
if (problem) {
|
|
179
|
+
process.stdout.write(`\n ${problem} `);
|
|
180
|
+
value = '';
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
finish(value.trim());
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (char === '\u007f' || char === '\b') {
|
|
187
|
+
if (value.length) {
|
|
188
|
+
value = value.slice(0, -1);
|
|
189
|
+
process.stdout.write('\b \b');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (char >= ' ' && char !== '\u007f' && value.length < 4096) {
|
|
193
|
+
value += char;
|
|
194
|
+
process.stdout.write(mask ? '*' : char);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
input.on('data', onData);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** Ask for the OpenAI-compatible endpoint root on first run; returns null if cancelled. */
|
|
202
|
+
async function setupEndpoint() {
|
|
203
|
+
const value = await setupLine('\n Endpoint URL (OpenAI-compatible root, e.g. https://api.openai.com/v1): ', false, (v) => (v.trim() ? (normalizeBaseUrl(v) ? null : 'Enter an http(s) URL without credentials, query or fragment:') : 'Endpoint cannot be blank:'));
|
|
204
|
+
return value ? normalizeBaseUrl(value) : null;
|
|
205
|
+
}
|
|
206
|
+
async function setupApiKey(host) {
|
|
207
|
+
const value = await setupLine(`\n Welcome to flashlite\n Connect your own API key for ${host}. It is saved only in your local config (mode 600).\n\n API key: `, true, (v) => (v.trim() ? null : 'API key cannot be blank. Try again:'));
|
|
208
|
+
if (value)
|
|
209
|
+
process.stdout.write('\n Saving key locally…');
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
async function runCheck(client, config) {
|
|
213
|
+
const source = config.baseUrlSource && config.baseUrlSource !== 'config' ? ` (${config.baseUrlSource})` : '';
|
|
214
|
+
say(`base: ${config.baseUrl}${source}`);
|
|
215
|
+
try {
|
|
216
|
+
const models = await client.listModels();
|
|
217
|
+
say(`models: ${models.length} in catalog`);
|
|
218
|
+
for (const m of models.slice(0, 5)) {
|
|
219
|
+
const ctx = m.contextLength ? ` (${m.contextLength.toLocaleString()} ctx)` : '';
|
|
220
|
+
say(` - ${m.id}${ctx}`);
|
|
221
|
+
}
|
|
222
|
+
if (models.length > 5)
|
|
223
|
+
say(' …');
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const e = err;
|
|
227
|
+
say(`models: FAILED (${e.status ?? '?'}) ${errorMessage(err)}`);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Session model: --model flag > config default > first-run bootstrap.
|
|
234
|
+
* The bootstrap prefers the model verified to work on this key, then the first catalog
|
|
235
|
+
* entry, and persists the choice — never a hardcoded guess.
|
|
236
|
+
*/
|
|
237
|
+
async function resolveSessionModel(flagsModel, config, client) {
|
|
238
|
+
if (flagsModel)
|
|
239
|
+
return flagsModel;
|
|
240
|
+
if (config.defaultModel)
|
|
241
|
+
return config.defaultModel;
|
|
242
|
+
let catalog;
|
|
243
|
+
try {
|
|
244
|
+
catalog = await client.listModels();
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
throw new Error('No default model configured and the catalog fetch failed — pass --model <id>. ' +
|
|
248
|
+
`(${errorMessage(err)})`);
|
|
249
|
+
}
|
|
250
|
+
const pick = catalog[0]?.id;
|
|
251
|
+
if (!pick)
|
|
252
|
+
throw new Error('The catalog is empty — pass --model <id>.');
|
|
253
|
+
saveConfigFile({ default_model: pick });
|
|
254
|
+
return pick;
|
|
255
|
+
}
|
|
256
|
+
async function runOneShot(client, config, model, prompt, images) {
|
|
257
|
+
const messages = [{ role: 'user', content: prompt, ...(images.length ? { images } : {}) }];
|
|
258
|
+
const req = {
|
|
259
|
+
model,
|
|
260
|
+
dialect: dialectFor(model),
|
|
261
|
+
messages,
|
|
262
|
+
system: textOnlyPrompt(),
|
|
263
|
+
stream: false,
|
|
264
|
+
maxTokens: tokenProfile(config, config.tokenSaver ?? 'off', config.boostMode ?? false).maxOutputTokens,
|
|
265
|
+
effort: config.reasoningEffort,
|
|
266
|
+
};
|
|
267
|
+
try {
|
|
268
|
+
const turn = await client.complete(req);
|
|
269
|
+
const content = turn.message.content || '(no response)';
|
|
270
|
+
say(renderMarkdown(content).replace(/\n+$/, ''));
|
|
271
|
+
if (turn.stopReason === 'length')
|
|
272
|
+
say('! response truncated — consider a shorter scope');
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
say('× ' + errorMessage(err));
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function main() {
|
|
281
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
282
|
+
if (!flags)
|
|
283
|
+
return 2;
|
|
284
|
+
if (flags.showVersion) {
|
|
285
|
+
console.log(version());
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (flags.showHelp) {
|
|
289
|
+
printHelp();
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
if (flags.setBaseUrl !== undefined || flags.resetBaseUrl) {
|
|
293
|
+
if (flags.prompt !== undefined || flags.check || flags.agent || flags.resume) {
|
|
294
|
+
console.error('--set-base-url and --reset-base-url only save the endpoint; do not combine them with -p, --agent, --check or --resume.');
|
|
295
|
+
return 2;
|
|
296
|
+
}
|
|
297
|
+
if (flags.resetBaseUrl) {
|
|
298
|
+
saveConfigFile({ base_url: '', default_model: null });
|
|
299
|
+
say('endpoint cleared — the next launch will ask for one');
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
const url = flags.setBaseUrl;
|
|
303
|
+
const current = resolveConfig().config.baseUrl;
|
|
304
|
+
const hostChanged = !current || new URL(url).host !== new URL(current).host;
|
|
305
|
+
saveConfigFile(hostChanged ? { base_url: url, default_model: null } : { base_url: url });
|
|
306
|
+
say(`endpoint saved: ${url}`);
|
|
307
|
+
if (hostChanged)
|
|
308
|
+
console.error('! default model cleared — the new endpoint serves different models; the next launch picks one from its catalog (or pass --model).');
|
|
309
|
+
console.error('! your saved API key is sent to this endpoint — make sure the key in config.json is valid there.');
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
if (flags.agent && (flags.prompt === undefined || flags.check || flags.resume)) {
|
|
313
|
+
console.error('--agent requires -p/--prompt and cannot be combined with --check or --resume.');
|
|
314
|
+
return 2;
|
|
315
|
+
}
|
|
316
|
+
if (flags.prompt !== undefined && !flags.agent && (flags.autoMode || flags.autoApprove || flags.mode)) {
|
|
317
|
+
console.error('-p is text-only: permission flags require --agent -p for a tool workflow.');
|
|
318
|
+
return 2;
|
|
319
|
+
}
|
|
320
|
+
if (flags.acknowledgeLegacyWorkspace && !flags.resume) {
|
|
321
|
+
console.error('--acknowledge-legacy-workspace requires --resume; it never retargets a known workspace.');
|
|
322
|
+
return 2;
|
|
323
|
+
}
|
|
324
|
+
const wantsAbsolute = flags.absoluteMode || flags.mode === 'absolute';
|
|
325
|
+
if (wantsAbsolute && (flags.prompt !== undefined || flags.check || !process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
326
|
+
console.error('unguarded auto requires an interactive REPL; not -p or --check.');
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
if (flags.resume && (flags.prompt !== undefined || flags.check || wantsAbsolute)) {
|
|
330
|
+
console.error('--resume is for the interactive REPL and cannot be combined with -p, --check or unguarded auto.');
|
|
331
|
+
return 2;
|
|
332
|
+
}
|
|
333
|
+
if (flags.check && flags.images.length) {
|
|
334
|
+
console.error('--image is for prompts, not --check');
|
|
335
|
+
return 2;
|
|
336
|
+
}
|
|
337
|
+
let resumed;
|
|
338
|
+
if (flags.resume) {
|
|
339
|
+
const id = flags.resume === 'latest' ? listSessions()[0]?.id : flags.resume;
|
|
340
|
+
if (!id) {
|
|
341
|
+
console.error('No saved sessions to resume.');
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
resumed = loadSession(id) ?? undefined;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
resumed = undefined;
|
|
349
|
+
}
|
|
350
|
+
if (!resumed) {
|
|
351
|
+
console.error('Could not resume that session. Use `flash --resume latest` to see the most recent valid session.');
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
const issue = sessionWorkspaceIssue(resumed.summary);
|
|
355
|
+
if (issue && issue !== 'legacy') {
|
|
356
|
+
console.error(issue);
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
if (issue === 'legacy') {
|
|
360
|
+
if (!flags.acknowledgeLegacyWorkspace) {
|
|
361
|
+
console.error('Legacy session has no workspace identity; it may refer to another directory. Review its origin, then explicitly use --acknowledge-legacy-workspace with --resume to continue here. No permissions are restored.');
|
|
362
|
+
return 1;
|
|
363
|
+
}
|
|
364
|
+
console.error('! acknowledged legacy session with unknown workspace · continuing in the current directory; no permissions restored');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
let images = [];
|
|
368
|
+
try {
|
|
369
|
+
for (const file of flags.images)
|
|
370
|
+
images = addImages(images, [await loadImage(file, process.cwd())]);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
console.error('× ' + errorMessage(err));
|
|
374
|
+
return 2;
|
|
375
|
+
}
|
|
376
|
+
for (const note of migrateConfigFile())
|
|
377
|
+
console.error('! ' + note);
|
|
378
|
+
const { config, warnings } = resolveConfig(flags.baseUrl);
|
|
379
|
+
if (flags.effort)
|
|
380
|
+
config.reasoningEffort = flags.effort;
|
|
381
|
+
for (const w of warnings)
|
|
382
|
+
console.error('! ' + w);
|
|
383
|
+
if (config.baseUrlSource === 'flag') {
|
|
384
|
+
console.error(`! endpoint ${new URL(config.baseUrl).host} — your saved API key will be sent there`);
|
|
385
|
+
}
|
|
386
|
+
if (!config.baseUrl) {
|
|
387
|
+
if (flags.prompt !== undefined || flags.check || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
388
|
+
console.error('No API endpoint configured. Pass --base-url <url> or start `flash` in a terminal to set one.');
|
|
389
|
+
return 1;
|
|
390
|
+
}
|
|
391
|
+
const endpoint = await setupEndpoint();
|
|
392
|
+
if (!endpoint) {
|
|
393
|
+
console.error('Setup cancelled. Run `flash` again to configure an endpoint.');
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
saveConfigFile({ base_url: endpoint });
|
|
397
|
+
config.baseUrl = endpoint;
|
|
398
|
+
config.baseUrlSource = 'config';
|
|
399
|
+
config.configPath = configPath();
|
|
400
|
+
}
|
|
401
|
+
if (!config.apiKey) {
|
|
402
|
+
if (flags.prompt !== undefined || flags.check || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
403
|
+
console.error('No API key found. Start `flash` in a terminal to configure one.');
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
const key = await setupApiKey(new URL(config.baseUrl).host);
|
|
407
|
+
if (!key) {
|
|
408
|
+
console.error('Setup cancelled. Run `flash` again to configure your API key.');
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
saveConfigFile({ api_key: key });
|
|
412
|
+
config.apiKey = key;
|
|
413
|
+
config.configPath = configPath();
|
|
414
|
+
}
|
|
415
|
+
const client = new RouterClient({
|
|
416
|
+
baseUrl: config.baseUrl,
|
|
417
|
+
apiKey: config.apiKey,
|
|
418
|
+
promptCaching: config.promptCaching,
|
|
419
|
+
debug: flags.debug ? (line) => console.error('[debug] ' + line) : undefined,
|
|
420
|
+
});
|
|
421
|
+
if (flags.check)
|
|
422
|
+
return await runCheck(client, config);
|
|
423
|
+
const model = await resolveSessionModel(flags.model ?? resumed?.summary.model, config, client);
|
|
424
|
+
if (flags.prompt !== undefined && flags.agent) {
|
|
425
|
+
const abort = new AbortController();
|
|
426
|
+
const stop = () => abort.abort();
|
|
427
|
+
process.once('SIGINT', stop);
|
|
428
|
+
process.once('SIGTERM', stop);
|
|
429
|
+
try {
|
|
430
|
+
const outcome = await runHeadlessAgent({
|
|
431
|
+
client, config, model, prompt: flags.prompt, images,
|
|
432
|
+
autoMode: flags.autoMode, autoApprove: flags.autoApprove, signal: abort.signal,
|
|
433
|
+
warn: (notice) => console.error('! ' + notice),
|
|
434
|
+
});
|
|
435
|
+
const answer = outcome.history.findLast((message) => message.role === 'assistant' && !message.toolCalls?.length)?.content;
|
|
436
|
+
if (answer)
|
|
437
|
+
say(renderMarkdown(answer).replace(/\n+$/, ''));
|
|
438
|
+
if (outcome.error)
|
|
439
|
+
console.error('× ' + outcome.error);
|
|
440
|
+
return outcome.exitCode;
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
process.off('SIGINT', stop);
|
|
444
|
+
process.off('SIGTERM', stop);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (flags.prompt !== undefined) {
|
|
448
|
+
return await runOneShot(client, config, model, flags.prompt, images);
|
|
449
|
+
}
|
|
450
|
+
startRepl({ client, config, model, images, resume: resumed, legacyWorkspaceAcknowledged: flags.acknowledgeLegacyWorkspace, autoApprove: flags.autoApprove, autoMode: flags.autoMode, absoluteMode: flags.absoluteMode, mode: flags.mode });
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
process.exitCode = await main();
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
console.error('× ' + errorMessage(err));
|
|
458
|
+
process.exitCode = 1;
|
|
459
|
+
}
|