@skein-code/cli 0.2.1 → 0.2.3

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/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.tsx
4
4
  import { createInterface as createInterface2 } from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
- import { writeFile as writeFile2 } from "node:fs/promises";
6
+ import { writeFile as writeFile3 } from "node:fs/promises";
7
7
  import { basename as basename12, resolve as resolve22 } from "node:path";
8
8
  import { Command, Option } from "commander";
9
9
  import chalk3 from "chalk";
@@ -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.2.1",
223
+ version: "0.2.3",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -383,13 +383,15 @@ async function resolveProjectNamespace(workspace) {
383
383
  const { workspace: root, canonical, legacy } = projectNamespacePaths(workspace);
384
384
  await assertNamespacePathsSeparated(legacy, canonical);
385
385
  const [canonicalExists, legacyExists] = await Promise.all([isDirectory(canonical), isDirectory(legacy)]);
386
- const activeKind = canonicalExists ? "canonical" : legacyExists ? "legacy" : "legacy";
386
+ const phase = legacyCompatibilityStatus().phase;
387
+ const activeKind = activeProjectNamespaceKind(canonicalExists, legacyExists, phase);
387
388
  return {
388
389
  workspace: root,
389
390
  canonical,
390
391
  legacy,
391
392
  active: activeKind === "canonical" ? canonical : legacy,
392
393
  activeKind,
394
+ phase,
393
395
  canonicalExists,
394
396
  legacyExists,
395
397
  conflict: canonicalExists && legacyExists
@@ -399,18 +401,25 @@ function resolveProjectNamespaceSync(workspace) {
399
401
  const { workspace: root, canonical, legacy } = projectNamespacePaths(workspace);
400
402
  const canonicalExists = isDirectorySync(canonical);
401
403
  const legacyExists = isDirectorySync(legacy);
402
- const activeKind = canonicalExists ? "canonical" : "legacy";
404
+ const phase = legacyCompatibilityStatus().phase;
405
+ const activeKind = activeProjectNamespaceKind(canonicalExists, legacyExists, phase);
403
406
  return {
404
407
  workspace: root,
405
408
  canonical,
406
409
  legacy,
407
410
  active: activeKind === "canonical" ? canonical : legacy,
408
411
  activeKind,
412
+ phase,
409
413
  canonicalExists,
410
414
  legacyExists,
411
415
  conflict: canonicalExists && legacyExists
412
416
  };
413
417
  }
418
+ function activeProjectNamespaceKind(canonicalExists, legacyExists, phase) {
419
+ if (canonicalExists) return "canonical";
420
+ if (legacyExists) return "legacy";
421
+ return phase === "active" ? "legacy" : "canonical";
422
+ }
414
423
  function resolveHomeNamespace(environment = process.env) {
415
424
  const paths = homeNamespacePaths(environment);
416
425
  if (environment.SKEIN_HOME?.trim()) return paths.canonical;
@@ -421,12 +430,14 @@ async function resolveHomeStorageNamespace(environment = process.env) {
421
430
  await assertNamespacePathsSeparated(legacy, canonical);
422
431
  const [canonicalExists, legacyExists] = await Promise.all([isDirectory(canonical), isDirectory(legacy)]);
423
432
  const activeKind = canonicalExists ? "canonical" : "legacy";
433
+ const phase = legacyCompatibilityStatus({ environment }).phase;
424
434
  return {
425
435
  workspace: root,
426
436
  canonical,
427
437
  legacy,
428
438
  active: activeKind === "canonical" ? canonical : legacy,
429
439
  activeKind,
440
+ phase,
430
441
  canonicalExists,
431
442
  legacyExists,
432
443
  conflict: canonicalExists && legacyExists
@@ -9752,10 +9763,11 @@ async function runDoctor(config, options = {}) {
9752
9763
  let legacyCompatibility;
9753
9764
  try {
9754
9765
  namespace = await inspectProjectNamespace(root);
9766
+ const activeNamespace = resolveProjectNamespaceSync(root);
9755
9767
  checks.push({
9756
9768
  name: "Storage namespace",
9757
9769
  ok: namespace.status !== "conflict",
9758
- detail: namespace.status === "ready" ? `legacy .mosaic detected; migrate to ${namespace.destination}` : namespace.status === "conflict" ? `conflict in ${namespace.conflicts.length} path(s); migration paused` : !namespace.sourceExists && !namespace.destinationExists ? `no durable state yet; first write uses ${namespace.source}` : `canonical .skein namespace active at ${namespace.destination}`,
9770
+ detail: namespace.status === "ready" ? `legacy .mosaic detected; migrate to ${namespace.destination}` : namespace.status === "conflict" ? `conflict in ${namespace.conflicts.length} path(s); migration paused` : !namespace.sourceExists && !namespace.destinationExists ? `no durable state yet; first write uses ${activeNamespace.active}` : `canonical .skein namespace active at ${namespace.destination}`,
9759
9771
  required: false
9760
9772
  });
9761
9773
  } catch (error) {
@@ -11123,6 +11135,9 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11123
11135
  if (item.kind === "banner") {
11124
11136
  return /* @__PURE__ */ jsx(Banner, { model: item.model, engine: item.engine, workspace: item.workspace, version: item.version, width, glyphs }, item.id);
11125
11137
  }
11138
+ if (item.kind === "update") {
11139
+ return /* @__PURE__ */ jsx(UpdateNotice, { current: item.current, latest: item.latest, command: item.command, width, glyphs }, item.id);
11140
+ }
11126
11141
  const color = item.tone === "error" ? theme.error : item.tone === "success" ? theme.success : theme.muted;
11127
11142
  const noticeGlyph = item.tone === "error" ? glyphs.error : item.tone === "success" ? glyphs.success : glyphs.info;
11128
11143
  return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color, wrap: "wrap", children: `${noticeGlyph} ${sanitizeTerminalText(item.text)}` }) }, item.id);
@@ -11696,6 +11711,20 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
11696
11711
  /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`type a request ${glyphs.separator} /help for commands ${glyphs.separator} @ to attach files`, innerWidth) })
