@baryonlabs/cli 0.4.2 → 0.4.4

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
@@ -13,6 +13,7 @@ import {
13
13
  update,
14
14
  help,
15
15
  welcome,
16
+ installStatusExtension,
16
17
  } from "../src/commands.js";
17
18
  import { loadConfig, piProviderConfigured, hasConfig, prunePiPackages } from "../src/config.js";
18
19
  import { DEPRECATED_EXTENSIONS } from "../src/constants.js";
@@ -120,6 +121,9 @@ async function main() {
120
121
  } catch {
121
122
  /* best-effort */
122
123
  }
124
+ // Keep the /status·/my slash-command extension present + up to date
125
+ // (cheap content-compare copy into ~/.pi/agent/extensions/).
126
+ installStatusExtension();
123
127
  const cfg = loadConfig();
124
128
  // Show which room (project/분반 · seat) this key is in — best-effort.
125
129
  try {
@@ -0,0 +1,159 @@
1
+ /**
2
+ * baryon-status/v1 — `/status` + `/my` slash commands for the baryon CLI.
3
+ *
4
+ * Claude Code's `/status`-style seat dashboard inside a pi session: today's
5
+ * requests vs the class daily cap, cumulative cost vs the seat budget,
6
+ * lifetime totals, top models and guardrail flags — all served by the
7
+ * baryon gateway `GET /v1/status` (key-authenticated, seat-scoped).
8
+ *
9
+ * Installed by `@baryonlabs/cli` into ~/.pi/agent/extensions/ (auto-discovered
10
+ * by pi). Reads the key from $BARYON_API_KEY or ~/.baryon/config.json, so it
11
+ * also works when pi is launched directly (not via the baryon wrapper).
12
+ */
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+
17
+ interface BaryonConfig {
18
+ apiKey?: string;
19
+ baseUrl?: string;
20
+ defaultModel?: string;
21
+ }
22
+
23
+ function loadBaryonConfig(): BaryonConfig {
24
+ try {
25
+ const file = path.join(os.homedir(), ".baryon", "config.json");
26
+ return JSON.parse(fs.readFileSync(file, "utf8"));
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ function resolveAuth(): { apiKey: string; baseUrl: string; defaultModel?: string } | null {
33
+ const cfg = loadBaryonConfig();
34
+ const apiKey = process.env.BARYON_API_KEY || cfg.apiKey || "";
35
+ const baseUrl = (process.env.BARYON_BASE_URL || cfg.baseUrl || "https://api.baryon.ai/v1").replace(/\/+$/, "");
36
+ if (!apiKey) return null;
37
+ return { apiKey, baseUrl, defaultModel: cfg.defaultModel };
38
+ }
39
+
40
+ async function fetchStatus(auth: { apiKey: string; baseUrl: string }): Promise<any> {
41
+ const ctrl = new AbortController();
42
+ const t = setTimeout(() => ctrl.abort(), 8000);
43
+ try {
44
+ const res = await fetch(`${auth.baseUrl}/status`, {
45
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
46
+ signal: ctrl.signal,
47
+ });
48
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
49
+ return await res.json();
50
+ } finally {
51
+ clearTimeout(t);
52
+ }
53
+ }
54
+
55
+ /** `[████████░░░░] 40%` — cap 없으면 null. */
56
+ function bar(used: number, cap: number | null | undefined, width = 12): string | null {
57
+ if (!cap || cap <= 0) return null;
58
+ const ratio = Math.min(used / cap, 1);
59
+ const filled = Math.round(ratio * width);
60
+ const icon = ratio >= 1 ? "🔴" : ratio >= 0.8 ? "🟠" : "🟢";
61
+ return `${icon} ${"█".repeat(filled)}${"░".repeat(width - filled)} ${Math.round(ratio * 100)}%`;
62
+ }
63
+
64
+ function usd(n: number | null | undefined): string {
65
+ return n == null ? "—" : `$${Number(n).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}`;
66
+ }
67
+
68
+ function fmt(n: number | null | undefined): string {
69
+ return n == null ? "—" : Number(n).toLocaleString("en-US");
70
+ }
71
+
72
+ function maskKey(key: string): string {
73
+ if (key.length <= 12) return `${key.slice(0, 4)}…`;
74
+ return `${key.slice(0, 12)}…${key.slice(-4)}`;
75
+ }
76
+
77
+ function renderStatus(s: any): string {
78
+ const lines: string[] = [];
79
+ const room = s.project ? `${s.project}${s.active ? "" : " (비활성)"}` : "미배정";
80
+ lines.push(`📊 Baryon 사용량 — 🏫 ${room} · 좌석 ${s.seat ?? "—"}`);
81
+ lines.push("");
82
+
83
+ const daily = bar(s.today_requests, s.daily_cap);
84
+ lines.push(
85
+ `오늘 요청 ${fmt(s.today_requests)}${s.daily_cap ? ` / ${fmt(s.daily_cap)} (남음 ${fmt(s.daily_remaining)})` : " (일일 한도 없음)"}`,
86
+ );
87
+ if (daily) lines.push(` ${daily}`);
88
+
89
+ const cost = bar(s.cost_usd, s.cost_cap);
90
+ lines.push(
91
+ `누적 비용 ${usd(s.cost_usd)}${s.cost_cap ? ` / ${usd(s.cost_cap)} (남음 ${usd(s.cost_remaining)})` : " (좌석 예산 없음)"}`,
92
+ );
93
+ if (cost) lines.push(` ${cost}`);
94
+
95
+ const t = s.totals || {};
96
+ lines.push(`전체 누계 요청 ${fmt(t.requests)} · 토큰 ${fmt(t.totalTokens)}`);
97
+
98
+ if (Array.isArray(s.top_models) && s.top_models.length) {
99
+ lines.push("");
100
+ lines.push("많이 쓴 모델");
101
+ for (const m of s.top_models.slice(0, 3)) {
102
+ lines.push(` · ${m.model} — 요청 ${fmt(m.requests)} · ${usd(m.cost_usd)}`);
103
+ }
104
+ }
105
+
106
+ const g = s.guardrails || {};
107
+ const rails = [g.pii && "PII차단", g.jailbreak && "탈옥차단", g.moderation && "모더레이션"].filter(Boolean);
108
+ if (rails.length || s.seat_rpm) {
109
+ lines.push("");
110
+ const parts = [];
111
+ if (rails.length) parts.push(`가드레일 ${rails.join("·")}`);
112
+ if (s.seat_rpm) parts.push(`좌석 ${s.seat_rpm}rpm`);
113
+ lines.push(parts.join(" · "));
114
+ }
115
+ return lines.join("\n");
116
+ }
117
+
118
+ function renderMy(s: any, auth: { apiKey: string; baseUrl: string; defaultModel?: string }): string {
119
+ const lines: string[] = [];
120
+ lines.push("👤 내 Baryon 계정");
121
+ lines.push("");
122
+ lines.push(`이메일 ${s.email ?? "—"}`);
123
+ lines.push(`분반 ${s.project ?? "미배정"}${s.project && !s.active ? " (비활성)" : ""}`);
124
+ lines.push(`좌석 ${s.seat ?? "—"}`);
125
+ lines.push(`키 ${maskKey(auth.apiKey)}`);
126
+ lines.push(`엔드포인트 ${auth.baseUrl}`);
127
+ if (auth.defaultModel) lines.push(`기본 모델 ${auth.defaultModel}`);
128
+ if (Array.isArray(s.allowed_models) && s.allowed_models.length) {
129
+ lines.push(`허용 모델 ${s.allowed_models.slice(0, 6).join(", ")}${s.allowed_models.length > 6 ? " …" : ""}`);
130
+ }
131
+ lines.push("");
132
+ lines.push(`오늘 ${fmt(s.today_requests)}회 · 누적 ${usd(s.cost_usd)} — 자세히: /status`);
133
+ return lines.join("\n");
134
+ }
135
+
136
+ async function run(ctx: any, render: (s: any, auth: any) => string): Promise<void> {
137
+ const auth = resolveAuth();
138
+ if (!auth) {
139
+ ctx.ui.notify("Baryon 키가 없습니다 — `baryon setup` 으로 로그인하세요.", "warning");
140
+ return;
141
+ }
142
+ try {
143
+ const s = await fetchStatus(auth);
144
+ ctx.ui.notify(render(s, auth), "info");
145
+ } catch (e: any) {
146
+ ctx.ui.notify(`Baryon 상태 조회 실패: ${e?.message || e} (게이트웨이/네트워크 확인)`, "error");
147
+ }
148
+ }
149
+
150
+ export default function (pi: any) {
151
+ pi.registerCommand("status", {
152
+ description: "Baryon 좌석 사용량·한도 (오늘 요청/일일캡, 비용/예산, 모델별)",
153
+ handler: async (_args: string, ctx: any) => run(ctx, renderStatus),
154
+ });
155
+ pi.registerCommand("my", {
156
+ description: "내 Baryon 계정 (이메일·분반·좌석·키·엔드포인트)",
157
+ handler: async (_args: string, ctx: any) => run(ctx, renderMy),
158
+ });
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baryonlabs/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Baryon CLI — AI 코딩·학습 에이전트. baryon.ai API에 기본 연결된 pi 코딩 에이전트 래퍼. 한 줄 설치, 상용·로컬 모델 전환.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin",
11
11
  "src",
12
12
  "skills",
13
+ "extensions",
13
14
  "README.md",
14
15
  "LICENSE"
15
16
  ],
package/src/commands.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  HOMEPAGE,
26
26
  KEYS_URL,
27
27
  KEY_PREFIX,
28
+ PI_AGENT_DIR,
28
29
  PI_PACKAGE,
29
30
  PI_SKILLS_DIR,
30
31
  PREFERRED_DEFAULT_MODEL,
@@ -112,6 +113,10 @@ export async function setup(args) {
112
113
  installSkills();
113
114
  }
114
115
 
116
+ // /status·/my slash commands inside pi (seat usage dashboard). Cheap local
117
+ // file copy — also self-healed on every plain `baryon` launch.
118
+ if (installStatusExtension()) ok(t("statusExt.ok"));
119
+
115
120
  if (flags["no-browser"]) {
116
121
  info(t("setup.skipBrowser"));
117
122
  } else {
@@ -344,6 +349,33 @@ export function installDefaults() {
344
349
  return okc;
345
350
  }
346
351
 
352
+ /**
353
+ * Install (or refresh) the bundled baryon-status pi extension, which registers
354
+ * the `/status` + `/my` slash commands (seat usage dashboard via the gateway's
355
+ * `GET /v1/status`). Copied to ~/.pi/agent/extensions/ — pi auto-discovers
356
+ * loose .ts files there. Content-compared so CLI upgrades refresh it in place.
357
+ * Returns true when a copy happened (fresh install or update), false otherwise.
358
+ */
359
+ export function installStatusExtension() {
360
+ try {
361
+ const src = path.join(
362
+ path.dirname(fileURLToPath(import.meta.url)),
363
+ "..",
364
+ "extensions",
365
+ "baryon-status.ts",
366
+ );
367
+ const dstDir = path.join(PI_AGENT_DIR, "extensions");
368
+ const dst = path.join(dstDir, "baryon-status.ts");
369
+ const content = fs.readFileSync(src, "utf8");
370
+ if (fs.existsSync(dst) && fs.readFileSync(dst, "utf8") === content) return false;
371
+ fs.mkdirSync(dstDir, { recursive: true });
372
+ fs.writeFileSync(dst, content);
373
+ return true;
374
+ } catch {
375
+ return false; // best-effort — never block setup/launch
376
+ }
377
+ }
378
+
347
379
  // Install the default Agent Skills pack (pdf/pptx/xlsx) into ~/.pi/agent/skills/,
348
380
  // which pi auto-discovers. All three are subfolders of one repo, so we shallow-
349
381
  // clone once and copy each. Idempotent (skips skills already present) and safe to
package/src/constants.js CHANGED
@@ -61,7 +61,7 @@ export const PI_SKILLS_DIR = path.join(PI_AGENT_DIR, "skills");
61
61
  *
62
62
  * Baryon 3-tier lineup (기준: baryon-ai-api docs/MODELS.md — 이름은 계약,
63
63
  * 실제 업스트림 모델은 게이트웨이 라우팅으로 조절):
64
- * 그저(geujeo) 단순·빠름 · 두루(duru) 범용 기본 · 그루(guru) 사고·기획.
64
+ * 빨리(ppalli) 단순·빠름 · 두루(duru) 범용 기본 · 그루(guru) 사고·기획.
65
65
  */
66
66
  export const DEFAULT_MODELS = [
67
67
  {
@@ -73,8 +73,8 @@ export const DEFAULT_MODELS = [
73
73
  maxTokens: 8192,
74
74
  },
75
75
  {
76
- id: "baryon-geujeo",
77
- name: "그저 (Geujeo) — 단순·빠름",
76
+ id: "baryon-ppalli",
77
+ name: "빨리 (Ppalli) — 단순·빠름",
78
78
  reasoning: false,
79
79
  input: ["text"],
80
80
  contextWindow: 262144,
package/src/i18n.js CHANGED
@@ -130,6 +130,9 @@ const MESSAGES = {
130
130
  "skills.itemOk": "skill: {name} — {note}",
131
131
  "skills.itemFail": "{name} 스킬 설치 실패 — 건너뜀 ({error})",
132
132
  "skills.cloneFail": "스킬 저장소 clone 실패(네트워크/git 확인) — 일부 스킬 건너뜀",
133
+
134
+ // ── /status·/my extension ────────────────────────────────────────────
135
+ "statusExt.ok": "pi 명령 /status·/my 설치됨 (좌석 사용량·계정 조회)",
133
136
  "skills.summary": "기본 스킬 {ok}/{total} 설치 → {dir}",
134
137
  "skills.bannerListTitle": "Baryon 기본 스킬 팩",
135
138
  "skills.notInstalled": "(미설치)",
@@ -272,6 +275,9 @@ const MESSAGES = {
272
275
  "skills.noSkillMd": "{name} — no SKILL.md ({dir}), skipped",
273
276
  "skills.itemOk": "skill: {name} — {note}",
274
277
  "skills.itemFail": "{name} skill install failed — skipped ({error})",
278
+
279
+ // ── /status·/my extension ────────────────────────────────────────────
280
+ "statusExt.ok": "Installed pi commands /status·/my (seat usage & account)",
275
281
  "skills.cloneFail": "Skill repo clone failed (check network/git) — some skills skipped",
276
282
  "skills.summary": "Default skills {ok}/{total} installed → {dir}",
277
283
  "skills.bannerListTitle": "Baryon default skills pack",