sprag-cli 3.79.0 → 3.80.0
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.ko.md +34 -3
- package/README.md +34 -3
- package/bin/cli.js +3 -1
- package/docs/TEAM.ko.md +4 -2
- package/docs/TEAM.md +4 -2
- package/package.json +1 -1
- package/src/formatters/statusline.js +28 -3
- package/src/member-id.js +5 -0
- package/src/office-species.js +49 -0
- package/src/team-sync.js +45 -2
package/README.ko.md
CHANGED
|
@@ -258,13 +258,44 @@ sprag feedback "Bedrock에서 5m TTL 칩이 사라지지 않아요"
|
|
|
258
258
|
|
|
259
259
|
## 팀
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
CLI는 무료이고 컴퓨터 한 대에서 혼자 동작합니다. 조직에는 **Sprag Pro** 와 **Sprag Enterprise** 가 같은 로컬 세션 로그로 만든 공용 대시보드를 더해 줍니다. 도입은 [sales@sprag.io](mailto:sales@sprag.io)로 문의해 주세요.
|
|
262
|
+
|
|
263
|
+
### Pro: 팀 대시보드
|
|
264
|
+
|
|
265
|
+
<a href="https://sprag.io/assets/video/plan-pro-ko.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/plan-pro-ko.gif" width="640" alt="팀원마다 동물 캐릭터로 앉아 있는 가상 사무실, 지출과 절감액, 좌석, 30일 차트가 있는 조직 대시보드, 한 번 승인하면 모든 컴퓨터에 적용되는 팀 규칙을 차례로 보여 줍니다."></a>
|
|
266
|
+
|
|
267
|
+
- 조직 대시보드와 30일 추이 차트
|
|
268
|
+
- 사람마다 동물 캐릭터가 앉아 있는 가상 사무실
|
|
269
|
+
- 컴퓨터별 상태가 보이는 좌석 명단
|
|
270
|
+
- 세션 시작 때 적용되는 팀 승인 규칙
|
|
271
|
+
- `sprag team join` 으로 조직 배포
|
|
272
|
+
- 오너와 어드민 역할, 활동 기록
|
|
273
|
+
|
|
274
|
+
### Enterprise: 프롬프트 모니터링
|
|
275
|
+
|
|
276
|
+
<a href="https://sprag.io/assets/video/plan-ent-ko.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/plan-ent-ko.gif" width="640" alt="사용자별 프롬프트 사용량과 30일 추이, 한 사람의 프롬프트 검색, 팀별로 프롬프트 수집을 켜는 설정을 보여 줍니다."></a>
|
|
277
|
+
|
|
278
|
+
Pro 의 기능에 더해 다음을 제공합니다.
|
|
279
|
+
|
|
280
|
+
- 팀별 프롬프트 수집, 비밀 값은 가림
|
|
281
|
+
- 정책을 켜기 전 기록은 수집하지 않음
|
|
282
|
+
- 사용자별 프롬프트 사용량과 30일 추이
|
|
283
|
+
- 사용자별 프롬프트 목록과 검색
|
|
284
|
+
- 모든 멤버에게 보이는 수집 정책
|
|
285
|
+
|
|
286
|
+
프롬프트 수집은 조직 오너나 어드민이 켜기 전까지 꺼져 있습니다.
|
|
287
|
+
|
|
288
|
+
<sub>두 영상 모두 데모 대시보드에서 예시 데이터로 녹화했습니다. 전체 영상: <a href="https://sprag.io/assets/video/plan-pro-ko.mp4">Pro (30초)</a> · <a href="https://sprag.io/assets/video/plan-ent-ko.mp4">Enterprise (30초)</a></sub>
|
|
289
|
+
|
|
290
|
+
### 컴퓨터가 보내는 것
|
|
291
|
+
|
|
292
|
+
Sprag 은 켜지 않으면 로컬에서만 동작합니다. `sprag team join <토큰>` 을 실행하거나(조직 전체는 `SPRAG_TEAM_TOKEN`) 하면 컴퓨터마다 하루에 한 행씩 올라갑니다. 내용은 지출, 절감액, 위임 실행 횟수, 모델 계열 비중입니다. 대화 기록, 파일 경로, 프로젝트 이름은 보내지 않습니다. 프롬프트도 Enterprise 조직이 수집 정책을 켜지 않는 한 보내지 않으며, `sprag team status --show-payload` 로 올라갈 내용을 그대로 볼 수 있습니다. 그 대신 팀 소유자는 대시보드에서 구성원별 합계를 보고, 모든 구성원에게 자동으로 적용될 래칫 룰을 승인할 수 있습니다.
|
|
262
293
|
|
|
263
294
|
`SPRAG_NO_TEAM_SYNC=1` 은 팀 관련 통신을 모두 끄고, `sprag team leave` 는 참여를 해제합니다.
|
|
264
295
|
|
|
265
296
|
- [팀 기능 안내](docs/TEAM.ko.md)
|
|
266
|
-
-
|
|
267
|
-
-
|
|
297
|
+
- 오너와 어드민용 팀 콘솔: https://sprag.io/team/
|
|
298
|
+
- 플랜 안내: https://sprag.io/license/
|
|
268
299
|
|
|
269
300
|
## 라이선스
|
|
270
301
|
|
package/README.md
CHANGED
|
@@ -312,13 +312,44 @@ For a bug, please include the tool version (`sprag --version`) and your OS. For
|
|
|
312
312
|
|
|
313
313
|
## Teams
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
The CLI is free and works on its own on one machine. For organizations, **Sprag Pro** and **Sprag Enterprise** add a shared dashboard built from the same local session logs. To start, write to [sales@sprag.io](mailto:sales@sprag.io).
|
|
316
|
+
|
|
317
|
+
### Pro: team dashboard
|
|
318
|
+
|
|
319
|
+
<a href="https://sprag.io/assets/video/plan-pro-en.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/plan-pro-en.gif" width="640" alt="A virtual office where every teammate sits as an animal character, then the organization dashboard with spend, savings, seats and a 30-day chart, then team rules approved once and picked up by every machine."></a>
|
|
320
|
+
|
|
321
|
+
- Organization dashboard with a 30-day chart
|
|
322
|
+
- A virtual office with an animal for each person
|
|
323
|
+
- Seat roster with each machine's status
|
|
324
|
+
- Team-approved rules applied at session start
|
|
325
|
+
- Rollout with `sprag team join`
|
|
326
|
+
- Owner and admin roles, activity log
|
|
327
|
+
|
|
328
|
+
### Enterprise: prompt monitoring
|
|
329
|
+
|
|
330
|
+
<a href="https://sprag.io/assets/video/plan-ent-en.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/plan-ent-en.gif" width="640" alt="Per-user prompt usage with a 30-day trend, a prompt search for one person, and the team setting that turns prompt capture on."></a>
|
|
331
|
+
|
|
332
|
+
Everything in Pro, plus:
|
|
333
|
+
|
|
334
|
+
- Prompt capture per team, with secrets redacted
|
|
335
|
+
- Nothing from before the policy was turned on
|
|
336
|
+
- Per-user prompt usage and a 30-day trend
|
|
337
|
+
- Each user's prompt list with search
|
|
338
|
+
- A capture policy every member can see
|
|
339
|
+
|
|
340
|
+
Prompt capture is off unless an organization owner or admin turns it on.
|
|
341
|
+
|
|
342
|
+
<sub>Both videos are recorded from the demo dashboard with sample data. Full videos: <a href="https://sprag.io/assets/video/plan-pro-en.mp4">Pro (30s)</a> · <a href="https://sprag.io/assets/video/plan-ent-en.mp4">Enterprise (30s)</a></sub>
|
|
343
|
+
|
|
344
|
+
### What a machine sends
|
|
345
|
+
|
|
346
|
+
Sprag stays local unless you opt in. With `sprag team join <token>` (or `SPRAG_TEAM_TOKEN` for a whole organization), each machine uploads one row per day: spend, savings, delegated-run count and model-family share. Transcripts, file paths and project names are never sent. Prompts are not sent either, unless an Enterprise organization turns on prompt capture, and `sprag team status --show-payload` prints exactly what would go up. In return the team owner sees per-member totals in a dashboard and can approve ratchet rules that every member picks up automatically.
|
|
316
347
|
|
|
317
348
|
`SPRAG_NO_TEAM_SYNC=1` turns all team traffic off, and `sprag team leave` removes the membership.
|
|
318
349
|
|
|
319
350
|
- [Team layer guide](docs/TEAM.md)
|
|
320
|
-
-
|
|
321
|
-
- Plans
|
|
351
|
+
- Team console for owners and admins: https://sprag.io/team/
|
|
352
|
+
- Plans: https://sprag.io/license/
|
|
322
353
|
|
|
323
354
|
## License
|
|
324
355
|
|
package/bin/cli.js
CHANGED
|
@@ -682,7 +682,7 @@ async function main() {
|
|
|
682
682
|
}
|
|
683
683
|
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
684
684
|
const noSessionOut = withBaseStatusline(formatNoSession(
|
|
685
|
-
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip(PKG_VERSION) },
|
|
685
|
+
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip(PKG_VERSION), team: (await import('../src/team-sync.js')).readTeamChip() },
|
|
686
686
|
{ color: colorOk, mode: labelMode, segments: segmentsFromArgs() },
|
|
687
687
|
), { input: readStdinRaw() });
|
|
688
688
|
if (guard) guard.finish(noSessionOut);
|
|
@@ -989,6 +989,8 @@ async function main() {
|
|
|
989
989
|
options: { days, windowHours, windowLabel, version: PKG_VERSION },
|
|
990
990
|
// Cached-only; the background refresh it may trigger lands on a later render.
|
|
991
991
|
update: format === 'statusline' ? readUpdateChip(PKG_VERSION) : null,
|
|
992
|
+
// Member id and office animal from team.json; a file read, never the network.
|
|
993
|
+
team: format === 'statusline' ? (await import('../src/team-sync.js')).readTeamChip() : null,
|
|
992
994
|
// The totals are already in hand above; passing them spares a second read of
|
|
993
995
|
// both ledgers on every render.
|
|
994
996
|
star: format === 'statusline'
|
package/docs/TEAM.ko.md
CHANGED
|
@@ -24,7 +24,9 @@ Sprag 은 기본적으로 로컬에서만 동작합니다. 팀 기능은 켜야
|
|
|
24
24
|
|
|
25
25
|
하루 단위 집계 업로드는 마지막 성공 이후 5분에 한 번 이하로 실행됩니다. SessionStart 훅(그리고 작은 로컬 파일만 읽는 프롬프트 훅)이 띄운 분리된 자식 프로세스가 보냅니다. 네트워크가 끊기는 등 동기화가 실패하면 5분 주기 대신 15분, 30분, 60분 뒤에 다시 시도합니다(최대 60분 간격). 마지막으로 올린 날짜를 기억해 두었다가 그 전날부터 최대 30일 치를 다시 보내므로, 며칠 꺼져 있던 컴퓨터도 처음 성공하는 동기화에서 빠진 날을 따라잡습니다. 하루 단위 행은 덮어쓰기라 다시 보내도 안전합니다. 프롬프트 수집을 켠 엔터프라이즈 팀의 프롬프트 업로드는 따로 30분 타이머로 움직입니다("프롬프트 사용량 (엔터프라이즈)" 절 참고). statusline 을 그리는 경로에서는 절대 실행되지 않습니다.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
팀에 참여한 동안 statusline 에 `team` 칩이 나타나 멤버 ID 와 가상 사무실(Sprag 타운)에서 받은 동물을 보여 줍니다. 예를 들면 `member-1e500474 🦊 여우` 입니다. 동물은 5분마다 도는 전체 동기화가 서버에서 받아 `team.json` 에 저장한 순위로 정합니다. 서버가 아직 이 기능을 지원하지 않거나 마지막으로 받은 지 하루가 넘었다면 멤버 ID 만 표시합니다. 칩을 그릴 때는 로컬 파일만 읽습니다. 버전 칩 바로 앞에 놓이며, `--segments team` 으로 이 칩만 따로 볼 수 있습니다.
|
|
28
|
+
|
|
29
|
+
목적지는 `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` 입니다(Supabase, ap-northeast-2 리전). 호출하는 RPC 는 `team_join`, `ledger_upsert`, `team_rules_get`, `team_rule_propose`, `team_leave`, `prompt_chunk_put`(엔터프라이즈 프롬프트 수집일 때만), `member_set_sync`(업로드 스위치 상태 호출, "업로드 스위치" 참고), `member_set_info`(OS 이름과 버전, `arch` 인자는 항상 빈 값), `member_standing`(statusline 칩에 쓸 본인의 사무실 동물과 순위만 받고 다른 사람의 수치는 받지 않음) 입니다.
|
|
28
30
|
|
|
29
31
|
## CLI에서 팀 만들기
|
|
30
32
|
|
|
@@ -117,7 +119,7 @@ Sprag 은 룰을 `~/.claude/ratchet-team.md`(Codex 는 `<CODEX_HOME>/ratchet-tea
|
|
|
117
119
|
|
|
118
120
|
## 대시보드
|
|
119
121
|
|
|
120
|
-
https://sprag.io/team/ 에서 등록된 오너나 어드민이 이메일 코드로 로그인하고, 팀을 만들고, 초대 토큰을 받습니다.
|
|
122
|
+
https://sprag.io/team/ 에서 등록된 오너나 어드민이 이메일 코드로 로그인하고, 팀을 만들고, 초대 토큰을 받습니다. 새 조직은 sales@sprag.io 로 문의합니다. 오너, 어드민, 초대 대기 중 어느 쪽에도 없는 이메일은 코드를 보내기 전에 거절하며, 조직 오너에게 추가를 요청하라고 안내합니다. 초대받은 사람은 처음 로그인할 때 자동으로 오너나 어드민이 됩니다. 여기서 구성원별 지출과 절감액을 보고, 규칙 탭에서 조직 룰과 팀 룰을 관리하고, 구성원을 내보낼 수 있습니다. 조직 패널에는 요금제, 한도 대비 사용 좌석, 어드민, 전체 팀이 보입니다. 슈퍼유저는 sprag.io/ops/ 에서 모든 조직을 봅니다.
|
|
121
123
|
|
|
122
124
|
대시보드 맨 위의 기간 선택(7일, 30일, 90일, 이번 달, 지난 달, 직접 선택)이 멤버 표, 사용량 차트, 프롬프트 사용량 탭의 기간을 정합니다. 기본값은 최근 30일이고, 내 컴퓨터의 달력 날짜를 쓰며 종료일도 포함합니다. 최대 365일까지 고를 수 있습니다. 고른 기간은 주소(`?from=2026-10-01&to=2026-10-18`)에 남으므로 새로고침하거나 링크를 공유해도 같은 기간이 보입니다. sprag.io/ops/ 슈퍼유저 콘솔에도 같은 선택이 있어 조직 표, 조직 머리말, 조직의 프롬프트 사용량에 적용됩니다.
|
|
123
125
|
|
package/docs/TEAM.md
CHANGED
|
@@ -24,7 +24,9 @@ Not sent: prompts, transcripts, file paths, project names, rule text, API keys.
|
|
|
24
24
|
|
|
25
25
|
The daily aggregate upload runs at most once every 5 minutes after the last successful one, from a detached child process that the SessionStart hook (and the prompt hook, which only reads a small local file) spawns. If a sync fails, for example because the network is down, it is retried after 15, then 30, then 60 minutes (60 at most) instead of every 5 minutes. The CLI remembers the last day it uploaded and re-sends from the day before it, up to 30 days, so a machine that was offline for days catches up on its first successful sync. Re-sending a day is safe because each day is an upsert. The prompt upload of an Enterprise team with capture on has its own 30-minute timer (see "Prompt usage (Enterprise)"). It never runs on the statusline path.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
While this machine is joined, the statusline shows a `team` chip with your member id and your animal from the virtual office (Sprag Town), for example `member-1e500474 🦊 Fox`. The animal comes from the standing the full sync fetches every 5 minutes and stores in `team.json`; if the server does not offer it yet, or the last fetch is more than a day old, only the member id is shown. Rendering the chip reads local files only. It sits just before the version chip, and `--segments team` shows it on its own.
|
|
28
|
+
|
|
29
|
+
Destination: `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` (Supabase, region ap-northeast-2). The calls are the RPCs `team_join`, `ledger_upsert`, `team_rules_get`, `team_rule_propose`, `team_leave`, `prompt_chunk_put` (Enterprise prompt capture only), `member_set_sync` (the upload-switch status call, see "The upload switch") `member_set_info` (OS name and version; the `arch` argument is always empty) and `member_standing` (your own office animal and rank for the statusline chip; it returns nobody else's figures); `sprag team create` also calls `team_create` with your sign-in session.
|
|
28
30
|
|
|
29
31
|
## Create a team from the CLI
|
|
30
32
|
|
|
@@ -118,7 +120,7 @@ Create a team inside an organization you administer from the CLI with `sprag tea
|
|
|
118
120
|
|
|
119
121
|
## The dashboard
|
|
120
122
|
|
|
121
|
-
https://sprag.io/team/ is where a registered owner or admin signs in with an email code, creates a team and gets the invite token.
|
|
123
|
+
https://sprag.io/team/ is where a registered owner or admin signs in with an email code, creates a team and gets the invite token. A new organization starts by writing to sales@sprag.io. An email that is not an owner, an admin or a pending invite is refused before any code is sent, with a message to ask the organization's owner to add it. Invited people become owner or admin automatically on their first sign-in. From there they see per-member spend and savings, manage organization and team rules on the Rules tab, and remove members. The organization panel shows the plan, seats used against the limit, admins and every team. The superuser sees every organization at sprag.io/ops/.
|
|
122
124
|
|
|
123
125
|
A date range control at the top of the dashboard (7d, 30d, 90d, This month, Last month or Custom) sets the period for the members table, the Usage chart and the prompt usage tab. It defaults to the last 30 days, uses your local calendar days with the end date included, and allows 365 days at most. The choice is kept in the page address (`?from=2026-10-01&to=2026-10-18`), so a reload or a shared link shows the same period. The superuser console at /ops/ has the same control for the organization table, the organization header and its prompt usage.
|
|
124
126
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.80.0",
|
|
4
4
|
"description": "Model-delegation router for Claude Code and Codex: lookups, checks and test runs go to cheaper models, documents go in as compact Markdown, and a live statusline shows spend and savings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@ import { bedrockDisplayFromId } from '../stdin-payload.js';
|
|
|
29
29
|
import { modelFamily } from '../savings-ledger.js';
|
|
30
30
|
import { shareLegend } from '../model-share.js';
|
|
31
31
|
import { STAR_URL } from '../star-note.js';
|
|
32
|
+
import { speciesLabel } from '../office-species.js';
|
|
32
33
|
|
|
33
34
|
// The 8-color ANSI defaults (RED=31, GREEN=32, YELLOW=33…) read as garish
|
|
34
35
|
// next to each other — terminal palettes set them with unbalanced perceptual
|
|
@@ -365,6 +366,23 @@ function buildVersionSeg(version, update, c, isIcon, verbose, g) {
|
|
|
365
366
|
return `${c(GRAY)}v${version}${age ? ` ✓${age} ago` : ''}${c(RESET)}`;
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
/** A standing older than this is not shown; the 5-minute team sync refreshes it long before. */
|
|
370
|
+
const TEAM_STANDING_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Team chip: the member id and, once the server has told us, the animal that
|
|
374
|
+
* member is in the team's virtual office (`member-1e500474 🦊 Fox`). Renders only
|
|
375
|
+
* for a joined machine. A missing, unknown or day-old standing leaves just the
|
|
376
|
+
* member label. Gray like the other identity chips; the emoji brings its own colour.
|
|
377
|
+
*/
|
|
378
|
+
function buildTeamSeg(team, c, now = Date.now()) {
|
|
379
|
+
if (!team || typeof team !== 'object' || !team.label) return null;
|
|
380
|
+
const st = team.standing;
|
|
381
|
+
const fresh = st && Number.isFinite(st.fetchedAt) && now - st.fetchedAt <= TEAM_STANDING_MAX_AGE_MS;
|
|
382
|
+
const animal = fresh ? speciesLabel(st.grade, team.lang) : null;
|
|
383
|
+
return `${c(GRAY)}${team.label}${animal ? ` ${animal}` : ''}${c(RESET)}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
368
386
|
/**
|
|
369
387
|
* Harness 🅷 segment builder — shared by the full report and the no-session
|
|
370
388
|
* fallback line. Best-effort: never throws into the statusline (corrupted
|
|
@@ -532,7 +550,7 @@ function buildCapWarnSeg(capWarn, c, isIcon, g, color, mode) {
|
|
|
532
550
|
* harness, model, and effort chips still render around the "no session data"
|
|
533
551
|
* note.
|
|
534
552
|
*/
|
|
535
|
-
export function formatNoSession({ caps = null, model = null, effort = null, windowLabel = '', version = '', update = null } = {}, { color = true, mode = 'icon', segments = null } = {}) {
|
|
553
|
+
export function formatNoSession({ caps = null, model = null, effort = null, windowLabel = '', version = '', update = null, team = null } = {}, { color = true, mode = 'icon', segments = null } = {}) {
|
|
536
554
|
const c = (v) => (color ? v : '');
|
|
537
555
|
// narrow shares the icon layout and swaps only the glyphs, so both modes take
|
|
538
556
|
// the same branches from here on.
|
|
@@ -554,6 +572,8 @@ export function formatNoSession({ caps = null, model = null, effort = null, wind
|
|
|
554
572
|
const effortSeg = buildEffortSeg(effort, c, isIcon, g);
|
|
555
573
|
if (effortSeg) segs.push(effortSeg);
|
|
556
574
|
segs.push(`${c(GRAY)}${g.hit} no session data${windowLabel ? ` · ${windowLabel}` : ''}${c(RESET)}`);
|
|
575
|
+
const teamSeg = buildTeamSeg(team, c);
|
|
576
|
+
if (teamSeg) segs.push(teamSeg);
|
|
557
577
|
if (versionSeg && !(update && update.available)) segs.push(versionSeg);
|
|
558
578
|
return segs.join(' · ') + (color ? '\x1b[K' : '');
|
|
559
579
|
}
|
|
@@ -1157,7 +1177,9 @@ export const DEFAULT_SEGMENT_ORDER = [
|
|
|
1157
1177
|
'hit', 'saved', 'period',
|
|
1158
1178
|
// Calendar month.
|
|
1159
1179
|
'month',
|
|
1160
|
-
'harness', 'korean',
|
|
1180
|
+
'harness', 'korean',
|
|
1181
|
+
// Joined machines only: member id and office animal. Identity, so it sits with version.
|
|
1182
|
+
'team', 'version',
|
|
1161
1183
|
];
|
|
1162
1184
|
|
|
1163
1185
|
// Legacy `--segments` aliases: `5h` ↔ `five_hour`, `7d` ↔ `seven_day`.
|
|
@@ -1186,6 +1208,7 @@ function segmentsNamed(name, s, usageByKey) {
|
|
|
1186
1208
|
case 'rules': return [s.rules];
|
|
1187
1209
|
case 'subagents': return [s.subagents];
|
|
1188
1210
|
case 'korean': return [s.korean];
|
|
1211
|
+
case 'team': return [s.team];
|
|
1189
1212
|
case 'model': return [s.model];
|
|
1190
1213
|
case 'effort': return [s.effort];
|
|
1191
1214
|
// The routing chips (already empty when a headline line owns the story), then
|
|
@@ -1298,7 +1321,7 @@ function buildHeadlines(delegated, doc2md, want, eol) {
|
|
|
1298
1321
|
* @param {boolean} [opts.verbose=false] - longer layout with labels
|
|
1299
1322
|
* @param {boolean} [opts.timer=true] - show TTL countdown segment
|
|
1300
1323
|
* @param {'text'|'icon'} [opts.mode='text'] - label style. 'icon' uses 🧠 ⏳ 💰 instead of word labels.
|
|
1301
|
-
* @param {string[]|null} [opts.segments] - segments to render, in the order given. Names: cap-warn, spike, version, harness (a full score with no warning draws only when named here), korean (draws only when named here), model, effort, hit, ttl, month, saved, delegated, doc2md, star, ctx, period, `subagents` (this session's subagent runs; also rides with `delegated`), `rules` (only when named here), `usage` (every rate-limit window), plus per-window keys (`five_hour`, `seven_day`, …). `5h`/`7d` are kept as aliases for back-compat. cap-warn and spike keep the lead regardless of where they appear. Null/undefined = all, in the default order.
|
|
1324
|
+
* @param {string[]|null} [opts.segments] - segments to render, in the order given. Names: cap-warn, spike, version, harness (a full score with no warning draws only when named here), korean (draws only when named here), team (member id and office animal; joined machines only), model, effort, hit, ttl, month, saved, delegated, doc2md, star, ctx, period, `subagents` (this session's subagent runs; also rides with `delegated`), `rules` (only when named here), `usage` (every rate-limit window), plus per-window keys (`five_hour`, `seven_day`, …). `5h`/`7d` are kept as aliases for back-compat. cap-warn and spike keep the lead regardless of where they appear. Null/undefined = all, in the default order.
|
|
1302
1325
|
* @param {boolean} [opts.singleLine=false] - force the legacy one-line layout. By default, when the delegation ledger has lifetime savings, the routing totals lead on their own first line and everything else moves to line 2 (Claude Code renders multi-line statuslines; `--single-line` is the escape hatch for terminals that only show the first line).
|
|
1303
1326
|
*/
|
|
1304
1327
|
export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null, singleLine = false } = {}) {
|
|
@@ -1374,6 +1397,8 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
1374
1397
|
// Version / upgrade chip. Read from a cache written by a detached background
|
|
1375
1398
|
// check — this render path never touches the network.
|
|
1376
1399
|
version: buildVersionSeg(options.version, data.update, c, isIcon, verbose, g),
|
|
1400
|
+
// Member id and office animal, from team.json; null unless this machine joined a team.
|
|
1401
|
+
team: buildTeamSeg(data.team, c),
|
|
1377
1402
|
// Delegation-rule registry (`rules 10 · ⚠1`), opt-in through `--segments`.
|
|
1378
1403
|
rules: buildRulesSeg(data.ruleStats, c, g),
|
|
1379
1404
|
};
|
package/src/member-id.js
CHANGED
|
@@ -32,6 +32,11 @@ function readValid(file) {
|
|
|
32
32
|
} catch (e) { debug('member-id', e); return null; }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** The stored id, or null. Read-only: the statusline must not create files. */
|
|
36
|
+
export function readMemberId({ dir = userDataDir() } = {}) {
|
|
37
|
+
return readValid(memberIdPath(dir));
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
// Never throws: a failed write still returns the id it generated, and the
|
|
36
41
|
// cache hands the same one back on the next call.
|
|
37
42
|
export function memberId({ dir = userDataDir(), random = () => randomBytes(4).toString('hex') } = {}) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* office-species — the animal each member gets in the team's virtual office
|
|
3
|
+
* ("Sprag Town"), as the statusline shows it: an emoji and a name per grade.
|
|
4
|
+
*
|
|
5
|
+
* The keys are the GRADES keys of site/assets/scene/office-scene.js plus
|
|
6
|
+
* 'ghost' (no uploaded data), and the names are the `of.g.<key>` strings of
|
|
7
|
+
* site/team/index.html. test/office-species.test.js pins both, so the console
|
|
8
|
+
* and the terminal cannot drift apart.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SPECIES = {
|
|
12
|
+
dragon: { emoji: '🐉', en: 'Dragon', ko: '용' },
|
|
13
|
+
unicorn: { emoji: '🦄', en: 'Unicorn', ko: '유니콘' },
|
|
14
|
+
lion: { emoji: '🦁', en: 'Lion', ko: '사자' },
|
|
15
|
+
tiger: { emoji: '🐯', en: 'Tiger', ko: '호랑이' },
|
|
16
|
+
eagle: { emoji: '🦅', en: 'Eagle', ko: '독수리' },
|
|
17
|
+
wolf: { emoji: '🐺', en: 'Wolf', ko: '늑대' },
|
|
18
|
+
cheetah: { emoji: '🐆', en: 'Cheetah', ko: '치타' },
|
|
19
|
+
fox: { emoji: '🦊', en: 'Fox', ko: '여우' },
|
|
20
|
+
owl: { emoji: '🦉', en: 'Owl', ko: '부엉이' },
|
|
21
|
+
husky: { emoji: '🐕', en: 'Husky', ko: '허스키' },
|
|
22
|
+
panda: { emoji: '🐼', en: 'Panda', ko: '판다' },
|
|
23
|
+
koala: { emoji: '🐨', en: 'Koala', ko: '코알라' },
|
|
24
|
+
penguin: { emoji: '🐧', en: 'Penguin', ko: '펭귄' },
|
|
25
|
+
raccoon: { emoji: '🦝', en: 'Raccoon', ko: '너구리' },
|
|
26
|
+
cat: { emoji: '🐱', en: 'Cat', ko: '고양이' },
|
|
27
|
+
dog: { emoji: '🐶', en: 'Dog', ko: '강아지' },
|
|
28
|
+
bear: { emoji: '🐻', en: 'Bear', ko: '곰' },
|
|
29
|
+
monkey: { emoji: '🐵', en: 'Monkey', ko: '원숭이' },
|
|
30
|
+
deer: { emoji: '🦌', en: 'Deer', ko: '사슴' },
|
|
31
|
+
elephant: { emoji: '🐘', en: 'Elephant', ko: '코끼리' },
|
|
32
|
+
pig: { emoji: '🐷', en: 'Pig', ko: '돼지' },
|
|
33
|
+
sheep: { emoji: '🐑', en: 'Sheep', ko: '양' },
|
|
34
|
+
rabbit: { emoji: '🐰', en: 'Rabbit', ko: '토끼' },
|
|
35
|
+
duck: { emoji: '🦆', en: 'Duck', ko: '오리' },
|
|
36
|
+
hedgehog: { emoji: '🦔', en: 'Hedgehog', ko: '고슴도치' },
|
|
37
|
+
squirrel: { emoji: '🐿️', en: 'Squirrel', ko: '다람쥐' },
|
|
38
|
+
hamster: { emoji: '🐹', en: 'Hamster', ko: '햄스터' },
|
|
39
|
+
frog: { emoji: '🐸', en: 'Frog', ko: '개구리' },
|
|
40
|
+
mouse: { emoji: '🐭', en: 'Mouse', ko: '생쥐' },
|
|
41
|
+
chick: { emoji: '🐤', en: 'Chick', ko: '병아리' },
|
|
42
|
+
ghost: { emoji: '👻', en: 'Ghost', ko: '유령' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** `"<emoji> <name>"` for a grade key in the given language, or null for an unknown key. */
|
|
46
|
+
export function speciesLabel(grade, lang = 'en') {
|
|
47
|
+
const s = Object.prototype.hasOwnProperty.call(SPECIES, grade) ? SPECIES[grade] : null;
|
|
48
|
+
return s ? `${s.emoji} ${lang === 'ko' ? s.ko : s.en}` : null;
|
|
49
|
+
}
|
package/src/team-sync.js
CHANGED
|
@@ -28,7 +28,8 @@ import { release as osRelease } from 'node:os';
|
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
import { userDataDir } from './paths.js';
|
|
30
30
|
import { writeStateFile } from './state-file.js';
|
|
31
|
-
import { memberId } from './member-id.js';
|
|
31
|
+
import { memberId, readMemberId } from './member-id.js';
|
|
32
|
+
import { userLanguage } from './config.js';
|
|
32
33
|
import { debug } from './debug.js';
|
|
33
34
|
import { loadLedger, modelFamily, roundUsd } from './savings-ledger.js';
|
|
34
35
|
import { sessionCost, pricedParts } from './claude-price.js';
|
|
@@ -72,6 +73,45 @@ export function saveTeamState(state, dir = userDataDir()) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The member's animal from the virtual office, stored as `standing` in team.json
|
|
78
|
+
* for the statusline chip. Best effort: a server older than member_standing
|
|
79
|
+
* (PGRST202) or any failure leaves the state untouched and never fails the caller.
|
|
80
|
+
*/
|
|
81
|
+
async function refreshStanding({ memberKey, dir, fetch, now }) {
|
|
82
|
+
try {
|
|
83
|
+
const r = await rpc('member_standing', { member_key: memberKey }, { fetch });
|
|
84
|
+
if (!r || typeof r !== 'object' || typeof r.grade !== 'string') return;
|
|
85
|
+
const cur = loadTeamState(dir);
|
|
86
|
+
if (!isJoined(cur) || cur.memberKey !== memberKey) return;
|
|
87
|
+
saveTeamState({
|
|
88
|
+
...cur,
|
|
89
|
+
standing: { grade: r.grade, tier: r.tier ?? null, rank: Number.isFinite(r.rank) ? r.rank : null, ranked: Number.isFinite(r.ranked) ? r.ranked : null, crown: !!r.crown, fetchedAt: now },
|
|
90
|
+
}, dir);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
debug('team-sync', e); // PGRST202 "Could not find the function": an older server, ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* What the statusline `team` chip needs, from team.json and the member-id file
|
|
98
|
+
* alone: no network and no file creation. Null when not joined (or left).
|
|
99
|
+
*/
|
|
100
|
+
export function readTeamChip({ dir = userDataDir() } = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const state = loadTeamState(dir);
|
|
103
|
+
if (!isJoined(state)) return null;
|
|
104
|
+
const label = String(state.displayName || '').trim() || readMemberId({ dir });
|
|
105
|
+
if (!label) return null;
|
|
106
|
+
const st = state.standing;
|
|
107
|
+
return {
|
|
108
|
+
label,
|
|
109
|
+
lang: userLanguage(),
|
|
110
|
+
standing: st && typeof st.grade === 'string' ? { grade: st.grade, fetchedAt: Number(st.fetchedAt) || 0 } : null,
|
|
111
|
+
};
|
|
112
|
+
} catch (e) { debug('team-sync', e); return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
75
115
|
export function teamSyncDisabled(env = process.env) { return envVar('NO_TEAM_SYNC', env) === '1'; }
|
|
76
116
|
export function isJoined(state) { return !!(state && state.memberKey && !state.left); }
|
|
77
117
|
|
|
@@ -433,7 +473,9 @@ export async function joinTeam({
|
|
|
433
473
|
lastSyncRows: 0,
|
|
434
474
|
};
|
|
435
475
|
saveTeamState(state, dir);
|
|
436
|
-
|
|
476
|
+
const out = await reportDeviceInfo({ state, dir, fetch, env });
|
|
477
|
+
await refreshStanding({ memberKey: r.member_key, dir, fetch, now });
|
|
478
|
+
return out;
|
|
437
479
|
}
|
|
438
480
|
|
|
439
481
|
export async function leaveTeam({ fetch, dir = userDataDir(), now = Date.now() } = {}) {
|
|
@@ -566,6 +608,7 @@ export async function syncTeam({
|
|
|
566
608
|
delete next.lastError;
|
|
567
609
|
saveTeamState(next, dir);
|
|
568
610
|
refreshTeamRatchetFiles({ dir });
|
|
611
|
+
await refreshStanding({ memberKey: state.memberKey, dir, fetch, now });
|
|
569
612
|
const prompts = await syncPrompts({ state: next, dir, fetch, now });
|
|
570
613
|
return { uploaded: rows.length, rules: rules.length, ...(prompts !== null ? { prompts } : {}) };
|
|
571
614
|
} catch (e) {
|