11697
11712
  ] });
11698
11713
  }
11714
+ function UpdateNotice({ current, latest, command: command2, width, glyphs }) {
11715
+ const theme = useTheme();
11716
+ const parts = [
11717
+ { text: glyphs.up, color: theme.accent, bold: true },
11718
+ { text: " a new version is available ", color: theme.text, bold: false },
11719
+ { text: `v${current}`, color: theme.dim, bold: false },
11720
+ { text: ` ${glyphs.arrow} `, color: theme.muted, bold: false },
11721
+ { text: `v${latest}`, color: theme.success, bold: true },
11722
+ { text: ` ${command2}`, color: theme.dim, bold: false }
11723
+ ];
11724
+ const rendered = truncateDisplay(parts.map((part) => part.text).join(""), safeWidth(width));
11725
+ const truncated = rendered.length < parts.map((part) => part.text).join("").length;
11726
+ return /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: truncated ? /* @__PURE__ */ jsx(Text, { color: theme.muted, children: rendered }) : parts.map((part, index) => /* @__PURE__ */ jsx(Text, { color: part.color, bold: part.bold, children: part.text }, index)) });
11727
+ }
11699
11728
  function RichText({ value, glyphs }) {
11700
11729
  const theme = useTheme();
11701
11730
  let inCode = false;
@@ -11788,6 +11817,141 @@ function safeWidth(width) {
11788
11817
  return Math.max(1, Math.floor(Number.isFinite(width) ? width : 80));
11789
11818
  }
11790
11819
 
11820
+ // src/utils/update-check.ts
11821
+ import { mkdir as mkdir9, readFile as readFile15, writeFile as writeFile2 } from "node:fs/promises";
11822
+ import { join as join16 } from "node:path";
11823
+ var PACKAGE_NAME = "@skein-code/cli";
11824
+ var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
11825
+ var CACHE_FILE = "update-check.json";
11826
+ var CHECK_INTERVAL_MS = 1e3 * 60 * 60 * 24;
11827
+ var FETCH_TIMEOUT_MS = 3e3;
11828
+ var MAX_BODY_BYTES = 1e6;
11829
+ function truthyEnv(value) {
11830
+ return value !== void 0 && value !== "" && value !== "false" && value !== "0";
11831
+ }
11832
+ function isCi(env) {
11833
+ return truthyEnv(env.CI) || truthyEnv(env.CONTINUOUS_INTEGRATION) || truthyEnv(env.GITHUB_ACTIONS) || truthyEnv(env.GITLAB_CI) || truthyEnv(env.BUILD_NUMBER);
11834
+ }
11835
+ function isUpdateCheckDisabled(env = process.env) {
11836
+ if (truthyEnv(env.SKEIN_NO_UPDATE_CHECK) || truthyEnv(env.MOSAIC_NO_UPDATE_CHECK) || truthyEnv(env.NO_UPDATE_NOTIFIER)) {
11837
+ return true;
11838
+ }
11839
+ if (env.NODE_ENV === "test") return true;
11840
+ return isCi(env);
11841
+ }
11842
+ function parseVersion(value) {
11843
+ const cleaned = value.trim().replace(/^v/iu, "");
11844
+ const buildAt = cleaned.indexOf("+");
11845
+ const noBuild = buildAt === -1 ? cleaned : cleaned.slice(0, buildAt);
11846
+ const dashAt = noBuild.indexOf("-");
11847
+ const mainStr = dashAt === -1 ? noBuild : noBuild.slice(0, dashAt);
11848
+ const preStr = dashAt === -1 ? "" : noBuild.slice(dashAt + 1);
11849
+ const parts = mainStr.split(".");
11850
+ if (parts.length !== 3) return null;
11851
+ const [major, minor, patch] = parts.map((part) => Number(part));
11852
+ if ([major, minor, patch].some((n) => n === void 0 || !Number.isInteger(n) || n < 0)) return null;
11853
+ return { main: [major, minor, patch], pre: preStr ? preStr.split(".") : [] };
11854
+ }
11855
+ function compareVersions(a, b) {
11856
+ const pa = parseVersion(a);
11857
+ const pb = parseVersion(b);
11858
+ if (!pa || !pb) return a === b ? 0 : a < b ? -1 : 1;
11859
+ for (let i = 0; i < 3; i++) {
11860
+ const x = pa.main[i];
11861
+ const y = pb.main[i];
11862
+ if (x !== y) return x < y ? -1 : 1;
11863
+ }
11864
+ if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
11865
+ if (pa.pre.length === 0) return 1;
11866
+ if (pb.pre.length === 0) return -1;
11867
+ const len = Math.max(pa.pre.length, pb.pre.length);
11868
+ for (let i = 0; i < len; i++) {
11869
+ const x = pa.pre[i];
11870
+ const y = pb.pre[i];
11871
+ if (x === void 0) return -1;
11872
+ if (y === void 0) return 1;
11873
+ const xNum = /^\d+$/u.test(x);
11874
+ const yNum = /^\d+$/u.test(y);
11875
+ if (xNum && yNum) {
11876
+ const nx = Number(x);
11877
+ const ny = Number(y);
11878
+ if (nx !== ny) return nx < ny ? -1 : 1;
11879
+ } else if (xNum !== yNum) {
11880
+ return xNum ? -1 : 1;
11881
+ } else if (x !== y) {
11882
+ return x < y ? -1 : 1;
11883
+ }
11884
+ }
11885
+ return 0;
11886
+ }
11887
+ function updateCachePath(env = process.env) {
11888
+ return join16(resolveHomeNamespace(env), CACHE_FILE);
11889
+ }
11890
+ function upgradeCommand() {
11891
+ return `npm i -g ${PACKAGE_NAME}`;
11892
+ }
11893
+ function updateNoticeText(notice) {
11894
+ return `Update available ${notice.current} \u2192 ${notice.latest} \xB7 run ${notice.command}`;
11895
+ }
11896
+ function noticeIfNewer(latest, current) {
11897
+ if (latest && compareVersions(latest, current) > 0) {
11898
+ return { current, latest, command: upgradeCommand() };
11899
+ }
11900
+ return void 0;
11901
+ }
11902
+ async function readUpdateCache(env = process.env) {
11903
+ try {
11904
+ const raw = await readFile15(updateCachePath(env), "utf8");
11905
+ const parsed = JSON.parse(raw);
11906
+ if (typeof parsed?.checkedAt === "number" && (typeof parsed.latest === "string" || parsed.latest === null)) {
11907
+ return { checkedAt: parsed.checkedAt, latest: parsed.latest };
11908
+ }
11909
+ } catch {
11910
+ }
11911
+ return null;
11912
+ }
11913
+ async function writeUpdateCache(cache, env) {
11914
+ try {
11915
+ const dir = resolveHomeNamespace(env);
11916
+ await mkdir9(dir, { recursive: true, mode: 448 });
11917
+ await writeFile2(updateCachePath(env), JSON.stringify(cache), "utf8");
11918
+ } catch {
11919
+ }
11920
+ }
11921
+ async function fetchLatestVersion(fetchImpl, url = REGISTRY_URL) {
11922
+ try {
11923
+ const response = await fetchImpl(url, {
11924
+ headers: { accept: "application/json" },
11925
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
11926
+ });
11927
+ if (!response.ok) return null;
11928
+ const body = await response.text();
11929
+ if (body.length > MAX_BODY_BYTES) return null;
11930
+ const parsed = JSON.parse(body);
11931
+ return typeof parsed.version === "string" ? parsed.version : null;
11932
+ } catch {
11933
+ return null;
11934
+ }
11935
+ }
11936
+ async function resolveCachedUpdateNotice(currentVersion, env = process.env) {
11937
+ if (isUpdateCheckDisabled(env)) return void 0;
11938
+ const cache = await readUpdateCache(env);
11939
+ return noticeIfNewer(cache?.latest ?? null, currentVersion);
11940
+ }
11941
+ async function refreshUpdateCache(currentVersion, options = {}) {
11942
+ const env = options.env ?? process.env;
11943
+ if (isUpdateCheckDisabled(env)) return void 0;
11944
+ const cache = await readUpdateCache(env);
11945
+ const now = Date.now();
11946
+ if (!options.force && cache && now - cache.checkedAt < CHECK_INTERVAL_MS) {
11947
+ return noticeIfNewer(cache.latest, currentVersion);
11948
+ }
11949
+ const latest = await fetchLatestVersion(options.fetchImpl ?? fetch);
11950
+ const effectiveLatest = latest ?? cache?.latest ?? null;
11951
+ await writeUpdateCache({ checkedAt: now, latest: effectiveLatest }, env);
11952
+ return noticeIfNewer(effectiveLatest, currentVersion);
11953
+ }
11954
+
11791
11955
  // src/ui/composer.tsx
11792
11956
  import { useEffect, useRef, useState } from "react";
11793
11957
  import { Text as Text2, useInput, usePaste } from "ink";
@@ -12710,6 +12874,29 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
12710
12874
  useEffect2(() => {
12711
12875
  setSuggestionIndex(0);
12712
12876
  }, [input2]);
12877
+ useEffect2(() => {
12878
+ let cancelled = false;
12879
+ const showNotice = (notice) => {
12880
+ if (cancelled || !notice) return;
12881
+ setTimeline((items) => {
12882
+ const bannerIndex = items.findIndex((item) => item.kind === "banner");
12883
+ if (bannerIndex === -1) return items;
12884
+ const existing = items.find((item) => item.kind === "update");
12885
+ if (existing) {
12886
+ if (existing.kind === "update" && existing.latest === notice.latest) return items;
12887
+ return items.map((item) => item === existing ? { id: item.id, kind: "update", current: notice.current, latest: notice.latest, command: notice.command } : item);
12888
+ }
12889
+ const next = items.slice();
12890
+ next.splice(bannerIndex + 1, 0, { id: nextId(), kind: "update", current: notice.current, latest: notice.latest, command: notice.command });
12891
+ return next;
12892
+ });
12893
+ };
12894
+ void resolveCachedUpdateNotice(package_default.version).then(showNotice);
12895
+ void refreshUpdateCache(package_default.version).then(showNotice);
12896
+ return () => {
12897
+ cancelled = true;
12898
+ };
12899
+ }, []);
12713
12900
  useEffect2(() => {
12714
12901
  if (suggestionMode !== "mention" || !mentionToken) {
12715
12902
  mentionRequest.current += 1;
@@ -14901,9 +15088,9 @@ function scopeKey(scope, context) {
14901
15088
  }
14902
15089
 
14903
15090
  // src/skills/catalog.ts
14904
- import { lstat as lstat19, readFile as readFile15, readdir as readdir7, realpath as realpath8 } from "node:fs/promises";
15091
+ import { lstat as lstat19, readFile as readFile16, readdir as readdir7, realpath as realpath8 } from "node:fs/promises";
14905
15092
  import { homedir as homedir4 } from "node:os";
14906
- import { basename as basename11, join as join16, resolve as resolve20 } from "node:path";
15093
+ import { basename as basename11, join as join17, resolve as resolve20 } from "node:path";
14907
15094
  import { parse as parseYaml3 } from "yaml";
14908
15095
  var SkillCatalog = class {
14909
15096
  constructor(workspace, config) {
@@ -14923,7 +15110,7 @@ var SkillCatalog = class {
14923
15110
  for (const location of locations) {
14924
15111
  const entries = await safeDirectories(location.path);
14925
15112
  for (const entry of entries) {
14926
- const skillPath = join16(location.path, entry, "SKILL.md");
15113
+ const skillPath = join17(location.path, entry, "SKILL.md");
14927
15114
  const metadata = await readMetadata(skillPath);
14928
15115
  if (!metadata) continue;
14929
15116
  const descriptor = {
@@ -14974,10 +15161,10 @@ function discoveryLocations(workspace, configured) {
14974
15161
  const home = homedir4();
14975
15162
  const workspaceRoot = resolve20(workspace);
14976
15163
  return [
14977
- { path: join16(home, ".agents", "skills"), scope: "user", trusted: true },
14978
- { path: join16(home, ".claude", "skills"), scope: "user", trusted: true },
14979
- { path: join16(home, ".augment", "skills"), scope: "user", trusted: true },
14980
- { path: join16(resolveHomeNamespace(), "skills"), scope: "user", trusted: true },
15164
+ { path: join17(home, ".agents", "skills"), scope: "user", trusted: true },
15165
+ { path: join17(home, ".claude", "skills"), scope: "user", trusted: true },
15166
+ { path: join17(home, ".augment", "skills"), scope: "user", trusted: true },
15167
+ { path: join17(resolveHomeNamespace(), "skills"), scope: "user", trusted: true },
14981
15168
  ...configured.map((path) => {
14982
15169
  const resolved = resolve20(workspaceRoot, path);
14983
15170
  return {
@@ -14986,10 +15173,10 @@ function discoveryLocations(workspace, configured) {
14986
15173
  trusted: !isInside(workspaceRoot, resolved)
14987
15174
  };
14988
15175
  }),
14989
- { path: join16(workspace, ".agents", "skills"), scope: "workspace", trusted: false },
14990
- { path: join16(workspace, ".claude", "skills"), scope: "workspace", trusted: false },
14991
- { path: join16(workspace, ".augment", "skills"), scope: "workspace", trusted: false },
14992
- { path: join16(resolveProjectNamespaceSync(workspaceRoot).active, "skills"), scope: "workspace", trusted: false }
15176
+ { path: join17(workspace, ".agents", "skills"), scope: "workspace", trusted: false },
15177
+ { path: join17(workspace, ".claude", "skills"), scope: "workspace", trusted: false },
15178
+ { path: join17(workspace, ".augment", "skills"), scope: "workspace", trusted: false },
15179
+ { path: join17(resolveProjectNamespaceSync(workspaceRoot).active, "skills"), scope: "workspace", trusted: false }
14993
15180
  ];
14994
15181
  }
14995
15182
  async function safeDirectories(path) {
@@ -15033,7 +15220,7 @@ async function safeRead(path, maxBytes) {
15033
15220
  const resolvedParent = await realpath8(parent);
15034
15221
  const resolvedPath = await realpath8(path);
15035
15222
  if (!resolvedPath.startsWith(`${resolvedParent}/`)) return void 0;
15036
- return await readFile15(path, "utf8");
15223
+ return await readFile16(path, "utf8");
15037
15224
  } catch {
15038
15225
  return void 0;
15039
15226
  }
@@ -15462,7 +15649,14 @@ program.command("status").description("Show model, context, workspace, and index
15462
15649
  const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
15463
15650
  const engine = new ContextEngine(config);
15464
15651
  const status = await engine.status();
15465
- printObject({ config: configSummary(config), context: status }, options.json === true);
15652
+ const namespace = resolveProjectNamespaceSync(config.workspaceRoots[0] ?? process.cwd());
15653
+ const update = await refreshUpdateCache(package_default.version).catch(() => void 0);
15654
+ if (options.json === true) {
15655
+ const updateJson = update ? { current: update.current, latest: update.latest, command: update.command } : { current: package_default.version, latest: null, command: upgradeCommand() };
15656
+ printObject({ config: configSummary(config), context: status, namespace, update: updateJson }, true);
15657
+ } else {
15658
+ printStatusSummary(config, status, namespace, update);
15659
+ }
15466
15660
  });
15467
15661
  program.command("doctor").description("Diagnose prerequisites and safe fallbacks").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").option("--visual", "inspect terminal rendering, glyphs, and keyboard support").action(async (options) => {
15468
15662
  const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
@@ -15564,7 +15758,7 @@ sessionCommand.command("export <id>").description("Export a session as Markdown"
15564
15758
  const store = new SessionStore(workspaceOption(options.workspace));
15565
15759
  const session = await requireSessionSelector(store, id);
15566
15760
  const markdown = sessionMarkdown(session);
15567
- if (options.output) await writeFile2(resolve22(options.output), markdown, "utf8");
15761
+ if (options.output) await writeFile3(resolve22(options.output), markdown, "utf8");
15568
15762
  else process.stdout.write(markdown);
15569
15763
  });
15570
15764
  var checkpointCommand = program.command("checkpoint").description("Inspect and restore pre-mutation snapshots");
@@ -16202,6 +16396,40 @@ function printObject(value, json) {
16202
16396
  else process.stdout.write(`${JSON.stringify(value, null, 2)}
16203
16397
  `);
16204
16398
  }
16399
+ function printStatusSummary(config, context, namespace, update) {
16400
+ const glyphs = cliGlyphs;
16401
+ const dim = (text) => chalk3.dim(text);
16402
+ const line = (level, name, detail) => {
16403
+ const icon = level === "ok" ? chalk3.green(glyphs.success) : level === "error" ? chalk3.red(glyphs.error) : chalk3.yellow("!");
16404
+ process.stdout.write(`${icon} ${name.padEnd(16)} ${dim(detail)}
16405
+ `);
16406
+ };
16407
+ const keyReady = Boolean(config.model.apiKey) || config.model.provider === "compatible";
16408
+ const endpoint = redactEndpoint(config.model.baseUrl);
16409
+ const local = context.local ?? {};
16410
+ const selected = String(context.selected ?? "local");
16411
+ const engineDetail = selected === "contextengine" ? "ContextEngine (external)" : selected === "unindexed" ? `ContextEngine available; index not built ${glyphs.separator} run ${PRODUCT_COMMAND} index` : selected === "unavailable" ? `ContextEngine required but unavailable ${glyphs.separator} run ${PRODUCT_COMMAND} doctor` : "local index";
16412
+ const indexFiles = local.files ?? 0;
16413
+ const indexReady = Boolean(local.available) && indexFiles > 0;
16414
+ const indexDetail = indexReady ? `${indexFiles} files ${glyphs.separator} ${local.chunks ?? 0} chunks` : `not built ${glyphs.separator} run ${PRODUCT_COMMAND} index`;
16415
+ process.stdout.write(`${chalk3.hex("#A78BFA").bold(`${glyphs.brand} ${PRODUCT_NAME.toUpperCase()} STATUS`)}
16416
+
16417
+ `);
16418
+ line("ok", "Model", `${config.model.provider}/${config.model.model}`);
16419
+ line("ok", "Endpoint", endpoint);
16420
+ line(keyReady ? "ok" : "warn", "API key", keyReady ? "configured" : `missing ${glyphs.separator} set it, then run ${PRODUCT_COMMAND} doctor to verify`);
16421
+ line(selected === "unavailable" ? "error" : "ok", "Context engine", engineDetail);
16422
+ line(indexReady ? "ok" : "warn", "Code index", indexDetail);
16423
+ line("ok", "Workspace", config.workspaceRoots.join(` ${glyphs.separator} `));
16424
+ const namespaceName = namespace.activeKind === "canonical" ? ".skein" : ".mosaic";
16425
+ const storageDetail = namespace.activeKind === "canonical" ? `${namespaceName} (canonical)` : namespace.phase === "active" ? `${namespaceName} (legacy; new projects switch to .skein from 0.3.0)` : `${namespaceName} (legacy; run ${PRODUCT_COMMAND} migrate --yes before removal)`;
16426
+ const storageReady = namespace.activeKind === "canonical" || namespace.phase === "active";
16427
+ line(storageReady ? "ok" : "warn", "Storage", storageDetail);
16428
+ line(update ? "warn" : "ok", "Version", update ? updateNoticeText(update) : `v${package_default.version} (up to date)`);
16429
+ process.stdout.write(`
16430
+ ${dim(`Run ${PRODUCT_COMMAND} status --json for the full machine-readable record.`)}
16431
+ `);
16432
+ }
16205
16433
  function printSessionList(sessions) {
16206
16434
  if (!sessions.length) {
16207
16435
  process.stdout.write("No saved sessions.\n");