@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +55 -16
  3. package/docs/app-server.md +86 -0
  4. package/docs/install.md +1 -1
  5. package/docs/providers.md +20 -6
  6. package/package.json +3 -2
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/cli.ts +25 -10
  9. package/src/apps/commands.ts +10 -6
  10. package/src/apps/completions.ts +15 -13
  11. package/src/apps/control-transport.ts +89 -0
  12. package/src/apps/model-catalog.ts +2 -1
  13. package/src/apps/preferences.ts +20 -0
  14. package/src/apps/provider-connect.ts +4 -4
  15. package/src/apps/shell.ts +73 -50
  16. package/src/apps/startup-input.ts +7 -2
  17. package/src/apps/startup.ts +52 -19
  18. package/src/apps/ui/app.ts +210 -56
  19. package/src/apps/ui/control-model.ts +26 -0
  20. package/src/apps/ui/surface.ts +2 -0
  21. package/src/core/completion-action.ts +20 -0
  22. package/src/core/completion-review.ts +38 -29
  23. package/src/core/control-protocol.ts +62 -0
  24. package/src/core/control-view.ts +42 -0
  25. package/src/core/event-validation.ts +1 -1
  26. package/src/core/events.ts +2 -0
  27. package/src/core/harness-diagnostics.ts +206 -0
  28. package/src/core/model-preferences.ts +28 -0
  29. package/src/core/oauth-fields.ts +44 -0
  30. package/src/core/permissions.ts +13 -7
  31. package/src/core/program-status.ts +31 -0
  32. package/src/core/project.ts +25 -1
  33. package/src/core/request-progress.ts +87 -0
  34. package/src/core/request-state.ts +114 -0
  35. package/src/core/settings.ts +8 -1
  36. package/src/core/tool-input.ts +6 -0
  37. package/src/runtime/auth.ts +10 -3
  38. package/src/runtime/catalog.ts +21 -4
  39. package/src/runtime/control-connection.ts +104 -0
  40. package/src/runtime/harness-eval.ts +3 -1
  41. package/src/runtime/loop.ts +72 -19
  42. package/src/runtime/model.ts +9 -0
  43. package/src/runtime/native-eval.ts +6 -3
  44. package/src/runtime/oauth.ts +80 -3
  45. package/src/runtime/pi-provider.ts +169 -12
  46. package/src/runtime/program-status.ts +38 -0
  47. package/src/runtime/provider-connect.ts +2 -2
  48. package/src/runtime/provider-presets.ts +1 -1
  49. package/src/runtime/provider-request.ts +13 -2
  50. package/src/runtime/provider.ts +23 -9
  51. package/src/runtime/session-control.ts +198 -0
  52. package/src/runtime/tools.ts +11 -9
  53. package/src/store/resources.ts +13 -22
  54. package/src/store/user-settings.ts +30 -0
  55. package/src/tui/ui/composer.ts +3 -2
  56. package/src/tui/ui/control-panel.ts +25 -0
  57. package/src/tui/ui/control-sidebar.ts +49 -0
  58. package/src/tui/ui/mascot.ts +1 -1
  59. package/src/tui/ui/screen.ts +107 -11
  60. package/src/tui/ui/status.ts +8 -0
@@ -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 /login. */
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
- * /login only exists inside a live session. A one-shot prompt and a pipe
777
- * cannot answer it, so those runs must still refuse -- naming /login there
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; /login is not one of them.
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 /login to connect one — a Claude, ChatGPT or Copilot subscription, an API key, a gateway or a local server — " +
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
- const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) && (!tool.optional || args.tools?.includes(tool.name))).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
851
- const selected = selectTools(available, toolSelectionOf(args));
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 /login exists; run() refuses until a real
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
- if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
1093
- typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
1094
- await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions, session.config.frozen.tools) });
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
- " bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
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" +