@skein-code/cli 0.3.28 → 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.28.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.28",
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,10 @@ 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",
239
243
  "MCP trust and revoke confirmation rendering now has a deterministic cross-platform release regression gate",
240
244
  "MCP now follows a no-network search and inspect review before explicit workspace-bound manifest trust and lazy activation",
241
245
  "Declarative capability manifests expose redacted source, version, tools, permissions, network, command, path, sensitive-field, process, and completion-evidence effects",
@@ -319,6 +323,7 @@ var package_default = {
319
323
  "benchmark:context": "tsx scripts/benchmark-local-index.ts",
320
324
  "benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
321
325
  "benchmark:token-economy": "tsx scripts/benchmark-token-economy.ts",
326
+ "benchmark:terminal-ui": "tsx scripts/benchmark-terminal-ui.ts",
322
327
  "verify:package": "node scripts/verify-package.mjs",
323
328
  "release:verify": "npm run check && npm run verify:package --",
324
329
  typecheck: "tsc --noEmit",
@@ -346,6 +351,7 @@ var package_default = {
346
351
  "@types/diff": "^8.0.0",
347
352
  "@types/node": "^22.19.7",
348
353
  "@types/react": "^19.2.7",
354
+ "@xterm/headless": "^6.0.0",
349
355
  tsup: "^8.5.1",
350
356
  tsx: "^4.21.0",
351
357
  vitest: "^4.0.18"
@@ -14770,6 +14776,20 @@ import { constants as constants5 } from "node:fs";
14770
14776
  import chalk from "chalk";
14771
14777
 
14772
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
+ }
14773
14793
  function resolveKittyKeyboardConfig(environment = process.env) {
14774
14794
  const override = environment.SKEIN_KITTY_KEYBOARD?.trim().toLowerCase();
14775
14795
  if (override && ["1", "true", "yes", "on", "enabled"].includes(override)) {
@@ -14791,6 +14811,9 @@ function enabledKittyKeyboard() {
14791
14811
  function disabledKittyKeyboard() {
14792
14812
  return { mode: "disabled", flags: ["disambiguateEscapeCodes"] };
14793
14813
  }
14814
+ function truthy(value) {
14815
+ return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
14816
+ }
14794
14817
 
14795
14818
  // src/cli/glyphs.ts
14796
14819
  var unicodeGlyphs = {
@@ -16219,7 +16242,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
16219
16242
  if (!items.length) {
16220
16243
  return /* @__PURE__ */ jsx(Box, { paddingLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "Start with a request, @file, or /help." }) });
16221
16244
  }
16222
- 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) => {
16223
16246
  if (item.kind === "user") {
16224
16247
  return /* @__PURE__ */ jsxs(Box, { marginBottom: compact3 || item.clipped ? 0 : 1, children: [
16225
16248
  /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: glyphs.prompt }) }),
@@ -16690,7 +16713,7 @@ function PermissionCard({ call, category, reason, width = 80, glyphMode = "auto"
16690
16713
  { text: "[Esc]", color: theme.muted }
16691
16714
  ];
16692
16715
  const marker = glyphs.borderStyle === "classic" ? "!" : "\u258E";
16693
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
16716
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, "aria-role": "radiogroup", children: [
16694
16717
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: title }) }),
16695
16718
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: tool }) }),
16696
16719
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.text, children: summaryLine }) }),
@@ -16753,7 +16776,7 @@ function PromptBar({ busy, value, placeholder, width = 80, mode = "chat", queueC
16753
16776
  ] }),
16754
16777
  /* @__PURE__ */ jsx(Text, { color: theme.text, children: truncateDisplay(safeQueuePreview, queuePreviewWidth) })
16755
16778
  ] }) : null,
