@skein-code/cli 0.3.27 → 0.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,7 +96,7 @@ To build, verify, and install a local package artifact from this checkout:
96
96
 
97
97
  ```bash
98
98
  npm run verify:package -- --output-dir artifacts/package
99
- npm install -g ./artifacts/package/skein-code-cli-0.3.27.tgz
99
+ npm install -g ./artifacts/package/skein-code-cli-0.3.29.tgz
100
100
  ```
101
101
 
102
102
  To install the published package from npm:
@@ -252,7 +252,13 @@ as `accent`, `text`, `muted`, `success`, and `error`. Set
252
252
  `SKEIN_GLYPHS=ascii` when a terminal or multiplexer renders Unicode symbols
253
253
  inconsistently. `NO_COLOR=1` or `ui.color: false` removes palette colors while
254
254
  keeping status symbols and semantic labels intact. `/density compact` and
255
- `/density comfortable` control vertical rhythm. Skein enables Kitty keyboard
255
+ `/density comfortable` control vertical rhythm. `TERM=dumb` automatically uses
256
+ ASCII, monochrome, reduced-motion, non-incremental output. Set
257
+ `SKEIN_SCREEN_READER=1` (or Ink's `INK_SCREEN_READER=true`) for linear
258
+ screen-reader output with semantic timeline, permission-choice, and input roles;
259
+ this profile applies the same low-motion fallbacks while preserving keyboard
260
+ commands. `SKEIN_REDUCE_MOTION=1` disables activity animation independently.
261
+ Skein enables Kitty keyboard
256
262
  enhancements without probing when Kitty, WezTerm, Ghostty, or foot declares
257
263
  support; set `SKEIN_KITTY_KEYBOARD=on|off` to override detection.
258
264
 
@@ -639,7 +645,16 @@ npm test
639
645
  npm run build
640
646
  npm run check
641
647
  npm run test:pty
648
+ npm run benchmark:terminal-ui
642
649
  npm run release:verify
643
650
  ```
644
651
 
652
+ The PTY suite exercises 20/24/40/80/120 columns, a 40×10 viewport,
653
+ `TERM=dumb`, ASCII/`NO_COLOR`, and screen-reader interaction. It replays raw
654
+ output in a headless terminal and checks the final visible frame for overflow,
655
+ stale panels, control-sequence leaks, and missing ready/permission/error state.
656
+ `benchmark:terminal-ui` is a local single-process regression gate with p95
657
+ budgets of 25 ms for input processing and 150 ms for streaming renders; it is
658
+ not a universal hardware performance claim.
659
+
645
660
  Skein is licensed under MIT.
package/dist/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.27",
223
+ version: "0.3.29",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,11 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "TERM=dumb and screen-reader sessions now use deterministic ASCII, monochrome, reduced-motion, non-incremental rendering without losing keyboard input",
240
+ "SKEIN_SCREEN_READER=1 enables Ink accessibility output with semantic timeline, permission, and composer roles plus clean phase boundaries",
241
+ "PTY release gates now replay eight terminal scenarios through a headless emulator and reject final-frame overflow, stale panels, announcement joins, and missing status",
242
+ "A local terminal UI benchmark enforces 25 ms input and 150 ms streaming-render p95 budgets while retaining the final streamed chunk",
243
+ "MCP trust and revoke confirmation rendering now has a deterministic cross-platform release regression gate",
239
244
  "MCP now follows a no-network search and inspect review before explicit workspace-bound manifest trust and lazy activation",
240
245
  "Declarative capability manifests expose redacted source, version, tools, permissions, network, command, path, sensitive-field, process, and completion-evidence effects",
241
246
  "Persistent disable and revoke unload active remote schemas while manifest fingerprint changes fail closed to untrusted",
@@ -318,6 +323,7 @@ var package_default = {
318
323
  "benchmark:context": "tsx scripts/benchmark-local-index.ts",
319
324
  "benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
320
325
  "benchmark:token-economy": "tsx scripts/benchmark-token-economy.ts",
326
+ "benchmark:terminal-ui": "tsx scripts/benchmark-terminal-ui.ts",
321
327
  "verify:package": "node scripts/verify-package.mjs",
322
328
  "release:verify": "npm run check && npm run verify:package --",
323
329
  typecheck: "tsc --noEmit",
@@ -345,6 +351,7 @@ var package_default = {
345
351
  "@types/diff": "^8.0.0",
346
352
  "@types/node": "^22.19.7",
347
353
  "@types/react": "^19.2.7",
354
+ "@xterm/headless": "^6.0.0",
348
355
  tsup: "^8.5.1",
349
356
  tsx: "^4.21.0",
350
357
  vitest: "^4.0.18"
@@ -14769,6 +14776,20 @@ import { constants as constants5 } from "node:fs";
14769
14776
  import chalk from "chalk";
14770
14777
 
14771
14778
  // src/ui/terminal-capabilities.ts
14779
+ function resolveTerminalAccessibility(environment = process.env) {
14780
+ const screenReader = truthy(environment.SKEIN_SCREEN_READER) || environment.INK_SCREEN_READER?.trim().toLowerCase() === "true";
14781
+ const dumb = environment.TERM?.trim().toLowerCase() === "dumb";
14782
+ const reducedMotion = screenReader || dumb || truthy(environment.SKEIN_REDUCE_MOTION);
14783
+ const ascii = screenReader || dumb || environment.SKEIN_GLYPHS === "ascii" || environment.MOSAIC_GLYPHS === "ascii";
14784
+ const color = !screenReader && !dumb && environment.NO_COLOR === void 0;
14785
+ return {
14786
+ screenReader,
14787
+ reducedMotion,
14788
+ ascii,
14789
+ color,
14790
+ incrementalRendering: !screenReader && !dumb
14791
+ };
14792
+ }
14772
14793
  function resolveKittyKeyboardConfig(environment = process.env) {
14773
14794
  const override = environment.SKEIN_KITTY_KEYBOARD?.trim().toLowerCase();
14774
14795
  if (override && ["1", "true", "yes", "on", "enabled"].includes(override)) {
@@ -14790,6 +14811,9 @@ function enabledKittyKeyboard() {
14790
14811
  function disabledKittyKeyboard() {
14791
14812
  return { mode: "disabled", flags: ["disambiguateEscapeCodes"] };
14792
14813
  }
14814
+ function truthy(value) {
14815
+ return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
14816
+ }
14793
14817
 
14794
14818
  // src/cli/glyphs.ts
14795
14819
  var unicodeGlyphs = {
@@ -16218,7 +16242,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
16218
16242
  if (!items.length) {
16219
16243
  return /* @__PURE__ */ jsx(Box, { paddingLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "Start with a request, @file, or /help." }) });
16220
16244
  }
16221
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
16245
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", "aria-role": "list", children: items.map((item, index) => {
16222
16246
  if (item.kind === "user") {
16223
16247
  return /* @__PURE__ */ jsxs(Box, { marginBottom: compact3 || item.clipped ? 0 : 1, children: [
16224
16248
  /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: glyphs.prompt }) }),
@@ -16689,7 +16713,7 @@ function PermissionCard({ call, category, reason, width = 80, glyphMode = "auto"
16689
16713
  { text: "[Esc]", color: theme.muted }
16690
16714
  ];
16691
16715
  const marker = glyphs.borderStyle === "classic" ? "!" : "\u258E";
16692
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
16716
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, "aria-role": "radiogroup", children: [
16693
16717
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: title }) }),
16694
16718
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: tool }) }),
16695
16719
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.text, children: summaryLine }) }),
@@ -16752,7 +16776,7 @@ function PromptBar({ busy, value, placeholder, width = 80, mode = "chat", queueC
16752
16776
  ] }),
16753
16777
  /* @__PURE__ */ jsx(Text, { color: theme.text, children: truncateDisplay(safeQueuePreview, queuePreviewWidth) })
16754
16778
  ] }) : null,
16755
- /* @__PURE__ */ jsxs(Box, { children: [
16779
+ /* @__PURE__ */ jsxs(Box, { "aria-role": "textbox", children: [
16756
16780
  /* @__PURE__ */ jsx(Text, { bold: true, color: shell ? theme.warning : theme.accent, children: shell ? "! " : `${glyphs.prompt} ` }),
16757
16781
  children
16758
16782
  ] }),
@@ -18416,13 +18440,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18416
18440
  const terminalHeight = Math.max(1, rows || 24);
18417
18441
  const horizontalPadding = terminalWidth >= 24 ? 1 : 0;
18418
18442
  const contentWidth = Math.max(1, terminalWidth - horizontalPadding * 2);
18419
- const glyphMode = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii" ? "ascii" : "auto";
18443
+ const terminalAccessibility = resolveTerminalAccessibility();
18444
+ const glyphMode = terminalAccessibility.ascii ? "ascii" : "auto";
18420
18445
  const glyphs = resolveGlyphs(glyphMode);
18421
18446
  const separator = ` ${glyphs.separator} `;
18422
18447
  const ellipsis = terminalEllipsis();
18423
18448
  const initialSession = runner.getSession();
18424
18449
  const setupProblem = config.model.provider !== "compatible" && !config.model.apiKey ? `No ${config.model.provider} API key found. Set it and restart: export ${providerApiKeyEnv(config.model.provider)}=<your-key>${separator}then run ${PRODUCT_COMMAND} again. Use ${PRODUCT_COMMAND} doctor to verify, or --model to switch provider.` : config.model.provider === "compatible" && !config.model.baseUrl ? `No model endpoint configured. Set one and restart: export SKEIN_BASE_URL=<endpoint>${separator}or pass --base-url <endpoint>. Run ${PRODUCT_COMMAND} doctor to verify.` : void 0;
18425
- const colorEnabled = config.ui.color && !process.env.NO_COLOR;
18450
+ const colorEnabled = config.ui.color && terminalAccessibility.color;
18426
18451
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
18427
18452
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
18428
18453
  const [compact3, setCompact] = useState2(config.ui.compact);
@@ -18558,13 +18583,13 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18558
18583
  setHistorySearch((current) => current ? setHistorySearchQuery(current, input2) : current);
18559
18584
  }, [input2]);
18560
18585
  useEffect2(() => {
18561
- if (!busy || reducedMotion()) {
18586
+ if (!busy || terminalAccessibility.reducedMotion) {
18562
18587
  setFrameIndex(0);
18563
18588
  return void 0;
18564
18589
  }
18565
18590
  const timer = setInterval(() => setFrameIndex((value) => (value + 1) % spinnerFrames().length), 120);
18566
18591
  return () => clearInterval(timer);
18567
- }, [busy]);
18592
+ }, [busy, terminalAccessibility.reducedMotion]);
18568
18593
  const requestPermission = useCallback((call, category, reason) => {
18569
18594
  return new Promise((resolve28) => setPermission({
18570
18595
  call,
@@ -20003,14 +20028,17 @@ function permissionPosture(config) {
20003
20028
  }
20004
20029
  async function runInteractiveTui(options) {
20005
20030
  await reloadUserThemes();
20031
+ const terminalAccessibility = resolveTerminalAccessibility();
20006
20032
  const instance = render2(/* @__PURE__ */ jsx3(SkeinApp, { ...options }), {
20007
20033
  exitOnCtrlC: false,
20008
20034
  patchConsole: true,
20009
- incrementalRendering: true,
20035
+ incrementalRendering: terminalAccessibility.incrementalRendering,
20036
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
20010
20037
  maxFps: 30,
20011
20038
  kittyKeyboard: resolveKittyKeyboardConfig()
20012
20039
  });
20013
20040
  await instance.waitUntilExit();
20041
+ if (terminalAccessibility.screenReader) process.stdout.write("\n");
20014
20042
  }
20015
20043
  function initialTimeline(session, banner, setupProblem) {
20016
20044
  const items = session.messages.filter((message2) => (message2.role === "user" || message2.role === "assistant") && visibleMessage(message2)).slice(-20).map((message2) => ({ id: message2.id, kind: message2.role, text: message2.content }));
@@ -20200,12 +20228,9 @@ function toolDetail2(call) {
20200
20228
  return keys.slice(0, 3).join(", ");
20201
20229
  }
20202
20230
  function spinnerFrames() {
20203
- const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
20231
+ const ascii = resolveTerminalAccessibility().ascii;
20204
20232
  return ascii ? [".", "o", "O", "o"] : ["\u25CC", "\u25CD", "\u25CE", "\u25C9", "\u25CE", "\u25CD"];
20205
20233
  }
20206
- function reducedMotion() {
20207
- return process.env.SKEIN_REDUCE_MOTION === "1" || process.env.SKEIN_REDUCE_MOTION === "true" || process.env.INK_SCREEN_READER === "true" || process.env.TERM === "dumb";
20208
- }
20209
20234
 
20210
20235
  // src/ui/workspace-preparation.tsx
20211
20236
  import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
@@ -20228,10 +20253,11 @@ function WorkspacePreparationView({
20228
20253
  }) {
20229
20254
  const theme = useTheme();
20230
20255
  const safeWidth2 = Math.max(1, Math.floor(width));
20231
- const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
20232
20256
  const compact3 = safeWidth2 < 48;
20233
20257
  const constrained = height < 14;
20234
- const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
20258
+ const horizontalPadding = safeWidth2 >= 24 && !constrained ? 2 : 0;
20259
+ const innerWidth = Math.max(1, safeWidth2 - horizontalPadding * 2);
20260
+ const ascii = resolveTerminalAccessibility().ascii;
20235
20261
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
20236
20262
  const separator = ascii ? "|" : "\xB7";
20237
20263
  const brand = ascii ? "*" : PRODUCT_MARK;
@@ -20241,7 +20267,7 @@ function WorkspacePreparationView({
20241
20267
  const modelLine = `model ${sanitizeTerminalText(model)}`;
20242
20268
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
20243
20269
  const steps = preparationSteps(phase, progress, readiness, error, { ascii, spinner });
20244
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: safeWidth2 >= 24 ? 2 : 0, children: [
20270
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: horizontalPadding, children: [
20245
20271
  /* @__PURE__ */ jsxs4(Box3, { children: [
20246
20272
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: theme.accent, children: [
20247
20273
  brand,
@@ -20259,11 +20285,11 @@ function WorkspacePreparationView({
20259
20285
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
20260
20286
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
20261
20287
  ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
20262
- /* @__PURE__ */ jsx4(Box3, { marginTop: 1, flexDirection: constrained ? "row" : "column", children: steps.map((step2) => constrained ? /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.dim, children: [
20288
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, flexDirection: constrained ? "row" : "column", children: steps.map((step2, index) => constrained ? /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.dim, children: [
20289
+ index ? " " : "",
20263
20290
  step2.glyph,
20264
20291
  " ",
20265
- step2.id === "persist" ? "save" : step2.id,
20266
- step2.id === "verify" ? "" : " "
20292
+ step2.id === "persist" ? "save" : step2.id
20267
20293
  ] }, step2.id) : /* @__PURE__ */ jsxs4(Box3, { height: 1, overflowY: "hidden", children: [
20268
20294
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.border, children: [
20269
20295
  step2.glyph,
@@ -20275,7 +20301,7 @@ function WorkspacePreparationView({
20275
20301
  truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact3 ? 12 : 14)))
20276
20302
  ] })
20277
20303
  ] }, step2.id)) }),
20278
- /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
20304
+ /* @__PURE__ */ jsxs4(Box3, { marginTop: constrained ? 0 : 1, children: [
20279
20305
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: error ? theme.error : readiness ? theme.success : theme.accent, children: [
20280
20306
  readiness ? ascii ? "[ok]" : "\u2713" : error ? ascii ? "[x]" : "\xD7" : spinner,
20281
20307
  " "
@@ -20384,7 +20410,8 @@ function WorkspacePreparationApp({
20384
20410
  }
20385
20411
  async function runWorkspacePreparation(engine, config, options = {}) {
20386
20412
  let result;
20387
- const colorEnabled = config.ui.color && !process.env.NO_COLOR;
20413
+ const terminalAccessibility = resolveTerminalAccessibility();
20414
+ const colorEnabled = config.ui.color && terminalAccessibility.color;
20388
20415
  const theme = resolveThemeWithColor(config.ui.theme, colorEnabled);
20389
20416
  const instance = render3(
20390
20417
  /* @__PURE__ */ jsx4(ThemeProvider, { theme, children: /* @__PURE__ */ jsx4(
@@ -20406,11 +20433,13 @@ async function runWorkspacePreparation(engine, config, options = {}) {
20406
20433
  ...options.stderr ? { stderr: options.stderr } : {},
20407
20434
  exitOnCtrlC: false,
20408
20435
  patchConsole: false,
20409
- incrementalRendering: true,
20436
+ incrementalRendering: terminalAccessibility.incrementalRendering,
20437
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
20410
20438
  kittyKeyboard: resolveKittyKeyboardConfig()
20411
20439
  }
20412
20440
  );
20413
20441
  await instance.waitUntilExit();
20442
+ if (terminalAccessibility.screenReader) (options.stdout ?? process.stdout).write("\n");
20414
20443
  return result ?? { status: "cancelled" };
20415
20444
  }
20416
20445
  function preparationLabel(phase, progress, readiness, compact3 = false) {
@@ -20757,7 +20786,7 @@ function isLoopbackHostname(hostname) {
20757
20786
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(normalized);
20758
20787
  }
20759
20788
  function OnboardingApp({ initialConfig, saveConfig, onFinish }) {
20760
- const colorEnabled = initialConfig.ui.color && !process.env.NO_COLOR;
20789
+ const colorEnabled = initialConfig.ui.color && resolveTerminalAccessibility().color;
20761
20790
  const theme = useMemo2(() => resolveThemeWithColor(initialConfig.ui.theme, colorEnabled), [colorEnabled, initialConfig.ui.theme]);
20762
20791
  return /* @__PURE__ */ jsx5(ThemeProvider, { theme, children: /* @__PURE__ */ jsx5(OnboardingFlow, { initialConfig, saveConfig, onFinish }) });
20763
20792
  }
@@ -20980,6 +21009,7 @@ function providerLabel(provider) {
20980
21009
  }
20981
21010
  async function runFirstRunOnboarding(initialConfig, options = {}) {
20982
21011
  let result;
21012
+ const terminalAccessibility = resolveTerminalAccessibility();
20983
21013
  const instance = render4(
20984
21014
  /* @__PURE__ */ jsx5(
20985
21015
  OnboardingApp,
@@ -20997,11 +21027,13 @@ async function runFirstRunOnboarding(initialConfig, options = {}) {
20997
21027
  ...options.stderr ? { stderr: options.stderr } : {},
20998
21028
  exitOnCtrlC: false,
20999
21029
  patchConsole: false,
21000
- incrementalRendering: true,
21030
+ incrementalRendering: terminalAccessibility.incrementalRendering,
21031
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
21001
21032
  kittyKeyboard: resolveKittyKeyboardConfig()
21002
21033
  }
21003
21034
  );
21004
21035
  await instance.waitUntilExit();
21036
+ if (terminalAccessibility.screenReader) (options.stdout ?? process.stdout).write("\n");
21005
21037
  return result ?? { status: "cancelled" };
21006
21038
  }
21007
21039