sprag-cli 3.75.2 → 3.77.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/docs/TEAM.ko.md CHANGED
@@ -17,6 +17,7 @@ Sprag 은 기본적으로 로컬에서만 동작합니다. 팀 기능은 켜야
17
17
  | Sprag 버전 | 설치된 버전 |
18
18
  | 에이전트 | `claude` 또는 `codex` |
19
19
  | 표시 이름 | 참여할 때 정한 이름이며, 기본값은 컴퓨터마다 한 번 만드는 `member-3f9a2c71` 같은 무작위 구성원 ID |
20
+ | 기기 ID | 위와 같은 무작위 구성원 ID입니다. 표시 이름을 바꿔도 참여할 때 함께 보내며, 같은 컴퓨터가 다시 참여하면 조직 안에서 기존 좌석을 그대로 쓰게 하는 데 씁니다 |
20
21
  | 운영체제 | OS 이름과 버전(예: macOS 26, Windows 11, Ubuntu 24.04)입니다. 참여한 직후 한 번 보내고, 바뀔 때만 다시 보냅니다. 호스트 이름, 사용자 이름, 경로처럼 컴퓨터를 이름으로 알아볼 수 있는 값은 보내지 않습니다 |
21
22
 
22
23
  보내지 않는 것은 프롬프트, 대화 기록, 파일 경로, 프로젝트 이름, 룰 내용, API 키입니다. 프롬프트 수집을 켠 엔터프라이즈 팀의 옵트인 프롬프트 업로드만 예외이며 "프롬프트 사용량 (엔터프라이즈)" 절에 적었습니다. 룰은 직접 `sprag team propose` 를 실행할 때만 밖으로 나갑니다.
@@ -27,20 +28,20 @@ Sprag 은 기본적으로 로컬에서만 동작합니다. 팀 기능은 켜야
27
28
 
28
29
  ## CLI에서 팀 만들기
29
30
 
30
- 웹 대시보드 없이 팀을 만들 수 있습니다. 로그인은 메일로 받는 6자리 코드(15분간 유효)로 하며, Sprag 은 로그인 세션을 저장하지 않습니다.
31
+ 웹 대시보드 없이도, 내가 오너나 어드민인 조직 안에 팀을 만들 수 있습니다. 조직 ID는 대시보드에 있고, 조직 개설은 Sprag가 합니다("조직과 좌석" 절 참고). 로그인은 메일로 받는 6자리 코드(15분간 유효)로 하며, Sprag 은 로그인 세션을 저장하지 않습니다.
31
32
 
32
33
  터미널에서는 명령 하나를 실행하고 묻는 대로 답하면 됩니다 (이메일, 코드, 팀 이름).
33
34
 
34
35
  ```bash
35
- sprag team create
36
+ sprag team create --org <조직 ID>
36
37
  ```
37
38
 
38
39
  에이전트나 터미널이 없는 셸에서는 두 단계로 나눕니다.
39
40
 
40
41
  ```bash
41
- sprag team create --email you@example.com --name "Acme"
42
+ sprag team create --org <조직 ID> --email you@example.com --name "Acme"
42
43
  # 메일로 코드가 오면 이어서 실행합니다
43
- sprag team create --email you@example.com --name "Acme" --code 123456
44
+ sprag team create --org <조직 ID> --email you@example.com --name "Acme" --code 123456
44
45
  ```
45
46
 
46
47
  첫 단계는 코드만 보냅니다. 두 번째 단계에서 코드를 확인하고 팀을 만든 뒤 초대 토큰, 멤버용 명령(`npm i -g sprag-cli@latest && sprag team join <토큰>`), 조직용 환경 변수(`export SPRAG_TEAM_TOKEN=<토큰>`), 대시보드 주소를 출력합니다. 같은 이메일로 https://sprag.io/team/ 에 로그인하면 팀을 관리할 수 있습니다.
