@visual-z/xeno 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +143 -33
- package/docs/install.md +18 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +42 -14
- 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.ts +400 -268
- 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/progress.ts +9 -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/store/workspace.ts +6 -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 {
|
|
@@ -165,6 +175,15 @@ export interface Args {
|
|
|
165
175
|
provider: string;
|
|
166
176
|
apiKey: string;
|
|
167
177
|
listModels: boolean;
|
|
178
|
+
modelSearch?: string;
|
|
179
|
+
help?: boolean;
|
|
180
|
+
version?: boolean;
|
|
181
|
+
exportFile?: string;
|
|
182
|
+
offline?: boolean;
|
|
183
|
+
tuiMode?: "regular" | "fullscreen";
|
|
184
|
+
noThemes?: boolean;
|
|
185
|
+
themePaths?: string[];
|
|
186
|
+
sessionId?: string;
|
|
168
187
|
modelsCommand: boolean;
|
|
169
188
|
modelsRefresh: boolean;
|
|
170
189
|
fresh: boolean;
|
|
@@ -180,6 +199,10 @@ export interface Args {
|
|
|
180
199
|
allowTools: string[];
|
|
181
200
|
/** `--approve` true, `--no-approve` false. Omitted when neither. */
|
|
182
201
|
approve?: boolean;
|
|
202
|
+
configCommand?: boolean;
|
|
203
|
+
local?: boolean;
|
|
204
|
+
packageCommand?: PackageCommand;
|
|
205
|
+
packagePaths?: {source:string;path:string}[];
|
|
183
206
|
/**
|
|
184
207
|
* `-p` / `--print`. Distinct from a leftover positional prompt: only this
|
|
185
208
|
* flag reads piped stdin, so a one-shot that did not ask for a pipe cannot
|
|
@@ -192,8 +215,14 @@ export interface Args {
|
|
|
192
215
|
thinking?: string;
|
|
193
216
|
/** `--models <patterns>`. Comma-separated; empty when the flag was not passed. */
|
|
194
217
|
models?: string;
|
|
218
|
+
modelsExplicit?: boolean;
|
|
195
219
|
noSkills: boolean;
|
|
196
220
|
noPromptTemplates: boolean;
|
|
221
|
+
noExtensions?: boolean;
|
|
222
|
+
extensionPaths?: string[];
|
|
223
|
+
extensionFlags?: Record<string, string | boolean>;
|
|
224
|
+
extensionArgv?: string[];
|
|
225
|
+
extensionInputs?: Record<string, FlagInput>;
|
|
197
226
|
skillPaths: string[];
|
|
198
227
|
promptTemplatePaths: string[];
|
|
199
228
|
/** `-c` / `--continue`. */
|
|
@@ -266,10 +295,12 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
266
295
|
"--prompt-template",
|
|
267
296
|
"--no-skills",
|
|
268
297
|
"--no-prompt-templates",
|
|
298
|
+
"--extension", "-e", "--no-extensions", "-ne", "--extension-flag",
|
|
269
299
|
"--mode",
|
|
270
300
|
"--verbose",
|
|
271
301
|
"--print", "-p",
|
|
272
302
|
"--help", "-h",
|
|
303
|
+
"--version", "-v", "--export", "--session-id", "--offline", "--tui-mode", "--use-theme", "--no-themes", "-ns", "-np", "-nbt",
|
|
273
304
|
];
|
|
274
305
|
|
|
275
306
|
/**
|
|
@@ -341,7 +372,8 @@ export function silenceTtyEarly(opts: {
|
|
|
341
372
|
};
|
|
342
373
|
}
|
|
343
374
|
|
|
344
|
-
export function parseArgs(argv: string[]): Args {
|
|
375
|
+
export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
|
|
376
|
+
let selectedTheme: string | undefined;
|
|
345
377
|
const args: Args = {
|
|
346
378
|
workspace: process.cwd(), logDir: "", maxTurns: 40, prompt: "",
|
|
347
379
|
// Overridable so a node boundary can be forced in a short session --
|
|
@@ -362,6 +394,11 @@ export function parseArgs(argv: string[]): Args {
|
|
|
362
394
|
};
|
|
363
395
|
for (let i = 0; i < argv.length; i++) {
|
|
364
396
|
const a = argv[i]!;
|
|
397
|
+
if (["--log", "--session", "--fork", "--session-dir", "--state-dir"].includes(a) &&
|
|
398
|
+
(!argv[i + 1] || argv[i + 1]!.startsWith("-"))) {
|
|
399
|
+
console.error(`${a} requires a value`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
365
402
|
if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
|
|
366
403
|
else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
|
|
367
404
|
else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
|
|
@@ -382,13 +419,22 @@ export function parseArgs(argv: string[]): Args {
|
|
|
382
419
|
else if (a === "--model" || a === "-m") args.model = argv[++i] ?? "";
|
|
383
420
|
else if (a === "--provider") args.provider = argv[++i] ?? "";
|
|
384
421
|
else if (a === "--api-key") args.apiKey = argv[++i] ?? "";
|
|
385
|
-
else if (a === "--list-models") args.listModels = true;
|
|
422
|
+
else if (a === "--list-models") { args.listModels = true; const next = argv[i + 1]; if (next && !next.startsWith("-") && !next.startsWith("@")) args.modelSearch = argv[++i]!; }
|
|
423
|
+
else if (a === "--version" || a === "-v") args.version = true;
|
|
424
|
+
else if (a === "--export") { const file = argv[++i]; if (!file || file.startsWith("-")) throw new Error("--export requires a session path"); args.exportFile = file; }
|
|
425
|
+
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; }
|
|
426
|
+
else if (a === "--offline") args.offline = true;
|
|
427
|
+
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
428
|
else if (a === "models" && i === 0) {
|
|
387
429
|
// Subcommand, not a prompt. Do not consume the rest: a later
|
|
388
430
|
// `--models-url` must still be an unknown-flag error, and `-h`
|
|
389
431
|
// must still print help.
|
|
390
432
|
args.modelsCommand = true;
|
|
391
433
|
}
|
|
434
|
+
else if (a === "config" && i === 0) args.configCommand = true;
|
|
435
|
+
else if ((a === "install" || a === "remove" || a === "update" || a === "list") && i === 0) args.packageCommand = {operation:a,sources:[]};
|
|
436
|
+
else if ((a === "--local" || a === "-l") && (args.packageCommand || args.configCommand)) { args.local=true; if(args.packageCommand)args.packageCommand.local=true; }
|
|
437
|
+
else if (a === "--extensions" && args.packageCommand?.operation === "update") { /* Package-only update; xeno never self-updates. */ }
|
|
392
438
|
else if (a === "--refresh" && args.modelsCommand) {
|
|
393
439
|
args.modelsRefresh = true;
|
|
394
440
|
}
|
|
@@ -410,70 +456,41 @@ export function parseArgs(argv: string[]): Args {
|
|
|
410
456
|
else if (a === "--tools" || a === "-t") args.tools = parseToolList(argv[++i] ?? "");
|
|
411
457
|
else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
|
|
412
458
|
else if (a === "--no-tools" || a === "-nt") args.noTools = true;
|
|
413
|
-
else if (a === "--no-builtin-tools") args.noBuiltinTools = true;
|
|
459
|
+
else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
|
|
414
460
|
else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
|
|
415
461
|
else if (a === "--approve" || a === "-a") args.approve = true;
|
|
416
462
|
else if (a === "--no-approve" || a === "-na") args.approve = false;
|
|
417
|
-
else if (a === "--theme")
|
|
463
|
+
else if (a === "--theme") {
|
|
464
|
+
const theme = argv[++i]; if (!theme || theme.startsWith("-")) throw new Error("--theme requires a name or path");
|
|
465
|
+
if (theme.includes("/") || theme.includes("\\") || theme.endsWith(".json")) { (args.themePaths ??= []).push(theme); if (theme.endsWith(".json")) args.theme = theme; }
|
|
466
|
+
else args.theme = theme;
|
|
467
|
+
}
|
|
468
|
+
else if (a === "--use-theme") { const theme = argv[++i]; if (!theme || theme.startsWith("-")) throw new Error("--use-theme requires a name"); selectedTheme = theme; }
|
|
469
|
+
else if (a === "--no-themes") args.noThemes = true;
|
|
418
470
|
else if (a === "--thinking") args.thinking = argv[++i] ?? "";
|
|
419
|
-
else if (a === "--models")
|
|
471
|
+
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
472
|
else if (a === "--skill") args.skillPaths.push(argv[++i] ?? "");
|
|
421
473
|
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;
|
|
474
|
+
else if (a === "--no-skills" || a === "-ns") args.noSkills = true;
|
|
475
|
+
else if (a === "--no-prompt-templates" || a === "-np") args.noPromptTemplates = true;
|
|
476
|
+
else if (a === "--no-extensions" || a === "-ne") args.noExtensions = true;
|
|
477
|
+
else if (a === "--extension" || a === "-e") {
|
|
478
|
+
const path = argv[++i]; if (!path || path.startsWith("-")) throw new Error("--extension requires a path");
|
|
479
|
+
(args.extensionPaths ??= []).push(path);
|
|
480
|
+
}
|
|
481
|
+
else if (a === "--extension-flag") {
|
|
482
|
+
const value = argv[++i] ?? "", at = value.indexOf("=");
|
|
483
|
+
if (at <= 0) throw new Error("--extension-flag requires name=value");
|
|
484
|
+
(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);
|
|
485
|
+
}
|
|
424
486
|
else if (a === "--mode") args.mode = argv[++i] ?? "";
|
|
425
487
|
else if (a === "--verbose") args.verbose = true;
|
|
426
488
|
else if (a === "-p" || a === "--print") {
|
|
427
489
|
args.print = true;
|
|
428
|
-
const next = argv[++i] ?? "";
|
|
429
|
-
args.prompt += (args.prompt ? " " : "") + next;
|
|
430
490
|
}
|
|
431
491
|
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
|
-
);
|
|
492
|
+
if (options.deferExtensions) { args.help = true; continue; }
|
|
493
|
+
printCliHelp();
|
|
477
494
|
process.exit(0);
|
|
478
495
|
} else if (a === "--") {
|
|
479
496
|
// Everything after `--` is content, never a flag. The delimiter
|
|
@@ -483,9 +500,23 @@ export function parseArgs(argv: string[]): Args {
|
|
|
483
500
|
}
|
|
484
501
|
break;
|
|
485
502
|
} else if (a.startsWith("-") && a !== "-") {
|
|
486
|
-
|
|
487
|
-
|
|
503
|
+
const equal = a.indexOf("="), name = a.slice(2, equal === -1 ? undefined : equal), definition = options.flags?.get(name);
|
|
504
|
+
if (a.startsWith("--") && (definition || options.deferExtensions)) {
|
|
505
|
+
const next = argv[i + 1], input: FlagInput = { inline: equal !== -1,
|
|
506
|
+
...(equal !== -1 ? { value: a.slice(equal + 1) } : next !== undefined && !next.startsWith("-") && !next.startsWith("@") ? { value: next } : {}) };
|
|
507
|
+
if (definition) {
|
|
508
|
+
const value = extensionFlagValue(name, definition, input);
|
|
509
|
+
if (value !== undefined) (args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[name] = value;
|
|
510
|
+
if (!input.inline && input.value !== undefined && (definition.type === "string" || input.value === "true" || input.value === "false")) i++;
|
|
511
|
+
} else {
|
|
512
|
+
args.extensionArgv = [...argv]; (args.extensionInputs ??= Object.create(null) as Record<string, FlagInput>)[name] = input;
|
|
513
|
+
if (!input.inline && input.value !== undefined) i++;
|
|
514
|
+
}
|
|
515
|
+
} else rejectUnknownFlag(a);
|
|
516
|
+
} else if(args.packageCommand)args.packageCommand.sources.push(a);
|
|
517
|
+
else args.prompt += (args.prompt ? " " : "") + a;
|
|
488
518
|
}
|
|
519
|
+
if (selectedTheme !== undefined) args.theme = selectedTheme;
|
|
489
520
|
return args;
|
|
490
521
|
}
|
|
491
522
|
|
|
@@ -621,7 +652,7 @@ function countCatalogDelta(
|
|
|
621
652
|
return map;
|
|
622
653
|
}
|
|
623
654
|
for (const spec of parseUserCatalog(json).models) {
|
|
624
|
-
map.set(spec
|
|
655
|
+
map.set(modelKey(spec), JSON.stringify(spec));
|
|
625
656
|
}
|
|
626
657
|
return map;
|
|
627
658
|
};
|
|
@@ -650,13 +681,13 @@ export async function runModelsRefresh(opts: {
|
|
|
650
681
|
| { ok: true; count: number; added: number; changed: number }
|
|
651
682
|
| { ok: false; reason: string }
|
|
652
683
|
> {
|
|
653
|
-
const before = await
|
|
684
|
+
const before = await readCatalogCacheText();
|
|
654
685
|
const result = await refreshRemoteCatalog({
|
|
655
686
|
fetch: opts.fetch,
|
|
656
687
|
write: opts.write,
|
|
657
688
|
});
|
|
658
689
|
if (!result.ok) return result;
|
|
659
|
-
const after = await
|
|
690
|
+
const after = await readCatalogCacheText();
|
|
660
691
|
const delta = countCatalogDelta(before.text, after.text);
|
|
661
692
|
return { ok: true, count: result.count, added: delta.added, changed: delta.changed };
|
|
662
693
|
}
|
|
@@ -729,6 +760,10 @@ export interface App {
|
|
|
729
760
|
askSlot: AskSlot;
|
|
730
761
|
ui?: UiState;
|
|
731
762
|
slash?: SlashCatalog;
|
|
763
|
+
resources?: ResourceIndex;
|
|
764
|
+
projectTrusted?: boolean;
|
|
765
|
+
extensions?: ExtensionRegistry;
|
|
766
|
+
shutdownRequested?: boolean;
|
|
732
767
|
}
|
|
733
768
|
|
|
734
769
|
export interface UiState {
|
|
@@ -749,13 +784,6 @@ function toUiState(loaded: UiResources, color: boolean): UiState {
|
|
|
749
784
|
};
|
|
750
785
|
}
|
|
751
786
|
|
|
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
787
|
/** `--models a,b` and settings.scopedModels share this split so a stray space cannot invent a pattern. */
|
|
760
788
|
export function splitModelPatterns(raw: string | undefined): string[] {
|
|
761
789
|
if (raw === undefined || raw.length === 0) return [];
|
|
@@ -791,7 +819,7 @@ export function missingCredentialMessage(spec: ModelSpec, explicit: boolean, cat
|
|
|
791
819
|
}
|
|
792
820
|
|
|
793
821
|
export function isInteractiveSession(args: Args): boolean {
|
|
794
|
-
return args.prompt.length === 0 && stdinIsTty() && stdoutIsTty();
|
|
822
|
+
return !args.print && !args.help && args.mode !== "json" && args.prompt.length === 0 && stdinIsTty() && stdoutIsTty();
|
|
795
823
|
}
|
|
796
824
|
|
|
797
825
|
/**
|
|
@@ -847,8 +875,9 @@ export function toolSelectionOf(args: Args) {
|
|
|
847
875
|
* the provider bills, and an extra field there would look like a tool
|
|
848
876
|
* argument.
|
|
849
877
|
*/
|
|
850
|
-
export function visibleSchemas(args: Args) {
|
|
851
|
-
const
|
|
878
|
+
export function visibleSchemas(args: Args, extensions?: ExtensionRegistry) {
|
|
879
|
+
const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
|
|
880
|
+
const available = [...TOOL_SCHEMAS.filter(tool => !extensions?.tools.has(tool.name)).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
|
|
852
881
|
return selectTools(available, toolSelectionOf(args)).map((tool) => {
|
|
853
882
|
const { source, ...schema } = tool;
|
|
854
883
|
void source;
|
|
@@ -872,9 +901,11 @@ export function createSessionFor(opts: {
|
|
|
872
901
|
io: AppIO;
|
|
873
902
|
askSlot: AskSlot;
|
|
874
903
|
oauth?: boolean;
|
|
904
|
+
effects?: ResolveEffects;
|
|
905
|
+
extensions?: ExtensionRegistry;
|
|
875
906
|
}): Session {
|
|
876
907
|
const { args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot } = opts;
|
|
877
|
-
const schemas = visibleSchemas(args);
|
|
908
|
+
const schemas = visibleSchemas(args, opts.extensions);
|
|
878
909
|
const names = schemas.map((t) => t.name);
|
|
879
910
|
const sessionRules: PermissionRule[] = [];
|
|
880
911
|
const tools: ToolContext = {
|
|
@@ -885,6 +916,7 @@ export function createSessionFor(opts: {
|
|
|
885
916
|
rules: rulesWithAllowTools(args.allowTools),
|
|
886
917
|
sessionRules,
|
|
887
918
|
askSlot,
|
|
919
|
+
...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
|
|
888
920
|
};
|
|
889
921
|
return new Session(
|
|
890
922
|
{
|
|
@@ -916,21 +948,171 @@ export function createSessionFor(opts: {
|
|
|
916
948
|
providerConfig,
|
|
917
949
|
credential.ok ? credential.key : "",
|
|
918
950
|
retryHandler(io),
|
|
919
|
-
opts.oauth === true ? { oauth: true } :
|
|
951
|
+
{ ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
920
952
|
),
|
|
921
953
|
systemClock(),
|
|
922
954
|
);
|
|
923
955
|
}
|
|
924
956
|
|
|
957
|
+
/** One location policy for startup, /new, /resume and /fork. */
|
|
958
|
+
export function sessionDirFor(args: Args, workspace: string): string | undefined {
|
|
959
|
+
if (args.sessionDir) return args.sessionDir;
|
|
960
|
+
return args.stateDir === undefined ? undefined : statePaths(workspace, args.stateDir).sessions;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
type OpenSessionOptions = Omit<Parameters<typeof createSessionFor>[0], "providerConfig" | "credential" | "oauth" | "system"> & {
|
|
964
|
+
catalog: readonly ModelSpec[];
|
|
965
|
+
auth: AuthFile;
|
|
966
|
+
credentialOf: (spec: ModelSpec) => Resolved;
|
|
967
|
+
interactive: boolean;
|
|
968
|
+
settings?: Settings;
|
|
969
|
+
/** CLI overrides belong to this open only; /resume must not inherit them. */
|
|
970
|
+
modelFlags?: { model: string; provider: string };
|
|
971
|
+
preparedResources?: { workspace: string; value: Awaited<ReturnType<typeof loadSessionResources>> };
|
|
972
|
+
fallback?: ProviderConfig;
|
|
973
|
+
projectTrusted?: boolean;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
/** Pi replaces cwd-scoped resources with the session runtime; publish only after all reads succeed. */
|
|
977
|
+
async function prepareExtensionPackages(args: Args, workspace: string): Promise<void> {
|
|
978
|
+
const remote=(args.extensionPaths??[]).filter(path=>/^(npm:|git:|git\+|https?:\/\/|ssh:\/\/|git@)/.test(path));
|
|
979
|
+
if(!remote.length)return;
|
|
980
|
+
if(args.offline || process.env["XENO_OFFLINE"] === "1") throw new Error("--offline prevents remote extension package preparation; install it explicitly beforehand");
|
|
981
|
+
const manager=new PackageManager({workspace});
|
|
982
|
+
for(const source of remote)(args.packagePaths??=[]).push({source,path:await manager.temporary(source)});
|
|
983
|
+
args.extensionPaths=(args.extensionPaths??[]).filter(path=>!remote.includes(path));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
export async function loadSessionResources(args: Args, workspace: string, io: AppIO, trustOverride?: boolean): Promise<{
|
|
987
|
+
loadedContext: LoadedContext; slash: SlashCatalog; ui: UiState; index: ResourceIndex; trusted: boolean; system: string; extensions: ExtensionRegistry;
|
|
988
|
+
}> {
|
|
989
|
+
const { trusted } = await projectTrust(workspace, trustOverride ?? args.approve);
|
|
990
|
+
await prepareExtensionPackages(args,workspace);
|
|
991
|
+
const index = await discoverResources({ ...args, workspace, trusted });
|
|
992
|
+
const extensions = new ExtensionRegistry({ reserved: COMMANDS.map(command => command.name), reservedFlags: KNOWN_FLAGS.map(flag => flag.replace(/^-+/, "")),
|
|
993
|
+
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 });
|
|
994
|
+
await extensions.load(index);
|
|
995
|
+
if(args.extensionArgv) {
|
|
996
|
+
const validated = parseArgs(args.extensionArgv, { flags: extensions.flags, ...(args.help ? { deferExtensions: true } : {}) });
|
|
997
|
+
if (validated.extensionArgv) rejectUnknownFlag("--" + Object.keys(validated.extensionInputs ?? {})[0]);
|
|
998
|
+
args.prompt = validated.prompt; args.extensionFlags = validated.extensionFlags ?? {};
|
|
999
|
+
}
|
|
1000
|
+
const loaded = await loadUiResources({ ...args, workspace, trusted, ...(args.theme ? { theme: args.theme } : {}) }, index);
|
|
1001
|
+
const loadedContext = await loadContext({ workspace, cwd: workspace, configDir: configDir(),
|
|
1002
|
+
noContextFiles: args.noContextFiles, appendSystemPrompt: args.appendSystemPrompt,
|
|
1003
|
+
...(args.systemPrompt === null ? {} : { systemPrompt: args.systemPrompt }) });
|
|
1004
|
+
for (const notice of loadedContext.notices) if (notice.kind === "skipped" || notice.truncated) io.say(io.palette.yellow(formatNotice(notice)));
|
|
1005
|
+
const text = await loadTextResources({ workspace, configDir: configDir(), trusted, noSkills: args.noSkills,
|
|
1006
|
+
noPromptTemplates: args.noPromptTemplates, skillPaths: args.skillPaths, promptTemplatePaths: args.promptTemplatePaths }, index);
|
|
1007
|
+
const slash = resolveSlashCatalog(text.prompts, text.skills);
|
|
1008
|
+
for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
|
|
1009
|
+
const system = appendSkillsToSystem(assembleSystemPrompt(systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)), loadedContext), text.skills);
|
|
1010
|
+
const ui = toUiState(loaded, colorEnabled());
|
|
1011
|
+
if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
|
|
1012
|
+
return { loadedContext, slash: { ...slash.catalog, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Pi's session-before-model order, with xeno's N.8 checkpoint on an explicit
|
|
1017
|
+
* model change. Opening never sends a request to the previous provider.
|
|
1018
|
+
* Callers publish the returned session/config/credential together on success.
|
|
1019
|
+
*/
|
|
1020
|
+
export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
1021
|
+
session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
|
|
1022
|
+
workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
|
|
1023
|
+
}> {
|
|
1024
|
+
const recorded = await readCursor(opts.logDir);
|
|
1025
|
+
const priorCursor = recorded === null ? null : await recoverCut(opts.logDir, recorded);
|
|
1026
|
+
const workspace = priorCursor?.workspace ?? opts.workspace;
|
|
1027
|
+
if (opts.args.pathRoot) {
|
|
1028
|
+
const rel = relative(opts.args.pathRoot, workspace);
|
|
1029
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
|
|
1030
|
+
}
|
|
1031
|
+
if (opts.preparedResources && opts.preparedResources.workspace !== workspace) await opts.preparedResources.value.extensions.dispose();
|
|
1032
|
+
const resources = opts.preparedResources?.workspace === workspace ? opts.preparedResources.value
|
|
1033
|
+
: await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
|
|
1034
|
+
const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
|
|
1035
|
+
const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
|
|
1036
|
+
const unknown = unknownToolNames(named).filter(name => !resources.extensions.tools.has(name));
|
|
1037
|
+
if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
|
|
1038
|
+
const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
|
|
1039
|
+
const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
|
|
1040
|
+
const parsedModel = flags.model ? scopedModels([flags.model], flags.provider ? opts.catalog.filter(model => model.provider === flags.provider) : opts.catalog, true).entries[0] : undefined;
|
|
1041
|
+
if (parsedModel) flags.model = modelKey(parsedModel.model);
|
|
1042
|
+
const explicit = flags.model.length > 0 || flags.provider.length > 0;
|
|
1043
|
+
const settings = opts.settings ?? {};
|
|
1044
|
+
const preferred = saved ?? opts.fallback?.spec;
|
|
1045
|
+
const selected = !explicit && preferred !== undefined
|
|
1046
|
+
? { ok: true as const, spec: preferred }
|
|
1047
|
+
: selectModel(settings, flags, (spec) => opts.credentialOf(spec).ok, opts.catalog);
|
|
1048
|
+
let spec: ModelSpec;
|
|
1049
|
+
if (selected.ok) spec = selected.spec;
|
|
1050
|
+
else {
|
|
1051
|
+
const missingAll = selected.reason.startsWith("no model has a credential");
|
|
1052
|
+
if (!missingAll || !interactive) {
|
|
1053
|
+
throw new Error(missingAll ? describeUnauthenticatedScript(opts.catalog) : selected.reason);
|
|
1054
|
+
}
|
|
1055
|
+
const fallback = selectModel(settings, flags, () => true, opts.catalog);
|
|
1056
|
+
if (!fallback.ok) throw new Error(fallback.reason);
|
|
1057
|
+
spec = fallback.spec;
|
|
1058
|
+
}
|
|
1059
|
+
const credential = opts.credentialOf(spec);
|
|
1060
|
+
if (!credential.ok && !interactive) {
|
|
1061
|
+
throw new Error(credential.reason.startsWith("no credential for ")
|
|
1062
|
+
? missingCredentialMessage(spec, explicit || saved !== undefined, opts.catalog) : credential.reason);
|
|
1063
|
+
}
|
|
1064
|
+
const explicitEffort = asThinkingLevel(opts.args.thinking), requestedEffort = explicitEffort ?? parsedModel?.thinking;
|
|
1065
|
+
if (opts.args.thinking && (!explicitEffort || !supportedThinkingLevels(spec).includes(explicitEffort))) {
|
|
1066
|
+
throw new Error(`thinking ${opts.args.thinking} is not supported by ${modelKey(spec)}; supported: ${supportedThinkingLevels(spec).join(", ")}`);
|
|
1067
|
+
}
|
|
1068
|
+
const providerConfig: ProviderConfig = {
|
|
1069
|
+
spec, maxTokens: outputTokenBudget(spec),
|
|
1070
|
+
effort: clampThinkingLevel(spec, requestedEffort ?? asThinkingLevel(priorCursor?.thinking) ??
|
|
1071
|
+
settings.modelThinkingLevels?.[modelKey(spec)] ?? opts.fallback?.effort ?? asThinkingLevel(settings.defaultThinking) ??
|
|
1072
|
+
asThinkingLevel(settings.thinking) ?? DEFAULT_PROVIDER.effort),
|
|
1073
|
+
thinkingDisplay: opts.fallback?.thinkingDisplay ?? DEFAULT_PROVIDER.thinkingDisplay,
|
|
1074
|
+
};
|
|
1075
|
+
const restoring = saved ?? spec;
|
|
1076
|
+
const restoringCredential = saved === undefined ? credential : opts.credentialOf(restoring);
|
|
1077
|
+
const oauth = (model: ModelSpec, got: Resolved) => got.ok && storedIsOAuth(opts.auth, model.provider) && opts.args.apiKey.length === 0;
|
|
1078
|
+
const session = createSessionFor({
|
|
1079
|
+
...opts,
|
|
1080
|
+
system: resources.system,
|
|
1081
|
+
extensions: resources.extensions,
|
|
1082
|
+
workspace: priorCursor?.workspace ?? opts.workspace,
|
|
1083
|
+
blobDir: priorCursor?.blob_dir ?? (priorCursor?.workspace && priorCursor.workspace !== opts.workspace
|
|
1084
|
+
? statePaths(priorCursor.workspace, opts.args.stateDir).blobs : opts.blobDir),
|
|
1085
|
+
providerConfig: { ...providerConfig, spec: restoring, maxTokens: outputTokenBudget(restoring) },
|
|
1086
|
+
credential: restoringCredential,
|
|
1087
|
+
...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
|
|
1088
|
+
});
|
|
1089
|
+
await session.start();
|
|
1090
|
+
if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
|
|
1091
|
+
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
|
|
1092
|
+
await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions) });
|
|
1093
|
+
}
|
|
1094
|
+
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1095
|
+
await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
1096
|
+
{ ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1097
|
+
opts.io.say(opts.io.palette.yellow(` switched ${restoring.id} → ${spec.id} at a model checkpoint`));
|
|
1098
|
+
}
|
|
1099
|
+
resources.ui.settings = { ...resources.ui.settings, thinking: providerConfig.effort,
|
|
1100
|
+
...(opts.args.modelsExplicit || opts.args.models ? { scopedModels: splitModelPatterns(opts.args.models) } : {}) };
|
|
1101
|
+
if (resources.ui.settings.scopedModels) for (const warning of scopedModels(resources.ui.settings.scopedModels, opts.catalog).diagnostics) opts.io.say(` ${warning}`);
|
|
1102
|
+
return { session, priorCursor, providerConfig, credential, workspace: session.config.tools.root,
|
|
1103
|
+
blobDir: session.config.blobDir ?? opts.blobDir, system: session.config.frozen.system, resources };
|
|
1104
|
+
}
|
|
1105
|
+
|
|
925
1106
|
export async function startup(args: Args): Promise<App> {
|
|
1107
|
+
if (args.extensionArgv && (args.listModels || args.modelsCommand || args.configCommand)) rejectUnknownFlag("--" + Object.keys(args.extensionInputs ?? {})[0]);
|
|
926
1108
|
const C = createPalette(colorEnabled());
|
|
927
|
-
|
|
1109
|
+
let printMode = isPrintMode(args);
|
|
928
1110
|
const jsonMode = isJsonMode(args);
|
|
929
|
-
if ((args.mode ?? "").length > 0 && args.mode !== "json") {
|
|
930
|
-
console.error(`unknown --mode ${args.mode} (supported: json)`);
|
|
1111
|
+
if ((args.mode ?? "").length > 0 && args.mode !== "json" && args.mode !== "text") {
|
|
1112
|
+
console.error(`unknown --mode ${args.mode} (supported: text, json)`);
|
|
931
1113
|
process.exit(1);
|
|
932
1114
|
}
|
|
933
|
-
|
|
1115
|
+
let divert = printMode || jsonMode;
|
|
934
1116
|
const io: AppIO = {
|
|
935
1117
|
palette: C,
|
|
936
1118
|
say: (line) => (divert ? console.error(line) : console.log(line)),
|
|
@@ -943,6 +1125,11 @@ export async function startup(args: Args): Promise<App> {
|
|
|
943
1125
|
}
|
|
944
1126
|
: {}),
|
|
945
1127
|
};
|
|
1128
|
+
const refreshOutputMode = () => {
|
|
1129
|
+
printMode = isPrintMode(args); divert = printMode || jsonMode;
|
|
1130
|
+
if (printMode && !jsonMode) io.answer = text => { process.stdout.write(text.endsWith("\n") ? text : `${text}\n`); };
|
|
1131
|
+
else delete io.answer;
|
|
1132
|
+
};
|
|
946
1133
|
const effects = processEffects();
|
|
947
1134
|
|
|
948
1135
|
const authRead = await readJsonFile<AuthFile>(authPath());
|
|
@@ -955,12 +1142,13 @@ export async function startup(args: Args): Promise<App> {
|
|
|
955
1142
|
io.say(C.dim(" fetches models.dev into ~/.xeno/models.json; startup never does this."));
|
|
956
1143
|
process.exit(0);
|
|
957
1144
|
}
|
|
1145
|
+
if (args.offline || process.env["XENO_OFFLINE"] === "1") throw new Error("--offline prevents catalog refresh");
|
|
958
1146
|
const result = await runModelsRefresh({
|
|
959
1147
|
fetch: globalThis.fetch,
|
|
960
|
-
write:
|
|
1148
|
+
write: writeCatalogCache,
|
|
961
1149
|
});
|
|
962
1150
|
if (result.ok) {
|
|
963
|
-
io.say(` added ${result.added}, changed ${result.changed} (${result.count} total) → ${
|
|
1151
|
+
io.say(` added ${result.added}, changed ${result.changed} (${result.count} total) → ${catalogCachePath()}`);
|
|
964
1152
|
process.exit(0);
|
|
965
1153
|
} else {
|
|
966
1154
|
io.say(C.yellow(` ${result.reason}`));
|
|
@@ -970,17 +1158,16 @@ export async function startup(args: Args): Promise<App> {
|
|
|
970
1158
|
}
|
|
971
1159
|
|
|
972
1160
|
// File only. Passing fetch here would make the test of D16 un-runnable.
|
|
973
|
-
const
|
|
974
|
-
const loaded = loadCatalog({ userText: userFile.text });
|
|
1161
|
+
const loaded = loadCatalog(await readModelSources());
|
|
975
1162
|
for (const warning of loaded.warnings) io.say(C.yellow(` ${warning}`));
|
|
976
1163
|
const catalog = loaded.models;
|
|
977
1164
|
|
|
978
1165
|
const credentialOf = (spec: ModelSpec) => resolveCredential(spec, auth, args.apiKey, effects);
|
|
979
|
-
const hasCredential = (spec: ModelSpec) => credentialOf(spec).ok;
|
|
980
1166
|
|
|
981
1167
|
if (args.listModels) {
|
|
982
1168
|
io.say(C.bold("models"));
|
|
983
|
-
|
|
1169
|
+
const query = args.modelSearch?.toLowerCase();
|
|
1170
|
+
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
1171
|
io.say(C.bold(group.provider));
|
|
985
1172
|
for (const spec of group.models) {
|
|
986
1173
|
const got = credentialOf(spec);
|
|
@@ -992,18 +1179,6 @@ export async function startup(args: Args): Promise<App> {
|
|
|
992
1179
|
process.exit(0);
|
|
993
1180
|
}
|
|
994
1181
|
|
|
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
1182
|
/**
|
|
1008
1183
|
* Resolving the workspace is the first thing that can fail, and it fails for
|
|
1009
1184
|
* ordinary reasons -- a typo in `-w`, a directory that was moved. Reported as
|
|
@@ -1031,139 +1206,81 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1031
1206
|
}
|
|
1032
1207
|
}
|
|
1033
1208
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
let loadProjectSettings =
|
|
1037
|
-
if (args.
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (decision === true) loadProjectSettings = true;
|
|
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
|
-
}
|
|
1209
|
+
await prepareExtensionPackages(args,workspace);
|
|
1210
|
+
const trust = await projectTrust(workspace, args.approve);
|
|
1211
|
+
let loadProjectSettings = trust.trusted;
|
|
1212
|
+
if (args.configCommand) {
|
|
1213
|
+
if (!stdinIsTty() || !stdoutIsTty()) {
|
|
1214
|
+
const index = await discoverResources({ ...args, workspace, trusted: loadProjectSettings });
|
|
1215
|
+
for (const entry of index.entries) io.say(`${entry.enabled ? "[x]" : "[ ]"} ${entry.kind} ${entry.scope}${entry.package?" · "+entry.package.source:""} ${entry.path}`);
|
|
1216
|
+
} else await runResourceConfig({ ...args, workspace, trusted: loadProjectSettings });
|
|
1217
|
+
process.exit(0);
|
|
1059
1218
|
}
|
|
1219
|
+
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);
|
|
1220
|
+
if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await pickProjectTrust(workspace);
|
|
1221
|
+
if (hasProjectResources && !loadProjectSettings) io.say(C.dim(" Project resources not loaded (project is not trusted). /trust changes resource trust; tool permissions are separate."));
|
|
1060
1222
|
|
|
1061
|
-
|
|
1223
|
+
// Resolve the resource owner before factories run. Parsing flags must not load a different cwd first.
|
|
1224
|
+
let resourceWorkspace = workspace;
|
|
1225
|
+
const prepareFlags = (args.extensionArgv && args.resume !== true) || args.help;
|
|
1226
|
+
if (prepareFlags) {
|
|
1227
|
+
const target = args.logDir || args.session || args.forkFrom || (typeof args.resume === "string" ? args.resume : undefined);
|
|
1228
|
+
let resourceLog: string | undefined;
|
|
1229
|
+
if (target) { const found = await findSessionByPrefix(workspace, target, sessionDirFor(args, workspace)); if (found.ok) resourceLog = found.session.path; }
|
|
1230
|
+
else if (args.sessionId) resourceLog = join(sessionsDir(workspace, sessionDirFor(args, workspace)), args.sessionId);
|
|
1231
|
+
else if (args.continueLatest) resourceLog = (await listSessions(workspace, sessionDirFor(args, workspace), Infinity)).sessions.find(session => session.ok)?.path;
|
|
1232
|
+
if (resourceLog) resourceWorkspace = (await readCursor(resourceLog))?.workspace ?? workspace;
|
|
1233
|
+
}
|
|
1234
|
+
const preparedResources = prepareFlags
|
|
1235
|
+
? { workspace: resourceWorkspace, value: await loadSessionResources(args, resourceWorkspace, io, resourceWorkspace === workspace ? loadProjectSettings : undefined) } : undefined;
|
|
1236
|
+
if (args.help) { printCliHelp(preparedResources?.value.extensions.flags); await preparedResources?.value.extensions.dispose(); process.exit(0); }
|
|
1237
|
+
refreshOutputMode();
|
|
1238
|
+
const uiLoaded = preparedResources ? undefined : await loadUiResources({
|
|
1239
|
+
...args,
|
|
1062
1240
|
workspace,
|
|
1063
1241
|
trusted: loadProjectSettings,
|
|
1064
1242
|
...(args.theme.length > 0 ? { theme: args.theme } : {}),
|
|
1065
1243
|
});
|
|
1066
|
-
for (const line of uiLoaded
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1244
|
+
for (const line of uiLoaded?.diagnostics ?? []) io.say(C.yellow(` ${line}`));
|
|
1245
|
+
const ui = preparedResources?.value.ui ?? toUiState(uiLoaded!, colorEnabled());
|
|
1246
|
+
const mergedSettings = ui.settings;
|
|
1069
1247
|
const scopedFromFlag = splitModelPatterns(args.models);
|
|
1070
|
-
if (scopedFromFlag.length > 0) {
|
|
1248
|
+
if (args.modelsExplicit || scopedFromFlag.length > 0) {
|
|
1071
1249
|
ui.settings = { ...ui.settings, scopedModels: scopedFromFlag };
|
|
1072
1250
|
}
|
|
1073
1251
|
|
|
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
1252
|
const paths = statePaths(workspace, args.stateDir);
|
|
1122
|
-
const sessionDir = (args
|
|
1123
|
-
|
|
1124
|
-
(args.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1253
|
+
const sessionDir = sessionDirFor(args, workspace);
|
|
1254
|
+
let sessionOpts: ResolveLogOpts = {
|
|
1255
|
+
...(args.logDir ? { explicit: resolve(args.logDir) } : {}),
|
|
1256
|
+
fresh: args.fresh,
|
|
1257
|
+
...(args.continueLatest === undefined ? {} : { continueLatest: args.continueLatest }),
|
|
1258
|
+
...(args.noSession === undefined ? {} : { noSession: args.noSession }),
|
|
1259
|
+
...(args.session === undefined ? {} : { session: args.session }),
|
|
1260
|
+
...(args.sessionId === undefined ? {} : { sessionId: args.sessionId }),
|
|
1261
|
+
...(args.resume === undefined ? {} : { resume: args.resume }),
|
|
1262
|
+
...(args.forkFrom === undefined ? {} : { forkFrom: args.forkFrom }),
|
|
1263
|
+
...(sessionDir === undefined ? {} : { sessionDir }),
|
|
1264
|
+
};
|
|
1129
1265
|
let logDir: string;
|
|
1130
1266
|
try {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (!found.ok) {
|
|
1135
|
-
io.err(C.red(`\n ${found.reason}\n`));
|
|
1136
|
-
process.exit(1);
|
|
1267
|
+
if (planSessionEntry(sessionOpts).kind === "select") {
|
|
1268
|
+
if (printMode || jsonMode || !stdinIsTty() || !stdoutIsTty()) {
|
|
1269
|
+
throw new Error("--resume needs an interactive terminal. Pass -r <id> or --session <id> instead");
|
|
1137
1270
|
}
|
|
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;
|
|
1271
|
+
const picked = await pickStartupSession({ workspace, style: ui.style, say: io.say, keymap: ui.keymap,
|
|
1272
|
+
...(sessionDir === undefined ? {} : { sessionDir }) });
|
|
1273
|
+
if (picked === null) { io.say(C.dim("No session selected")); process.exit(0); }
|
|
1274
|
+
sessionOpts = { explicit: picked };
|
|
1154
1275
|
}
|
|
1276
|
+
const resolved = await resolveLogDir(workspace, sessionOpts);
|
|
1277
|
+
if (resolved.legacyNotice !== null && !args.logDir) io.say(C.yellow(` ${resolved.legacyNotice}`));
|
|
1278
|
+
logDir = resolved.logDir;
|
|
1155
1279
|
} catch (err) {
|
|
1156
1280
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1157
1281
|
io.err(C.red(`\n ${detail}\n`));
|
|
1158
1282
|
process.exit(1);
|
|
1159
1283
|
}
|
|
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
1284
|
// Snapshots are on by default here. They are optional in the loop because a
|
|
1168
1285
|
// read-only branch has nothing to snapshot, but a session a person drives is
|
|
1169
1286
|
// exactly the one that will want to go back, and the content has to have been
|
|
@@ -1172,64 +1289,27 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1172
1289
|
await mkdir(logDir, { recursive: true });
|
|
1173
1290
|
await mkdir(blobDir, { recursive: true });
|
|
1174
1291
|
|
|
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);
|
|
1292
|
+
// Resource ownership follows the resolved session workspace. openSessionFor
|
|
1293
|
+
// loads and freezes it once; explicit reloads checkpoint a changed prefix (D6).
|
|
1190
1294
|
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
1295
|
|
|
1215
1296
|
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
|
-
|
|
1297
|
+
let opened: Awaited<ReturnType<typeof openSessionFor>>;
|
|
1225
1298
|
try {
|
|
1226
|
-
await
|
|
1299
|
+
opened = await openSessionFor({
|
|
1300
|
+
args, workspace, logDir, blobDir, io, askSlot, catalog, auth, credentialOf, effects,
|
|
1301
|
+
settings: mergedSettings, modelFlags: { model: args.model, provider: args.provider }, projectTrusted: loadProjectSettings,
|
|
1302
|
+
...(preparedResources ? { preparedResources } : {}),
|
|
1303
|
+
interactive: isInteractiveSession(args),
|
|
1304
|
+
});
|
|
1227
1305
|
} catch (err) {
|
|
1228
1306
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1229
1307
|
io.err(C.red(`\n cannot start a session in ${workspace}`));
|
|
1230
1308
|
io.err(C.dim(` ${detail}\n`));
|
|
1231
1309
|
process.exit(1);
|
|
1232
1310
|
}
|
|
1311
|
+
const { session, priorCursor, providerConfig, credential } = opened;
|
|
1312
|
+
refreshOutputMode();
|
|
1233
1313
|
|
|
1234
1314
|
const sessionName = args.sessionName;
|
|
1235
1315
|
if (sessionName !== undefined && sessionName.length > 0) {
|
|
@@ -1239,17 +1319,19 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1239
1319
|
}
|
|
1240
1320
|
}
|
|
1241
1321
|
|
|
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()],
|
|
1322
|
+
if (opened.resources.ui.settings.quietStartup !== true || args.verbose) printBanner(args, io, {
|
|
1323
|
+
workspace: opened.workspace, logDir, providerConfig, credential, loadedContext: opened.resources.loadedContext, session, priorCursor, catalog,
|
|
1324
|
+
keymap: opened.resources.ui.keymap,
|
|
1325
|
+
skills: [...opened.resources.slash.skills.keys()],
|
|
1326
|
+
prompts: [...opened.resources.slash.prompts.keys()],
|
|
1247
1327
|
tools: visible.map((t) => t.name),
|
|
1248
1328
|
});
|
|
1249
1329
|
|
|
1250
1330
|
return {
|
|
1251
|
-
args, session, workspace, logDir, blobDir, system, providerConfig, credential,
|
|
1252
|
-
auth, effects, loadedContext, credentialOf, catalog, io, askSlot,
|
|
1331
|
+
args, session, workspace: opened.workspace, logDir, blobDir: opened.blobDir, system: opened.system, providerConfig, credential,
|
|
1332
|
+
auth, effects, loadedContext: opened.resources.loadedContext, credentialOf, catalog, io, askSlot,
|
|
1333
|
+
ui: opened.resources.ui, slash: opened.resources.slash, resources: opened.resources.index, projectTrusted: opened.resources.trusted,
|
|
1334
|
+
extensions: opened.resources.extensions,
|
|
1253
1335
|
};
|
|
1254
1336
|
}
|
|
1255
1337
|
|
|
@@ -1321,3 +1403,53 @@ function printBanner(
|
|
|
1321
1403
|
}
|
|
1322
1404
|
io.say("");
|
|
1323
1405
|
}
|
|
1406
|
+
|
|
1407
|
+
export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
1408
|
+
console.log(
|
|
1409
|
+
"usage: xeno [-m <model>] [--provider <name>] [--api-key <key>]\n" +
|
|
1410
|
+
" [-w <workspace>] [--log <dir>] [--max-turns N]\n" +
|
|
1411
|
+
" [--context-policy windowed|segmented|continuous|adaptive]\n" +
|
|
1412
|
+
" [--trigger N --hard N] [--list-models] [--fresh]\n" +
|
|
1413
|
+
" [-c|--continue] [-r|--resume [id]] [--session path|id]\n" +
|
|
1414
|
+
" [--fork path|id] [--no-session] [--session-dir dir]\n" +
|
|
1415
|
+
" [-n|--name name] [--session-id id] [--version|-v]\n" +
|
|
1416
|
+
" [models --refresh]\n" +
|
|
1417
|
+
" [install|remove <source>... [--local]] [update [source] [--extensions]] [list]\n" +
|
|
1418
|
+
" [config [--local]] [--export session [output.html]] [--offline]\n" +
|
|
1419
|
+
" [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
|
|
1420
|
+
" [--no-context-files|-nc]\n" +
|
|
1421
|
+
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
1422
|
+
" [--no-builtin-tools]\n" +
|
|
1423
|
+
" [--allow-tools names] [--approve|--no-approve]\n" +
|
|
1424
|
+
" [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
|
|
1425
|
+
" [--skill <path>] [--no-skills]\n" +
|
|
1426
|
+
" [--prompt-template <path>] [--no-prompt-templates]\n" +
|
|
1427
|
+
" [--extension <path>] [--no-extensions] [--extension-flag name=value]\n" +
|
|
1428
|
+
" [--mode text|json] [--tui-mode regular|fullscreen] [--verbose]\n" +
|
|
1429
|
+
" [-p <prompt>] [prompt...]\n\n" +
|
|
1430
|
+
" Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
|
|
1431
|
+
" -c continues the newest (or starts one); -r opens the session picker.\n" +
|
|
1432
|
+
" --session / -r <id> pick one (unique prefix only). Resume restores its model;\n" +
|
|
1433
|
+
" an explicit --model switches at a checkpoint before the next turn.\n" +
|
|
1434
|
+
" --no-session runs without writing .xeno/sessions/. --fork copies into a new one.\n" +
|
|
1435
|
+
" A leftover .xeno/log is not moved; continue it with --log .xeno/log.\n\n" +
|
|
1436
|
+
" With a prompt, runs it and exits. Without one, reads prompts from stdin.\n" +
|
|
1437
|
+
" Commands: " + COMMANDS.map(command => "/" + command.name).join(" ") + "\n\n" +
|
|
1438
|
+
" Credentials resolve in this order: --api-key, then ~/.xeno/auth.json,\n" +
|
|
1439
|
+
" then the provider's environment variable.\n" +
|
|
1440
|
+
" --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
|
|
1441
|
+
" --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
|
|
1442
|
+
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
1443
|
+
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
1444
|
+
" bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
|
|
1445
|
+
" --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
|
|
1446
|
+
" --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
|
|
1447
|
+
" --path-root <dir> lets file tools reach absolute paths under <dir> (e.g. / in a container);\n" +
|
|
1448
|
+
" relative paths, fingerprints and /back stay in the workspace, so edits outside it are not rewound.\n" +
|
|
1449
|
+
" --approve loads .xeno/settings.json this run; --no-approve skips it.\n" +
|
|
1450
|
+
" Project trust does not affect tool execution.\n" +
|
|
1451
|
+
" --mode json writes one JSON object per event to stdout; diagnostics stay on stderr.\n" +
|
|
1452
|
+
" --verbose expands the startup header (AGENTS.md, skills, prompt templates).",
|
|
1453
|
+
);
|
|
1454
|
+
if (flags?.size) console.log("\nExtension CLI flags:\n" + [...flags].map(([name, flag]) => ` --${name}${flag.type === "string" ? " <value>" : ""} ${flag.description ?? ""}`).join("\n"));
|
|
1455
|
+
}
|