@visual-z/xeno 0.3.0 → 0.5.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 +55 -16
- package/docs/app-server.md +86 -0
- package/docs/install.md +1 -1
- package/docs/providers.md +20 -6
- package/package.json +3 -2
- package/src/apps/app-server.ts +28 -0
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +10 -6
- package/src/apps/completions.ts +15 -13
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/preferences.ts +20 -0
- package/src/apps/provider-connect.ts +4 -4
- package/src/apps/shell.ts +73 -50
- package/src/apps/startup-input.ts +7 -2
- package/src/apps/startup.ts +52 -19
- package/src/apps/ui/app.ts +210 -56
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/surface.ts +2 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +38 -29
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/event-validation.ts +1 -1
- package/src/core/events.ts +2 -0
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/model-preferences.ts +28 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/permissions.ts +13 -7
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +25 -1
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/settings.ts +8 -1
- package/src/core/tool-input.ts +6 -0
- package/src/runtime/auth.ts +10 -3
- package/src/runtime/catalog.ts +21 -4
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/harness-eval.ts +3 -1
- package/src/runtime/loop.ts +72 -19
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval.ts +6 -3
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +2 -2
- package/src/runtime/provider-presets.ts +1 -1
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tools.ts +11 -9
- package/src/store/resources.ts +13 -22
- package/src/store/user-settings.ts +30 -0
- package/src/tui/ui/composer.ts +3 -2
- package/src/tui/ui/control-panel.ts +25 -0
- package/src/tui/ui/control-sidebar.ts +49 -0
- package/src/tui/ui/mascot.ts +1 -1
- package/src/tui/ui/screen.ts +107 -11
- package/src/tui/ui/status.ts +8 -0
package/src/apps/startup.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
13
13
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { Session, systemClock } from "../runtime/loop.ts";
|
|
15
15
|
import { DEFAULT_PROVIDER, type ProviderConfig } from "../runtime/provider.ts";
|
|
16
|
-
import { createProvider, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
|
|
16
|
+
import { createProvider, providerSessionId, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
|
|
17
17
|
import { ExtensionRegistry } from "../runtime/extensions.ts";
|
|
18
18
|
import { DISCOVERY_TOOL_NAME } from "../runtime/tool-discovery.ts";
|
|
19
19
|
import { declaredToolNames } from "../core/tool-discovery.ts";
|
|
@@ -177,6 +177,8 @@ export interface Args {
|
|
|
177
177
|
/** `--no-builtin-tools`. False when the flag was not passed. */
|
|
178
178
|
noBuiltinTools: boolean;
|
|
179
179
|
allowTools: string[];
|
|
180
|
+
/** `--yolo` / `-y`: skip tool approval for this process only. */
|
|
181
|
+
yolo: boolean;
|
|
180
182
|
/** `--approve` true, `--no-approve` false. Omitted when neither. */
|
|
181
183
|
approve?: boolean;
|
|
182
184
|
configCommand?: boolean;
|
|
@@ -224,6 +226,8 @@ export interface Args {
|
|
|
224
226
|
sessionName?: string;
|
|
225
227
|
/** `--mode json`. Empty / omitted when the flag was not passed. */
|
|
226
228
|
mode?: string;
|
|
229
|
+
/** Explicit local adapter endpoint. An owned mode-0700 directory is required. */
|
|
230
|
+
controlSocket?: string;
|
|
227
231
|
/** `--verbose`. Expands the startup header's counts into lists. */
|
|
228
232
|
verbose?: boolean;
|
|
229
233
|
}
|
|
@@ -269,6 +273,7 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
269
273
|
"--no-tools", "-nt",
|
|
270
274
|
"--no-builtin-tools",
|
|
271
275
|
"--allow-tools",
|
|
276
|
+
"--yolo", "-y",
|
|
272
277
|
"--approve", "-a",
|
|
273
278
|
"--no-approve", "-na",
|
|
274
279
|
"--theme",
|
|
@@ -280,6 +285,7 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
280
285
|
"--no-prompt-templates",
|
|
281
286
|
"--extension", "-e", "--no-extensions", "-ne", "--extension-flag",
|
|
282
287
|
"--mode",
|
|
288
|
+
"--control-socket",
|
|
283
289
|
"--verbose",
|
|
284
290
|
"--print", "-p",
|
|
285
291
|
"--help", "-h",
|
|
@@ -340,7 +346,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
340
346
|
model: "", provider: "", apiKey: "", listModels: false, modelsCommand: false, modelsRefresh: false, fresh: false,
|
|
341
347
|
noContextFiles: false, appendSystemPrompt: [],
|
|
342
348
|
systemPrompt: null,
|
|
343
|
-
excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [],
|
|
349
|
+
excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [], yolo: false,
|
|
344
350
|
print: false,
|
|
345
351
|
theme: "",
|
|
346
352
|
thinking: "",
|
|
@@ -425,6 +431,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
425
431
|
else if (a === "--no-tools" || a === "-nt") args.noTools = true;
|
|
426
432
|
else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
|
|
427
433
|
else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
|
|
434
|
+
else if (a === "--yolo" || a === "-y") args.yolo = true;
|
|
428
435
|
else if (a === "--approve" || a === "-a") args.approve = true;
|
|
429
436
|
else if (a === "--no-approve" || a === "-na") args.approve = false;
|
|
430
437
|
else if (a === "--theme") {
|
|
@@ -451,6 +458,11 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
451
458
|
(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);
|
|
452
459
|
}
|
|
453
460
|
else if (a === "--mode") args.mode = argv[++i] ?? "";
|
|
461
|
+
else if (a === "--control-socket") {
|
|
462
|
+
const value = argv[++i];
|
|
463
|
+
if (!value || value.startsWith("-")) throw Error("--control-socket requires a path");
|
|
464
|
+
args.controlSocket = resolve(value);
|
|
465
|
+
}
|
|
454
466
|
else if (a === "--verbose") args.verbose = true;
|
|
455
467
|
else if (a === "-p" || a === "--print") {
|
|
456
468
|
args.print = true;
|
|
@@ -713,7 +725,7 @@ export interface App {
|
|
|
713
725
|
blobDir: string;
|
|
714
726
|
system: string;
|
|
715
727
|
providerConfig: ProviderConfig;
|
|
716
|
-
/** May be `{ ok: false }` in an interactive session waiting on /
|
|
728
|
+
/** May be `{ ok: false }` in an interactive session waiting on /provider. */
|
|
717
729
|
credential: Resolved;
|
|
718
730
|
auth: AuthFile;
|
|
719
731
|
effects: ResolveEffects;
|
|
@@ -773,8 +785,8 @@ export function whyNot(spec: ModelSpec, got: Resolved, auth: AuthFile, C: Palett
|
|
|
773
785
|
}
|
|
774
786
|
|
|
775
787
|
/**
|
|
776
|
-
* /
|
|
777
|
-
* cannot answer it, so those runs must still refuse -- naming /
|
|
788
|
+
* /provider only exists inside a live session. A one-shot prompt and a pipe
|
|
789
|
+
* cannot answer it, so those runs must still refuse -- naming /provider there
|
|
778
790
|
* is how this file used to deadlock a first run.
|
|
779
791
|
*/
|
|
780
792
|
/**
|
|
@@ -792,7 +804,7 @@ export function isInteractiveSession(args: Args): boolean {
|
|
|
792
804
|
|
|
793
805
|
/**
|
|
794
806
|
* Script-facing missing-key text. Environment variables and --api-key are
|
|
795
|
-
* the routes that actually work without a tty; /
|
|
807
|
+
* the routes that actually work without a tty; /provider is not one of them.
|
|
796
808
|
*/
|
|
797
809
|
export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CATALOG): string {
|
|
798
810
|
const providers = [...new Set(catalog.map((m) => m.provider))];
|
|
@@ -809,7 +821,7 @@ export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CA
|
|
|
809
821
|
* action that works from here; the interactive routes are named instead.
|
|
810
822
|
*/
|
|
811
823
|
export function describeUnauthenticatedSession(_catalog: readonly ModelSpec[] = CATALOG): string {
|
|
812
|
-
return "no provider connected yet. Type /
|
|
824
|
+
return "no provider connected yet. Type /provider to connect one — a Claude, ChatGPT, Copilot or Grok account, an API key, a gateway or a local server — " +
|
|
813
825
|
`or export a key such as ${["anthropic", "openai", "google", "deepseek"].map(provider => envLabel(provider).split(" or ")[0]).join(", ")}.`;
|
|
814
826
|
}
|
|
815
827
|
|
|
@@ -845,10 +857,14 @@ export function toolSelectionOf(args: Args) {
|
|
|
845
857
|
* the provider bills, and an extra field there would look like a tool
|
|
846
858
|
* argument.
|
|
847
859
|
*/
|
|
848
|
-
export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
|
|
860
|
+
export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = [], builtinDefaults?: readonly string[]) {
|
|
849
861
|
const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
|
|
850
|
-
|
|
851
|
-
|
|
862
|
+
// O.7/D6: a cold app open uses saved capabilities as defaults, never today's profile.
|
|
863
|
+
// Explicit selection still wins; extension loading and current exclusions remain live.
|
|
864
|
+
const retained = builtinDefaults !== undefined && args.tools === undefined && args.toolProfile === undefined ? new Set(builtinDefaults) : undefined;
|
|
865
|
+
const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) &&
|
|
866
|
+
(retained === undefined ? !tool.optional || args.tools?.includes(tool.name) : retained.has(tool.name))).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
|
|
867
|
+
const selected = selectTools(available, { ...toolSelectionOf(args), ...(retained ? { profile: "full" as const } : {}) });
|
|
852
868
|
const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
|
|
853
869
|
.map(({ name, description, input_schema }) => ({ name, description, input_schema }));
|
|
854
870
|
const deferredNames = new Set(deferred.map(tool => tool.name));
|
|
@@ -901,7 +917,7 @@ export function createSessionFor(opts: {
|
|
|
901
917
|
...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
|
|
902
918
|
allow: names,
|
|
903
919
|
deferredTools: deferred,
|
|
904
|
-
rules: rulesWithAllowTools(args.allowTools),
|
|
920
|
+
rules: rulesWithAllowTools(args.allowTools, { yolo: args.yolo }),
|
|
905
921
|
sessionRules,
|
|
906
922
|
askSlot,
|
|
907
923
|
...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
|
|
@@ -933,13 +949,13 @@ export function createSessionFor(opts: {
|
|
|
933
949
|
: {}),
|
|
934
950
|
},
|
|
935
951
|
// Empty key is a placeholder the shell must not send through. Interactive
|
|
936
|
-
// startup reaches this path so /
|
|
952
|
+
// startup reaches this path so /provider exists; run() refuses until a real
|
|
937
953
|
// credential is installed on the session.
|
|
938
954
|
createProvider(
|
|
939
955
|
providerConfig,
|
|
940
956
|
credential.ok ? credential.key : "",
|
|
941
957
|
retryHandler(io),
|
|
942
|
-
{ ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
958
|
+
{ sessionId: providerSessionId(logDir), ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
943
959
|
),
|
|
944
960
|
systemClock(),
|
|
945
961
|
);
|
|
@@ -1089,13 +1105,22 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1089
1105
|
opts.startupPhase?.("Opening session history");
|
|
1090
1106
|
await session.start();
|
|
1091
1107
|
resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1108
|
+
const restoredTools = sessionToolCatalog(opts.args, resources.extensions, session.config.frozen.tools,
|
|
1109
|
+
priorCursor === null ? undefined : declaredToolNames(session.config.frozen.tools));
|
|
1110
|
+
const refreshResources = resources.extensions.loaded.length > 0 || session.config.frozen.tools.some(tool =>
|
|
1111
|
+
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length);
|
|
1112
|
+
const explicitTools = opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
|
|
1113
|
+
opts.args.noBuiltinTools || opts.args.excludeTools.length > 0;
|
|
1114
|
+
if (refreshResources || (priorCursor !== null && explicitTools)) {
|
|
1115
|
+
await session.reloadResources({ system: refreshResources ? resources.system : session.config.frozen.system, tools: restoredTools.schemas });
|
|
1095
1116
|
}
|
|
1117
|
+
// Admission follows this app's selected declaration; current process approval rules stay intact.
|
|
1118
|
+
// Keep this out of Session.start(): an SDK caller can intentionally supply a narrower allowlist.
|
|
1119
|
+
session.config.tools.allow = declaredToolNames(session.config.frozen.tools);
|
|
1120
|
+
session.config.tools.deferredTools = restoredTools.deferred;
|
|
1096
1121
|
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1097
1122
|
await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
1098
|
-
{ ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1123
|
+
{ sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1099
1124
|
opts.io.say(opts.io.palette.yellow(` switched ${restoring.id} → ${spec.id} at a model checkpoint`));
|
|
1100
1125
|
}
|
|
1101
1126
|
resources.ui.settings = { ...resources.ui.settings, thinking: providerConfig.effort,
|
|
@@ -1445,14 +1470,19 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1445
1470
|
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
1446
1471
|
" [--tool-profile coding|readonly|research|data|full]\n" +
|
|
1447
1472
|
" [--acceptance-file contract.json]\n" +
|
|
1473
|
+
" [--control-socket /private/directory/control.sock]\n" +
|
|
1448
1474
|
" [--no-builtin-tools]\n" +
|
|
1449
|
-
" [--allow-tools names] [--approve|--no-approve]\n" +
|
|
1475
|
+
" [--allow-tools names] [--yolo|-y] [--approve|--no-approve]\n" +
|
|
1450
1476
|
" [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
|
|
1451
1477
|
" [--skill <path>] [--no-skills]\n" +
|
|
1452
1478
|
" [--prompt-template <path>] [--no-prompt-templates]\n" +
|
|
1453
1479
|
" [--extension <path>] [--no-extensions] [--extension-flag name=value]\n" +
|
|
1454
1480
|
" [--mode text|json] [--tui-mode regular|fullscreen] [--verbose]\n" +
|
|
1455
1481
|
" [-p <prompt>] [prompt...]\n\n" +
|
|
1482
|
+
" xeno app-server [session/model flags] runs the experimental JSONL control interface.\n" +
|
|
1483
|
+
" xeno app-server schema prints its versioned request schema without startup.\n" +
|
|
1484
|
+
" --control-socket attaches local adapters to the same live session as the TUI.\n" +
|
|
1485
|
+
" /control opens goal, progress and context panels; XENO_PROGRAM_STATUS=auto|1|0 controls terminal status.\n\n" +
|
|
1456
1486
|
" Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
|
|
1457
1487
|
" -c continues the newest (or starts one); -r opens the session picker.\n" +
|
|
1458
1488
|
" --session / -r <id> pick one (unique prefix only). Resume restores its model;\n" +
|
|
@@ -1468,7 +1498,10 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1468
1498
|
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
1469
1499
|
" New sessions use coding (10 tools); full exposes default built-ins.\n" +
|
|
1470
1500
|
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
1471
|
-
"
|
|
1501
|
+
" Shell entry points (exec_command, environment, bash, target) ask before they run.\n" +
|
|
1502
|
+
" --allow-tools exec_command skips asks for that tool for this process.\n" +
|
|
1503
|
+
" --yolo (-y) skips all tool approvals for this process, including resumed sessions.\n" +
|
|
1504
|
+
" Tool exclusions and path limits still apply; YOLO does not change project trust.\n" +
|
|
1472
1505
|
" --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
|
|
1473
1506
|
" --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
|
|
1474
1507
|
" --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +
|