@@ -99,14 +100,16 @@ Sprag 은 룰을 `~/.claude/ratchet-team.md`(Codex 는 `<CODEX_HOME>/ratchet-tea
99
100
 
100
101
  ## 조직과 좌석
101
102
 
102
- 모든 팀은 조직에 속하고, 좌석은 팀이 아니라 조직 단위로 구매합니다. 조직을 지정하지 않고 만든 팀에는 조직이 자동으로 새로 생기므로 기존 설정은 그대로 동작합니다.
103
+ 모든 팀은 조직에 속하고, 좌석은 팀이 아니라 조직 단위로 구매합니다. 조직은 Sprag만 개설합니다. sales@sprag.io 로 문의하면 슈퍼유저가 sprag.io/ops/ 에서 조직을 열고 첫 오너를 지정하며, 그다음부터 오너와 어드민이 그 안에 팀을 만듭니다.
104
+
105
+ 컴퓨터 한 대는 조직마다 좌석 하나를 씁니다. `sprag team leave` 뒤에 다시 참여하거나, 재설치하거나, 같은 조직의 다른 팀 초대로 참여해도 서버가 기기 ID로 예전 행을 찾아 초대받은 팀으로 옮기고 멤버 키만 새로 발급합니다. 행도 좌석도 두 개가 되지 않습니다. 다른 조직에 참여하면 그 조직의 좌석을 따로 씁니다.
103
106
 
104
107
  - **좌석이란.** 팀을 떠나지 않았고, 최근 30일 안에 무엇이든 보냈거나 업로드를 멈춘 상태인 컴퓨터 한 대(멤버 키 하나)입니다. 멈추지 않았는데 30일 넘게 연락이 없는 컴퓨터는 좌석을 차지하지 않습니다. 떠난 멤버도 세지 않습니다.
105
108
  - **두 번째 컴퓨터.** 같은 사람이 컴퓨터 두 대를 쓰면 오너나 어드민이 두 번째 컴퓨터를 첫 번째에 합쳐 좌석 하나를 같이 쓰게 할 수 있습니다. 대시보드의 멤버 행에서 "좌석 합치기"를 누르고, "합치기 해제"를 누르면 다시 따로 셉니다. 합쳐진 행에는 "OO님과 자리를 같이 씁니다"가 보입니다.
106
109
  - **한도.** 조직마다 좌석 한도가 있고, 한도를 넘는 참여는 거절됩니다. `sprag team join` 은 "이 조직에 남은 좌석이 없습니다. 조직 오너나 어드민에게 좌석 추가를 요청한 뒤 다시 참여하세요"를 출력하고 종료 코드 1로 끝납니다. 환경 변수로 하는 자동 참여는 조용히 넘어갔다가 다음 세션 시작 때 다시 시도합니다. 한도를 현재 사용량보다 낮춰도 기존 멤버는 그대로 쓰고, Sprag가 다시 올릴 때까지 새 참여만 거절됩니다.
107
110
  - **역할.** 권한 단계는 세 가지입니다. 슈퍼유저는 Sprag 직원이며 요금제와 좌석 한도를 정하고, 조직을 정지하고, 오너를 추가하고, sprag.io/ops/ 에서 모든 조직을 봅니다. 오너는 회사의 최상위 단계로, 어드민을 지정하거나 해제하고 어드민이 하는 일을 모두 합니다. 어드민은 회사의 관리 단계로, 팀을 만들고 구성원과 룰과 좌석을 관리합니다. 팀을 만든 사람에게도 특별한 권한이 없으며, 팀은 조직의 오너와 어드민, 그리고 슈퍼유저만 관리합니다. 오너와 어드민은 대시보드에서 좌석 한도를 볼 수 있습니다. 좌석을 늘리려면 sales@sprag.io 로 문의합니다.
108
111
 
109
- 기존 조직 안에 팀을 만들려면 `sprag team create --org <조직 ID>` 를 씁니다. 명령은 조직 이름과 사용 중인 좌석 수를 함께 출력하고, `sprag team status` 는 조직 이름을 보여 줍니다.
112
+ 관리하는 조직 안에 팀을 만들려면 `sprag team create --org <조직 ID>` 를 씁니다. 조직은 Sprag만 개설하므로 `--org` 없이 실행하면 서버가 거절합니다. 명령은 조직 이름과 사용 중인 좌석 수를 함께 출력하고, `sprag team status` 는 조직 이름을 보여 줍니다.
110
113
 
111
114
  ## 업로드 스위치
112
115
 
package/docs/TEAM.md CHANGED
@@ -17,6 +17,7 @@ After you join, Sprag uploads one row per local calendar day:
17
17
  | Sprag version | the installed version |
18
18
  | Agent | `claude` or `codex` |
19
19
  | Display name | the name you chose at join; the default is a random member ID such as `member-3f9a2c71`, created once per machine |
20
+ | Machine ID | the same random member ID, sent at join even when you choose another display name, so that a machine joining again reuses its seat in the organization instead of taking a second one |
20
21
  | Operating system | OS name and version (for example macOS 26, Windows 11, Ubuntu 24.04); sent once after joining and again only if it changes. Nothing that identifies the machine by name: no hostname, no user name, no path |
21
22
 
22
23
  Not sent: prompts, transcripts, file paths, project names, rule text, API keys. The one exception is the opt-in prompt upload of an Enterprise team with prompt capture on, described under "Prompt usage (Enterprise)". A rule leaves your machine only when you run `sprag team propose` yourself.
@@ -27,20 +28,20 @@ Destination: `https://hiycfjwswtkixyoqkznm.supabase.co/rest/v1/rpc/*` (Supabase,
27
28
 
28
29
  ## Create a team from the CLI
29
30
 
30
- 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.
31
+ You can set up a team without the web dashboard, inside an organization you own or administer (the organization id is in the dashboard; Sprag opens organizations, see "Organizations and seats"). Signing in uses a 6-digit code emailed to you (valid 15 minutes). Sprag never stores the sign-in session.
31
32
 
32
33
  In a terminal, run one command and answer the prompts (email, code, team name):
33
34
 
34
35
  ```bash
35
- sprag team create
36
+ sprag team create --org <org-id>
36
37
  ```
37
38
 
38
39
  From an agent or any shell without a terminal, it takes two steps:
39
40
 
40
41
  ```bash
41
- sprag team create --email you@example.com --name "Acme"
42
+ sprag team create --org <org-id> --email you@example.com --name "Acme"
42
43
  # the code arrives by email, then:
43
- sprag team create --email you@example.com --name "Acme" --code 123456
44
+ sprag team create --org <org-id> --email you@example.com --name "Acme" --code 123456
44
45
  ```
45
46
 
46
47
  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.
@@ -99,7 +100,9 @@ Sprag writes the rules to `~/.claude/ratchet-team.md` (Codex: `<CODEX_HOME>/ratc
99
100
 
100
101
  ## Organizations and seats
101
102
 
102
- Every team belongs to an organization, and seats are bought per organization, not per team. Teams you create without naming an organization get a new one automatically, so existing setups keep working.
103
+ Every team belongs to an organization, and seats are bought per organization, not per team. Only Sprag opens organizations: write to sales@sprag.io, and the superuser opens one at sprag.io/ops/ and names its first owner. Owners and admins then create teams inside it.
104
+
105
+ A machine holds one seat per organization. When it joins again, with `sprag team join` after `sprag team leave`, after a reinstall, or with the invite of another team in the same organization, the server finds its earlier row by the machine ID, moves it to the invited team and issues a new member key; no second row and no second seat appear. Joins to a different organization take a seat there.
103
106
 
104
107
  - **What a seat is.** One machine (one member key) that has not left and either sent anything in the last 30 days or is paused. A machine that has been silent for 30 days or more without being paused holds no seat. Members who left are not counted.
105
108
  - **Second machine.** When the same person uses two machines, an owner or admin can merge the second one into the first so the two share one seat. In the dashboard, use "Merge seat" on the member row; "Unmerge" gives it its own seat back. A merged row shows "shares a seat with <name>".
@@ -107,7 +110,7 @@ Every team belongs to an organization, and seats are bought per organization, no
107
110
  - **Roles.** There are three permission levels. The superuser is Sprag staff: it sets the plan and the seat limit, suspends organizations, adds owners, and sees every organization at sprag.io/ops/. An owner is a company's top level: it appoints and removes admins, and does everything an admin does. An admin is a company's management level: it creates teams and manages members, rules and seats. The person who created a team has no special rights over it; only the organization's owners and admins and the superuser manage teams. Owners and admins see the seat limit in the dashboard. To add seats, write to sales@sprag.io.
108
111
  - **The plan.** Sprag sets the plan (`pro` or `enterprise`) on the organization; owners and admins cannot change it. Prompt capture can be switched on only while the organization is on Enterprise. To change the plan, contact us.
109
112
 
110
- Create a team inside an existing organization from the CLI with `sprag team create --org <org-id>`. The command prints the organization name and how many seats are used. `sprag team status` shows the organization name.
113
+ Create a team inside an organization you administer from the CLI with `sprag team create --org <org-id>`; without `--org` the server refuses, because only Sprag opens organizations. The command prints the organization name and how many seats are used. `sprag team status` shows the organization name.
111
114
 
112
115
  ## The upload switch
113
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.75.2",
3
+ "version": "3.77.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": {
@@ -33,6 +33,7 @@ const T = {
33
33
  ].join('\n'),
34
34
  willSend: 'Joining uploads daily aggregates only, per local day:\n - spend (USD)\n - savings (USD)\n - delegation run counts\n - model-family share\nNo prompts, file paths or transcripts are sent.',
35
35
  asName: (n) => `Display name: ${n}`,
36
+ asDevice: (d) => `Machine ID: ${d} (lets the organization count this machine once if it joins again)`,
36
37
  ask: 'Join? [y/N] ',
37
38
  needYes: 'Not a terminal. Re-run with --yes to confirm: sprag team join <token> --yes',
38
39
  noToken: 'Missing invite token. Pass it as an argument or set SPRAG_TEAM_TOKEN.',
@@ -43,6 +44,7 @@ const T = {
43
44
  seats: (u, l) => `${u}/${l} seats used`,
44
45
  orgCreated: (n, u, l) => `Organization: ${n}${u != null && l != null ? ` (${u}/${l} seats used)` : ''}`,
45
46
  badOrg: '--org needs an organization id.',
47
+ noOrgCreate: 'Only Sprag opens new organizations. Pass --org <id> for an organization you administer (the id is on the dashboard), or write to sales@sprag.io to open one.',
46
48
  left: 'Left the team. Nothing is uploaded any more.',
47
49
  notJoined: 'Not in a team. Join with: sprag team join <token>',
48
50
  team: 'Team', plan: 'Plan', capture: 'Prompt capture', name: 'Display name',
@@ -96,6 +98,7 @@ const T = {
96
98
  ].join('\n'),
97
99
  willSend: '참여하면 로컬 날짜별 하루 단위 집계만 올라갑니다.\n - 지출(USD)\n - 절감(USD)\n - 위임 실행 횟수\n - 모델 계열 비중\n프롬프트, 파일 경로, 대화 기록은 보내지 않습니다.',
98
100
  asName: (n) => `표시 이름: ${n}`,
101
+ asDevice: (d) => `기기 ID: ${d} (다시 참여해도 조직이 이 컴퓨터를 한 좌석으로 셉니다)`,
99
102
  ask: '참여할까요? [y/N] ',
100
103
  needYes: '터미널이 아닙니다. 확인하려면 --yes 를 붙여 다시 실행하세요: sprag team join <토큰> --yes',
101
104
  noToken: '초대 토큰이 없습니다. 인자로 넘기거나 SPRAG_TEAM_TOKEN 을 설정하세요.',
@@ -106,6 +109,7 @@ const T = {
106
109
  seats: (u, l) => `좌석 ${u}/${l} 사용 중`,
107
110
  orgCreated: (n, u, l) => `조직: ${n}${u != null && l != null ? ` (좌석 ${u}/${l} 사용 중)` : ''}`,
108
111
  badOrg: '--org 에는 조직 ID 가 필요합니다',
112
+ noOrgCreate: '새 조직은 Sprag가 개설합니다. 관리하는 조직이 있으면 --org <ID> 를 붙여 주세요(ID 는 대시보드에 있습니다). 조직 개설은 sales@sprag.io 로 문의해 주세요.',
109
113
  left: '팀에서 나왔습니다. 이제 아무것도 업로드하지 않습니다.',
110
114
  notJoined: '참여한 팀이 없습니다. 참여하려면: sprag team join <토큰>',
111
115
  team: '팀', plan: '플랜', capture: '프롬프트 수집', name: '표시 이름',
@@ -210,7 +214,13 @@ async function createFlow({ m, out, hasFlag, getArg, version, fetch, interactive
210
214
  if (!name) { console.error(m.needName); return process.exit(2); }
211
215
 
212
216
  const { accessToken, userId } = await verifySignInCode({ email, code: useCode, fetch });
213
- const team = await createTeam({ name, accessToken, userId, orgId: org || null, fetch });
217
+ let team;
218
+ try {
219
+ team = await createTeam({ name, accessToken, userId, orgId: org || null, fetch });
220
+ } catch (e) {
221
+ if (/only Sprag operators/i.test(String(e && e.message))) { console.error(m.noOrgCreate); return process.exit(1); }
222
+ throw e;
223
+ }
214
224
  out(m.created(team.name || name));
215
225
  if (team.orgName && noJoin) out(m.orgCreated(team.orgName, null, null));
216
226
  out(`${m.inviteToken}: ${team.inviteToken}`);
@@ -248,6 +258,7 @@ export async function run({ args, hasFlag, getArg, version, fetch, interactive,
248
258
  if (!name) name = memberId();
249
259
  out(m.willSend);
250
260
  out(m.asName(name));
261
+ out(m.asDevice(memberId()));
251
262
  if (!hasFlag('--yes')) {
252
263
  if (!process.stdin.isTTY || !process.stdout.isTTY) { console.error(m.needYes); return process.exit(2); }
253
264
  if (!(await confirm(m.ask))) { out(m.aborted); return undefined; }
package/src/team-sync.js CHANGED
@@ -390,12 +390,20 @@ export async function joinTeam({
390
390
  token, displayName, agent = 'claude', autoJoined = false, fetch, dir = userDataDir(), version, now = Date.now(), env = process.env,
391
391
  } = {}) {
392
392
  if (!TOKEN_RE.test(String(token || ''))) throw new Error('invalid invite token (expected spt_ + 32 hex characters)');
393
- const name = String(displayName || '').trim() || memberId({ dir });
393
+ const device = memberId({ dir });
394
+ const name = String(displayName || '').trim() || device;
395
+ const params = { invite_token: token, display_name: name, agent, sprag_version: version || '', auto_joined: !!autoJoined };
394
396
  let r;
395
397
  try {
396
- r = await rpc('team_join', {
397
- invite_token: token, display_name: name, agent, sprag_version: version || '', auto_joined: !!autoJoined,
398
- }, { fetch });
398
+ // device_id lets the server find this machine's earlier row in the organization and reuse its seat
399
+ // instead of adding a second one. A server older than that answers "Could not find the function";
400
+ // the join is then repeated without it.
401
+ try {
402
+ r = await rpc('team_join', { ...params, device_id: device }, { fetch });
403
+ } catch (e) {
404
+ if (!/device_id|Could not find the function|PGRST202/i.test(String(e && e.message))) throw e;
405
+ r = await rpc('team_join', params, { fetch });
406
+ }
399
407
  } catch (e) {
400
408
  if (isSeatLimitError(e)) throw new SeatLimitError(e.message);
401
409
  throw e;