@baryonlabs/cli 0.2.2 → 0.3.1

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/bin/baryon.js CHANGED
@@ -14,10 +14,24 @@ import {
14
14
  } from "../src/commands.js";
15
15
  import { loadConfig, piProviderConfigured, hasConfig } from "../src/config.js";
16
16
  import { runPi, resolvePiEntry } from "../src/pi.js";
17
+ import { checkLatest } from "../src/api.js";
17
18
  import { spawnSync } from "node:child_process";
18
19
  import { createRequire } from "node:module";
19
20
  import { c, err, log, sym } from "../src/ui.js";
20
21
 
22
+ /** Best-effort: warn loudly when a newer CLI exists. The gateway enforces the
23
+ * minimum version (426), so cloud use is blocked until you update; this is the
24
+ * friendly heads-up. Silent when offline / opted out. */
25
+ async function warnIfOutdated() {
26
+ const r = await checkLatest();
27
+ if (r?.outdated) {
28
+ log(
29
+ `\n ${sym.warn} ${c.yellow(`업데이트 필요: @baryonlabs/cli ${r.current} → ${r.latest}`)}\n` +
30
+ ` ${c.dim("baryon.ai 사용에 최신 버전이 필요합니다.")} ${c.lime("baryon update")} ${c.dim("를 실행하세요.\n")}`,
31
+ );
32
+ }
33
+ }
34
+
21
35
  const require = createRequire(import.meta.url);
22
36
 
23
37
  function showVersion() {
@@ -87,6 +101,7 @@ async function main() {
87
101
  );
88
102
  return 1;
89
103
  }
104
+ await warnIfOutdated();
90
105
  const cfg = loadConfig();
91
106
  return runPi(argv, cfg);
92
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baryonlabs/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Baryon CLI — AI 코딩·학습 에이전트. baryon.ai API에 기본 연결된 pi 코딩 에이전트 래퍼. 한 줄 설치, 상용·로컬 모델 전환.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -1,10 +1,51 @@
1
1
  // Minimal baryon.ai (OpenAI-compatible) helpers: model discovery + reachability.
2
- import { DEFAULT_MODELS } from "./constants.js";
2
+ import { CLIENT_VERSION, DEFAULT_MODELS } from "./constants.js";
3
3
 
4
4
  function joinUrl(base, suffix) {
5
5
  return base.replace(/\/+$/, "") + suffix;
6
6
  }
7
7
 