16756
- /* @__PURE__ */ jsxs(Box, { children: [
16779
+ /* @__PURE__ */ jsxs(Box, { "aria-role": "textbox", children: [
16757
16780
  /* @__PURE__ */ jsx(Text, { bold: true, color: shell ? theme.warning : theme.accent, children: shell ? "! " : `${glyphs.prompt} ` }),
16758
16781
  children
16759
16782
  ] }),
@@ -18417,13 +18440,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18417
18440
  const terminalHeight = Math.max(1, rows || 24);
18418
18441
  const horizontalPadding = terminalWidth >= 24 ? 1 : 0;
18419
18442
  const contentWidth = Math.max(1, terminalWidth - horizontalPadding * 2);
18420
- 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";
18421
18445
  const glyphs = resolveGlyphs(glyphMode);
18422
18446
  const separator = ` ${glyphs.separator} `;
18423
18447
  const ellipsis = terminalEllipsis();
18424
18448
  const initialSession = runner.getSession();
18425
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;
18426
- const colorEnabled = config.ui.color && !process.env.NO_COLOR;
18450
+ const colorEnabled = config.ui.color && terminalAccessibility.color;
18427
18451
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
18428
18452
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
18429
18453
  const [compact3, setCompact] = useState2(config.ui.compact);
@@ -18559,13 +18583,13 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18559
18583
  setHistorySearch((current) => current ? setHistorySearchQuery(current, input2) : current);
18560
18584
  }, [input2]);
18561
18585
  useEffect2(() => {
18562
- if (!busy || reducedMotion()) {
18586
+ if (!busy || terminalAccessibility.reducedMotion) {
18563
18587
  setFrameIndex(0);
18564
18588
  return void 0;
18565
18589
  }
18566
18590
  const timer = setInterval(() => setFrameIndex((value) => (value + 1) % spinnerFrames().length), 120);
18567
18591
  return () => clearInterval(timer);
18568
- }, [busy]);
18592
+ }, [busy, terminalAccessibility.reducedMotion]);
18569
18593
  const requestPermission = useCallback((call, category, reason) => {
18570
18594
  return new Promise((resolve28) => setPermission({
18571
18595
  call,
@@ -20004,14 +20028,17 @@ function permissionPosture(config) {
20004
20028
  }
20005
20029
  async function runInteractiveTui(options) {
20006
20030
  await reloadUserThemes();
20031
+ const terminalAccessibility = resolveTerminalAccessibility();
20007
20032
  const instance = render2(/* @__PURE__ */ jsx3(SkeinApp, { ...options }), {
20008
20033
  exitOnCtrlC: false,
20009
20034
  patchConsole: true,
20010
- incrementalRendering: true,
20035
+ incrementalRendering: terminalAccessibility.incrementalRendering,
20036
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
20011
20037
  maxFps: 30,
20012
20038
  kittyKeyboard: resolveKittyKeyboardConfig()
20013
20039
  });
20014
20040
  await instance.waitUntilExit();
20041
+ if (terminalAccessibility.screenReader) process.stdout.write("\n");
20015
20042
  }
20016
20043
  function initialTimeline(session, banner, setupProblem) {
20017
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 }));
@@ -20201,12 +20228,9 @@ function toolDetail2(call) {
20201
20228
  return keys.slice(0, 3).join(", ");
20202
20229
  }
20203
20230
  function spinnerFrames() {
20204
- const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
20231
+ const ascii = resolveTerminalAccessibility().ascii;
20205
20232
  return ascii ? [".", "o", "O", "o"] : ["\u25CC", "\u25CD", "\u25CE", "\u25C9", "\u25CE", "\u25CD"];
20206
20233
  }
20207
- function reducedMotion() {
20208
- return process.env.SKEIN_REDUCE_MOTION === "1" || process.env.SKEIN_REDUCE_MOTION === "true" || process.env.INK_SCREEN_READER === "true" || process.env.TERM === "dumb";
20209
- }
20210
20234
 
20211
20235
  // src/ui/workspace-preparation.tsx
20212
20236
  import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
@@ -20229,10 +20253,11 @@ function WorkspacePreparationView({
20229
20253
  }) {
20230
20254
  const theme = useTheme();
20231
20255
  const safeWidth2 = Math.max(1, Math.floor(width));
20232
- const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
20233
20256
  const compact3 = safeWidth2 < 48;
20234
20257
  const constrained = height < 14;
20235
- 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;
20236
20261
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
20237
20262
  const separator = ascii ? "|" : "\xB7";
20238
20263
  const brand = ascii ? "*" : PRODUCT_MARK;
@@ -20242,7 +20267,7 @@ function WorkspacePreparationView({
20242
20267
  const modelLine = `model ${sanitizeTerminalText(model)}`;
20243
20268
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
20244
20269
  const steps = preparationSteps(phase, progress, readiness, error, { ascii, spinner });
20245
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: safeWidth2 >= 24 ? 2 : 0, children: [
20270
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: horizontalPadding, children: [
20246
20271
  /* @__PURE__ */ jsxs4(Box3, { children: [
20247
20272
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: theme.accent, children: [
20248
20273
  brand,
@@ -20260,11 +20285,11 @@ function WorkspacePreparationView({
20260
20285
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
20261
20286
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
20262
20287
  ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
20263
- /* @__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 ? " " : "",
20264
20290
  step2.glyph,
20265
20291
  " ",
20266
- step2.id === "persist" ? "save" : step2.id,
20267
- step2.id === "verify" ? "" : " "
20292
+ step2.id === "persist" ? "save" : step2.id
20268
20293
  ] }, step2.id) : /* @__PURE__ */ jsxs4(Box3, { height: 1, overflowY: "hidden", children: [
20269
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: [
20270
20295
  step2.glyph,
@@ -20276,7 +20301,7 @@ function WorkspacePreparationView({
20276
20301
  truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact3 ? 12 : 14)))
20277
20302
  ] })
20278
20303
  ] }, step2.id)) }),
20279
- /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
20304
+ /* @__PURE__ */ jsxs4(Box3, { marginTop: constrained ? 0 : 1, children: [
20280
20305
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: error ? theme.error : readiness ? theme.success : theme.accent, children: [
20281
20306
  readiness ? ascii ? "[ok]" : "\u2713" : error ? ascii ? "[x]" : "\xD7" : spinner,
20282
20307
  " "
@@ -20385,7 +20410,8 @@ function WorkspacePreparationApp({
20385
20410
  }
20386
20411
  async function runWorkspacePreparation(engine, config, options = {}) {
20387
20412
  let result;
20388
- const colorEnabled = config.ui.color && !process.env.NO_COLOR;
20413
+ const terminalAccessibility = resolveTerminalAccessibility();
20414
+ const colorEnabled = config.ui.color && terminalAccessibility.color;
20389
20415
  const theme = resolveThemeWithColor(config.ui.theme, colorEnabled);
20390
20416
  const instance = render3(
20391
20417
  /* @__PURE__ */ jsx4(ThemeProvider, { theme, children: /* @__PURE__ */ jsx4(
@@ -20407,11 +20433,13 @@ async function runWorkspacePreparation(engine, config, options = {}) {
20407
20433
  ...options.stderr ? { stderr: options.stderr } : {},
20408
20434
  exitOnCtrlC: false,
20409
20435
  patchConsole: false,
20410
- incrementalRendering: true,
20436
+ incrementalRendering: terminalAccessibility.incrementalRendering,
20437
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
20411
20438
  kittyKeyboard: resolveKittyKeyboardConfig()
20412
20439
  }
20413
20440
  );
20414
20441
  await instance.waitUntilExit();
20442
+ if (terminalAccessibility.screenReader) (options.stdout ?? process.stdout).write("\n");
20415
20443
  return result ?? { status: "cancelled" };
20416
20444
  }
20417
20445
  function preparationLabel(phase, progress, readiness, compact3 = false) {
@@ -20758,7 +20786,7 @@ function isLoopbackHostname(hostname) {
20758
20786
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(normalized);
20759
20787
  }
20760
20788
  function OnboardingApp({ initialConfig, saveConfig, onFinish }) {
20761
- const colorEnabled = initialConfig.ui.color && !process.env.NO_COLOR;
20789
+ const colorEnabled = initialConfig.ui.color && resolveTerminalAccessibility().color;
20762
20790
  const theme = useMemo2(() => resolveThemeWithColor(initialConfig.ui.theme, colorEnabled), [colorEnabled, initialConfig.ui.theme]);
20763
20791
  return /* @__PURE__ */ jsx5(ThemeProvider, { theme, children: /* @__PURE__ */ jsx5(OnboardingFlow, { initialConfig, saveConfig, onFinish }) });
20764
20792
  }
@@ -20981,6 +21009,7 @@ function providerLabel(provider) {
20981
21009
  }
20982
21010
  async function runFirstRunOnboarding(initialConfig, options = {}) {
20983
21011
  let result;
21012
+ const terminalAccessibility = resolveTerminalAccessibility();
20984
21013
  const instance = render4(
20985
21014
  /* @__PURE__ */ jsx5(
20986
21015
  OnboardingApp,
@@ -20998,11 +21027,13 @@ async function runFirstRunOnboarding(initialConfig, options = {}) {
20998
21027
  ...options.stderr ? { stderr: options.stderr } : {},
20999
21028
  exitOnCtrlC: false,
21000
21029
  patchConsole: false,
21001
- incrementalRendering: true,
21030
+ incrementalRendering: terminalAccessibility.incrementalRendering,
21031
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
21002
21032
  kittyKeyboard: resolveKittyKeyboardConfig()
21003
21033
  }
21004
21034
  );
21005
21035
  await instance.waitUntilExit();
21036
+ if (terminalAccessibility.screenReader) (options.stdout ?? process.stdout).write("\n");
21006
21037
  return result ?? { status: "cancelled" };
21007
21038
  }
21008
21039