@visual-z/xeno 0.1.4 → 0.2.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/CHANGELOG.md +25 -0
- package/README.md +143 -33
- package/docs/install.md +23 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +45 -18
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup-input.ts +94 -0
- package/src/apps/startup.ts +413 -295
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
package/src/apps/startup.ts
CHANGED
|
@@ -13,25 +13,35 @@ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
13
13
|
import { Session, systemClock } from "../runtime/loop.ts";
|
|
14
14
|
import { DEFAULT_PROVIDER, type ProviderConfig } from "../runtime/provider.ts";
|
|
15
15
|
import { createProvider, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { ExtensionRegistry } from "../runtime/extensions.ts";
|
|
17
|
+
import { CATALOG, outputTokenBudget, type ModelSpec } from "../runtime/model.ts";
|
|
18
|
+
import { asThinkingLevel, clampThinkingLevel, supportedThinkingLevels } from "../core/thinking.ts";
|
|
19
|
+
import { modelKey } from "../core/session-position.ts";
|
|
20
|
+
import { extensionFlagValue, type ExtensionFlag, type FlagInput } from "../core/extension-flags.ts";
|
|
21
|
+
import { scopedModels } from "../runtime/model-scope.ts";
|
|
17
22
|
import {
|
|
18
23
|
resolveCredential, envLabel, storedIsOAuth,
|
|
19
24
|
type AuthFile, type ResolveEffects, type Resolved, describeMissingCredential } from "../runtime/auth.ts";
|
|
20
25
|
import { authPath, configDir, readJsonFile } from "../store/config.ts";
|
|
21
26
|
import { loadUiResources, loadTextResources, type UiResources } from "../store/resources.ts";
|
|
27
|
+
import { discoverResources, type ResourceIndex } from "../store/resource-index.ts";
|
|
28
|
+
import { runResourceConfig } from "./resource-browser.ts";
|
|
29
|
+
import { PackageManager, type PackageCommand } from "../runtime/package-manager.ts";
|
|
30
|
+
import { pickProjectTrust } from "./project-trust.ts";
|
|
22
31
|
import { appendSkillsToSystem } from "../core/skills.ts";
|
|
23
|
-
import { resolveSlashCatalog, type SlashCatalog } from "./commands.ts";
|
|
32
|
+
import { COMMANDS, resolveSlashCatalog, type SlashCatalog } from "./commands.ts";
|
|
24
33
|
import { createStyle, IDENTITY, type Style } from "../tui/style.ts";
|
|
25
34
|
import type { Settings } from "../core/settings.ts";
|
|
26
|
-
import {
|
|
35
|
+
import { readModelSources, readCatalogCacheText, writeCatalogCache, catalogCachePath } from "../store/models.ts";
|
|
27
36
|
import { refreshRemoteCatalog } from "../runtime/models-fetch.ts";
|
|
28
37
|
import { readCursor, writeCursor, type Cursor } from "../store/log.ts";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
} from "../
|
|
38
|
+
import { resolveLogDir, findSessionByPrefix, listSessions, sessionsDir, type ResolveLogOpts } from "../store/sessions.ts";
|
|
39
|
+
import { recoverCut } from "../store/recovery.ts";
|
|
40
|
+
import { planSessionEntry, restoreSessionModel } from "../core/session-entry.ts";
|
|
41
|
+
import { pickStartupSession } from "./session-picker.ts";
|
|
32
42
|
import { loadContext, assembleSystemPrompt, formatNotice, type LoadedContext } from "../store/context-files.ts";
|
|
33
43
|
import {
|
|
34
|
-
|
|
44
|
+
projectTrust,
|
|
35
45
|
} from "../store/trust.ts";
|
|
36
46
|
import { stat } from "node:fs/promises";
|
|
37
47
|
import {
|
|
@@ -47,6 +57,7 @@ import { DEFAULT_CONTEXT_POLICY, isContextPolicy, type ContextPolicy } from "../
|
|
|
47
57
|
import { windowBudget } from "../core/context-window.ts";
|
|
48
58
|
import { colorEnabled, createPalette, describeError, stdinIsTty, stdoutIsTty, type Palette } from "./render.ts";
|
|
49
59
|
import { type ActionId, type Keymap } from "../core/keymap.ts";
|
|
60
|
+
import type { StartupInput } from "./startup-input.ts";
|
|
50
61
|
|
|
51
62
|
export const SYSTEM_PROMPT = systemPromptFor(TOOL_SCHEMAS.map((tool) => tool.name));
|
|
52
63
|
|
|
@@ -165,6 +176,15 @@ export interface Args {
|
|
|
165
176
|
provider: string;
|
|
166
177
|
apiKey: string;
|
|
167
178
|
listModels: boolean;
|
|
179
|
+
modelSearch?: string;
|
|
180
|
+
help?: boolean;
|
|
181
|
+
version?: boolean;
|
|
182
|
+
exportFile?: string;
|
|
183
|
+
offline?: boolean;
|
|
184
|
+
tuiMode?: "regular" | "fullscreen";
|
|
185
|
+
noThemes?: boolean;
|
|
186
|
+
themePaths?: string[];
|
|
187
|
+
sessionId?: string;
|
|
168
188
|
modelsCommand: boolean;
|
|
169
189
|
modelsRefresh: boolean;
|
|
170
190
|
fresh: boolean;
|
|
@@ -180,6 +200,10 @@ export interface Args {
|
|
|
180
200
|
allowTools: string[];
|
|
181
201
|
/** `--approve` true, `--no-approve` false. Omitted when neither. */
|
|
182
202
|
approve?: boolean;
|
|
203
|
+
configCommand?: boolean;
|
|
204
|
+
local?: boolean;
|
|
205
|
+
packageCommand?: PackageCommand;
|
|
206
|
+
packagePaths?: {source:string;path:string}[];
|
|
183
207
|
/**
|
|
184
208
|
* `-p` / `--print`. Distinct from a leftover positional prompt: only this
|
|
185
209
|
* flag reads piped stdin, so a one-shot that did not ask for a pipe cannot
|
|
@@ -192,8 +216,14 @@ export interface Args {
|
|
|
192
216
|
thinking?: string;
|
|
193
217
|
/** `--models <patterns>`. Comma-separated; empty when the flag was not passed. */
|
|
194
218
|
models?: string;
|
|
219
|
+
modelsExplicit?: boolean;
|
|
195
220
|
noSkills: boolean;
|
|
196
221
|
noPromptTemplates: boolean;
|
|
222
|
+
noExtensions?: boolean;
|
|
223
|
+
extensionPaths?: string[];
|
|
224
|
+
extensionFlags?: Record<string, string | boolean>;
|
|
225
|
+
extensionArgv?: string[];
|
|
226
|
+
extensionInputs?: Record<string, FlagInput>;
|
|
197
227
|
skillPaths: string[];
|
|
198
228
|
promptTemplatePaths: string[];
|
|
199
229
|
/** `-c` / `--continue`. */
|
|
@@ -266,10 +296,12 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
266
296
|
"--prompt-template",
|
|
267
297
|
"--no-skills",
|
|
268
298
|
"--no-prompt-templates",
|
|
299
|
+
"--extension", "-e", "--no-extensions", "-ne", "--extension-flag",
|
|
269
300
|
"--mode",
|
|
270
301
|
"--verbose",
|
|
271
302
|
"--print", "-p",
|
|
272
303
|
"--help", "-h",
|
|
304
|
+
"--version", "-v", "--export", "--session-id", "--offline", "--tui-mode", "--use-theme", "--no-themes", "-ns", "-np", "-nbt",
|
|
273
305
|
];
|
|
274
306
|
|
|
275
307
|
/**
|
|
@@ -315,33 +347,8 @@ function rejectUnknownFlag(flag: string): never {
|
|
|
315
347
|
process.exit(1);
|
|
316
348
|
}
|
|
317
349
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
*
|
|
321
|
-
* Reading auth, scanning context files and printing the header all happen
|
|
322
|
-
* before the TUI attaches. In cooked mode the tty echoes whatever is typed
|
|
323
|
-
* during that window, so keystrokes land *between* the banner's own lines
|
|
324
|
-
* and look like output the program produced. Raw mode is the TUI's anyway;
|
|
325
|
-
* taking it at the first instruction just closes the gap.
|
|
326
|
-
*
|
|
327
|
-
* `stdin` is a parameter so the decision is testable without a real tty.
|
|
328
|
-
* Returns an undo so a caller that bails out can put the terminal back.
|
|
329
|
-
*/
|
|
330
|
-
export function silenceTtyEarly(opts: {
|
|
331
|
-
interactive: boolean;
|
|
332
|
-
stdin?: Pick<NodeJS.ReadStream, "isTTY" | "setRawMode" | "resume">;
|
|
333
|
-
}): () => void {
|
|
334
|
-
const stdin = opts.stdin ?? process.stdin;
|
|
335
|
-
if (!opts.interactive || stdin.isTTY !== true) return () => {};
|
|
336
|
-
stdin.setRawMode(true);
|
|
337
|
-
// Leave input buffered until ProcessTerminal installs its listener. Resuming
|
|
338
|
-
// here drops keystrokes typed while startup or transcript loading awaits I/O.
|
|
339
|
-
return () => {
|
|
340
|
-
if (stdin.isTTY === true) stdin.setRawMode(false);
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export function parseArgs(argv: string[]): Args {
|
|
350
|
+
export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
|
|
351
|
+
let selectedTheme: string | undefined;
|
|
345
352
|
const args: Args = {
|
|
346
353
|
workspace: process.cwd(), logDir: "", maxTurns: 40, prompt: "",
|
|
347
354
|
// Overridable so a node boundary can be forced in a short session --
|
|
@@ -362,6 +369,11 @@ export function parseArgs(argv: string[]): Args {
|
|
|
362
369
|
};
|
|
363
370
|
for (let i = 0; i < argv.length; i++) {
|
|
364
371
|
const a = argv[i]!;
|
|
372
|
+
if (["--log", "--session", "--fork", "--session-dir", "--state-dir"].includes(a) &&
|
|
373
|
+
(!argv[i + 1] || argv[i + 1]!.startsWith("-"))) {
|
|
374
|
+
console.error(`${a} requires a value`);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
365
377
|
if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
|
|
366
378
|
else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
|
|
367
379
|
else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
|
|
@@ -382,13 +394,22 @@ export function parseArgs(argv: string[]): Args {
|
|
|
382
394
|
else if (a === "--model" || a === "-m") args.model = argv[++i] ?? "";
|
|
383
395
|
else if (a === "--provider") args.provider = argv[++i] ?? "";
|
|
384
396
|
else if (a === "--api-key") args.apiKey = argv[++i] ?? "";
|
|
385
|
-
else if (a === "--list-models") args.listModels = true;
|
|
397
|
+
else if (a === "--list-models") { args.listModels = true; const next = argv[i + 1]; if (next && !next.startsWith("-") && !next.startsWith("@")) args.modelSearch = argv[++i]!; }
|
|
398
|
+
else if (a === "--version" || a === "-v") args.version = true;
|
|
399
|
+
else if (a === "--export") { const file = argv[++i]; if (!file || file.startsWith("-")) throw new Error("--export requires a session path"); args.exportFile = file; }
|
|
400
|
+
else if (a === "--session-id") { const id = argv[++i]; if (!id || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(id)) throw new Error("--session-id requires a plain session identifier"); args.sessionId = id; }
|
|
401
|
+
else if (a === "--offline") args.offline = true;
|
|
402
|
+
else if (a === "--tui-mode") { const mode = argv[++i]; if (mode !== "regular" && mode !== "fullscreen") throw new Error("--tui-mode requires regular or fullscreen"); args.tuiMode = mode; }
|
|
386
403
|
else if (a === "models" && i === 0) {
|
|
387
404
|
// Subcommand, not a prompt. Do not consume the rest: a later
|
|
388
405
|
// `--models-url` must still be an unknown-flag error, and `-h`
|
|
389
406
|
// must still print help.
|
|
390
407
|
args.modelsCommand = true;
|
|
391
408
|
}
|
|
409
|
+
else if (a === "config" && i === 0) args.configCommand = true;
|
|
410
|
+
else if ((a === "install" || a === "remove" || a === "update" || a === "list") && i === 0) args.packageCommand = {operation:a,sources:[]};
|
|
411
|
+
else if ((a === "--local" || a === "-l") && (args.packageCommand || args.configCommand)) { args.local=true; if(args.packageCommand)args.packageCommand.local=true; }
|
|
412
|
+
else if (a === "--extensions" && args.packageCommand?.operation === "update") { /* Package-only update; xeno never self-updates. */ }
|
|
392
413
|
else if (a === "--refresh" && args.modelsCommand) {
|
|
393
414
|
args.modelsRefresh = true;
|
|
394
415
|
}
|
|
@@ -410,70 +431,41 @@ export function parseArgs(argv: string[]): Args {
|
|
|
410
431
|
else if (a === "--tools" || a === "-t") args.tools = parseToolList(argv[++i] ?? "");
|
|
411
432
|
else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
|
|
412
433
|
else if (a === "--no-tools" || a === "-nt") args.noTools = true;
|
|
413
|
-
else if (a === "--no-builtin-tools") args.noBuiltinTools = true;
|
|
434
|
+
else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
|
|
414
435
|
else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
|
|
415
436
|
else if (a === "--approve" || a === "-a") args.approve = true;
|
|
416
437
|
else if (a === "--no-approve" || a === "-na") args.approve = false;
|
|
417
|
-
else if (a === "--theme")
|
|
438
|
+
else if (a === "--theme") {
|
|
439
|
+
const theme = argv[++i]; if (!theme || theme.startsWith("-")) throw new Error("--theme requires a name or path");
|
|
440
|
+
if (theme.includes("/") || theme.includes("\\") || theme.endsWith(".json")) { (args.themePaths ??= []).push(theme); if (theme.endsWith(".json")) args.theme = theme; }
|
|
441
|
+
else args.theme = theme;
|
|
442
|
+
}
|
|
443
|
+
else if (a === "--use-theme") { const theme = argv[++i]; if (!theme || theme.startsWith("-")) throw new Error("--use-theme requires a name"); selectedTheme = theme; }
|
|
444
|
+
else if (a === "--no-themes") args.noThemes = true;
|
|
418
445
|
else if (a === "--thinking") args.thinking = argv[++i] ?? "";
|
|
419
|
-
else if (a === "--models")
|
|
446
|
+
else if (a === "--models") { const value = argv[++i]; if (value === undefined || value.startsWith("--")) throw new Error("--models requires a value"); args.models = value; args.modelsExplicit = true; }
|
|
420
447
|
else if (a === "--skill") args.skillPaths.push(argv[++i] ?? "");
|
|
421
448
|
else if (a === "--prompt-template") args.promptTemplatePaths.push(argv[++i] ?? "");
|
|
422
|
-
else if (a === "--no-skills") args.noSkills = true;
|
|
423
|
-
else if (a === "--no-prompt-templates") args.noPromptTemplates = true;
|
|
449
|
+
else if (a === "--no-skills" || a === "-ns") args.noSkills = true;
|
|
450
|
+
else if (a === "--no-prompt-templates" || a === "-np") args.noPromptTemplates = true;
|
|
451
|
+
else if (a === "--no-extensions" || a === "-ne") args.noExtensions = true;
|
|
452
|
+
else if (a === "--extension" || a === "-e") {
|
|
453
|
+
const path = argv[++i]; if (!path || path.startsWith("-")) throw new Error("--extension requires a path");
|
|
454
|
+
(args.extensionPaths ??= []).push(path);
|
|
455
|
+
}
|
|
456
|
+
else if (a === "--extension-flag") {
|
|
457
|
+
const value = argv[++i] ?? "", at = value.indexOf("=");
|
|
458
|
+
if (at <= 0) throw new Error("--extension-flag requires name=value");
|
|
459
|
+
(args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[value.slice(0, at)] = value.slice(at + 1) === "true" ? true : value.slice(at + 1) === "false" ? false : value.slice(at + 1);
|
|
460
|
+
}
|
|
424
461
|
else if (a === "--mode") args.mode = argv[++i] ?? "";
|
|
425
462
|
else if (a === "--verbose") args.verbose = true;
|
|
426
463
|
else if (a === "-p" || a === "--print") {
|
|
427
464
|
args.print = true;
|
|
428
|
-
const next = argv[++i] ?? "";
|
|
429
|
-
args.prompt += (args.prompt ? " " : "") + next;
|
|
430
465
|
}
|
|
431
466
|
else if (a === "--help" || a === "-h") {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
" [-w <workspace>] [--log <dir>] [--max-turns N]\n" +
|
|
435
|
-
" [--context-policy windowed|segmented|continuous|adaptive]\n" +
|
|
436
|
-
" [--trigger N --hard N] [--list-models] [--fresh]\n" +
|
|
437
|
-
" [-c|--continue] [-r|--resume [id]] [--session path|id]\n" +
|
|
438
|
-
" [--fork path|id] [--no-session] [--session-dir dir]\n" +
|
|
439
|
-
" [-n|--name name]\n" +
|
|
440
|
-
" [models --refresh]\n" +
|
|
441
|
-
" [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
|
|
442
|
-
" [--no-context-files|-nc]\n" +
|
|
443
|
-
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
444
|
-
" [--no-builtin-tools]\n" +
|
|
445
|
-
" [--allow-tools names] [--approve|--no-approve]\n" +
|
|
446
|
-
" [--theme <name|path>] [--thinking <level>] [--models a,b]\n" +
|
|
447
|
-
" [--skill <path>] [--no-skills]\n" +
|
|
448
|
-
" [--prompt-template <path>] [--no-prompt-templates]\n" +
|
|
449
|
-
" [--mode json] [--verbose]\n" +
|
|
450
|
-
" [-p <prompt>] [prompt...]\n\n" +
|
|
451
|
-
" A workspace that already holds a session is resumed. --fresh starts a\n" +
|
|
452
|
-
" separate one alongside it; nothing is ever deleted.\n" +
|
|
453
|
-
" -c continues the newest; --session / -r <id> pick one (unique prefix only).\n" +
|
|
454
|
-
" --no-session runs without writing .xeno/sessions/. --fork copies into a new one.\n" +
|
|
455
|
-
" A leftover .xeno/log is not moved; continue it with --log .xeno/log.\n\n" +
|
|
456
|
-
" With a prompt, runs it and exits. Without one, reads prompts from stdin.\n" +
|
|
457
|
-
" Commands: /model /thinking /login /new /resume /name /session /usage /state\n" +
|
|
458
|
-
" /export /copy /fork /clone\n" +
|
|
459
|
-
" /back [n [files|context]] [confirm] /trust /settings /hotkeys\n" +
|
|
460
|
-
" /help /quit\n\n" +
|
|
461
|
-
" Credentials resolve in this order: --api-key, then ~/.xeno/auth.json,\n" +
|
|
462
|
-
" then the provider's environment variable.\n" +
|
|
463
|
-
" --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
|
|
464
|
-
" --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
|
|
465
|
-
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
466
|
-
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
467
|
-
" bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
|
|
468
|
-
" --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
|
|
469
|
-
" --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
|
|
470
|
-
" --path-root <dir> lets file tools reach absolute paths under <dir> (e.g. / in a container);\n" +
|
|
471
|
-
" relative paths, fingerprints and /back stay in the workspace, so edits outside it are not rewound.\n" +
|
|
472
|
-
" --approve loads .xeno/settings.json this run; --no-approve skips it.\n" +
|
|
473
|
-
" Project trust does not affect tool execution.\n" +
|
|
474
|
-
" --mode json writes one JSON object per event to stdout; diagnostics stay on stderr.\n" +
|
|
475
|
-
" --verbose expands the startup header (AGENTS.md, skills, prompt templates).",
|
|
476
|
-
);
|
|
467
|
+
if (options.deferExtensions) { args.help = true; continue; }
|
|
468
|
+
printCliHelp();
|
|
477
469
|
process.exit(0);
|
|
478
470
|
} else if (a === "--") {
|
|
479
471
|
// Everything after `--` is content, never a flag. The delimiter
|
|
@@ -483,9 +475,23 @@ export function parseArgs(argv: string[]): Args {
|
|
|
483
475
|
}
|
|
484
476
|
break;
|
|
485
477
|
} else if (a.startsWith("-") && a !== "-") {
|
|
486
|
-
|
|
487
|
-
|
|
478
|
+
const equal = a.indexOf("="), name = a.slice(2, equal === -1 ? undefined : equal), definition = options.flags?.get(name);
|
|
479
|
+
if (a.startsWith("--") && (definition || options.deferExtensions)) {
|
|
480
|
+
const next = argv[i + 1], input: FlagInput = { inline: equal !== -1,
|
|
481
|
+
...(equal !== -1 ? { value: a.slice(equal + 1) } : next !== undefined && !next.startsWith("-") && !next.startsWith("@") ? { value: next } : {}) };
|
|
482
|
+
if (definition) {
|
|
483
|
+
const value = extensionFlagValue(name, definition, input);
|
|
484
|
+
if (value !== undefined) (args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[name] = value;
|
|
485
|
+
if (!input.inline && input.value !== undefined && (definition.type === "string" || input.value === "true" || input.value === "false")) i++;
|
|
486
|
+
} else {
|
|
487
|
+
args.extensionArgv = [...argv]; (args.extensionInputs ??= Object.create(null) as Record<string, FlagInput>)[name] = input;
|
|
488
|
+
if (!input.inline && input.value !== undefined) i++;
|
|
489
|
+
}
|
|
490
|
+
} else rejectUnknownFlag(a);
|
|
491
|
+
} else if(args.packageCommand)args.packageCommand.sources.push(a);
|
|
492
|
+
else args.prompt += (args.prompt ? " " : "") + a;
|
|
488
493
|
}
|
|
494
|
+
if (selectedTheme !== undefined) args.theme = selectedTheme;
|
|
489
495
|
return args;
|
|
490
496
|
}
|
|
491
497
|
|
|
@@ -621,7 +627,7 @@ function countCatalogDelta(
|
|
|
621
627
|
return map;
|
|
622
628
|
}
|
|
623
629
|
for (const spec of parseUserCatalog(json).models) {
|
|
624
|
-
map.set(spec
|
|
630
|
+
map.set(modelKey(spec), JSON.stringify(spec));
|
|
625
631
|
}
|
|
626
632
|
return map;
|
|
627
633
|
};
|
|
@@ -650,13 +656,13 @@ export async function runModelsRefresh(opts: {
|
|
|
650
656
|
| { ok: true; count: number; added: number; changed: number }
|
|
651
657
|
| { ok: false; reason: string }
|
|
652
658
|
> {
|
|
653
|
-
const before = await
|
|
659
|
+
const before = await readCatalogCacheText();
|
|
654
660
|
const result = await refreshRemoteCatalog({
|
|
655
661
|
fetch: opts.fetch,
|
|
656
662
|
write: opts.write,
|
|
657
663
|
});
|
|
658
664
|
if (!result.ok) return result;
|
|
659
|
-
const after = await
|
|
665
|
+
const after = await readCatalogCacheText();
|
|
660
666
|
const delta = countCatalogDelta(before.text, after.text);
|
|
661
667
|
return { ok: true, count: result.count, added: delta.added, changed: delta.changed };
|
|
662
668
|
}
|
|
@@ -729,6 +735,10 @@ export interface App {
|
|
|
729
735
|
askSlot: AskSlot;
|
|
730
736
|
ui?: UiState;
|
|
731
737
|
slash?: SlashCatalog;
|
|
738
|
+
resources?: ResourceIndex;
|
|
739
|
+
projectTrusted?: boolean;
|
|
740
|
+
extensions?: ExtensionRegistry;
|
|
741
|
+
shutdownRequested?: boolean;
|
|
732
742
|
}
|
|
733
743
|
|
|
734
744
|
export interface UiState {
|
|
@@ -749,13 +759,6 @@ function toUiState(loaded: UiResources, color: boolean): UiState {
|
|
|
749
759
|
};
|
|
750
760
|
}
|
|
751
761
|
|
|
752
|
-
function asEffort(value: string | undefined): Effort | undefined {
|
|
753
|
-
if (value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max") {
|
|
754
|
-
return value;
|
|
755
|
-
}
|
|
756
|
-
return undefined;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
762
|
/** `--models a,b` and settings.scopedModels share this split so a stray space cannot invent a pattern. */
|
|
760
763
|
export function splitModelPatterns(raw: string | undefined): string[] {
|
|
761
764
|
if (raw === undefined || raw.length === 0) return [];
|
|
@@ -791,7 +794,7 @@ export function missingCredentialMessage(spec: ModelSpec, explicit: boolean, cat
|
|
|
791
794
|
}
|
|
792
795
|
|
|
793
796
|
export function isInteractiveSession(args: Args): boolean {
|
|
794
|
-
return args.prompt.length === 0 && stdinIsTty() && stdoutIsTty();
|
|
797
|
+
return !args.print && !args.help && args.mode !== "json" && args.prompt.length === 0 && stdinIsTty() && stdoutIsTty();
|
|
795
798
|
}
|
|
796
799
|
|
|
797
800
|
/**
|
|
@@ -847,8 +850,9 @@ export function toolSelectionOf(args: Args) {
|
|
|
847
850
|
* the provider bills, and an extra field there would look like a tool
|
|
848
851
|
* argument.
|
|
849
852
|
*/
|
|
850
|
-
export function visibleSchemas(args: Args) {
|
|
851
|
-
const
|
|
853
|
+
export function visibleSchemas(args: Args, extensions?: ExtensionRegistry) {
|
|
854
|
+
const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
|
|
855
|
+
const available = [...TOOL_SCHEMAS.filter(tool => !extensions?.tools.has(tool.name)).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
|
|
852
856
|
return selectTools(available, toolSelectionOf(args)).map((tool) => {
|
|
853
857
|
const { source, ...schema } = tool;
|
|
854
858
|
void source;
|
|
@@ -872,9 +876,11 @@ export function createSessionFor(opts: {
|
|
|
872
876
|
io: AppIO;
|
|
873
877
|
askSlot: AskSlot;
|
|
874
878
|
oauth?: boolean;
|
|
879
|
+
effects?: ResolveEffects;
|
|
880
|
+
extensions?: ExtensionRegistry;
|
|
875
881
|
}): Session {
|
|
876
882
|
const { args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot } = opts;
|
|
877
|
-
const schemas = visibleSchemas(args);
|
|
883
|
+
const schemas = visibleSchemas(args, opts.extensions);
|
|
878
884
|
const names = schemas.map((t) => t.name);
|
|
879
885
|
const sessionRules: PermissionRule[] = [];
|
|
880
886
|
const tools: ToolContext = {
|
|
@@ -885,6 +891,7 @@ export function createSessionFor(opts: {
|
|
|
885
891
|
rules: rulesWithAllowTools(args.allowTools),
|
|
886
892
|
sessionRules,
|
|
887
893
|
askSlot,
|
|
894
|
+
...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
|
|
888
895
|
};
|
|
889
896
|
return new Session(
|
|
890
897
|
{
|
|
@@ -916,21 +923,177 @@ export function createSessionFor(opts: {
|
|
|
916
923
|
providerConfig,
|
|
917
924
|
credential.ok ? credential.key : "",
|
|
918
925
|
retryHandler(io),
|
|
919
|
-
opts.oauth === true ? { oauth: true } :
|
|
926
|
+
{ ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
920
927
|
),
|
|
921
928
|
systemClock(),
|
|
922
929
|
);
|
|
923
930
|
}
|
|
924
931
|
|
|
925
|
-
|
|
932
|
+
/** One location policy for startup, /new, /resume and /fork. */
|
|
933
|
+
export function sessionDirFor(args: Args, workspace: string): string | undefined {
|
|
934
|
+
if (args.sessionDir) return args.sessionDir;
|
|
935
|
+
return args.stateDir === undefined ? undefined : statePaths(workspace, args.stateDir).sessions;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
type OpenSessionOptions = Omit<Parameters<typeof createSessionFor>[0], "providerConfig" | "credential" | "oauth" | "system"> & {
|
|
939
|
+
startupPhase?: (name: string) => void;
|
|
940
|
+
catalog: readonly ModelSpec[];
|
|
941
|
+
auth: AuthFile;
|
|
942
|
+
credentialOf: (spec: ModelSpec) => Resolved;
|
|
943
|
+
interactive: boolean;
|
|
944
|
+
settings?: Settings;
|
|
945
|
+
/** CLI overrides belong to this open only; /resume must not inherit them. */
|
|
946
|
+
modelFlags?: { model: string; provider: string };
|
|
947
|
+
preparedResources?: { workspace: string; value: Awaited<ReturnType<typeof loadSessionResources>> };
|
|
948
|
+
fallback?: ProviderConfig;
|
|
949
|
+
projectTrusted?: boolean;
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
/** Pi replaces cwd-scoped resources with the session runtime; publish only after all reads succeed. */
|
|
953
|
+
async function prepareExtensionPackages(args: Args, workspace: string): Promise<void> {
|
|
954
|
+
const remote=(args.extensionPaths??[]).filter(path=>/^(npm:|git:|git\+|https?:\/\/|ssh:\/\/|git@)/.test(path));
|
|
955
|
+
if(!remote.length)return;
|
|
956
|
+
if(args.offline || process.env["XENO_OFFLINE"] === "1") throw new Error("--offline prevents remote extension package preparation; install it explicitly beforehand");
|
|
957
|
+
const manager=new PackageManager({workspace});
|
|
958
|
+
for(const source of remote)(args.packagePaths??=[]).push({source,path:await manager.temporary(source)});
|
|
959
|
+
args.extensionPaths=(args.extensionPaths??[]).filter(path=>!remote.includes(path));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export async function loadSessionResources(args: Args, workspace: string, io: AppIO, trustOverride?: boolean): Promise<{
|
|
963
|
+
loadedContext: LoadedContext; slash: SlashCatalog; ui: UiState; index: ResourceIndex; trusted: boolean; system: string; extensions: ExtensionRegistry;
|
|
964
|
+
}> {
|
|
965
|
+
const { trusted } = await projectTrust(workspace, trustOverride ?? args.approve);
|
|
966
|
+
await prepareExtensionPackages(args,workspace);
|
|
967
|
+
const index = await discoverResources({ ...args, workspace, trusted });
|
|
968
|
+
const extensions = new ExtensionRegistry({ reserved: COMMANDS.map(command => command.name), reservedFlags: KNOWN_FLAGS.map(flag => flag.replace(/^-+/, "")),
|
|
969
|
+
flagValue: (name, definition) => args.extensionFlags && Object.hasOwn(args.extensionFlags, name) ? args.extensionFlags[name] : extensionFlagValue(name, definition, args.extensionInputs && Object.hasOwn(args.extensionInputs, name) ? args.extensionInputs[name] : undefined), report: io.say });
|
|
970
|
+
await extensions.load(index);
|
|
971
|
+
if(args.extensionArgv) {
|
|
972
|
+
const validated = parseArgs(args.extensionArgv, { flags: extensions.flags, ...(args.help ? { deferExtensions: true } : {}) });
|
|
973
|
+
if (validated.extensionArgv) rejectUnknownFlag("--" + Object.keys(validated.extensionInputs ?? {})[0]);
|
|
974
|
+
args.prompt = validated.prompt; args.extensionFlags = validated.extensionFlags ?? {};
|
|
975
|
+
}
|
|
976
|
+
const loaded = await loadUiResources({ ...args, workspace, trusted, ...(args.theme ? { theme: args.theme } : {}) }, index);
|
|
977
|
+
const loadedContext = await loadContext({ workspace, cwd: workspace, configDir: configDir(),
|
|
978
|
+
noContextFiles: args.noContextFiles, appendSystemPrompt: args.appendSystemPrompt,
|
|
979
|
+
...(args.systemPrompt === null ? {} : { systemPrompt: args.systemPrompt }) });
|
|
980
|
+
for (const notice of loadedContext.notices) if (notice.kind === "skipped" || notice.truncated) io.say(io.palette.yellow(formatNotice(notice)));
|
|
981
|
+
const text = await loadTextResources({ workspace, configDir: configDir(), trusted, noSkills: args.noSkills,
|
|
982
|
+
noPromptTemplates: args.noPromptTemplates, skillPaths: args.skillPaths, promptTemplatePaths: args.promptTemplatePaths }, index);
|
|
983
|
+
const slash = resolveSlashCatalog(text.prompts, text.skills);
|
|
984
|
+
for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
|
|
985
|
+
const system = appendSkillsToSystem(assembleSystemPrompt(systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)), loadedContext), text.skills);
|
|
986
|
+
const ui = toUiState(loaded, colorEnabled());
|
|
987
|
+
if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
|
|
988
|
+
return { loadedContext, slash: { ...slash.catalog, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Pi's session-before-model order, with xeno's N.8 checkpoint on an explicit
|
|
993
|
+
* model change. Opening never sends a request to the previous provider.
|
|
994
|
+
* Callers publish the returned session/config/credential together on success.
|
|
995
|
+
*/
|
|
996
|
+
export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
997
|
+
session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
|
|
998
|
+
workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
|
|
999
|
+
}> {
|
|
1000
|
+
opts.startupPhase?.("Reading session history");
|
|
1001
|
+
const recorded = await readCursor(opts.logDir);
|
|
1002
|
+
const priorCursor = recorded === null ? null : await recoverCut(opts.logDir, recorded);
|
|
1003
|
+
const workspace = priorCursor?.workspace ?? opts.workspace;
|
|
1004
|
+
if (opts.args.pathRoot) {
|
|
1005
|
+
const rel = relative(opts.args.pathRoot, workspace);
|
|
1006
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
|
|
1007
|
+
}
|
|
1008
|
+
if (opts.preparedResources && opts.preparedResources.workspace !== workspace) await opts.preparedResources.value.extensions.dispose();
|
|
1009
|
+
opts.startupPhase?.("Loading session resources");
|
|
1010
|
+
const resources = opts.preparedResources?.workspace === workspace ? opts.preparedResources.value
|
|
1011
|
+
: await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
|
|
1012
|
+
const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
|
|
1013
|
+
const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
|
|
1014
|
+
const unknown = unknownToolNames(named).filter(name => !resources.extensions.tools.has(name));
|
|
1015
|
+
if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
|
|
1016
|
+
const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
|
|
1017
|
+
const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
|
|
1018
|
+
const parsedModel = flags.model ? scopedModels([flags.model], flags.provider ? opts.catalog.filter(model => model.provider === flags.provider) : opts.catalog, true).entries[0] : undefined;
|
|
1019
|
+
if (parsedModel) flags.model = modelKey(parsedModel.model);
|
|
1020
|
+
const explicit = flags.model.length > 0 || flags.provider.length > 0;
|
|
1021
|
+
const settings = opts.settings ?? {};
|
|
1022
|
+
const preferred = saved ?? opts.fallback?.spec;
|
|
1023
|
+
opts.startupPhase?.("Resolving model credentials");
|
|
1024
|
+
const selected = !explicit && preferred !== undefined
|
|
1025
|
+
? { ok: true as const, spec: preferred }
|
|
1026
|
+
: selectModel(settings, flags, (spec) => opts.credentialOf(spec).ok, opts.catalog);
|
|
1027
|
+
let spec: ModelSpec;
|
|
1028
|
+
if (selected.ok) spec = selected.spec;
|
|
1029
|
+
else {
|
|
1030
|
+
const missingAll = selected.reason.startsWith("no model has a credential");
|
|
1031
|
+
if (!missingAll || !interactive) {
|
|
1032
|
+
throw new Error(missingAll ? describeUnauthenticatedScript(opts.catalog) : selected.reason);
|
|
1033
|
+
}
|
|
1034
|
+
const fallback = selectModel(settings, flags, () => true, opts.catalog);
|
|
1035
|
+
if (!fallback.ok) throw new Error(fallback.reason);
|
|
1036
|
+
spec = fallback.spec;
|
|
1037
|
+
}
|
|
1038
|
+
const credential = opts.credentialOf(spec);
|
|
1039
|
+
if (!credential.ok && !interactive) {
|
|
1040
|
+
throw new Error(credential.reason.startsWith("no credential for ")
|
|
1041
|
+
? missingCredentialMessage(spec, explicit || saved !== undefined, opts.catalog) : credential.reason);
|
|
1042
|
+
}
|
|
1043
|
+
const explicitEffort = asThinkingLevel(opts.args.thinking), requestedEffort = explicitEffort ?? parsedModel?.thinking;
|
|
1044
|
+
if (opts.args.thinking && (!explicitEffort || !supportedThinkingLevels(spec).includes(explicitEffort))) {
|
|
1045
|
+
throw new Error(`thinking ${opts.args.thinking} is not supported by ${modelKey(spec)}; supported: ${supportedThinkingLevels(spec).join(", ")}`);
|
|
1046
|
+
}
|
|
1047
|
+
const providerConfig: ProviderConfig = {
|
|
1048
|
+
spec, maxTokens: outputTokenBudget(spec),
|
|
1049
|
+
effort: clampThinkingLevel(spec, requestedEffort ?? asThinkingLevel(priorCursor?.thinking) ??
|
|
1050
|
+
settings.modelThinkingLevels?.[modelKey(spec)] ?? opts.fallback?.effort ?? asThinkingLevel(settings.defaultThinking) ??
|
|
1051
|
+
asThinkingLevel(settings.thinking) ?? DEFAULT_PROVIDER.effort),
|
|
1052
|
+
thinkingDisplay: opts.fallback?.thinkingDisplay ?? DEFAULT_PROVIDER.thinkingDisplay,
|
|
1053
|
+
};
|
|
1054
|
+
const restoring = saved ?? spec;
|
|
1055
|
+
const restoringCredential = saved === undefined ? credential : opts.credentialOf(restoring);
|
|
1056
|
+
const oauth = (model: ModelSpec, got: Resolved) => got.ok && storedIsOAuth(opts.auth, model.provider) && opts.args.apiKey.length === 0;
|
|
1057
|
+
const session = createSessionFor({
|
|
1058
|
+
...opts,
|
|
1059
|
+
system: resources.system,
|
|
1060
|
+
extensions: resources.extensions,
|
|
1061
|
+
workspace: priorCursor?.workspace ?? opts.workspace,
|
|
1062
|
+
blobDir: priorCursor?.blob_dir ?? (priorCursor?.workspace && priorCursor.workspace !== opts.workspace
|
|
1063
|
+
? statePaths(priorCursor.workspace, opts.args.stateDir).blobs : opts.blobDir),
|
|
1064
|
+
providerConfig: { ...providerConfig, spec: restoring, maxTokens: outputTokenBudget(restoring) },
|
|
1065
|
+
credential: restoringCredential,
|
|
1066
|
+
...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
|
|
1067
|
+
});
|
|
1068
|
+
opts.startupPhase?.("Preparing workspace snapshot");
|
|
1069
|
+
await session.start();
|
|
1070
|
+
if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
|
|
1071
|
+
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
|
|
1072
|
+
await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions) });
|
|
1073
|
+
}
|
|
1074
|
+
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1075
|
+
await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
1076
|
+
{ ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1077
|
+
opts.io.say(opts.io.palette.yellow(` switched ${restoring.id} → ${spec.id} at a model checkpoint`));
|
|
1078
|
+
}
|
|
1079
|
+
resources.ui.settings = { ...resources.ui.settings, thinking: providerConfig.effort,
|
|
1080
|
+
...(opts.args.modelsExplicit || opts.args.models ? { scopedModels: splitModelPatterns(opts.args.models) } : {}) };
|
|
1081
|
+
if (resources.ui.settings.scopedModels) for (const warning of scopedModels(resources.ui.settings.scopedModels, opts.catalog).diagnostics) opts.io.say(` ${warning}`);
|
|
1082
|
+
return { session, priorCursor, providerConfig, credential, workspace: session.config.tools.root,
|
|
1083
|
+
blobDir: session.config.blobDir ?? opts.blobDir, system: session.config.frozen.system, resources };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
1087
|
+
const withTerminal = <T>(run: (handoff: () => void) => Promise<T>): Promise<T> => input ? input.runWithTerminal(run) : run(() => {});
|
|
1088
|
+
if (args.extensionArgv && (args.listModels || args.modelsCommand || args.configCommand)) rejectUnknownFlag("--" + Object.keys(args.extensionInputs ?? {})[0]);
|
|
926
1089
|
const C = createPalette(colorEnabled());
|
|
927
|
-
|
|
1090
|
+
let printMode = isPrintMode(args);
|
|
928
1091
|
const jsonMode = isJsonMode(args);
|
|
929
|
-
if ((args.mode ?? "").length > 0 && args.mode !== "json") {
|
|
930
|
-
console.error(`unknown --mode ${args.mode} (supported: json)`);
|
|
1092
|
+
if ((args.mode ?? "").length > 0 && args.mode !== "json" && args.mode !== "text") {
|
|
1093
|
+
console.error(`unknown --mode ${args.mode} (supported: text, json)`);
|
|
931
1094
|
process.exit(1);
|
|
932
1095
|
}
|
|
933
|
-
|
|
1096
|
+
let divert = printMode || jsonMode;
|
|
934
1097
|
const io: AppIO = {
|
|
935
1098
|
palette: C,
|
|
936
1099
|
say: (line) => (divert ? console.error(line) : console.log(line)),
|
|
@@ -943,6 +1106,11 @@ export async function startup(args: Args): Promise<App> {
|
|
|
943
1106
|
}
|
|
944
1107
|
: {}),
|
|
945
1108
|
};
|
|
1109
|
+
const refreshOutputMode = () => {
|
|
1110
|
+
printMode = isPrintMode(args); divert = printMode || jsonMode;
|
|
1111
|
+
if (printMode && !jsonMode) io.answer = text => { process.stdout.write(text.endsWith("\n") ? text : `${text}\n`); };
|
|
1112
|
+
else delete io.answer;
|
|
1113
|
+
};
|
|
946
1114
|
const effects = processEffects();
|
|
947
1115
|
|
|
948
1116
|
const authRead = await readJsonFile<AuthFile>(authPath());
|
|
@@ -955,12 +1123,13 @@ export async function startup(args: Args): Promise<App> {
|
|
|
955
1123
|
io.say(C.dim(" fetches models.dev into ~/.xeno/models.json; startup never does this."));
|
|
956
1124
|
process.exit(0);
|
|
957
1125
|
}
|
|
1126
|
+
if (args.offline || process.env["XENO_OFFLINE"] === "1") throw new Error("--offline prevents catalog refresh");
|
|
958
1127
|
const result = await runModelsRefresh({
|
|
959
1128
|
fetch: globalThis.fetch,
|
|
960
|
-
write:
|
|
1129
|
+
write: writeCatalogCache,
|
|
961
1130
|
});
|
|
962
1131
|
if (result.ok) {
|
|
963
|
-
io.say(` added ${result.added}, changed ${result.changed} (${result.count} total) → ${
|
|
1132
|
+
io.say(` added ${result.added}, changed ${result.changed} (${result.count} total) → ${catalogCachePath()}`);
|
|
964
1133
|
process.exit(0);
|
|
965
1134
|
} else {
|
|
966
1135
|
io.say(C.yellow(` ${result.reason}`));
|
|
@@ -970,17 +1139,17 @@ export async function startup(args: Args): Promise<App> {
|
|
|
970
1139
|
}
|
|
971
1140
|
|
|
972
1141
|
// File only. Passing fetch here would make the test of D16 un-runnable.
|
|
973
|
-
|
|
974
|
-
const loaded = loadCatalog(
|
|
1142
|
+
input?.phase("Loading model catalog");
|
|
1143
|
+
const loaded = loadCatalog(await readModelSources());
|
|
975
1144
|
for (const warning of loaded.warnings) io.say(C.yellow(` ${warning}`));
|
|
976
1145
|
const catalog = loaded.models;
|
|
977
1146
|
|
|
978
1147
|
const credentialOf = (spec: ModelSpec) => resolveCredential(spec, auth, args.apiKey, effects);
|
|
979
|
-
const hasCredential = (spec: ModelSpec) => credentialOf(spec).ok;
|
|
980
1148
|
|
|
981
1149
|
if (args.listModels) {
|
|
982
1150
|
io.say(C.bold("models"));
|
|
983
|
-
|
|
1151
|
+
const query = args.modelSearch?.toLowerCase();
|
|
1152
|
+
for (const group of groupedModels(query ? catalog.filter(model => [modelKey(model), model.name].some(text => { let at=0; for(const char of text.toLowerCase()) if(char===query[at])at++; return at===query.length; })) : catalog)) {
|
|
984
1153
|
io.say(C.bold(group.provider));
|
|
985
1154
|
for (const spec of group.models) {
|
|
986
1155
|
const got = credentialOf(spec);
|
|
@@ -992,18 +1161,6 @@ export async function startup(args: Args): Promise<App> {
|
|
|
992
1161
|
process.exit(0);
|
|
993
1162
|
}
|
|
994
1163
|
|
|
995
|
-
const named = [
|
|
996
|
-
...(args.tools ?? []),
|
|
997
|
-
...args.excludeTools,
|
|
998
|
-
...args.allowTools,
|
|
999
|
-
];
|
|
1000
|
-
const unknown = unknownToolNames(named);
|
|
1001
|
-
if (unknown.length > 0) {
|
|
1002
|
-
io.err(C.red(`\n unknown tool: ${unknown.join(", ")}`));
|
|
1003
|
-
io.err(C.dim(` known: ${TOOL_SCHEMAS.map((t) => t.name).join(", ")}\n`));
|
|
1004
|
-
process.exit(1);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
1164
|
/**
|
|
1008
1165
|
* Resolving the workspace is the first thing that can fail, and it fails for
|
|
1009
1166
|
* ordinary reasons -- a typo in `-w`, a directory that was moved. Reported as
|
|
@@ -1012,6 +1169,7 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1012
1169
|
* It has to happen before model selection so a trusted `.xeno/settings.json`
|
|
1013
1170
|
* can override the default model. Trust does not affect tools.
|
|
1014
1171
|
*/
|
|
1172
|
+
input?.phase("Opening workspace");
|
|
1015
1173
|
let workspace: string;
|
|
1016
1174
|
try {
|
|
1017
1175
|
workspace = await workspaceRoot(args.workspace);
|
|
@@ -1031,139 +1189,83 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1031
1189
|
}
|
|
1032
1190
|
}
|
|
1033
1191
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
else if (decision === false) loadProjectSettings = false;
|
|
1045
|
-
else {
|
|
1046
|
-
loadProjectSettings = false;
|
|
1047
|
-
try {
|
|
1048
|
-
await stat(projectSettingsPath(workspace));
|
|
1049
|
-
io.say(
|
|
1050
|
-
C.dim(
|
|
1051
|
-
` ${projectSettingsPath(workspace)} not loaded (project is not trusted). ` +
|
|
1052
|
-
`Trust does not affect tool execution. /trust saves a decision, or pass --approve this run.`,
|
|
1053
|
-
),
|
|
1054
|
-
);
|
|
1055
|
-
} catch {
|
|
1056
|
-
// No project settings file: nothing to load, no notice.
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1192
|
+
input?.phase("Preparing extensions and project settings");
|
|
1193
|
+
await prepareExtensionPackages(args,workspace);
|
|
1194
|
+
const trust = await projectTrust(workspace, args.approve);
|
|
1195
|
+
let loadProjectSettings = trust.trusted;
|
|
1196
|
+
if (args.configCommand) {
|
|
1197
|
+
if (!stdinIsTty() || !stdoutIsTty()) {
|
|
1198
|
+
const index = await discoverResources({ ...args, workspace, trusted: loadProjectSettings });
|
|
1199
|
+
for (const entry of index.entries) io.say(`${entry.enabled ? "[x]" : "[ ]"} ${entry.kind} ${entry.scope}${entry.package?" · "+entry.package.source:""} ${entry.path}`);
|
|
1200
|
+
} else await withTerminal(handoff => runResourceConfig({ ...args, workspace, trusted: loadProjectSettings }, handoff));
|
|
1201
|
+
process.exit(0);
|
|
1059
1202
|
}
|
|
1203
|
+
const hasProjectResources = (await Promise.all(["settings.json", "keybindings.json", "skills", "prompts", "themes", "extensions"].map(name => stat(join(workspace, ".xeno", name)).then(() => true, () => false)))).some(Boolean);
|
|
1204
|
+
if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await withTerminal(handoff => pickProjectTrust(workspace, handoff));
|
|
1205
|
+
if (hasProjectResources && !loadProjectSettings) io.say(C.dim(" Project resources not loaded (project is not trusted). /trust changes resource trust; tool permissions are separate."));
|
|
1060
1206
|
|
|
1061
|
-
|
|
1207
|
+
// Resolve the resource owner before factories run. Parsing flags must not load a different cwd first.
|
|
1208
|
+
let resourceWorkspace = workspace;
|
|
1209
|
+
const prepareFlags = (args.extensionArgv && args.resume !== true) || args.help;
|
|
1210
|
+
if (prepareFlags) {
|
|
1211
|
+
const target = args.logDir || args.session || args.forkFrom || (typeof args.resume === "string" ? args.resume : undefined);
|
|
1212
|
+
let resourceLog: string | undefined;
|
|
1213
|
+
if (target) { const found = await findSessionByPrefix(workspace, target, sessionDirFor(args, workspace)); if (found.ok) resourceLog = found.session.path; }
|
|
1214
|
+
else if (args.sessionId) resourceLog = join(sessionsDir(workspace, sessionDirFor(args, workspace)), args.sessionId);
|
|
1215
|
+
else if (args.continueLatest) resourceLog = (await listSessions(workspace, sessionDirFor(args, workspace), Infinity)).sessions.find(session => session.ok)?.path;
|
|
1216
|
+
if (resourceLog) resourceWorkspace = (await readCursor(resourceLog))?.workspace ?? workspace;
|
|
1217
|
+
}
|
|
1218
|
+
input?.phase("Loading session resources");
|
|
1219
|
+
const preparedResources = prepareFlags
|
|
1220
|
+
? { workspace: resourceWorkspace, value: await loadSessionResources(args, resourceWorkspace, io, resourceWorkspace === workspace ? loadProjectSettings : undefined) } : undefined;
|
|
1221
|
+
if (args.help) { printCliHelp(preparedResources?.value.extensions.flags); await preparedResources?.value.extensions.dispose(); process.exit(0); }
|
|
1222
|
+
refreshOutputMode();
|
|
1223
|
+
const uiLoaded = preparedResources ? undefined : await loadUiResources({
|
|
1224
|
+
...args,
|
|
1062
1225
|
workspace,
|
|
1063
1226
|
trusted: loadProjectSettings,
|
|
1064
1227
|
...(args.theme.length > 0 ? { theme: args.theme } : {}),
|
|
1065
1228
|
});
|
|
1066
|
-
for (const line of uiLoaded
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1229
|
+
for (const line of uiLoaded?.diagnostics ?? []) io.say(C.yellow(` ${line}`));
|
|
1230
|
+
const ui = preparedResources?.value.ui ?? toUiState(uiLoaded!, colorEnabled());
|
|
1231
|
+
const mergedSettings = ui.settings;
|
|
1069
1232
|
const scopedFromFlag = splitModelPatterns(args.models);
|
|
1070
|
-
if (scopedFromFlag.length > 0) {
|
|
1233
|
+
if (args.modelsExplicit || scopedFromFlag.length > 0) {
|
|
1071
1234
|
ui.settings = { ...ui.settings, scopedModels: scopedFromFlag };
|
|
1072
1235
|
}
|
|
1073
1236
|
|
|
1074
|
-
const selected = selectModel(mergedSettings, { model: args.model, provider: args.provider }, hasCredential, catalog);
|
|
1075
|
-
const interactive = isInteractiveSession(args);
|
|
1076
|
-
let spec: ModelSpec;
|
|
1077
|
-
if (selected.ok) {
|
|
1078
|
-
spec = selected.spec;
|
|
1079
|
-
} else {
|
|
1080
|
-
const missingAll = selected.reason.startsWith("no model has a credential");
|
|
1081
|
-
if (!missingAll || !interactive) {
|
|
1082
|
-
io.err(C.red(`\n ${missingAll ? describeUnauthenticatedScript(catalog) : selected.reason}\n`));
|
|
1083
|
-
process.exit(1);
|
|
1084
|
-
}
|
|
1085
|
-
// Same order as selectModel, ignoring credentials, so the banner names a
|
|
1086
|
-
// real catalog model rather than inventing one.
|
|
1087
|
-
const fallback = selectModel(mergedSettings, { model: args.model, provider: args.provider }, () => true, catalog);
|
|
1088
|
-
const picked = fallback.ok ? fallback.spec : catalog[0];
|
|
1089
|
-
if (picked === undefined) {
|
|
1090
|
-
io.err(C.red("\n catalog is empty\n"));
|
|
1091
|
-
process.exit(1);
|
|
1092
|
-
}
|
|
1093
|
-
spec = picked;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const credential = credentialOf(spec);
|
|
1097
|
-
if (!credential.ok && !interactive) {
|
|
1098
|
-
io.err(C.red(`\n ${missingCredentialMessage(spec, args.model.length > 0, catalog)}\n`));
|
|
1099
|
-
process.exit(1);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
const providerConfig: ProviderConfig = {
|
|
1103
|
-
spec,
|
|
1104
|
-
maxTokens: outputTokenBudget(spec),
|
|
1105
|
-
// --thinking wins, then a Ctrl+S default, then /settings thinking / builtin.
|
|
1106
|
-
effort:
|
|
1107
|
-
asEffort(args.thinking) ??
|
|
1108
|
-
asEffort(mergedSettings.defaultThinking) ??
|
|
1109
|
-
asEffort(mergedSettings.thinking) ??
|
|
1110
|
-
DEFAULT_PROVIDER.effort,
|
|
1111
|
-
thinkingDisplay: DEFAULT_PROVIDER.thinkingDisplay,
|
|
1112
|
-
};
|
|
1113
|
-
/**
|
|
1114
|
-
* `--fresh` opens a NEW session beside the old one rather than clearing it.
|
|
1115
|
-
*
|
|
1116
|
-
* Deleting a session's history is destructive and irreversible, and it must
|
|
1117
|
-
* never be the side effect of a flag that reads like "start over". The old
|
|
1118
|
-
* log stays where it is, still resumable by pointing --log at it. A leftover
|
|
1119
|
-
* `.xeno/log` from before named sessions is the same: pointed at, not moved.
|
|
1120
|
-
*/
|
|
1121
1237
|
const paths = statePaths(workspace, args.stateDir);
|
|
1122
|
-
const sessionDir = (args
|
|
1123
|
-
|
|
1124
|
-
(args.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1238
|
+
const sessionDir = sessionDirFor(args, workspace);
|
|
1239
|
+
let sessionOpts: ResolveLogOpts = {
|
|
1240
|
+
...(args.logDir ? { explicit: resolve(args.logDir) } : {}),
|
|
1241
|
+
fresh: args.fresh,
|
|
1242
|
+
...(args.continueLatest === undefined ? {} : { continueLatest: args.continueLatest }),
|
|
1243
|
+
...(args.noSession === undefined ? {} : { noSession: args.noSession }),
|
|
1244
|
+
...(args.session === undefined ? {} : { session: args.session }),
|
|
1245
|
+
...(args.sessionId === undefined ? {} : { sessionId: args.sessionId }),
|
|
1246
|
+
...(args.resume === undefined ? {} : { resume: args.resume }),
|
|
1247
|
+
...(args.forkFrom === undefined ? {} : { forkFrom: args.forkFrom }),
|
|
1248
|
+
...(sessionDir === undefined ? {} : { sessionDir }),
|
|
1249
|
+
};
|
|
1129
1250
|
let logDir: string;
|
|
1130
1251
|
try {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (!found.ok) {
|
|
1135
|
-
io.err(C.red(`\n ${found.reason}\n`));
|
|
1136
|
-
process.exit(1);
|
|
1252
|
+
if (planSessionEntry(sessionOpts).kind === "select") {
|
|
1253
|
+
if (printMode || jsonMode || !stdinIsTty() || !stdoutIsTty()) {
|
|
1254
|
+
throw new Error("--resume needs an interactive terminal. Pass -r <id> or --session <id> instead");
|
|
1137
1255
|
}
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const resolved = await resolveLogDir(workspace, {
|
|
1143
|
-
...(args.logDir ? { explicit: resolve(args.logDir) } : {}),
|
|
1144
|
-
fresh: args.fresh,
|
|
1145
|
-
...(args.continueLatest === true ? { continueLatest: true } : {}),
|
|
1146
|
-
...(args.noSession === true ? { noSession: true } : {}),
|
|
1147
|
-
...(sessionPick !== undefined ? { session: sessionPick } : {}),
|
|
1148
|
-
...(sessionDir !== undefined ? { sessionDir } : {}),
|
|
1149
|
-
});
|
|
1150
|
-
if (resolved.legacyNotice !== null && !args.logDir) {
|
|
1151
|
-
io.say(C.yellow(` ${resolved.legacyNotice}`));
|
|
1152
|
-
}
|
|
1153
|
-
logDir = resolved.logDir;
|
|
1256
|
+
const picked = await withTerminal(handoff => pickStartupSession({ workspace, style: ui.style, say: io.say, keymap: ui.keymap,
|
|
1257
|
+
...(sessionDir === undefined ? {} : { sessionDir }) }, handoff));
|
|
1258
|
+
if (picked === null) { io.say(C.dim("No session selected")); process.exit(0); }
|
|
1259
|
+
sessionOpts = { explicit: picked };
|
|
1154
1260
|
}
|
|
1261
|
+
const resolved = await resolveLogDir(workspace, sessionOpts);
|
|
1262
|
+
if (resolved.legacyNotice !== null && !args.logDir) io.say(C.yellow(` ${resolved.legacyNotice}`));
|
|
1263
|
+
logDir = resolved.logDir;
|
|
1155
1264
|
} catch (err) {
|
|
1156
1265
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1157
1266
|
io.err(C.red(`\n ${detail}\n`));
|
|
1158
1267
|
process.exit(1);
|
|
1159
1268
|
}
|
|
1160
|
-
if (args.resume === true && sessionPick === undefined && (args.forkFrom ?? "").length === 0 && args.noSession !== true) {
|
|
1161
|
-
// `-r` with no id is a selector, not a guess. Print mode cannot pick.
|
|
1162
|
-
if (isPrintMode(args)) {
|
|
1163
|
-
io.err(C.red("\n -r needs a session id. Pass -r <id> or --session <id>.\n"));
|
|
1164
|
-
process.exit(1);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
1269
|
// Snapshots are on by default here. They are optional in the loop because a
|
|
1168
1270
|
// read-only branch has nothing to snapshot, but a session a person drives is
|
|
1169
1271
|
// exactly the one that will want to go back, and the content has to have been
|
|
@@ -1172,64 +1274,28 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1172
1274
|
await mkdir(logDir, { recursive: true });
|
|
1173
1275
|
await mkdir(blobDir, { recursive: true });
|
|
1174
1276
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
* them at a node boundary would change `system` and invalidate the prefix
|
|
1178
|
-
* cache for the whole conversation (D6). A reload is a cut; that is a
|
|
1179
|
-
* later card.
|
|
1180
|
-
*/
|
|
1181
|
-
const loadedContext = await loadContext({
|
|
1182
|
-
workspace,
|
|
1183
|
-
cwd: process.cwd(),
|
|
1184
|
-
configDir: configDir(),
|
|
1185
|
-
noContextFiles: args.noContextFiles,
|
|
1186
|
-
appendSystemPrompt: args.appendSystemPrompt,
|
|
1187
|
-
...(args.systemPrompt !== null ? { systemPrompt: args.systemPrompt } : {}),
|
|
1188
|
-
});
|
|
1189
|
-
const selection = toolSelectionOf(args);
|
|
1277
|
+
// Resource ownership follows the resolved session workspace. openSessionFor
|
|
1278
|
+
// loads and freezes it once; explicit reloads checkpoint a changed prefix (D6).
|
|
1190
1279
|
const visible = visibleSchemas(args);
|
|
1191
|
-
const filtered =
|
|
1192
|
-
selection.only !== undefined ||
|
|
1193
|
-
selection.none === true ||
|
|
1194
|
-
(selection.exclude !== undefined && selection.exclude.length > 0) ||
|
|
1195
|
-
selection.noBuiltin === true;
|
|
1196
|
-
const builtin = systemPromptFor((filtered ? visible : TOOL_SCHEMAS).map((t) => t.name), sessionEnvironment(workspace));
|
|
1197
|
-
const textLoaded = await loadTextResources({
|
|
1198
|
-
workspace,
|
|
1199
|
-
configDir: configDir(),
|
|
1200
|
-
trusted: loadProjectSettings,
|
|
1201
|
-
noSkills: args.noSkills,
|
|
1202
|
-
noPromptTemplates: args.noPromptTemplates,
|
|
1203
|
-
skillPaths: args.skillPaths,
|
|
1204
|
-
promptTemplatePaths: args.promptTemplatePaths,
|
|
1205
|
-
});
|
|
1206
|
-
for (const line of textLoaded.diagnostics) io.say(C.yellow(` ${line}`));
|
|
1207
|
-
const slashResolved = resolveSlashCatalog(textLoaded.prompts, textLoaded.skills);
|
|
1208
|
-
for (const line of slashResolved.diagnostics) io.say(C.yellow(` ${line}`));
|
|
1209
|
-
const slash = slashResolved.catalog;
|
|
1210
|
-
const system = appendSkillsToSystem(
|
|
1211
|
-
assembleSystemPrompt(builtin, loadedContext),
|
|
1212
|
-
[...slash.skills.values()],
|
|
1213
|
-
);
|
|
1214
1280
|
|
|
1215
1281
|
const askSlot: AskSlot = {};
|
|
1216
|
-
|
|
1217
|
-
const session = createSessionFor({
|
|
1218
|
-
args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot,
|
|
1219
|
-
...(oauth ? { oauth: true } : {}),
|
|
1220
|
-
});
|
|
1221
|
-
// Read before starting, so the banner can say whether this is a new session
|
|
1222
|
-
// or a continued one. start() moves the cursor, which would erase the answer.
|
|
1223
|
-
const priorCursor = await readCursor(logDir);
|
|
1224
|
-
|
|
1282
|
+
let opened: Awaited<ReturnType<typeof openSessionFor>>;
|
|
1225
1283
|
try {
|
|
1226
|
-
await
|
|
1284
|
+
opened = await openSessionFor({
|
|
1285
|
+
args, workspace, logDir, blobDir, io, askSlot, catalog, auth, credentialOf, effects,
|
|
1286
|
+
settings: mergedSettings, modelFlags: { model: args.model, provider: args.provider }, projectTrusted: loadProjectSettings,
|
|
1287
|
+
...(preparedResources ? { preparedResources } : {}),
|
|
1288
|
+
interactive: isInteractiveSession(args),
|
|
1289
|
+
...(input ? { startupPhase: input.phase } : {}),
|
|
1290
|
+
});
|
|
1227
1291
|
} catch (err) {
|
|
1228
1292
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1229
1293
|
io.err(C.red(`\n cannot start a session in ${workspace}`));
|
|
1230
1294
|
io.err(C.dim(` ${detail}\n`));
|
|
1231
1295
|
process.exit(1);
|
|
1232
1296
|
}
|
|
1297
|
+
const { session, priorCursor, providerConfig, credential } = opened;
|
|
1298
|
+
refreshOutputMode();
|
|
1233
1299
|
|
|
1234
1300
|
const sessionName = args.sessionName;
|
|
1235
1301
|
if (sessionName !== undefined && sessionName.length > 0) {
|
|
@@ -1239,17 +1305,19 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1239
1305
|
}
|
|
1240
1306
|
}
|
|
1241
1307
|
|
|
1242
|
-
printBanner(args, io, {
|
|
1243
|
-
workspace, logDir, providerConfig, credential, loadedContext, session, priorCursor, catalog,
|
|
1244
|
-
keymap: ui.keymap,
|
|
1245
|
-
skills: [...slash.skills.keys()],
|
|
1246
|
-
prompts: [...slash.prompts.keys()],
|
|
1308
|
+
if (opened.resources.ui.settings.quietStartup !== true || args.verbose) printBanner(args, io, {
|
|
1309
|
+
workspace: opened.workspace, logDir, providerConfig, credential, loadedContext: opened.resources.loadedContext, session, priorCursor, catalog,
|
|
1310
|
+
keymap: opened.resources.ui.keymap,
|
|
1311
|
+
skills: [...opened.resources.slash.skills.keys()],
|
|
1312
|
+
prompts: [...opened.resources.slash.prompts.keys()],
|
|
1247
1313
|
tools: visible.map((t) => t.name),
|
|
1248
1314
|
});
|
|
1249
1315
|
|
|
1250
1316
|
return {
|
|
1251
|
-
args, session, workspace, logDir, blobDir, system, providerConfig, credential,
|
|
1252
|
-
auth, effects, loadedContext, credentialOf, catalog, io, askSlot,
|
|
1317
|
+
args, session, workspace: opened.workspace, logDir, blobDir: opened.blobDir, system: opened.system, providerConfig, credential,
|
|
1318
|
+
auth, effects, loadedContext: opened.resources.loadedContext, credentialOf, catalog, io, askSlot,
|
|
1319
|
+
ui: opened.resources.ui, slash: opened.resources.slash, resources: opened.resources.index, projectTrusted: opened.resources.trusted,
|
|
1320
|
+
extensions: opened.resources.extensions,
|
|
1253
1321
|
};
|
|
1254
1322
|
}
|
|
1255
1323
|
|
|
@@ -1321,3 +1389,53 @@ function printBanner(
|
|
|
1321
1389
|
}
|
|
1322
1390
|
io.say("");
|
|
1323
1391
|
}
|
|
1392
|
+
|
|
1393
|
+
export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
1394
|
+
console.log(
|
|
1395
|
+
"usage: xeno [-m <model>] [--provider <name>] [--api-key <key>]\n" +
|
|
1396
|
+
" [-w <workspace>] [--log <dir>] [--max-turns N]\n" +
|
|
1397
|
+
" [--context-policy windowed|segmented|continuous|adaptive]\n" +
|
|
1398
|
+
" [--trigger N --hard N] [--list-models] [--fresh]\n" +
|
|
1399
|
+
" [-c|--continue] [-r|--resume [id]] [--session path|id]\n" +
|
|
1400
|
+
" [--fork path|id] [--no-session] [--session-dir dir]\n" +
|
|
1401
|
+
" [-n|--name name] [--session-id id] [--version|-v]\n" +
|
|
1402
|
+
" [models --refresh]\n" +
|
|
1403
|
+
" [install|remove <source>... [--local]] [update [source] [--extensions]] [list]\n" +
|
|
1404
|
+
" [config [--local]] [--export session [output.html]] [--offline]\n" +
|
|
1405
|
+
" [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
|
|
1406
|
+
" [--no-context-files|-nc]\n" +
|
|
1407
|
+
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
1408
|
+
" [--no-builtin-tools]\n" +
|
|
1409
|
+
" [--allow-tools names] [--approve|--no-approve]\n" +
|
|
1410
|
+
" [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
|
|
1411
|
+
" [--skill <path>] [--no-skills]\n" +
|
|
1412
|
+
" [--prompt-template <path>] [--no-prompt-templates]\n" +
|
|
1413
|
+
" [--extension <path>] [--no-extensions] [--extension-flag name=value]\n" +
|
|
1414
|
+
" [--mode text|json] [--tui-mode regular|fullscreen] [--verbose]\n" +
|
|
1415
|
+
" [-p <prompt>] [prompt...]\n\n" +
|
|
1416
|
+
" Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
|
|
1417
|
+
" -c continues the newest (or starts one); -r opens the session picker.\n" +
|
|
1418
|
+
" --session / -r <id> pick one (unique prefix only). Resume restores its model;\n" +
|
|
1419
|
+
" an explicit --model switches at a checkpoint before the next turn.\n" +
|
|
1420
|
+
" --no-session runs without writing .xeno/sessions/. --fork copies into a new one.\n" +
|
|
1421
|
+
" A leftover .xeno/log is not moved; continue it with --log .xeno/log.\n\n" +
|
|
1422
|
+
" With a prompt, runs it and exits. Without one, reads prompts from stdin.\n" +
|
|
1423
|
+
" Commands: " + COMMANDS.map(command => "/" + command.name).join(" ") + "\n\n" +
|
|
1424
|
+
" Credentials resolve in this order: --api-key, then ~/.xeno/auth.json,\n" +
|
|
1425
|
+
" then the provider's environment variable.\n" +
|
|
1426
|
+
" --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
|
|
1427
|
+
" --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
|
|
1428
|
+
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
1429
|
+
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
1430
|
+
" bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
|
|
1431
|
+
" --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
|
|
1432
|
+
" --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
|
|
1433
|
+
" --path-root <dir> lets file tools reach absolute paths under <dir> (e.g. / in a container);\n" +
|
|
1434
|
+
" relative paths, fingerprints and /back stay in the workspace, so edits outside it are not rewound.\n" +
|
|
1435
|
+
" --approve loads .xeno/settings.json this run; --no-approve skips it.\n" +
|
|
1436
|
+
" Project trust does not affect tool execution.\n" +
|
|
1437
|
+
" --mode json writes one JSON object per event to stdout; diagnostics stay on stderr.\n" +
|
|
1438
|
+
" --verbose expands the startup header (AGENTS.md, skills, prompt templates).",
|
|
1439
|
+
);
|
|
1440
|
+
if (flags?.size) console.log("\nExtension CLI flags:\n" + [...flags].map(([name, flag]) => ` --${name}${flag.type === "string" ? " <value>" : ""} ${flag.description ?? ""}`).join("\n"));
|
|
1441
|
+
}
|