8
+ /** Compare two semver strings. -1 / 0 / 1, prerelease-insensitive. */
9
+ function cmpSemver(a, b) {
10
+ const pa = String(a).split("-")[0].split(".").map(Number);
11
+ const pb = String(b).split("-")[0].split(".").map(Number);
12
+ for (let i = 0; i < 3; i++) {
13
+ const x = pa[i] || 0;
14
+ const y = pb[i] || 0;
15
+ if (x !== y) return x < y ? -1 : 1;
16
+ }
17
+ return 0;
18
+ }
19
+
20
+ /**
21
+ * Best-effort latest-version check against the npm registry. Returns
22
+ * { current, latest, outdated } or null on any failure (offline / 폐쇄망 /
23
+ * timeout / opt-out via BARYON_SKIP_UPDATE_CHECK). Never throws.
24
+ */
25
+ export async function checkLatest({ timeoutMs = 2500 } = {}) {
26
+ if (process.env.BARYON_SKIP_UPDATE_CHECK) return null;
27
+ const ctrl = new AbortController();
28
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
29
+ try {
30
+ const res = await fetch(
31
+ "https://registry.npmjs.org/@baryonlabs/cli/latest",
32
+ { signal: ctrl.signal, headers: { accept: "application/json" } },
33
+ );
34
+ if (!res.ok) return null;
35
+ const latest = (await res.json())?.version;
36
+ if (!latest) return null;
37
+ return {
38
+ current: CLIENT_VERSION,
39
+ latest,
40
+ outdated: cmpSemver(CLIENT_VERSION, latest) < 0,
41
+ };
42
+ } catch {
43
+ return null;
44
+ } finally {
45
+ clearTimeout(t);
46
+ }
47
+ }
48
+
8
49
  /**
9
50
  * Fetch the institution's model catalog from `${baseUrl}/models`.
10
51
  * Returns pi-shaped model entries, or null when discovery fails
package/src/commands.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // Built-in baryon subcommands. Anything not matched here is passed to pi.
2
2
  import { spawn } from "node:child_process";
3
- import { discoverModels, ping } from "./api.js";
3
+ import { checkLatest, discoverModels, ping } from "./api.js";
4
4
  import {
5
5
  hasConfig,
6
6
  loadConfig,
7
7
  piProviderConfigured,
8
+ prunePiPackages,
8
9
  saveConfig,
9
10
  syncPiModels,
10
11
  BARYON_CONFIG,
@@ -15,6 +16,7 @@ import {
15
16
  DEFAULT_BASE_URL,
16
17
  DEFAULT_EXTENSIONS,
17
18
  DEFAULT_MODELS,
19
+ DEPRECATED_EXTENSIONS,
18
20
  HOMEPAGE,
19
21
  KEYS_URL,
20
22
  KEY_PREFIX,
@@ -124,6 +126,15 @@ export async function doctor() {
124
126
  if (cfg.apiKey) ok(`API 키 설정됨 (${cfg.apiKey.slice(0, 4)}${"•".repeat(6)})`);
125
127
  else warn("API 키 없음");
126
128
 
129
+ // CLI version currency (best-effort; silent offline)
130
+ const ver = await checkLatest();
131
+ if (ver?.outdated) {
132
+ warn(`CLI 구버전 ${ver.current} — 최신 ${ver.latest}. baryon.ai 사용에 업데이트 필요 (\`baryon update\`)`);
133
+ problems++;
134
+ } else if (ver) {
135
+ ok(`CLI 최신 버전 (${ver.current})`);
136
+ }
137
+
127
138
  // pi provider registered?
128
139
  if (piProviderConfigured()) ok(`pi 프로바이더 ${c.lime(PROVIDER)} 등록됨 → ${c.dim(PI_MODELS_JSON)}`);
129
140
  else {
@@ -224,13 +235,29 @@ export function installDefaults() {
224
235
  return 0;
225
236
  }
226
237
 
238
+ // Self-heal machines broken by a previously-shipped conflicting extension
239
+ // (e.g. pi-search ↔ pi-web-fetch both registering `web_fetch`, which hard-fails
240
+ // every run). Remove them from pi's registry + disk before (re)installing.
241
+ const pruned = prunePiPackages(DEPRECATED_EXTENSIONS);
242
+ if (pruned.length) warn(`충돌 확장 제거: ${pruned.join(", ")} (자가 치유)`);
243
+
227
244
  log(` ${sym.info} 기본 확장 설치 중 (${DEFAULT_EXTENSIONS.length}종 · git clone, 잠시 걸립니다)…`);
228
245
  let okc = 0;
229
246
 
230
247
  for (const e of DEFAULT_EXTENSIONS) {
231
- const r = spawnSync(process.execPath, [entry, "install", e.src], { encoding: "utf8" });
248
+ // git clone is flaky under GitHub rate-limiting / transient network — retry
249
+ // a few times so a single class doesn't end up with "0/N" extensions.
250
+ let status = 1;
251
+
252
+ for (let attempt = 1; attempt <= 3; attempt++) {
253
+ const r = spawnSync(process.execPath, [entry, "install", e.src], { encoding: "utf8" });
254
+ status = r.status;
255
+ if (status === 0) break;
256
+ // Cross-platform synchronous backoff (no `sleep` binary — Windows lacks it).
257
+ if (attempt < 3) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
258
+ }
232
259
 
233
- if (r.status === 0) {
260
+ if (status === 0) {
234
261
  ok(`${e.name} — ${e.note}`);
235
262
  okc++;
236
263
  } else {
package/src/config.js CHANGED
@@ -10,11 +10,20 @@ import {
10
10
  DEFAULT_MODELS,
11
11
  PI_AGENT_DIR,
12
12
  PI_MODELS_JSON,
13
+ PI_SETTINGS_JSON,
13
14
  PROVIDER,
14
15
  SESSION_HEADER,
15
16
  SESSION_ID_ENV,
17
+ CLIENT_HEADER,
18
+ CLIENT_ENV,
16
19
  } from "./constants.js";
17
20
 
21
+ /** Headers the baryon provider must always send (resolved by pi from env). */
22
+ const PROVIDER_HEADERS = {
23
+ [SESSION_HEADER]: `$${SESSION_ID_ENV}`,
24
+ [CLIENT_HEADER]: `$${CLIENT_ENV}`,
25
+ };
26
+
18
27
  function readJson(file, fallback) {
19
28
  try {
20
29
  return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -71,9 +80,10 @@ export function syncPiModels({ baseUrl, models }) {
71
80
  api: "openai-completions",
72
81
  apiKey: `$${API_KEY_ENV}`,
73
82
  authHeader: true,
74
- // Per-launch session id (resolved by pi from the env at request time) so the
75
- // gateway can group a run's turns into one session. Gateway requires it.
76
- headers: { [SESSION_HEADER]: `$${SESSION_ID_ENV}` },
83
+ // Per-launch session id + client version (resolved by pi from env at request
84
+ // time): the gateway groups a run's turns into one session and enforces a
85
+ // minimum CLI version. Both are required by the gateway.
86
+ headers: { ...PROVIDER_HEADERS },
77
87
  models:
78
88
  Array.isArray(models) && models.length ? models : DEFAULT_MODELS,
79
89
  };
@@ -89,18 +99,61 @@ export function piProviderConfigured() {
89
99
  }
90
100
 
91
101
  /**
92
- * Auto-heal older installs: ensure the baryon provider sends the session
93
- * header. Safe to call on every launch — only writes when something changed.
94
- * Returns true if the provider exists (after any patch).
102
+ * Auto-heal older installs: ensure the baryon provider sends the session +
103
+ * client-version headers. Safe to call on every launch — only writes when
104
+ * something changed. Returns true if the provider exists (after any patch).
95
105
  */
96
106
  export function ensurePiSessionHeader() {
97
107
  const root = readJson(PI_MODELS_JSON, {});
98
108
  const p = root?.providers?.[PROVIDER];
99
109
  if (!p) return false;
100
- if (p.headers?.[SESSION_HEADER] === `$${SESSION_ID_ENV}`) return true;
101
- p.headers = { ...(p.headers || {}), [SESSION_HEADER]: `$${SESSION_ID_ENV}` };
110
+ const need = Object.entries(PROVIDER_HEADERS).some(
111
+ ([k, v]) => p.headers?.[k] !== v,
112
+ );
113
+ if (!need) return true;
114
+ p.headers = { ...(p.headers || {}), ...PROVIDER_HEADERS };
102
115
  writeJson(PI_MODELS_JSON, root);
103
116
  return true;
104
117
  }
105
118
 
119
+ /**
120
+ * Self-heal installs broken by a deprecated extension: drop its URL from pi's
121
+ * settings.json `packages` list and remove the cloned dir so pi stops loading
122
+ * it. Pure fs/JSON → works identically on macOS/Linux/Windows. Returns the list
123
+ * of names actually removed.
124
+ */
125
+ export function prunePiPackages(deprecated) {
126
+ const removed = [];
127
+ const srcs = new Set(deprecated.map((d) => d.src));
128
+
129
+ // 1) drop from settings.json packages[]
130
+ try {
131
+ const s = readJson(PI_SETTINGS_JSON, null);
132
+ if (s && Array.isArray(s.packages)) {
133
+ const kept = s.packages.filter((u) => !srcs.has(u));
134
+ if (kept.length !== s.packages.length) {
135
+ s.packages = kept;
136
+ writeJson(PI_SETTINGS_JSON, s);
137
+ }
138
+ }
139
+ } catch {
140
+ /* settings missing/unreadable — nothing to prune */
141
+ }
142
+
143
+ // 2) remove the cloned extension directory (git/github.com/<owner>/<name>)
144
+ for (const d of deprecated) {
145
+ const dir = path.join(PI_AGENT_DIR, "git", "github.com", d.owner, d.name);
146
+ try {
147
+ if (fs.existsSync(dir)) {
148
+ fs.rmSync(dir, { recursive: true, force: true });
149
+ removed.push(d.name);
150
+ }
151
+ } catch {
152
+ /* best-effort */
153
+ }
154
+ }
155
+
156
+ return removed;
157
+ }
158
+
106
159
  export { PI_MODELS_JSON, BARYON_CONFIG };
package/src/constants.js CHANGED
@@ -1,5 +1,16 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+
5
+ /** This CLI's version (from package.json). Sent to the gateway for the
6
+ * minimum-version gate, so old clients are forced to update. */
7
+ export const CLIENT_VERSION = (() => {
8
+ try {
9
+ return createRequire(import.meta.url)("../package.json").version;
10
+ } catch {
11
+ return "0.0.0";
12
+ }
13
+ })();
3
14
 
4
15
  /** Provider id registered inside pi's models.json */
5
16
  export const PROVIDER = "baryon";
@@ -19,6 +30,14 @@ export const API_KEY_ENV = "BARYON_API_KEY";
19
30
  export const SESSION_ID_ENV = "BARYON_SESSION_ID";
20
31
  export const SESSION_HEADER = "X-Baryon-Session";
21
32
 
33
+ /**
34
+ * Client-identity header. The baryon provider sends `baryon-cli/<version>` so
35
+ * the gateway can enforce a minimum CLI version (BARYON_MIN_CLI_VERSION). The
36
+ * value is resolved per launch from BARYON_CLIENT (see pi.js).
37
+ */
38
+ export const CLIENT_ENV = "BARYON_CLIENT";
39
+ export const CLIENT_HEADER = "X-Baryon-Client";
40
+
22
41
  /** Underlying coding agent package + binary. */
23
42
  export const PI_PACKAGE = "@earendil-works/pi-coding-agent";
24
43
  export const PI_BIN = "pi";
@@ -30,6 +49,8 @@ export const BARYON_CONFIG = path.join(BARYON_DIR, "config.json");
30
49
  /** pi's custom-provider/model registry. */
31
50
  export const PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
32
51
  export const PI_MODELS_JSON = path.join(PI_AGENT_DIR, "models.json");
52
+ /** pi's extension registry — a `{ packages: [<git url>, …] }` list loaded on startup. */
53
+ export const PI_SETTINGS_JSON = path.join(PI_AGENT_DIR, "settings.json");
33
54
 
34
55
  /**
35
56
  * Fallback model catalog used when /models discovery is unavailable
@@ -57,19 +78,36 @@ export const DEFAULT_MODELS = [
57
78
 
58
79
  /**
59
80
  * Extensions installed by default so `baryon` ships with sub-agents, a canvas,
60
- * an interactive shell, and web access/fetch out of the box. Installed via
81
+ * an interactive shell, and web access/search out of the box. Installed via
61
82
  * `pi install <src>` into ~/.pi/agent/settings.json (loaded on startup).
83
+ *
84
+ * Curated for conflict-free startup (verified in a clean container):
85
+ * - pi-search removed — it registers a `web_fetch` tool that collides with
86
+ * pi-web-fetch, hard-failing extension load on every run.
87
+ * - pi-web-fetch removed — requires `puppeteer` (chromium download), which is
88
+ * absent in fresh/CI environments, so the extension fails to load. pi-web-access
89
+ * already provides browsing + fetch_content + web_search without that dependency.
62
90
  */
63
91
  export const DEFAULT_EXTENSIONS = [
64
92
  { name: "pi-subagents", src: "https://github.com/nicobailon/pi-subagents", note: "서브에이전트(작업 분해·위임·통합)" },
65
93
  { name: "pi-canvas", src: "https://github.com/jyaunches/pi-canvas", note: "캔버스" },
66
94
  { name: "pi-interactive-shell", src: "https://github.com/nicobailon/pi-interactive-shell", note: "인터랙티브 셸" },
67
- { name: "pi-web-access", src: "https://github.com/nicobailon/pi-web-access", note: "웹 액세스(브라우징)" },
68
- { name: "pi-web-fetch", src: "https://github.com/georgebashi/pi-web-fetch", note: "웹 페치" },
69
- { name: "pi-search", src: "https://github.com/buddingnewinsights/pi-search", note: "웹 검색" },
95
+ { name: "pi-web-access", src: "https://github.com/nicobailon/pi-web-access", note: "웹 액세스(브라우징·검색·페치)" },
70
96
  { name: "pi-parallel-web-search", src: "https://github.com/philipp-spiess/pi-parallel-web-search", note: "병렬 웹 검색" }
71
97
  ];
72
98
 
99
+ /**
100
+ * Extensions previously shipped as defaults that BREAK startup and must be
101
+ * actively removed from existing installs (settings.json + cloned dir):
102
+ * - pi-web-fetch: `web_fetch` collides with pi-search; also needs puppeteer.
103
+ * - pi-search: `web_fetch` collision; superseded by pi-web-access.
104
+ * `baryon setup` self-heals an already-broken machine by pruning these.
105
+ */
106
+ export const DEPRECATED_EXTENSIONS = [
107
+ { name: "pi-web-fetch", src: "https://github.com/georgebashi/pi-web-fetch", owner: "georgebashi" },
108
+ { name: "pi-search", src: "https://github.com/buddingnewinsights/pi-search", owner: "buddingnewinsights" }
109
+ ];
110
+
73
111
  export const HOMEPAGE = "https://cli.baryon.ai";
74
112
  export const SUPPORT_EMAIL = "support@baryon.ai";
75
113
 
package/src/pi.js CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  PI_PACKAGE,
13
13
  PROVIDER,
14
14
  SESSION_ID_ENV,
15
+ CLIENT_ENV,
16
+ CLIENT_VERSION,
15
17
  } from "./constants.js";
16
18
  import { ensurePiSessionHeader } from "./config.js";
17
19
 
@@ -102,8 +104,10 @@ export function runPi(args, config, { injectTargeting = true } = {}) {
102
104
  if (config.baseUrl) env.BARYON_BASE_URL = config.baseUrl;
103
105
 
104
106
  // One session per launch: mint an id (unless the caller pinned one) and make
105
- // sure the provider forwards it. The gateway requires a session id.
107
+ // sure the provider forwards it + this CLI's version. The gateway requires a
108
+ // session id and enforces a minimum CLI version.
106
109
  if (!env[SESSION_ID_ENV]) env[SESSION_ID_ENV] = `cli_${randomUUID()}`;
110
+ env[CLIENT_ENV] = `baryon-cli/${CLIENT_VERSION}`;
107
111
  ensurePiSessionHeader();
108
112
 
109
113
  return new Promise((resolve, reject) => {