@skein-code/cli 0.2.2 → 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.2",
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",
@@ -11135,6 +11135,9 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
11135
11135
  if (item.kind === "banner") {
11136
11136
  return /* @__PURE__ */ jsx(Banner, { model: item.model, engine: item.engine, workspace: item.workspace, version: item.version, width, glyphs }, item.id);
11137
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
+ }
11138
11141
  const color = item.tone === "error" ? theme.error : item.tone === "success" ? theme.success : theme.muted;
11139
11142
  const noticeGlyph = item.tone === "error" ? glyphs.error : item.tone === "success" ? glyphs.success : glyphs.info;
11140
11143
  return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color, wrap: "wrap", children: `${noticeGlyph} ${sanitizeTerminalText(item.text)}` }) }, item.id);
@@ -11708,6 +11711,20 @@ function Banner({ model, engine, workspace, version, width, glyphs }) {
11708
11711
  /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncateDisplay(`type a request ${glyphs.separator} /help for commands ${glyphs.separator} @ to attach files`, innerWidth) })
11709
11712
  ] });
11710
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
+ }
11711
11728
  function RichText({ value, glyphs }) {
11712
11729
  const theme = useTheme();
11713
11730
  let inCode = false;
@@ -11800,6 +11817,141 @@ function safeWidth(width) {
11800
11817
  return Math.max(1, Math.floor(Number.isFinite(width) ? width : 80));
11801
11818
  }
11802
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
+
11803
11955
  // src/ui/composer.tsx
11804
11956
  import { useEffect, useRef, useState } from "react";
11805
11957
  import { Text as Text2, useInput, usePaste } from "ink";
@@ -12722,6 +12874,29 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
12722
12874
  useEffect2(() => {
12723
12875
  setSuggestionIndex(0);
12724
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
+ }, []);
12725
12900
  useEffect2(() => {
12726
12901
  if (suggestionMode !== "mention" || !mentionToken) {
12727
12902
  mentionRequest.current += 1;
@@ -14913,9 +15088,9 @@ function scopeKey(scope, context) {
14913
15088
  }
14914
15089
 
14915
15090
  // src/skills/catalog.ts
14916
- 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";
14917
15092
  import { homedir as homedir4 } from "node:os";
14918
- 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";
14919
15094
  import { parse as parseYaml3 } from "yaml";
14920
15095
  var SkillCatalog = class {
14921
15096
  constructor(workspace, config) {
@@ -14935,7 +15110,7 @@ var SkillCatalog = class {
14935
15110
  for (const location of locations) {
14936
15111
  const entries = await safeDirectories(location.path);
14937
15112
  for (const entry of entries) {
14938
- const skillPath = join16(location.path, entry, "SKILL.md");
15113
+ const skillPath = join17(location.path, entry, "SKILL.md");
14939
15114
  const metadata = await readMetadata(skillPath);
14940
15115
  if (!metadata) continue;
14941
15116
  const descriptor = {
@@ -14986,10 +15161,10 @@ function discoveryLocations(workspace, configured) {
14986
15161
  const home = homedir4();
14987
15162
  const workspaceRoot = resolve20(workspace);
14988
15163
  return [
14989
- { path: join16(home, ".agents", "skills"), scope: "user", trusted: true },
14990
- { path: join16(home, ".claude", "skills"), scope: "user", trusted: true },
14991
- { path: join16(home, ".augment", "skills"), scope: "user", trusted: true },
14992
- { 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 },
14993
15168
  ...configured.map((path) => {
14994
15169
  const resolved = resolve20(workspaceRoot, path);
14995
15170
  return {
@@ -14998,10 +15173,10 @@ function discoveryLocations(workspace, configured) {
14998
15173
  trusted: !isInside(workspaceRoot, resolved)
14999
15174
  };
15000
15175
  }),
15001
- { path: join16(workspace, ".agents", "skills"), scope: "workspace", trusted: false },
15002
- { path: join16(workspace, ".claude", "skills"), scope: "workspace", trusted: false },
15003
- { path: join16(workspace, ".augment", "skills"), scope: "workspace", trusted: false },
15004
- { 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 }
15005
15180
  ];
15006
15181
  }
15007
15182
  async function safeDirectories(path) {
@@ -15045,7 +15220,7 @@ async function safeRead(path, maxBytes) {
15045
15220
  const resolvedParent = await realpath8(parent);
15046
15221
  const resolvedPath = await realpath8(path);
15047
15222
  if (!resolvedPath.startsWith(`${resolvedParent}/`)) return void 0;
15048
- return await readFile15(path, "utf8");
15223
+ return await readFile16(path, "utf8");
15049
15224
  } catch {
15050
15225
  return void 0;
15051
15226
  }
@@ -15475,10 +15650,12 @@ program.command("status").description("Show model, context, workspace, and index
15475
15650
  const engine = new ContextEngine(config);
15476
15651
  const status = await engine.status();
15477
15652
  const namespace = resolveProjectNamespaceSync(config.workspaceRoots[0] ?? process.cwd());
15653
+ const update = await refreshUpdateCache(package_default.version).catch(() => void 0);
15478
15654
  if (options.json === true) {
15479
- printObject({ config: configSummary(config), context: status, namespace }, 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);
15480
15657
  } else {
15481
- printStatusSummary(config, status, namespace);
15658
+ printStatusSummary(config, status, namespace, update);
15482
15659
  }
15483
15660
  });
15484
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) => {
@@ -15581,7 +15758,7 @@ sessionCommand.command("export <id>").description("Export a session as Markdown"
15581
15758
  const store = new SessionStore(workspaceOption(options.workspace));
15582
15759
  const session = await requireSessionSelector(store, id);
15583
15760
  const markdown = sessionMarkdown(session);
15584
- if (options.output) await writeFile2(resolve22(options.output), markdown, "utf8");
15761
+ if (options.output) await writeFile3(resolve22(options.output), markdown, "utf8");
15585
15762
  else process.stdout.write(markdown);
15586
15763
  });
15587
15764
  var checkpointCommand = program.command("checkpoint").description("Inspect and restore pre-mutation snapshots");
@@ -16219,7 +16396,7 @@ function printObject(value, json) {
16219
16396
  else process.stdout.write(`${JSON.stringify(value, null, 2)}
16220
16397
  `);
16221
16398
  }
16222
- function printStatusSummary(config, context, namespace) {
16399
+ function printStatusSummary(config, context, namespace, update) {
16223
16400
  const glyphs = cliGlyphs;
16224
16401
  const dim = (text) => chalk3.dim(text);
16225
16402
  const line = (level, name, detail) => {
@@ -16248,6 +16425,7 @@ function printStatusSummary(config, context, namespace) {
16248
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)`;
16249
16426
  const storageReady = namespace.activeKind === "canonical" || namespace.phase === "active";
16250
16427
  line(storageReady ? "ok" : "warn", "Storage", storageDetail);
16428
+ line(update ? "warn" : "ok", "Version", update ? updateNoticeText(update) : `v${package_default.version} (up to date)`);
16251
16429
  process.stdout.write(`
16252
16430
  ${dim(`Run ${PRODUCT_COMMAND} status --json for the full machine-readable record.`)}
16253
16431
  `);