sprag-cli 3.68.0 → 3.70.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/NOTICE CHANGED
@@ -31,5 +31,8 @@ presets/korean-style/fluent-korean.md
31
31
  Trademarks
32
32
 
33
33
  Section 6 of the License grants no rights in the project's names or marks.
34
- "sprag" and the sprag logo are not licensed for use in a way that states or
35
- implies endorsement of a fork or a derived distribution.
34
+ "sprag", the sprag logo and the domain sprag.io are not licensed for use in a
35
+ way that states or implies endorsement of a fork or a derived distribution, or
36
+ as the name of a fork, a modified build or a hosted service. The permitted uses
37
+ are listed in TRADEMARK.md at the root of the repository
38
+ (https://github.com/rootstudioyaml/sprag/blob/main/TRADEMARK.md).
package/README.ko.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
6
6
  </picture>
7
7
 
8
- **Claude Code와 Codex가 쓰는 토큰을 첫 세션부터 줄입니다**
8
+ **Claude Code와 Codex에 붙이는 모델 위임 라우터. 조회·확인·테스트 실행은 더 싼 모델에 보내고, 아낀 금액은 상태줄에 보여 줍니다.**
9
9
 
10
10
  [![npm](https://img.shields.io/npm/v/sprag-cli.svg?label=sprag-cli)](https://www.npmjs.com/package/sprag-cli)
11
11
  [![downloads](https://img.shields.io/npm/dm/sprag-cli.svg)](https://www.npmjs.com/package/sprag-cli)
@@ -26,11 +26,11 @@
26
26
 
27
27
  ---
28
28
 
29
- **일하는 방식은 그대로 두고 토큰만 줄입니다.** 조회와 확인, 테스트 실행은 저렴한 모델에 넘기고, 문서는 가벼운 Markdown으로 바꿔 넣습니다(54만 토큰짜리 발표 자료가 2.3만 토큰이 됩니다). 세션마다 쓴 금액과 아낀 금액은 상태줄에 바로 나타납니다. `npm i -g sprag-cli` 하나로 설정이 끝나며, 프로젝트마다 따로 설정할 것은 없습니다.
29
+ **Sprag는 일마다 그 일을 한 번도 틀린 적 없는 가장 싼 모델에게 보냅니다.** Claude Code와 Codex는 프롬프트마다 모델을 바꿀 수 없으므로, 통하는 방법 하나를 씁니다. 메인 세션의 규칙이 조회, 상태 확인, 빌드·테스트 실행, 일괄 수정을 더 싼 모델로 고정한 서브에이전트에 넘깁니다. 규칙은 사용자의 세션 기록에서 나오고 하나씩 승인을 받으며, 위임한 실행마다 가격 차액을 원장에 적습니다. `npm i -g sprag-cli` 하나로 설정이 끝나며, 프로젝트마다 따로 설정할 것은 없습니다.
30
30
 
31
- **에이전트는 그대로 일하고, 퇴행만 막습니다.** 이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 그대로 통과시키고, 역회전하는 순간 잠기는 부품입니다.
31
+ 두 가지가 함께 따라옵니다. 문서는 가벼운 Markdown으로 바뀌어 컨텍스트에 들어가고(54만 토큰짜리 발표 자료가 2.3만 토큰이 됩니다), 상태줄은 세션마다 쓴 금액과 아낀 금액을 보여 주다가 캐시·컨텍스트·사용량 한도에 문제가 생기면 경고 칩을 맨 앞에 띄웁니다.
32
32
 
33
- Sprag는 Claude Code와 Codex가 남기는 세션 기록을 읽어 반복 실패와 위임 후보를 찾습니다. 사용자가 승인한 규칙은 다음 세션에도 적용하며, 위임 기록과 모델 단가를 확인할 수 있으면 예상 차액을 원장에 적습니다. 캐시와 사용량 한도 문제는 상태줄이나 패널에서 확인할 수 있습니다.
33
+ 이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 통과시키고 역회전하는 순간 잠기는 부품입니다. 기록에서 반복되는 실패는 세션 시작 때 불러오는 규칙이 되어, 에이전트는 그대로 일하고 퇴행만 걸립니다.
34
34
 
35
35
  세션 분석은 로컬에서 실행하므로 추가 모델 호출이나 API 키가 필요하지 않습니다. Codex의 선택 기능인 예산·단가 조회는 기존 인증 정보로 사용자가 설정한 LiteLLM에 접속합니다. CLI는 Node.js 20 이상과 TOML 파서 하나를 사용합니다.
36
36
 
@@ -250,6 +250,16 @@ sprag feedback "Bedrock에서 5m TTL 칩이 사라지지 않아요"
250
250
 
251
251
  버그를 제보하실 때는 도구 버전(`sprag --version`)과 OS 를 함께 적어 주십시오. statusline 이나 경고 관련 문제라면 statusline 출력이나 `sprag last` 결과를 덧붙여 주시면 원인을 빨리 찾을 수 있습니다.
252
252
 
253
+ ## 팀
254
+
255
+ Sprag 은 켜지 않으면 로컬에서만 동작합니다. `sprag team join <토큰>` 을 실행하거나(조직 전체는 `SPRAG_TEAM_TOKEN`) 하면 컴퓨터마다 하루에 한 행씩 올라갑니다. 내용은 지출, 절감액, 위임 실행 횟수, 모델 계열 비중입니다. 프롬프트, 대화 기록, 파일 경로, 프로젝트 이름은 보내지 않으며, `sprag team status --show-payload` 로 올라갈 내용을 그대로 볼 수 있습니다. 그 대신 팀 소유자는 대시보드에서 구성원별 합계를 보고, 모든 구성원에게 자동으로 적용될 래칫 룰을 승인할 수 있습니다.
256
+
257
+ `CTS_NO_TEAM_SYNC=1` 은 팀 관련 통신을 모두 끄고, `sprag team leave` 는 참여를 해제합니다.
258
+
259
+ - [팀 기능 안내](docs/TEAM.ko.md)
260
+ - 대시보드: https://sprag.io/team/
261
+ - 플랜과 가격: https://sprag.io/pricing/
262
+
253
263
  ## 라이선스
254
264
 
255
265
  Apache License 2.0 입니다. 원문은 [LICENSE](https://github.com/rootstudioyaml/sprag/blob/main/LICENSE) 에 있고, 그 라이선스가 요구하는 출처 표기는 [NOTICE](https://github.com/rootstudioyaml/sprag/blob/main/NOTICE) 에 정리했습니다.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
6
6
  </picture>
7
7
 
8
- **Cut the tokens Claude Code and Codex spend, from the first session.**
8
+ **A model-delegation router for Claude Code and Codex: lookups, checks and test runs go to cheaper models, and the savings show up in your statusline.**
9
9
 
10
10
  [![npm](https://img.shields.io/npm/v/sprag-cli.svg?label=sprag-cli)](https://www.npmjs.com/package/sprag-cli)
11
11
  [![downloads](https://img.shields.io/npm/dm/sprag-cli.svg)](https://www.npmjs.com/package/sprag-cli)
@@ -26,20 +26,22 @@ Same 12 requests, two terminals: **$6.68 → $1.83 (−72.6%)** with Sprag.<br>
26
26
 
27
27
  ---
28
28
 
29
- **Sprag saves tokens without changing how you work.** Lookups, checks and test
30
- runs go to a cheaper model, documents reach the context as compact Markdown (a
31
- 540k-token deck becomes 23k tokens), and the statusline shows what each session
32
- spent and saved while it runs. `npm i -g sprag-cli` sets it all up; nothing to
33
- configure per project.
29
+ **Sprag routes each piece of work to the cheapest model that has never got it
30
+ wrong.** Claude Code and Codex cannot switch models per prompt, so Sprag does it
31
+ the one way that works: rules in the main session hand lookups, status checks,
32
+ build and test runs and batch edits to sub-agents pinned to a cheaper model. The
33
+ rules come from your own session logs, you approve each one, and every delegated
34
+ run writes its price difference to a ledger. `npm i -g sprag-cli` sets it all
35
+ up; nothing to configure per project.
34
36
 
35
- Sprag is named after the sprag clutch: forward motion passes, backspin locks.
36
- Your agent keeps working, it just stops regressing.
37
+ Two more things ride along. Documents reach the context as compact Markdown (a
38
+ 540k-token deck becomes 23k tokens), and the statusline shows what each session
39
+ spent and saved, with a warning chip in front when the cache, the context or a
40
+ rate limit needs attention.
37
41
 
38
- It reads the sessions you already ran, and what it finds there holds for every
39
- session after. Repeated failures become rules loaded at session start. Work that
40
- a cheaper tier has never got wrong goes to a sub-agent on that tier, and every
41
- delegated run writes its own price difference to a ledger. Cache and rate-limit
42
- trouble reaches your statusline while you can still act on it.
42
+ The name is the sprag clutch: forward motion passes, backspin locks. Repeated
43
+ failures in your logs become rules loaded at session start, so the agent keeps
44
+ working and only the regressions get caught.
43
45
 
44
46
  Session analysis runs locally on the logs Claude Code and Codex already write.
45
47
  No extra model calls or API key are needed for analysis. Optional Codex gateway
@@ -302,6 +304,16 @@ It files a GitHub issue through the `gh` CLI when one is authenticated. Otherwis
302
304
 
303
305
  For a bug, please include the tool version (`sprag --version`) and your OS. For a statusline or warning issue, add the statusline output or the `sprag last` result.
304
306
 
307
+ ## Teams
308
+
309
+ 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. Prompts, transcripts, file paths and project names are never sent, 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.
310
+
311
+ `CTS_NO_TEAM_SYNC=1` turns all team traffic off, and `sprag team leave` removes the membership.
312
+
313
+ - [Team layer guide](docs/TEAM.md)
314
+ - Dashboard: https://sprag.io/team/
315
+ - Plans and pricing: https://sprag.io/pricing/
316
+
305
317
  ## License
306
318
 
307
319
  Apache License 2.0. The full text is in [LICENSE](https://github.com/rootstudioyaml/sprag/blob/main/LICENSE), and the attributions
package/bin/cli.js CHANGED
@@ -59,6 +59,7 @@ import { chipForIssues } from '../src/advice.js';
59
59
  import { debug } from '../src/debug.js';
60
60
  import { createArgs } from '../src/cli-args.js';
61
61
  import { readUpdateChip } from '../src/update-notice.js';
62
+ import { readStarChip } from '../src/star-note.js';
62
63
  import { selectAgent } from '../src/agent.js';
63
64
 
64
65
  const selection = (() => {
@@ -101,7 +102,7 @@ function segmentsFromArgs() {
101
102
  const KNOWN_SUBCOMMANDS = new Set([
102
103
  'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
103
104
  'doc2md', 'delegate', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
104
- 'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved', 'cowork',
105
+ 'seed', 'litellm-budget', 'profile-map', 'feedback', 'saved', 'cowork', 'team',
105
106
  ]);
106
107
 
107
108
  // Flags each entry path reads, collected from the hasFlag/getArg/numArg calls in
@@ -206,6 +207,7 @@ Usage:
206
207
  sprag update-check is a newer version out?
207
208
  sprag brief --hook internal: the UserPromptSubmit hook entry point
208
209
  sprag feedback "<msg>" file a bug report / feature request (no browser needed)
210
+ sprag team create|join|status|sync|leave opt-in team layer: daily aggregates only (CTS_NO_TEAM_SYNC=1 turns it off)
209
211
  sprag upgrade install the latest release
210
212
  sprag --install-hook install cache-monitor PostToolUse hook
211
213
  sprag --uninstall-hook remove that hook
@@ -301,6 +303,12 @@ async function main() {
301
303
  return (await import('../src/commands/feedback.js')).run({ args, getArg, version: PKG_VERSION, dataDir: userDataDir() });
302
304
  }
303
305
 
306
+ // Subcommand: team — opt-in team layer (join with an invite token, daily
307
+ // aggregates only, team rules). See src/commands/team.js and src/team-sync.js.
308
+ if (args[0] === 'team') {
309
+ return (await import('../src/commands/team.js')).run({ args, hasFlag, getArg, version: PKG_VERSION });
310
+ }
311
+
304
312
  if (args[0] === 'handoff') {
305
313
  return (await import('../src/commands/handoff.js')).run({ getArg });
306
314
  }
@@ -940,6 +948,11 @@ async function main() {
940
948
  options: { days, windowHours, windowLabel, version: PKG_VERSION },
941
949
  // Cached-only; the background refresh it may trigger lands on a later render.
942
950
  update: format === 'statusline' ? readUpdateChip(PKG_VERSION) : null,
951
+ // The totals are already in hand above; passing them spares a second read of
952
+ // both ledgers on every render.
953
+ star: format === 'statusline'
954
+ ? readStarChip({ savedUsd: delegationSaved + (Number(doc2mdTotals?.total) || 0) })
955
+ : null,
943
956
  lastActivity,
944
957
  monthSpend: monthSpendInfo,
945
958
  modelShare,
@@ -998,8 +1011,29 @@ async function main() {
998
1011
  singleLine: hasFlag('--single-line'),
999
1012
  });
1000
1013
  } else {
1014
+ // The first screen is the pitch: what share of recent work a cheaper model
1015
+ // could take. The scan is local and writes only sprag's own cache file;
1016
+ // --project skips it because the scan covers all projects, not one.
1017
+ let routing = null;
1018
+ if (!projectFilter && process.env.CTS_NO_ROUTE_SCAN !== '1') {
1019
+ try {
1020
+ const rs = await import('../src/route-scan.js');
1021
+ let cache = rs.readRouteScan();
1022
+ if (!cache) {
1023
+ process.stderr.write('Scanning for delegation candidates (first run)...\n');
1024
+ cache = await rs.runRouteScan({ days: 14 });
1025
+ }
1026
+ routing = {
1027
+ days: cache.days,
1028
+ total: cache.totalEpisodes,
1029
+ easy: cache.easyEpisodes,
1030
+ scannedAt: cache.scannedAt,
1031
+ candidates: rs.openCandidates(cache).slice(0, 3),
1032
+ };
1033
+ } catch (e) { debug('report:routing', e); }
1034
+ }
1001
1035
  const { formatReport } = await import('../src/formatters/table.js');
1002
- output = formatReport(data, { color: tableColor() });
1036
+ output = formatReport({ ...data, routing }, { color: tableColor() });
1003
1037
  }
1004
1038
 
1005
1039
  if (format === 'statusline') {
package/docs/CODEX.ko.md CHANGED
@@ -200,6 +200,7 @@ sprag delegate shared status --agent codex
200
200
  # 자동으로 정해진 난도별 모델을 바꾸고 싶을 때만:
201
201
  sprag delegate shared map T2 --from PARENT_MODEL --model SMALL_MODEL --effort high --agent codex
202
202
  sprag delegate shared map T1 --from PARENT_MODEL --model MEDIUM_MODEL --effort medium --agent codex
203
+ sprag delegate shared map T1H --from PARENT_MODEL --model LARGE_MODEL --effort high --agent codex
203
204
  ```
204
205
 
205
206
  등록 전에 전역과 프로젝트 범위를 선택합니다. 프로젝트에만 적용하려면 `--global` 대신
package/docs/CODEX.md CHANGED
@@ -302,6 +302,7 @@ sprag delegate shared status --agent codex
302
302
  # optional, to override the automatic tier models:
303
303
  sprag delegate shared map T2 --from PARENT_MODEL --model SMALL_MODEL --effort high --agent codex
304
304
  sprag delegate shared map T1 --from PARENT_MODEL --model MEDIUM_MODEL --effort medium --agent codex
305
+ sprag delegate shared map T1H --from PARENT_MODEL --model LARGE_MODEL --effort high --agent codex
305
306
  ```
306
307
 
307
308
  The map lines are optional. A tier without a mapping resolves automatically,
@@ -0,0 +1,119 @@
1
+ # 팀 기능
2
+
3
+ [← README](../README.ko.md) · [English](./TEAM.md)
4
+
5
+ Sprag 은 기본적으로 로컬에서만 동작합니다. 팀 기능은 켜야만 쓰이는 선택 기능입니다. 팀원마다 하루 지출과 절감액을 한 대시보드에서 보고, 팀 소유자가 승인한 래칫 룰을 함께 쓸 수 있습니다. `sprag team join <토큰>` 을 실행하거나 환경에 `SPRAG_TEAM_TOKEN` 을 지정하기 전에는 아무것도 전송하지 않습니다. `CTS_NO_TEAM_SYNC=1` 을 지정하면 다른 설정과 상관없이 팀 관련 네트워크 통신이 모두 꺼집니다.
6
+
7
+ ## 보내는 것과 보내지 않는 것
8
+
9
+ 참여하면 Sprag 은 로컬 날짜 기준으로 하루에 한 행씩 올립니다.
10
+
11
+ | 항목 | 내용 |
12
+ |---|---|
13
+ | 지출(USD) | 로컬 세션 로그로 계산한 금액 |
14
+ | 절감(USD) | 라우팅 원장과 doc2md 원장에서 가져온 금액 |
15
+ | 위임 실행 횟수 | 그날 위임한 실행의 수 |
16
+ | 모델 계열 비중 | 위임 실행 중 모델 계열별 비율 |
17
+ | Sprag 버전 | 설치된 버전 |
18
+ | 에이전트 | `claude` 또는 `codex` |
19
+ | 표시 이름 | 참여할 때 정한 이름이며, 기본값은 OS 사용자 이름 |
20
+
21
+ 보내지 않는 것은 프롬프트, 대화 기록, 파일 경로, 프로젝트 이름, 룰 내용, API 키입니다. 룰은 직접 `sprag team propose` 를 실행할 때만 밖으로 나갑니다.
22
+
23
+ 업로드는 6시간에 한 번 이하로 실행됩니다. SessionStart 훅이 띄운 분리된 자식 프로세스가 보내며, statusline 을 그리는 경로에서는 절대 실행되지 않습니다.
24
+
25
+ 목적지는 `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` 입니다(Supabase, ap-northeast-2 리전). 호출하는 RPC 는 `team_join`, `ledger_upsert`, `team_rules_get`, `team_rule_propose`, `team_leave` 입니다.
26
+
27
+ ## CLI에서 팀 만들기
28
+
29
+ 웹 대시보드 없이 팀을 만들 수 있습니다. 로그인은 메일로 받는 6자리 코드(15분간 유효)로 하며, Sprag 은 로그인 세션을 저장하지 않습니다.
30
+
31
+ 터미널에서는 명령 하나를 실행하고 묻는 대로 답하면 됩니다 (이메일, 코드, 팀 이름).
32
+
33
+ ```bash
34
+ sprag team create
35
+ ```
36
+
37
+ 에이전트나 터미널이 없는 셸에서는 두 단계로 나눕니다.
38
+
39
+ ```bash
40
+ sprag team create --email you@example.com --name "Acme"
41
+ # 메일로 코드가 오면 이어서 실행합니다
42
+ sprag team create --email you@example.com --name "Acme" --code 123456
43
+ ```
44
+
45
+ 첫 단계는 코드만 보냅니다. 두 번째 단계에서 코드를 확인하고 팀을 만든 뒤 초대 토큰, 멤버용 명령(`npm i -g sprag-cli@latest && sprag team join <토큰>`), 조직용 환경 변수(`export SPRAG_TEAM_TOKEN=<토큰>`), 대시보드 주소를 출력합니다. 같은 이메일로 https://sprag.io/team/ 에 로그인하면 팀을 관리할 수 있습니다.
46
+
47
+ 기본적으로 이 PC가 첫 멤버로 참여하고 동기화를 한 번 실행합니다. 참여하지 않으려면 `--no-join` 을 붙입니다. 이미 팀에 참여한 PC에서는 먼저 `sprag team leave` 를 실행하거나 `--no-join` 을 붙여야 합니다.
48
+
49
+ ## 개인으로 참여하기
50
+
51
+ 팀 소유자에게 초대 토큰을 받은 뒤 실행합니다.
52
+
53
+ ```bash
54
+ sprag team join <토큰> --name "미나"
55
+ ```
56
+
57
+ Sprag 이 올라갈 항목 목록을 보여 주고 확인을 묻습니다. `--name` 은 표시 이름이며 기본값은 OS 사용자 이름입니다. 터미널이 없는 셸에서는 `--yes` 를 붙여 확인을 대신합니다. 토큰을 생략하면 `SPRAG_TEAM_TOKEN` 을 읽습니다.
58
+
59
+ 결과는 `sprag team status` 로 확인합니다.
60
+
61
+ ## 조직 단위로 등록하기
62
+
63
+ 구성원이 각자 명령을 실행하지 않아도 모든 컴퓨터를 등록하려면 환경에 `SPRAG_TEAM_TOKEN=<토큰>` 을 지정합니다. 흔한 위치는 두 곳입니다.
64
+
65
+ - 셸 프로필: `export SPRAG_TEAM_TOKEN=<토큰>`
66
+ - Claude Code 관리 설정: `{"env":{"SPRAG_TEAM_TOKEN":"…"}}`
67
+
68
+ 컴퓨터는 다음 세션이 시작될 때 참여합니다. 그다음 첫 세션에는 팀 이름과 두 명령을 알리는 한 줄 안내가 나옵니다. `sprag team status --show-payload` 는 보내는 내용을 보여 주고, `sprag team leave` 는 전송을 멈춥니다.
69
+
70
+ `leave` 는 환경 변수보다 우선합니다. 팀에서 나온 컴퓨터는 변수가 남아 있어도 저절로 다시 참여하지 않습니다. 소유자가 새 토큰을 발급하고 구성원이 `sprag team join` 을 다시 실행해야 다시 참여합니다.
71
+
72
+ ## 팀 룰
73
+
74
+ 팀 소유자는 대시보드에서 룰을 승인합니다. 구성원도 룰을 제안할 수 있습니다.
75
+
76
+ ```bash
77
+ sprag team propose "커밋 전에 lint 실행" --scope ratchet
78
+ ```
79
+
80
+ `--scope` 는 `ratchet`(기본값) 또는 `model` 입니다. 제안은 소유자가 승인할 때까지 대기 상태로 남습니다.
81
+
82
+ 승인된 룰은 다음 동기화 때 내려옵니다. Sprag 은 이를 `~/.claude/ratchet-team.md`(Codex 는 `<CODEX_HOME>/ratchet-team.md`)에 쓰고, 그 파일이 있는 동안 `~/.claude/CLAUDE.md` 의 전역 harness 블록에서 import 합니다. Sprag 이 세션을 시작할 때 파일을 통째로 다시 쓰므로 직접 고치지 마십시오. `sprag team rules` 는 받아 둔 룰을 보여 줍니다. `sprag team leave` 는 멤버 키, 캐시, import 를 지웁니다.
83
+
84
+ ## 상태 확인, 전송 내용 확인, 나가기
85
+
86
+ | 명령 | 설명 |
87
+ |---|---|
88
+ | `sprag team join [토큰] [--name <이름>] [--yes]` | 팀에 참여 |
89
+ | `sprag team status [--show-payload]` | 팀, 플랜, 프롬프트 수집 설정, 표시 이름, 마지막 동기화를 보여 줍니다. `--show-payload` 를 붙이면 올라갈 하루 단위 행도 그대로 출력합니다 |
90
+ | `sprag team sync [--force]` | 지금 업로드하고 룰을 받습니다. `--force` 는 6시간 간격을 무시합니다 |
91
+ | `sprag team rules` | 받아 둔 팀 룰 보기 |
92
+ | `sprag team propose "<내용>" [--scope ratchet\|model]` | 팀에 룰 제안 |
93
+ | `sprag team leave` | 팀에서 나가고 업로드 중단 |
94
+
95
+ ## 대시보드
96
+
97
+ https://sprag.io/team/ 에서 소유자가 이메일 코드로 로그인하고, 팀을 만들고, 초대 토큰을 받습니다. 여기서 최근 30일 동안의 구성원별 지출과 절감액을 보고, 룰을 승인하거나 삭제하고, 구성원을 내보낼 수 있습니다.
98
+
99
+ ## 플랜
100
+
101
+ Free, Team(좌석당 월 $5), 비즈니스(좌석당 월 $10, 프롬프트 수집 포함)가 있습니다. 자세한 내용은 https://sprag.io/pricing/ 에 있습니다.
102
+
103
+ ## 개인정보 관련 참고
104
+
105
+ - 소유자는 구성원별 하루 합계를 봅니다. 세션이나 프롬프트는 보지 못합니다.
106
+ - 구성원은 `sprag team status --show-payload` 로 같은 내용을 직접 볼 수 있습니다.
107
+ - 프롬프트 수집은 비즈니스 정책이며, 소유자가 대시보드에서 켤 수 있습니다. 구성원은 `sprag team status` 에서 설정 상태를 확인합니다. 현재 CLI 는 프롬프트를 아직 올리지 않습니다. 업로드는 이후 별도 릴리스에서 다룹니다.
108
+ - 룰 내용은 `sprag team propose` 를 통해서만 밖으로 나갑니다.
109
+
110
+ ## 파일과 환경 변수
111
+
112
+ | 이름 | 용도 |
113
+ |---|---|
114
+ | `<사용자 데이터 디렉터리>/team.json` | 참여 상태와 멤버 키(권한 0600) |
115
+ | `<사용자 데이터 디렉터리>/team-rules.json` | 마지막 동기화 때 받은 룰 |
116
+ | `~/.claude/ratchet-team.md` | Claude Code 용 승인된 팀 룰이며 `~/.claude/CLAUDE.md` 에서 import 합니다 |
117
+ | `<CODEX_HOME>/ratchet-team.md` | Codex 용 같은 룰 |
118
+ | `SPRAG_TEAM_TOKEN` | 초대 토큰이며, 다음 세션 시작 때 컴퓨터를 등록합니다 |
119
+ | `CTS_NO_TEAM_SYNC=1` | 팀 관련 네트워크 통신을 모두 끕니다 |
package/docs/TEAM.md ADDED
@@ -0,0 +1,119 @@
1
+ # Team layer
2
+
3
+ [← README](../README.md) · [한국어](./TEAM.ko.md)
4
+
5
+ Sprag is local-only by default. The team layer is an opt-in add-on that lets a group see each member's daily spend and savings in one dashboard, and share the ratchet rules the team owner approves. Nothing is sent until you run `sprag team join <token>` or your environment sets `SPRAG_TEAM_TOKEN`. `CTS_NO_TEAM_SYNC=1` turns off all team network traffic, whatever else is configured.
6
+
7
+ ## What is sent and what is not
8
+
9
+ After you join, Sprag uploads one row per local calendar day:
10
+
11
+ | Field | Meaning |
12
+ |---|---|
13
+ | Spend (USD) | priced from your local session logs |
14
+ | Savings (USD) | from the routing and doc2md ledgers |
15
+ | Delegated runs | how many runs were delegated that day |
16
+ | Model-family share | the share of those runs that went to each model family |
17
+ | Sprag version | the installed version |
18
+ | Agent | `claude` or `codex` |
19
+ | Display name | the name you chose at join; the default is your OS user name |
20
+
21
+ Not sent: prompts, transcripts, file paths, project names, rule text, API keys. A rule leaves your machine only when you run `sprag team propose` yourself.
22
+
23
+ The upload runs at most once every 6 hours, from a detached child process that the SessionStart hook spawns. It never runs on the statusline path.
24
+
25
+ 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` and `team_leave`.
26
+
27
+ ## Create a team from the CLI
28
+
29
+ You can set up a team without the web dashboard. Signing in uses a 6-digit code emailed to you (valid 15 minutes). Sprag never stores the sign-in session.
30
+
31
+ In a terminal, run one command and answer the prompts (email, code, team name):
32
+
33
+ ```bash
34
+ sprag team create
35
+ ```
36
+
37
+ From an agent or any shell without a terminal, it takes two steps:
38
+
39
+ ```bash
40
+ sprag team create --email you@example.com --name "Acme"
41
+ # the code arrives by email, then:
42
+ sprag team create --email you@example.com --name "Acme" --code 123456
43
+ ```
44
+
45
+ The first step only sends the code. The second verifies it and creates the team. It then prints the invite token, the member command (`npm i -g sprag-cli@latest && sprag team join <token>`), the organization line (`export SPRAG_TEAM_TOKEN=<token>`) and the dashboard address. Sign in to https://sprag.io/team/ with the same email to manage the team.
46
+
47
+ By default this machine joins as the first member and runs one sync. Pass `--no-join` to skip that. If this machine is already in a team, run `sprag team leave` first or pass `--no-join`.
48
+
49
+ ## Join as an individual
50
+
51
+ The team owner gives you an invite token. Then:
52
+
53
+ ```bash
54
+ sprag team join <token> --name "Mina"
55
+ ```
56
+
57
+ Sprag prints the list of what will be uploaded and asks for confirmation. `--name` sets the display name (default: your OS user name). In a shell without a terminal, pass `--yes` to confirm. If you leave the token out, Sprag reads `SPRAG_TEAM_TOKEN`.
58
+
59
+ Check the result with `sprag team status`.
60
+
61
+ ## Enroll an organization
62
+
63
+ To enroll every machine without each person running a command, set `SPRAG_TEAM_TOKEN=<token>` in the environment. Two common places:
64
+
65
+ - a shell profile: `export SPRAG_TEAM_TOKEN=<token>`
66
+ - Claude Code managed settings: `{"env":{"SPRAG_TEAM_TOKEN":"…"}}`
67
+
68
+ The machine joins at the next session start. The first session after that prints a one-line notice that names the team and the two commands that matter: `sprag team status --show-payload` shows what is sent, and `sprag team leave` stops it.
69
+
70
+ `leave` wins over the environment. A machine that left does not rejoin on its own, even with the variable still set. It rejoins only after the owner issues a new token and the member runs `sprag team join` again.
71
+
72
+ ## Team rules
73
+
74
+ The team owner approves rules in the dashboard. Members can also suggest one:
75
+
76
+ ```bash
77
+ sprag team propose "Run lint before every commit" --scope ratchet
78
+ ```
79
+
80
+ `--scope` is `ratchet` (the default) or `model`. A proposal stays pending until the owner approves it.
81
+
82
+ Approved rules come back with the next sync. Sprag writes them to `~/.claude/ratchet-team.md` (Codex: `<CODEX_HOME>/ratchet-team.md`) and imports that file from the global harness block of `~/.claude/CLAUDE.md` for as long as the file exists. Do not edit the file by hand, because Sprag rewrites it at session start. `sprag team rules` lists the cached rules. `sprag team leave` removes the member key, the cache and the import.
83
+
84
+ ## Status, payload and leaving
85
+
86
+ | Command | What it does |
87
+ |---|---|
88
+ | `sprag team join [token] [--name <n>] [--yes]` | Join a team |
89
+ | `sprag team status [--show-payload]` | Team, plan, prompt-capture flag, display name, last sync. `--show-payload` also prints the exact daily rows that would be uploaded |
90
+ | `sprag team sync [--force]` | Upload now and pull rules. `--force` ignores the 6-hour interval |
91
+ | `sprag team rules` | Show the cached team rules |
92
+ | `sprag team propose "<text>" [--scope ratchet\|model]` | Propose a rule to the team |
93
+ | `sprag team leave` | Leave the team and stop uploading |
94
+
95
+ ## The dashboard
96
+
97
+ https://sprag.io/team/ is where the owner signs in with an email code, creates a team and gets the invite token. From there the owner sees per-member spend and savings for the last 30 days, approves or removes rules, and removes members.
98
+
99
+ ## Plans
100
+
101
+ Free, Team ($5 per seat per month) and Business ($10 per seat per month, with prompt capture). Details are on https://sprag.io/pricing/.
102
+
103
+ ## Privacy notes
104
+
105
+ - The owner sees per-member daily totals, not sessions or prompts.
106
+ - A member sees the same payload with `sprag team status --show-payload`.
107
+ - Prompt capture is a Business policy that the owner can switch on in the dashboard. Members see its state in `sprag team status`. The current CLI does not upload prompts yet; that is a separate, later release.
108
+ - Rule text leaves your machine only through `sprag team propose`.
109
+
110
+ ## Files and environment variables
111
+
112
+ | Name | Purpose |
113
+ |---|---|
114
+ | `<user data dir>/team.json` | Membership state and the member key (mode 0600) |
115
+ | `<user data dir>/team-rules.json` | Rules pulled at the last sync |
116
+ | `~/.claude/ratchet-team.md` | Approved team rules for Claude Code, imported from `~/.claude/CLAUDE.md` |
117
+ | `<CODEX_HOME>/ratchet-team.md` | The same rules for Codex |
118
+ | `SPRAG_TEAM_TOKEN` | Invite token; enrolls the machine at the next session start |
119
+ | `CTS_NO_TEAM_SYNC=1` | Disables all team network traffic |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.68.0",
4
- "description": "Cut Claude Code and Codex token costs: cheaper-model routing, compact Markdown for documents, and a live statusline of spend and savings.",
3
+ "version": "3.70.0",
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": {
7
7
  "sprag": "bin/cli.js",
@@ -14,6 +14,7 @@
14
14
  "publish:legacy": "node scripts/publish-legacy.mjs",
15
15
  "release:notes": "node scripts/release-notes.mjs",
16
16
  "site:docs": "node scripts/build-site-docs.mjs",
17
+ "site:serve": "node scripts/serve-site.mjs",
17
18
  "test": "node --test",
18
19
  "test:mods": "claude plugin test mod/sprag-doc2md && claude plugin test mod/sprag-delegation-meter && claude plugin test mod/sprag-statusline",
19
20
  "version": "node scripts/sync-site-version.mjs"
@@ -30,6 +31,8 @@
30
31
  "README.ko.md",
31
32
  "docs/CODEX.md",
32
33
  "docs/CODEX.ko.md",
34
+ "docs/TEAM.md",
35
+ "docs/TEAM.ko.md",
33
36
  "LICENSE",
34
37
  "NOTICE"
35
38
  ],
@@ -49,6 +52,10 @@
49
52
  "claude-code-statusline",
50
53
  "statusline",
51
54
  "model-routing",
55
+ "model-router",
56
+ "model-delegation",
57
+ "llm-router",
58
+ "claude-code-router",
52
59
  "subagent",
53
60
  "prompt-caching",
54
61
  "haiku",
package/src/agents.js CHANGED
@@ -26,10 +26,12 @@ const AGENT_MODEL = {
26
26
  'haiku-runner': 'haiku',
27
27
  'haiku-translate': 'haiku',
28
28
  'sonnet-worker': 'sonnet',
29
+ 'opus-worker': 'opus',
29
30
  // A T1 rule names its target by tier alone (`agent: 'sonnet'`). Without these
30
31
  // the phrase fell through to the haiku default, and a sonnet rule told the
31
32
  // session to delegate to haiku. resolveAgentName maps them to an agent file.
32
33
  sonnet: 'sonnet',
34
+ opus: 'opus',
33
35
  haiku: 'haiku',
34
36
  };
35
37
 
@@ -50,7 +52,7 @@ export function agentExists(name, root = process.cwd()) {
50
52
  }
51
53
 
52
54
  /** Agent a bare tier name stands for when that agent's file exists. */
53
- const TIER_AGENT = { sonnet: 'sonnet-worker', haiku: 'haiku-explore' };
55
+ const TIER_AGENT = { sonnet: 'sonnet-worker', opus: 'opus-worker', haiku: 'haiku-explore' };
54
56
 
55
57
  /**
56
58
  * The agent a rule target really names on this machine, or null when none is
@@ -36,7 +36,7 @@ export function validateCodexTarget(model, effort) {
36
36
 
37
37
  export function validateCodexSharedTarget({ tier, from, model, effort, provider }) {
38
38
  validateCodexTarget(model, effort);
39
- if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1 or T2.');
39
+ if (!SHARED_RULE_TIERS.includes(tier)) throw new Error('Shared rule tier must be T1, T1H or T2.');
40
40
  if (!safeId(from) || from === model) throw new Error('--from must name a different parent model.');
41
41
  if (!safeId(provider)) throw new Error('A shared model mapping requires a Codex provider ID.');
42
42
  return { tier, from, model, ...(effort === undefined ? {} : { effort }), provider };
@@ -78,7 +78,7 @@ function catalogCodexModels(home) {
78
78
  }
79
79
 
80
80
  /**
81
- * The models Codex resolves T2 and T1 to when the user mapped none, the way
81
+ * The models Codex resolves T2, T1 and T1H to when the user mapped none, the way
82
82
  * Claude Code resolves `haiku` and `sonnet`. Candidates are the models that
83
83
  * already ran in this user's Codex sessions plus, for the built-in OpenAI
84
84
  * provider only, the models Codex's catalog lists for the account (a gateway
@@ -86,7 +86,8 @@ function catalogCodexModels(home) {
86
86
  * qualifies only if it is priced on the parent's provider and costs less than
87
87
  * the parent, so the hint never names a model the gateway cannot serve. The
88
88
  * cheapest is T2 and the next one up is T1; with a single candidate T1 stays
89
- * on the main agent. Returns { targets, reason }.
89
+ * on the main agent. T1H is the most expensive cheaper model, and only when
90
+ * there are at least three candidates, so that it differs from T1. Returns { targets, reason }.
90
91
  */
91
92
  export function autoCodexSharedTargets({ dir = userDataDir(), home = codexUserDir(), model, provider, now = Date.now() } = {}) {
92
93
  if (!safeId(model) || !safeId(provider)) return { targets: [], reason: 'unknown parent model or provider' };
@@ -104,7 +105,9 @@ export function autoCodexSharedTargets({ dir = userDataDir(), home = codexUserDi
104
105
  ? `${model} is already the cheapest available model on ${provider}`
105
106
  : `no cheaper priced model has run in your Codex sessions on ${provider}` };
106
107
  }
107
- const targets = cheaper.slice(0, 2).map((x, i) => ({ tier: i === 0 ? 'T2' : 'T1', from: model, model: x.model, provider, auto: true }));
108
+ const picks = cheaper.slice(0, 2).map((x, i) => ({ tier: i === 0 ? 'T2' : 'T1', x }));
109
+ if (cheaper.length >= 3) picks.push({ tier: 'T1H', x: cheaper.at(-1) });
110
+ const targets = picks.map(({ tier, x }) => ({ tier, from: model, model: x.model, provider, auto: true }));
108
111
  return { targets, reason: null };
109
112
  }
110
113
 
@@ -240,7 +243,7 @@ export function codexDelegateMinContext(cfg) {
240
243
  // Codex tells the model to set spawn_agent's model only on the user's request,
241
244
  // so the shared hint says where this request came from.
242
245
  const SHARED_APPROVAL = 'The user approved this policy and its model mapping, so setting model here follows the user\'s request.';
243
- const SHARED_TIER_GUIDANCE = 'Choose T2 only for a simple bounded lookup or command. Use T1 for multi-step tool chains or work spanning multiple files. If the required tier is not mapped, keep the task on the main agent. Do not delegate design judgement, diagnosis, or irreversible actions.';
246
+ const SHARED_TIER_GUIDANCE = 'Choose T2 only for a simple bounded lookup or command. Use T1 for multi-step tool chains or work spanning multiple files. Use T1H only for large-scope work that T1 would likely get wrong. If the required tier is not mapped, keep the task on the main agent. Do not delegate design judgement, diagnosis, or irreversible actions.';
244
247
  const WAIT_TEXT = 'Wait for it to finish (call wait_agent with timeout_ms of at least 120000, and again if it times out) and do not do the delegated task yourself while it runs; then verify its result and report unfinished work. Do not spawn another CLI process. '
245
248
  + 'If sub-agent tools or the target model are unavailable, keep the task on the main agent and state that delegation was unavailable.';
246
249
 
@@ -183,10 +183,21 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
183
183
  try { (await import('../preset-ratchet.js')).preparePresetRatchet({ agent: 'codex' }); } catch (e) { debug('codex:preset', e); }
184
184
  }
185
185
  const { presetRatchetPath } = await import('../preset-ratchet.js');
186
+ let teamRatchetPath = () => '';
187
+ let teamNotice = null;
188
+ try {
189
+ const tr = await import('../team-ratchet.js');
190
+ teamRatchetPath = tr.teamRatchetPath;
191
+ if (event === 'session-start') {
192
+ tr.syncTeamRatchet({ agent: 'codex' });
193
+ tr.spawnTeamSyncIfDue();
194
+ teamNotice = tr.autoJoinNoticeBlock(undefined, 'en');
195
+ }
196
+ } catch (e) { debug('codex:team-ratchet', e); }
186
197
  // Lower priority survives the budget first. Rules and style move to a file
187
198
  // when they do not fit; offers and notices are simply left out.
188
199
  for (const [scope, ratchet] of [['global', codexHarnessPaths(root, 'global').ratchet],
189
- ['preset', presetRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
200
+ ['preset', presetRatchetPath({ agent: 'codex' })], ['team', teamRatchetPath({ agent: 'codex' })], ['project', codexHarnessPaths(root, 'project').ratchet]]) {
190
201
  try {
191
202
  if (statSync(ratchet).size > 32 * 1024) {
192
203
  parts.push({ priority: 1, text: `Read the ${scope} Sprag ratchet rules at ${JSON.stringify(ratchet)} before working; they exceed the automatic injection limit.` });
@@ -194,10 +205,11 @@ export async function codexHookOutput(event, payload, { cfg = loadConfig(), refr
194
205
  const text = readFileSync(ratchet, 'utf8').trim();
195
206
  // An empty preset file is only its header; injecting it would spend
196
207
  // tokens every session on no rules.
197
- if (text && (scope !== 'preset' || /^- /m.test(text))) parts.push({ priority: 1, spill: true, text: `[Sprag Codex ratchet: ${scope}]\n${text}` });
208
+ if (text && ((scope !== 'preset' && scope !== 'team') || /^- /m.test(text))) parts.push({ priority: 1, spill: true, text: `[Sprag Codex ratchet: ${scope}]\n${text}` });
198
209
  }
199
210
  } catch (e) { if (e?.code !== 'ENOENT') debug('codex:ratchet', e); }
200
211
  }
212
+ if (teamNotice) parts.push({ priority: 5, text: teamNotice });
201
213
  if (event === 'subagent-start') parts.push({ priority: 0, text:
202
214
  '[Sprag Codex delegation]\nStay within the assigned task and any caller-provided budget. Return findings with file references and actual verification results; state unfinished work. Use only available Codex tools and configured roles. Do not infer a model tier or invent a tool-call budget.' });
203
215
  // Codex reads the Korean guide from the global AGENTS.md, so the hook adds it