sprag-cli 3.70.0 → 3.72.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 +7 -1
- package/README.md +7 -1
- package/bin/cli.js +47 -5
- package/docs/CODEX.ko.md +2 -2
- package/docs/CODEX.md +4 -4
- package/docs/TEAM.ko.md +74 -19
- package/docs/TEAM.md +75 -19
- package/package.json +2 -1
- package/presets/agents.json +118 -0
- package/presets/doc2md/convert.py +1 -1
- package/presets/ratchet-rules.json +17 -2
- package/src/cli-entry.js +40 -0
- package/src/codex/hooks.js +11 -10
- package/src/codex/panel-setup.js +2 -1
- package/src/codex/panel.js +2 -1
- package/src/commands/brief.js +2 -0
- package/src/commands/codex.js +3 -2
- package/src/commands/harness.js +2 -3
- package/src/commands/install.js +19 -18
- package/src/commands/presets.js +158 -0
- package/src/commands/profile-map.js +2 -2
- package/src/commands/route-scan.js +9 -7
- package/src/commands/team.js +82 -26
- package/src/commands/update-check.js +2 -2
- package/src/debug.js +3 -2
- package/src/delegation-agents.js +48 -60
- package/src/doc2md.cjs +4 -3
- package/src/env.cjs +7 -0
- package/src/env.js +19 -0
- package/src/first-run-note.js +3 -2
- package/src/harness.js +4 -14
- package/src/hook.cjs +4 -3
- package/src/preset-sign.js +106 -0
- package/src/preset-sync.js +133 -0
- package/src/presets-source.js +209 -0
- package/src/prompt.js +2 -1
- package/src/route-scan.js +14 -1
- package/src/seed-rules.js +14 -16
- package/src/star-note.js +3 -2
- package/src/statusline-guard.js +144 -0
- package/src/stdin-payload.js +33 -4
- package/src/team-prompts.js +353 -0
- package/src/team-ratchet.js +116 -16
- package/src/team-sync.js +336 -37
- package/src/update-check.js +3 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subcommand: presets — which presets this machine uses, and fetching them.
|
|
3
|
+
*
|
|
4
|
+
* sprag presets [status] source, version, fetch times, signing key, next check
|
|
5
|
+
* sprag presets sync [--force] fetch the published bundle now (read-only)
|
|
6
|
+
* sprag presets show [--json] the effective ratchet, model-routing and agent presets
|
|
7
|
+
*
|
|
8
|
+
* The published bundle comes from the /ops console and applies only when its
|
|
9
|
+
* signature verifies; otherwise the presets bundled with this version are used.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const REASONS = {
|
|
13
|
+
en: {
|
|
14
|
+
'no-cache': 'no published bundle has been fetched yet',
|
|
15
|
+
disabled: 'remote presets are off (SPRAG_NO_PRESET_SYNC=1 or SPRAG_NO_UPDATE_CHECK=1)',
|
|
16
|
+
'no-keys': 'no signing keys yet: this version ships no public key, so a published bundle is not applied',
|
|
17
|
+
unsigned: 'the published bundle is not signed',
|
|
18
|
+
'unknown-kid': 'the bundle is signed with a key this version does not know',
|
|
19
|
+
'bad-sig': 'the signature does not match the bundle',
|
|
20
|
+
'min-cli': 'the bundle needs a newer sprag (min_cli)',
|
|
21
|
+
format: 'the bundle format is newer than this version understands',
|
|
22
|
+
shape: 'the bundle is malformed',
|
|
23
|
+
'too-large': 'the bundle is larger than 200 KB',
|
|
24
|
+
older: 'the bundle is older than the one already in use',
|
|
25
|
+
},
|
|
26
|
+
ko: {
|
|
27
|
+
'no-cache': '아직 받아 온 게시 번들이 없습니다',
|
|
28
|
+
disabled: '원격 프리셋이 꺼져 있습니다 (SPRAG_NO_PRESET_SYNC=1 또는 SPRAG_NO_UPDATE_CHECK=1)',
|
|
29
|
+
'no-keys': '아직 서명 키가 없습니다. 이 버전에 공개 키가 들어 있지 않아 게시 번들을 적용하지 않습니다',
|
|
30
|
+
unsigned: '게시 번들에 서명이 없습니다',
|
|
31
|
+
'unknown-kid': '이 버전이 모르는 키로 서명된 번들입니다',
|
|
32
|
+
'bad-sig': '서명이 번들 내용과 맞지 않습니다',
|
|
33
|
+
'min-cli': '더 새 버전의 sprag 가 필요한 번들입니다 (min_cli)',
|
|
34
|
+
format: '이 버전이 읽지 못하는 새 번들 형식입니다',
|
|
35
|
+
shape: '번들 형식이 잘못됐습니다',
|
|
36
|
+
'too-large': '번들이 200 KB 를 넘습니다',
|
|
37
|
+
older: '이미 쓰고 있는 번들보다 오래된 번들입니다',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function when(ms) {
|
|
42
|
+
if (!ms) return null;
|
|
43
|
+
const d = new Date(ms);
|
|
44
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
45
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function run({ args, hasFlag }) {
|
|
49
|
+
const sub = args[1] || 'status';
|
|
50
|
+
const { userLanguage } = await import('../config.js');
|
|
51
|
+
const ko = userLanguage() === 'ko';
|
|
52
|
+
const R = REASONS[ko ? 'ko' : 'en'];
|
|
53
|
+
const why = (r) => R[r] || r;
|
|
54
|
+
const src = await import('../presets-source.js');
|
|
55
|
+
const sync = await import('../preset-sync.js');
|
|
56
|
+
const { PRESET_SIGNING_KEYS } = await import('../preset-sign.js');
|
|
57
|
+
|
|
58
|
+
if (sub === 'sync') {
|
|
59
|
+
const res = await sync.syncPresets({ force: hasFlag('--force') });
|
|
60
|
+
if (hasFlag('--background')) return;
|
|
61
|
+
const v = res.version ? `v${res.version}` : '';
|
|
62
|
+
const lines = {
|
|
63
|
+
disabled: ko ? `받지 않았습니다. ${R.disabled}` : `Not fetched: ${R.disabled}`,
|
|
64
|
+
none: ko ? '서버에 게시된 프리셋 번들이 없습니다. 패키지에 들어 있는 프리셋을 씁니다' : 'Nothing is published on the server. The bundled presets stay in use.',
|
|
65
|
+
current: ko ? `이미 최신입니다 (${v})` : `Already current (${v}).`,
|
|
66
|
+
updated: ko ? `${v} 을 받았습니다. 다음 세션부터 적용됩니다` : `Fetched ${v}. It applies from the next session start.`,
|
|
67
|
+
rejected: ko ? `${v} 을 받았지만 적용하지 않습니다. ${why(res.reason)}` : `Fetched ${v} but did not apply it: ${why(res.reason)}.`,
|
|
68
|
+
error: ko ? `가져오지 못했습니다 (${res.reason}). 패키지에 들어 있는 프리셋을 계속 씁니다` : `Could not fetch (${res.reason}). The presets in use stay as they are.`,
|
|
69
|
+
};
|
|
70
|
+
console.log(lines[res.status] || res.status);
|
|
71
|
+
if (res.status === 'error') process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sub === 'show') {
|
|
76
|
+
const out = {
|
|
77
|
+
source: src.presetSource(),
|
|
78
|
+
ratchet_rules: src.ratchetPresetEntries(),
|
|
79
|
+
model_rules: src.modelPresets(),
|
|
80
|
+
agents: src.agentDefinitions(),
|
|
81
|
+
};
|
|
82
|
+
if (hasFlag('--json')) { console.log(JSON.stringify(out, null, 2)); return; }
|
|
83
|
+
const s = out.source;
|
|
84
|
+
console.log(s.kind === 'remote'
|
|
85
|
+
? (ko ? `출처: 게시 번들 v${s.version}` : `Source: published bundle v${s.version}`)
|
|
86
|
+
: (ko ? `출처: sprag ${src.cliVersion()} 동봉 프리셋` : `Source: bundled with sprag ${src.cliVersion()}`));
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(ko ? `래칫 프리셋 ${out.ratchet_rules.length}건` : `Ratchet presets (${out.ratchet_rules.length})`);
|
|
89
|
+
for (const r of out.ratchet_rules) {
|
|
90
|
+
const text = (ko ? r.ko : r.en).replace(/\s+/g, ' ');
|
|
91
|
+
console.log(` [${r.id}]${r.agents ? ` (${r.agents.join(', ')})` : ''} ${text.length > 110 ? text.slice(0, 109) + '…' : text}`);
|
|
92
|
+
}
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(ko ? `모델 라우팅 프리셋 ${out.model_rules.length}건` : `Model-routing presets (${out.model_rules.length})`);
|
|
95
|
+
for (const m of out.model_rules) {
|
|
96
|
+
console.log(` [${m.id}] ${m.tier} · ${ko ? m.label : m.labelEn} → ${m.agent}`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(ko ? `위임 에이전트 ${out.agents.length}개` : `Delegation agents (${out.agents.length})`);
|
|
100
|
+
for (const a of out.agents) {
|
|
101
|
+
console.log(` ${a.name} model=${a.model} effort=${a.effort} tools=${a.tools.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (sub !== 'status') {
|
|
107
|
+
console.error(ko ? '사용법: sprag presets [status|sync [--force]|show [--json]]' : 'Usage: sprag presets [status|sync [--force]|show [--json]]');
|
|
108
|
+
process.exitCode = 2;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cache = src.readRemoteCache();
|
|
113
|
+
const s = src.presetSource();
|
|
114
|
+
const disabled = src.presetSyncDisabled();
|
|
115
|
+
const lines = [];
|
|
116
|
+
if (s.kind === 'remote') {
|
|
117
|
+
lines.push(ko ? `프리셋 출처: 게시 번들 v${s.version} (받은 시각 ${when(s.fetchedAt)})`
|
|
118
|
+
: `Presets source: published bundle v${s.version} (fetched ${when(s.fetchedAt)})`);
|
|
119
|
+
} else {
|
|
120
|
+
lines.push(ko ? `프리셋 출처: sprag ${src.cliVersion()} 동봉 프리셋 (대체 경로)`
|
|
121
|
+
: `Presets source: bundled with sprag ${src.cliVersion()} (fallback)`);
|
|
122
|
+
// Nothing applied but something was fetched: the rejection is the answer.
|
|
123
|
+
const reason = s.reason === 'no-cache' && cache.rejected && cache.rejected.reason ? cache.rejected.reason : s.reason;
|
|
124
|
+
lines.push(ko ? ` 이유: ${why(reason)}` : ` Why: ${why(reason)}`);
|
|
125
|
+
}
|
|
126
|
+
// Signing key status, independent of whether anything has been fetched.
|
|
127
|
+
const seenKid = (cache.bundle && cache.bundle.sig && cache.bundle.sig.kid) || null;
|
|
128
|
+
let keyLine;
|
|
129
|
+
if (PRESET_SIGNING_KEYS.length === 0) {
|
|
130
|
+
keyLine = ko ? '서명 키: 아직 없음. 공개 키가 들어간 버전이 나오기 전까지 게시 번들을 적용하지 않습니다'
|
|
131
|
+
: 'Signing keys: none yet. Published bundles are not applied until a release ships the public key.';
|
|
132
|
+
} else if (s.kind === 'remote') {
|
|
133
|
+
keyLine = ko ? `서명 키: 정상 (${s.kid})` : `Signing key: ok (${s.kid})`;
|
|
134
|
+
} else if (seenKid && !PRESET_SIGNING_KEYS.some((k) => k.kid === seenKid)) {
|
|
135
|
+
keyLine = ko ? `서명 키: 모르는 키 (${seenKid})` : `Signing key: unknown kid (${seenKid})`;
|
|
136
|
+
} else {
|
|
137
|
+
keyLine = ko ? `서명 키: ${PRESET_SIGNING_KEYS.map((k) => k.kid).join(', ')}` : `Signing keys: ${PRESET_SIGNING_KEYS.map((k) => k.kid).join(', ')}`;
|
|
138
|
+
}
|
|
139
|
+
lines.push(keyLine);
|
|
140
|
+
if (cache.rejected && cache.rejected.version != null) {
|
|
141
|
+
lines.push(ko ? `마지막으로 거절한 번들: v${cache.rejected.version} (${why(cache.rejected.reason)})`
|
|
142
|
+
: `Last rejected: v${cache.rejected.version} (${why(cache.rejected.reason)})`);
|
|
143
|
+
}
|
|
144
|
+
if (cache.lastError && cache.lastError.message) {
|
|
145
|
+
lines.push(ko ? `마지막 오류: ${cache.lastError.message} (${when(cache.lastError.at)})`
|
|
146
|
+
: `Last error: ${cache.lastError.message} (${when(cache.lastError.at)})`);
|
|
147
|
+
}
|
|
148
|
+
lines.push(ko ? `마지막 확인: ${when(cache.checkedAt) || '없음'}` : `Last check: ${when(cache.checkedAt) || 'never'}`);
|
|
149
|
+
if (disabled) {
|
|
150
|
+
lines.push(ko ? '다음 확인: 꺼져 있음' : 'Next check: off');
|
|
151
|
+
} else {
|
|
152
|
+
const next = sync.nextPresetCheckAt(cache);
|
|
153
|
+
lines.push(!next || next <= Date.now()
|
|
154
|
+
? (ko ? '다음 확인: 다음 세션 시작 때' : 'Next check: at the next session start')
|
|
155
|
+
: (ko ? `다음 확인: ${when(next)} 이후 세션 시작 때` : `Next check: at the first session start after ${when(next)}`));
|
|
156
|
+
}
|
|
157
|
+
console.log(lines.join('\n'));
|
|
158
|
+
}
|
|
@@ -56,8 +56,8 @@ export async function run({ args, hasFlag }) {
|
|
|
56
56
|
const result = await refreshGatewayModelMap({ base, key });
|
|
57
57
|
if (!result) {
|
|
58
58
|
say(ko
|
|
59
|
-
? `LiteLLM 에서 모델 정보를 가져오지 못했습니다 (GET ${base}/model/info).
|
|
60
|
-
: `Could not fetch model info from LiteLLM (GET ${base}/model/info). Re-run with
|
|
59
|
+
? `LiteLLM 에서 모델 정보를 가져오지 못했습니다 (GET ${base}/model/info). SPRAG_DEBUG=1 로 다시 실행하면 원인이 보입니다.`
|
|
60
|
+
: `Could not fetch model info from LiteLLM (GET ${base}/model/info). Re-run with SPRAG_DEBUG=1 to see why.`);
|
|
61
61
|
process.exitCode = 1;
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
@@ -259,9 +259,8 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
259
259
|
try {
|
|
260
260
|
const cache = rs.readRouteScan();
|
|
261
261
|
if (!(await rs.shouldRescan(cache, { afterDelegation: true }))) return;
|
|
262
|
-
const {
|
|
263
|
-
|
|
264
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
262
|
+
const { spawnDetachedCli } = await import('../cli-entry.js');
|
|
263
|
+
spawnDetachedCli(['route-scan', '--refresh', '--quiet']);
|
|
265
264
|
} catch (e) {
|
|
266
265
|
// A missed refresh only means the figure lags, which is what happened
|
|
267
266
|
// before this hook existed. Never fail the tool call over it.
|
|
@@ -278,9 +277,8 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
278
277
|
let cache = rs.readRouteScan();
|
|
279
278
|
if (await rs.shouldRescan(cache)) {
|
|
280
279
|
try {
|
|
281
|
-
const {
|
|
282
|
-
|
|
283
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
280
|
+
const { spawnDetachedCli } = await import('../cli-entry.js');
|
|
281
|
+
spawnDetachedCli(['route-scan', '--refresh', '--quiet']);
|
|
284
282
|
} catch (e) { debug('route-scan:spawn-refresh', e); /* stale cache is still usable below */ }
|
|
285
283
|
}
|
|
286
284
|
const open = rs.openCandidates(cache);
|
|
@@ -296,6 +294,10 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
296
294
|
// would keep reading the old wording indefinitely. The call writes only
|
|
297
295
|
// when the rendering differs, so the common case is a read and a string
|
|
298
296
|
// compare.
|
|
297
|
+
// Central presets: kick the detached, read-only fetch when a day has
|
|
298
|
+
// passed. What it fetches is applied by the renders below at the NEXT
|
|
299
|
+
// session start; this one uses whatever was already cached.
|
|
300
|
+
try { (await import('../preset-sync.js')).spawnPresetSyncIfDue(); } catch (e) { debug('route-scan:preset-sync', e); }
|
|
299
301
|
// Agent files come first so the ratchet-model.md rendered next names them.
|
|
300
302
|
try { (await import('../delegation-agents.js')).syncDelegationAgents(); } catch (e) { debug('route-scan:delegation-agents', e); }
|
|
301
303
|
try { mrHook.syncAllFiles(); } catch (e) { debug('route-scan:sync-ratchet', e); }
|
|
@@ -522,7 +524,7 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
522
524
|
console.log(lang === 'ko' ? '등록 / 무시:' : 'Promote / dismiss:');
|
|
523
525
|
console.log(' sprag harness promote R<N> --global|--project');
|
|
524
526
|
console.log(' sprag route-scan dismiss <N>');
|
|
525
|
-
// 최초 1회만 — 매번 찍으면 도구가 광고판이 된다 (
|
|
527
|
+
// 최초 1회만 — 매번 찍으면 도구가 광고판이 된다 (SPRAG_NO_NOTE=1 로 끔)
|
|
526
528
|
(await import('../first-run-note.js')).printOnce('route-scan', lang);
|
|
527
529
|
return;
|
|
528
530
|
}
|
package/src/commands/team.js
CHANGED
|
@@ -12,21 +12,23 @@ import { createInterface } from 'node:readline';
|
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import { userInfo } from 'node:os';
|
|
14
14
|
import { userLanguage } from '../config.js';
|
|
15
|
+
import { pendingPrompts } from '../team-prompts.js';
|
|
15
16
|
import {
|
|
16
|
-
loadTeamState, isJoined, joinTeam, leaveTeam, syncTeam, autoJoin, aggregateDaily, rpc,
|
|
17
|
-
teamRulesCachePath, teamSyncDisabled, requestSignInCode, verifySignInCode, createTeam,
|
|
17
|
+
loadTeamState, deviceInfo, isJoined, joinTeam, leaveTeam, syncTeam, autoJoin, aggregateDaily, rpc,
|
|
18
|
+
teamRulesCachePath, teamSyncDisabled, requestSignInCode, verifySignInCode, createTeam, isSeatLimitError,
|
|
18
19
|
} from '../team-sync.js';
|
|
19
20
|
|
|
20
21
|
const T = {
|
|
21
22
|
en: {
|
|
22
23
|
usage: [
|
|
23
24
|
'Usage: sprag team <command>',
|
|
24
|
-
' create [--email <e>] [--name <n>] [--code <6 digits>] [--no-join] create a team (you become the owner)',
|
|
25
|
+
' create [--email <e>] [--name <n>] [--code <6 digits>] [--org <org-id>] [--no-join] create a team (you become the owner)',
|
|
25
26
|
' join [token] [--name <n>] [--yes] join a team (token or SPRAG_TEAM_TOKEN)',
|
|
26
27
|
' leave leave the team and stop uploading',
|
|
27
28
|
' status [--show-payload] membership, last sync, what is sent',
|
|
28
|
-
' sync [--force]
|
|
29
|
-
'
|
|
29
|
+
' sync [--force] [--rules-only] upload aggregates, prompts (Enterprise capture) and pull rules now',
|
|
30
|
+
' (--rules-only: pull organization and team rules only, upload nothing)',
|
|
31
|
+
' rules show cached organization and team rules',
|
|
30
32
|
' propose <text> [--scope ratchet|model] propose a rule to the team',
|
|
31
33
|
].join('\n'),
|
|
32
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.',
|
|
@@ -36,6 +38,11 @@ const T = {
|
|
|
36
38
|
noToken: 'Missing invite token. Pass it as an argument or set SPRAG_TEAM_TOKEN.',
|
|
37
39
|
aborted: 'Not joined.',
|
|
38
40
|
joined: (n) => `Joined team "${n}". Check it with: sprag team status`,
|
|
41
|
+
seatLimit: 'This organization has no free seat. Ask your organization owner or admin to add seats, then join again.',
|
|
42
|
+
org: 'Organization',
|
|
43
|
+
seats: (u, l) => `${u}/${l} seats used`,
|
|
44
|
+
orgCreated: (n, u, l) => `Organization: ${n}${u != null && l != null ? ` (${u}/${l} seats used)` : ''}`,
|
|
45
|
+
badOrg: '--org needs an organization id.',
|
|
39
46
|
left: 'Left the team. Nothing is uploaded any more.',
|
|
40
47
|
notJoined: 'Not in a team. Join with: sprag team join <token>',
|
|
41
48
|
team: 'Team', plan: 'Plan', capture: 'Prompt capture', name: 'Display name',
|
|
@@ -43,10 +50,15 @@ const T = {
|
|
|
43
50
|
neverSynced: 'Last sync: never',
|
|
44
51
|
lastError: 'Last error',
|
|
45
52
|
on: 'on', off: 'off',
|
|
46
|
-
|
|
53
|
+
lastPrompts: (t, n) => `Last prompt upload: ${t} (${n} prompts)`,
|
|
54
|
+
noPrompts: 'Last prompt upload: never',
|
|
55
|
+
pendingPrompts: (n) => `Prompts waiting to upload: ${n}`,
|
|
56
|
+
pendingExample: (e) => `Example (redacted): ${e.at} · ${e.project || '-'} · ${e.text}`,
|
|
57
|
+
disabled: 'Team sync is disabled (SPRAG_NO_TEAM_SYNC=1).',
|
|
47
58
|
skipped: (s) => `Skipped: ${s}`,
|
|
48
|
-
synced: (u, r) => `Synced: ${u} day rows uploaded, ${r}
|
|
49
|
-
|
|
59
|
+
synced: (u, r, p) => `Synced: ${u} day rows uploaded, ${r} rules${p ? `, ${p} prompts` : ''}.`,
|
|
60
|
+
rulesPulled: (r) => `Rules updated: ${r} organization and team rules. A new session applies them.`,
|
|
61
|
+
noRules: 'No organization or team rules yet.',
|
|
50
62
|
proposed: (id) => `Proposed. Id: ${id}`,
|
|
51
63
|
needText: 'Missing rule text. Usage: sprag team propose <text> [--scope ratchet|model]',
|
|
52
64
|
badScope: 'Scope must be ratchet or model.',
|
|
@@ -73,12 +85,13 @@ const T = {
|
|
|
73
85
|
ko: {
|
|
74
86
|
usage: [
|
|
75
87
|
'사용법: sprag team <명령>',
|
|
76
|
-
' create [--email <e>] [--name <이름>] [--code <6자리>] [--no-join] 팀 만들기 (만든 사람이
|
|
88
|
+
' create [--email <e>] [--name <이름>] [--code <6자리>] [--org <조직 ID>] [--no-join] 팀 만들기 (만든 사람이 조직 오너)',
|
|
77
89
|
' join [토큰] [--name <이름>] [--yes] 팀에 참여 (토큰 또는 SPRAG_TEAM_TOKEN)',
|
|
78
90
|
' leave 팀에서 나가고 업로드 중단',
|
|
79
91
|
' status [--show-payload] 참여 상태, 마지막 동기화, 보내는 내용',
|
|
80
|
-
' sync [--force]
|
|
81
|
-
'
|
|
92
|
+
' sync [--force] [--rules-only] 지금 집계를 올리고 규칙을 받기',
|
|
93
|
+
' (--rules-only: 조직·팀 규칙만 받고 아무것도 올리지 않음)',
|
|
94
|
+
' rules 받아 둔 조직·팀 규칙 보기',
|
|
82
95
|
' propose <내용> [--scope ratchet|model] 팀에 규칙 제안',
|
|
83
96
|
].join('\n'),
|
|
84
97
|
willSend: '참여하면 로컬 날짜별 하루 단위 집계만 올라갑니다.\n - 지출(USD)\n - 절감(USD)\n - 위임 실행 횟수\n - 모델 계열 비중\n프롬프트, 파일 경로, 대화 기록은 보내지 않습니다.',
|
|
@@ -88,6 +101,11 @@ const T = {
|
|
|
88
101
|
noToken: '초대 토큰이 없습니다. 인자로 넘기거나 SPRAG_TEAM_TOKEN 을 설정하세요.',
|
|
89
102
|
aborted: '참여하지 않았습니다.',
|
|
90
103
|
joined: (n) => `팀 "${n}" 에 참여했습니다. 확인: sprag team status`,
|
|
104
|
+
seatLimit: '이 조직에 남은 좌석이 없습니다. 조직 오너나 어드민에게 좌석 추가를 요청한 뒤 다시 참여하세요',
|
|
105
|
+
org: '조직',
|
|
106
|
+
seats: (u, l) => `좌석 ${u}/${l} 사용 중`,
|
|
107
|
+
orgCreated: (n, u, l) => `조직: ${n}${u != null && l != null ? ` (좌석 ${u}/${l} 사용 중)` : ''}`,
|
|
108
|
+
badOrg: '--org 에는 조직 ID 가 필요합니다',
|
|
91
109
|
left: '팀에서 나왔습니다. 이제 아무것도 업로드하지 않습니다.',
|
|
92
110
|
notJoined: '참여한 팀이 없습니다. 참여하려면: sprag team join <토큰>',
|
|
93
111
|
team: '팀', plan: '플랜', capture: '프롬프트 수집', name: '표시 이름',
|
|
@@ -95,10 +113,15 @@ const T = {
|
|
|
95
113
|
neverSynced: '마지막 동기화: 아직 없음',
|
|
96
114
|
lastError: '마지막 오류',
|
|
97
115
|
on: '켜짐', off: '꺼짐',
|
|
98
|
-
|
|
116
|
+
lastPrompts: (t, n) => `마지막 프롬프트 업로드: ${t} (${n}건)`,
|
|
117
|
+
noPrompts: '마지막 프롬프트 업로드: 아직 없음',
|
|
118
|
+
pendingPrompts: (n) => `업로드 대기 중인 프롬프트: ${n}건`,
|
|
119
|
+
pendingExample: (e) => `예시 (비밀값 가림): ${e.at} · ${e.project || '-'} · ${e.text}`,
|
|
120
|
+
disabled: '팀 동기화가 꺼져 있습니다 (SPRAG_NO_TEAM_SYNC=1).',
|
|
99
121
|
skipped: (s) => `건너뜀: ${s}`,
|
|
100
|
-
synced: (u, r) => `동기화 완료: ${u}일치 집계 업로드,
|
|
101
|
-
|
|
122
|
+
synced: (u, r, p) => `동기화 완료: ${u}일치 집계 업로드, 규칙 ${r}개${p ? `, 프롬프트 ${p}건` : ''}.`,
|
|
123
|
+
rulesPulled: (r) => `규칙을 받았습니다: 조직·팀 규칙 ${r}개. 새 세션부터 적용됩니다.`,
|
|
124
|
+
noRules: '아직 조직이나 팀 규칙이 없습니다.',
|
|
102
125
|
proposed: (id) => `제안했습니다. ID: ${id}`,
|
|
103
126
|
needText: '규칙 내용이 없습니다. 사용법: sprag team propose <내용> [--scope ratchet|model]',
|
|
104
127
|
badScope: '범위는 ratchet 또는 model 이어야 합니다.',
|
|
@@ -124,6 +147,20 @@ const T = {
|
|
|
124
147
|
},
|
|
125
148
|
};
|
|
126
149
|
|
|
150
|
+
/**
|
|
151
|
+
* `sprag team rules` lines: `- [org · model] text`, organization rules first.
|
|
152
|
+
* Rows without a level (older servers) are team rules. Removed ids are skipped.
|
|
153
|
+
*/
|
|
154
|
+
export function formatRuleLines(cache) {
|
|
155
|
+
const removed = new Set(Array.isArray(cache?.removed) ? cache.removed : []);
|
|
156
|
+
const rules = (Array.isArray(cache?.rules) ? cache.rules : [])
|
|
157
|
+
.filter((r) => r && typeof r.text === 'string' && r.text.trim() && !removed.has(r.id));
|
|
158
|
+
const org = rules.filter((r) => r.level === 'org');
|
|
159
|
+
const team = rules.filter((r) => r.level !== 'org');
|
|
160
|
+
return [...org, ...team].map((r) =>
|
|
161
|
+
`- [${r.level === 'org' ? 'org' : 'team'} · ${r.scope === 'model' ? 'model' : 'ratchet'}] ${r.text.trim().replace(/\s*\n\s*/g, ' ')}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
127
164
|
function prompt(question) {
|
|
128
165
|
return new Promise((resolve) => {
|
|
129
166
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -144,6 +181,8 @@ async function createFlow({ m, out, hasFlag, getArg, version, fetch, interactive
|
|
|
144
181
|
const st0 = loadTeamState();
|
|
145
182
|
if (isJoined(st0) && !noJoin) { console.error(m.alreadyJoined(st0.teamName || '')); return process.exit(1); }
|
|
146
183
|
|
|
184
|
+
const org = getArg('--org');
|
|
185
|
+
if (hasFlag('--org') && (!org || org.startsWith('--'))) { console.error(m.badOrg); return process.exit(2); }
|
|
147
186
|
let email = getArg('--email');
|
|
148
187
|
let name = getArg('--name');
|
|
149
188
|
const code = getArg('--code');
|
|
@@ -173,8 +212,9 @@ async function createFlow({ m, out, hasFlag, getArg, version, fetch, interactive
|
|
|
173
212
|
if (!name) { console.error(m.needName); return process.exit(2); }
|
|
174
213
|
|
|
175
214
|
const { accessToken, userId } = await verifySignInCode({ email, code: useCode, fetch });
|
|
176
|
-
const team = await createTeam({ name, accessToken, userId, fetch });
|
|
215
|
+
const team = await createTeam({ name, accessToken, userId, orgId: org || null, fetch });
|
|
177
216
|
out(m.created(team.name || name));
|
|
217
|
+
if (team.orgName && noJoin) out(m.orgCreated(team.orgName, null, null));
|
|
178
218
|
out(`${m.inviteToken}: ${team.inviteToken}`);
|
|
179
219
|
out(`${m.memberCmd}: npm i -g sprag-cli@latest && sprag team join ${team.inviteToken}`);
|
|
180
220
|
out(`${m.orgLine}: export SPRAG_TEAM_TOKEN=${team.inviteToken}`);
|
|
@@ -183,11 +223,12 @@ async function createFlow({ m, out, hasFlag, getArg, version, fetch, interactive
|
|
|
183
223
|
if (noJoin) { out(m.noJoinNote); return undefined; }
|
|
184
224
|
let display = 'member';
|
|
185
225
|
try { display = userInfo().username || 'member'; } catch { /* no passwd entry */ }
|
|
186
|
-
await joinTeam({ token: team.inviteToken, displayName: display, fetch, version });
|
|
226
|
+
const joined = await joinTeam({ token: team.inviteToken, displayName: display, fetch, version });
|
|
227
|
+
if (joined.orgName && joined.seatsUsed != null && joined.seatLimit != null) out(m.orgCreated(joined.orgName, joined.seatsUsed, joined.seatLimit));
|
|
187
228
|
if (!teamSyncDisabled()) {
|
|
188
229
|
const r = await syncTeam({ force: true, fetch, version });
|
|
189
230
|
if (r.error) console.error(m.syncFailed(r.error));
|
|
190
|
-
else if (!r.skipped) out(m.synced(r.uploaded, r.rules));
|
|
231
|
+
else if (!r.skipped) out(m.synced(r.uploaded, r.rules, r.prompts));
|
|
191
232
|
}
|
|
192
233
|
return undefined;
|
|
193
234
|
}
|
|
@@ -216,6 +257,7 @@ export async function run({ args, hasFlag, getArg, version, fetch, interactive,
|
|
|
216
257
|
}
|
|
217
258
|
const st = await joinTeam({ token, displayName: name, version });
|
|
218
259
|
out(m.joined(st.teamName));
|
|
260
|
+
if (st.orgName && st.seatsUsed != null && st.seatLimit != null) out(m.orgCreated(st.orgName, st.seatsUsed, st.seatLimit));
|
|
219
261
|
return undefined;
|
|
220
262
|
}
|
|
221
263
|
case 'leave':
|
|
@@ -226,33 +268,47 @@ export async function run({ args, hasFlag, getArg, version, fetch, interactive,
|
|
|
226
268
|
const st = loadTeamState();
|
|
227
269
|
if (!isJoined(st)) { out(m.notJoined); return undefined; }
|
|
228
270
|
out(`${m.team}: ${st.teamName}`);
|
|
271
|
+
if (st.orgName) out(`${m.org}: ${st.orgName}`);
|
|
229
272
|
out(`${m.plan}: ${st.plan}`);
|
|
230
273
|
out(`${m.capture}: ${st.promptCapture ? m.on : m.off}`);
|
|
274
|
+
if (st.promptCapture) {
|
|
275
|
+
out(st.lastPromptUploadAt ? m.lastPrompts(new Date(st.lastPromptUploadAt).toLocaleString(), st.lastPromptCount || 0) : m.noPrompts);
|
|
276
|
+
}
|
|
231
277
|
out(`${m.name}: ${st.displayName}`);
|
|
232
278
|
out(st.lastSyncAt ? m.lastSync(new Date(st.lastSyncAt).toLocaleString(), st.lastSyncRows || 0) : m.neverSynced);
|
|
233
279
|
if (st.lastError) out(`${m.lastError}: ${st.lastError}`);
|
|
234
|
-
if (hasFlag('--show-payload'))
|
|
280
|
+
if (hasFlag('--show-payload')) {
|
|
281
|
+
out(`device: ${JSON.stringify(st.deviceInfo || deviceInfo())}`);
|
|
282
|
+
out(JSON.stringify(await aggregateDaily(), null, 2));
|
|
283
|
+
if (st.promptCapture) {
|
|
284
|
+
const pend = await pendingPrompts();
|
|
285
|
+
out(m.pendingPrompts(pend.count));
|
|
286
|
+
if (pend.example) out(m.pendingExample(pend.example));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
235
289
|
return undefined;
|
|
236
290
|
}
|
|
237
291
|
case 'sync': {
|
|
292
|
+
const rulesOnly = hasFlag('--rules-only');
|
|
238
293
|
if (hasFlag('--background')) {
|
|
239
|
-
await autoJoin({ version });
|
|
240
|
-
await syncTeam({ version });
|
|
294
|
+
if (!hasFlag('--prompts-only') && !rulesOnly) await autoJoin({ version });
|
|
295
|
+
await syncTeam({ version, promptsOnly: hasFlag('--prompts-only'), rulesOnly });
|
|
241
296
|
return undefined;
|
|
242
297
|
}
|
|
243
|
-
|
|
298
|
+
// Run by hand, --rules-only always pulls (no hourly gate).
|
|
299
|
+
const r = await syncTeam({ force: hasFlag('--force') || rulesOnly, rulesOnly, version });
|
|
244
300
|
if (hasFlag('--quiet')) return undefined;
|
|
245
301
|
if (r.error) { console.error(r.error); return process.exit(1); }
|
|
246
302
|
if (r.skipped) out(r.skipped === 'disabled' ? m.disabled : m.skipped(r.skipped));
|
|
247
|
-
else out(m.
|
|
303
|
+
else if (r.rulesOnly) out(m.rulesPulled(r.rules));
|
|
304
|
+
else out(m.synced(r.uploaded, r.rules, r.prompts));
|
|
248
305
|
return undefined;
|
|
249
306
|
}
|
|
250
307
|
case 'rules': {
|
|
251
308
|
let cache = null;
|
|
252
309
|
try { cache = JSON.parse(readFileSync(teamRulesCachePath(), 'utf8')); } catch { /* none cached */ }
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const r of rules) out(`- [${r.scope}] ${r.text}`);
|
|
310
|
+
if (formatRuleLines(cache).length === 0) { out(m.noRules); return undefined; }
|
|
311
|
+
for (const line of formatRuleLines(cache)) out(line);
|
|
256
312
|
return undefined;
|
|
257
313
|
}
|
|
258
314
|
case 'propose': {
|
|
@@ -272,7 +328,7 @@ export async function run({ args, hasFlag, getArg, version, fetch, interactive,
|
|
|
272
328
|
return verb && verb !== 'help' ? process.exit(2) : undefined;
|
|
273
329
|
}
|
|
274
330
|
} catch (e) {
|
|
275
|
-
console.error(e.message);
|
|
331
|
+
console.error(isSeatLimitError(e) ? m.seatLimit : e.message);
|
|
276
332
|
return process.exit(1);
|
|
277
333
|
}
|
|
278
334
|
}
|
|
@@ -46,8 +46,8 @@ export async function run({ hasFlag, version, agent = 'claude' }) {
|
|
|
46
46
|
|
|
47
47
|
if (updateCheckDisabled()) {
|
|
48
48
|
console.log(lang === 'ko'
|
|
49
|
-
? `현재 버전 v${version} — 업데이트 확인이 환경 변수로 꺼져 있습니다 (
|
|
50
|
-
: `v${version} — update checks are disabled by env var (
|
|
49
|
+
? `현재 버전 v${version} — 업데이트 확인이 환경 변수로 꺼져 있습니다 (SPRAG_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER).`
|
|
50
|
+
: `v${version} — update checks are disabled by env var (SPRAG_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER).`);
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
package/src/debug.js
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* a genuinely broken path (bad permissions on the state dir, a corrupt hook
|
|
8
8
|
* payload) is indistinguishable from "nothing to do".
|
|
9
9
|
*
|
|
10
|
-
* `
|
|
10
|
+
* `SPRAG_DEBUG=1` makes those swallowed failures visible on stderr — which the
|
|
11
11
|
* statusline contract discards and hooks surface in Claude Code's debug
|
|
12
12
|
* output — without changing behavior in any way.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
import { envVar } from './env.js';
|
|
16
|
+
const ENABLED = !!envVar('DEBUG');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* @param {string} scope short label for where the failure happened
|
package/src/delegation-agents.js
CHANGED
|
@@ -12,16 +12,23 @@
|
|
|
12
12
|
* that marker are ever overwritten or deleted; a same-named user file is left.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { claudeUserDir } from './paths.js';
|
|
18
18
|
import { loadConfig } from './config.js';
|
|
19
|
+
import { agentDefinitions } from './presets-source.js';
|
|
19
20
|
|
|
20
21
|
export const OWNER_MARK = 'sprag:delegation-agent';
|
|
21
22
|
const MARK_LINE = `# managed by sprag (${OWNER_MARK}); edits are overwritten`;
|
|
22
23
|
|
|
24
|
+
// The definitions come from presets-source.js: the centrally published bundle
|
|
25
|
+
// when it verifies, else presets/agents.json. Both have one shape:
|
|
26
|
+
// { name, model, effort, tools: [..], description: { ko, en }, body: { ko, en } }
|
|
27
|
+
//
|
|
23
28
|
// Models are aliases on purpose: behind a gateway they resolve through
|
|
24
|
-
// ANTHROPIC_DEFAULT_*_MODEL, and a full model id would bypass that mapping.
|
|
29
|
+
// ANTHROPIC_DEFAULT_*_MODEL, and a full model id would bypass that mapping. The
|
|
30
|
+
// exception is opus-worker, which names the full id: Claude Code resolves a bare
|
|
31
|
+
// alias to an older model than the one the tier means.
|
|
25
32
|
//
|
|
26
33
|
// Effort is a separate axis from the tier and is set by how the work gets
|
|
27
34
|
// verified, not by how hard it is. Where the check is command output or a file
|
|
@@ -30,68 +37,25 @@ const MARK_LINE = `# managed by sprag (${OWNER_MARK}); edits are overwritten`;
|
|
|
30
37
|
// caller already fixed, against tests, so it takes the default for well-specified
|
|
31
38
|
// work, medium; the opus worker takes the larger, harder share of that work at high. The main session's effort is never touched here, and
|
|
32
39
|
// CLAUDE_CODE_EFFORT_LEVEL must stay unset: it overrides every agent's own value.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
role: 'Your role: read-only exploration, lookups, status checks, and questions about pasted logs or text.',
|
|
40
|
-
description: 'Cheap read-only exploration, lookup, status checks and summaries for delegated tasks. It has no MCP tools.',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'haiku-runner',
|
|
44
|
-
model: 'haiku',
|
|
45
|
-
effort: 'low',
|
|
46
|
-
tools: 'Bash, Read, Grep, Glob',
|
|
47
|
-
role: 'Your role: run the build, test, or git commands you are given and report the result.',
|
|
48
|
-
description: 'Cheap runner for build, test and git commands in delegated tasks. It has no MCP tools.',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'haiku-translate',
|
|
52
|
-
model: 'haiku',
|
|
53
|
-
effort: 'low',
|
|
54
|
-
tools: 'Read, Grep, Glob, Edit, Write',
|
|
55
|
-
role: 'Your role: batch translation and mechanical text conversion of the files or text you are given.',
|
|
56
|
-
description: 'Cheap batch translation and structured text conversion for delegated tasks. It has no MCP tools.',
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: 'sonnet-worker',
|
|
60
|
-
model: 'sonnet',
|
|
61
|
-
effort: 'medium',
|
|
62
|
-
tools: 'Read, Grep, Glob, Bash, Edit, Write, NotebookEdit',
|
|
63
|
-
role: 'Your role: multi-step work and the implementation half of a design the caller already fixed.',
|
|
64
|
-
description: 'Mid-tier worker for multi-step tasks and implementation handed over with a fixed design. It has no MCP tools.',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
// The full model id, not the `opus` alias: Claude Code resolves a bare
|
|
68
|
-
// alias to an older model than the one the tier means (haiku was observed
|
|
69
|
-
// resolving to Haiku 4.5). A host that remaps ANTHROPIC_DEFAULT_OPUS_MODEL
|
|
70
|
-
// is better served by the rule's `model: opus` fallback phrase.
|
|
71
|
-
name: 'opus-worker',
|
|
72
|
-
model: 'claude-opus-5-5',
|
|
73
|
-
effort: 'high',
|
|
74
|
-
tools: 'Read, Grep, Glob, Bash, Edit, Write, NotebookEdit',
|
|
75
|
-
role: 'Your role: large implementation and debugging work whose design the caller already fixed. Hand design judgement and irreversible actions back to the caller.',
|
|
76
|
-
description: 'Heavy worker for Fable sessions: large implementation, debugging and refactoring handed over with a fixed design. It has no MCP tools.',
|
|
77
|
-
},
|
|
78
|
-
];
|
|
40
|
+
//
|
|
41
|
+
// The files are rendered from the English copy. They are prompts the model
|
|
42
|
+
// reads, and the Korean copy is what the /ops console shows beside it.
|
|
43
|
+
export function definitions() {
|
|
44
|
+
return agentDefinitions();
|
|
45
|
+
}
|
|
79
46
|
|
|
80
47
|
function render(def) {
|
|
48
|
+
const body = String((def.body && def.body.en) || '').replace(/\s+$/, '');
|
|
81
49
|
return [
|
|
82
50
|
'---',
|
|
83
51
|
`name: ${def.name}`,
|
|
84
|
-
`description: ${def.description}`,
|
|
85
|
-
`tools: ${def.tools}`,
|
|
52
|
+
`description: ${(def.description && def.description.en) || ''}`,
|
|
53
|
+
`tools: ${(Array.isArray(def.tools) ? def.tools : [def.tools]).join(', ')}`,
|
|
86
54
|
`model: ${def.model}`,
|
|
87
55
|
`effort: ${def.effort}`,
|
|
88
56
|
MARK_LINE,
|
|
89
57
|
'---',
|
|
90
|
-
|
|
91
|
-
'Do only the delegated task, and stay within the caps (tool calls, output tokens) the caller wrote in the prompt.',
|
|
92
|
-
"Report concisely, in the language of the caller's prompt.",
|
|
93
|
-
'If you need an MCP tool or any tool not on your list, stop at once and report one line: "needs general-purpose: <tools needed>". The parent will delegate again.',
|
|
94
|
-
'Do not guess at anything you could not verify; say that you could not verify it.',
|
|
58
|
+
body,
|
|
95
59
|
'',
|
|
96
60
|
].join('\n');
|
|
97
61
|
}
|
|
@@ -110,13 +74,27 @@ function readOwned(path) {
|
|
|
110
74
|
try { return { text: readFileSync(path, 'utf8') }; } catch { return null; }
|
|
111
75
|
}
|
|
112
76
|
|
|
77
|
+
/** Names of the files in the agents dir that carry our marker. */
|
|
78
|
+
function ownedNames() {
|
|
79
|
+
let entries = [];
|
|
80
|
+
try { entries = readdirSync(agentsDir()); } catch { return []; }
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
if (!e.endsWith('.md')) continue;
|
|
84
|
+
const f = readOwned(join(agentsDir(), e));
|
|
85
|
+
if (f && isOwned(f.text)) out.push(e.slice(0, -3));
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
113
90
|
export function removeDelegationAgents() {
|
|
114
91
|
const removed = [];
|
|
115
|
-
|
|
116
|
-
|
|
92
|
+
const names = new Set([...definitions().map((d) => d.name), ...ownedNames()]);
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
const p = fileOf(name);
|
|
117
95
|
try {
|
|
118
96
|
const f = readOwned(p);
|
|
119
|
-
if (f && isOwned(f.text)) { unlinkSync(p); removed.push(
|
|
97
|
+
if (f && isOwned(f.text)) { unlinkSync(p); removed.push(name); }
|
|
120
98
|
} catch { /* one file failing does not stop the rest */ }
|
|
121
99
|
}
|
|
122
100
|
return { written: [], unchanged: [], skipped: [], removed };
|
|
@@ -128,7 +106,8 @@ export function syncDelegationAgents({ agent = 'claude' } = {}) {
|
|
|
128
106
|
try {
|
|
129
107
|
if (loadConfig().delegationAgents === false) return removeDelegationAgents();
|
|
130
108
|
} catch { /* an unreadable config means the default, on */ }
|
|
131
|
-
|
|
109
|
+
const defs = definitions();
|
|
110
|
+
for (const def of defs) {
|
|
132
111
|
try {
|
|
133
112
|
const p = fileOf(def.name);
|
|
134
113
|
const want = render(def);
|
|
@@ -143,13 +122,22 @@ export function syncDelegationAgents({ agent = 'claude' } = {}) {
|
|
|
143
122
|
res.skipped.push({ name: def.name, reason: `error: ${e.message}` });
|
|
144
123
|
}
|
|
145
124
|
}
|
|
125
|
+
// An agent the published presets dropped or renamed: its old file is ours
|
|
126
|
+
// (the marker says so) and nothing names it any more.
|
|
127
|
+
if (defs.length > 0) {
|
|
128
|
+
const keep = new Set(defs.map((d) => d.name));
|
|
129
|
+
for (const name of ownedNames()) {
|
|
130
|
+
if (keep.has(name)) continue;
|
|
131
|
+
try { unlinkSync(fileOf(name)); res.removed.push(name); } catch { /* left for the next sync */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
146
134
|
return res;
|
|
147
135
|
}
|
|
148
136
|
|
|
149
137
|
export function delegationAgentStatus() {
|
|
150
138
|
let enabled = true;
|
|
151
139
|
try { enabled = loadConfig().delegationAgents !== false; } catch { /* default on */ }
|
|
152
|
-
const agents =
|
|
140
|
+
const agents = definitions().map((def) => {
|
|
153
141
|
const f = existsSync(fileOf(def.name)) ? readOwned(fileOf(def.name)) : null;
|
|
154
142
|
const state = !f ? 'absent' : isOwned(f.text) ? 'installed-by-sprag' : 'user-file';
|
|
155
143
|
return { name: def.name, state };
|