sprag-cli 3.74.0 → 3.75.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.74.0",
3
+ "version": "3.75.1",
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": {
package/src/agents.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * directory and a copied rule would otherwise name an agent that is not there.
17
17
  */
18
18
 
19
- import { existsSync } from 'node:fs';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { claudeUserDir } from './paths.js';
22
22
  import { agentDefinitions } from './presets-source.js';
@@ -134,7 +134,8 @@ export function agentPhrase(name, { model, root } = {}) {
134
134
  const resolved = resolveAgentName(name, root);
135
135
  if (!resolved) return `model: ${model || modelOf(name) || 'haiku'}`;
136
136
  const tier = modelOf(resolved) || model || 'haiku';
137
- return `${resolved}(model 인자 생략. 이 에이전트가 없거나 MCP 도구가 필요하면 general-purpose에 model: ${tier})`;
137
+ const spec = agentSpecLabel(resolved, root);
138
+ return `${resolved}(${spec ? `${spec}. ` : ''}model 인자 생략. 이 에이전트가 없거나 MCP 도구가 필요하면 general-purpose에 model: ${tier})`;
138
139
  }
139
140
 
140
141
  /**
@@ -146,5 +147,52 @@ export function agentPhraseEn(name, { model, root } = {}) {
146
147
  const resolved = resolveAgentName(name, root);
147
148
  if (!resolved) return `a model: ${model || modelOf(name) || 'haiku'} subagent`;
148
149
  const tier = modelOf(resolved) || model || 'haiku';
149
- return `the ${resolved} subagent (omit the model argument; if it is unavailable or MCP tools are needed, use general-purpose with model: ${tier})`;
150
+ const spec = agentSpecLabel(resolved, root, ', ');
151
+ return `the ${resolved} subagent (${spec ? `${spec}; ` : ''}omit the model argument; if it is unavailable or MCP tools are needed, use general-purpose with model: ${tier})`;
152
+ }
153
+
154
+ /**
155
+ * The model and effort an installed agent file really pins, read from its
156
+ * frontmatter, or null when the file has none.
157
+ *
158
+ * The agent name says only the tier (`t1-worker`), and Claude Code draws a
159
+ * delegated run as `t1-worker(…)` with no model beside it. The phrase carries
160
+ * what the file says so the session can show it. The file is read rather
161
+ * than the preset because a user's own copy is never overwritten, and it may
162
+ * pin something else. A file without `model:` inherits the session model,
163
+ * which Claude Code writes as `inherit`.
164
+ */
165
+ export function agentSpec(name, root = process.cwd()) {
166
+ const paths = [];
167
+ if (root !== null) paths.push(join(root, '.claude', 'agents', `${name}.md`));
168
+ paths.push(join(claudeUserDir(), 'agents', `${name}.md`));
169
+ for (const p of paths) {
170
+ let text;
171
+ try { text = readFileSync(p, 'utf8'); } catch { continue; }
172
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
173
+ if (!m) return null;
174
+ const field = (key) => {
175
+ const hit = new RegExp(`^${key}:[ \\t]*(.*?)[ \\t]*$`, 'm').exec(m[1]);
176
+ return hit && hit[1] ? hit[1] : null;
177
+ };
178
+ return { model: field('model') || 'inherit', effort: field('effort') };
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Display name for a model value: `claude-sonnet-5-5` → `Sonnet 5.5`, a dated
185
+ * id loses its date. An alias or a gateway id stays as written, since an alias
186
+ * can resolve to an older model and that is worth seeing.
187
+ */
188
+ export function modelLabel(model) {
189
+ const m = /^claude-([a-z]+)-(\d+)-(\d+)(?:-\d{8})?$/.exec(String(model || ''));
190
+ return m ? `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}.${m[3]}` : String(model || '');
191
+ }
192
+
193
+ /** `Sonnet 5.5 · effort medium` for an installed agent, or '' when unknown. */
194
+ function agentSpecLabel(name, root, sep = ' · ') {
195
+ const spec = agentSpec(name, root);
196
+ if (!spec) return '';
197
+ return spec.effort ? `${modelLabel(spec.model)}${sep}effort ${spec.effort}` : modelLabel(spec.model);
150
198
  }
@@ -71,6 +71,12 @@ export function isTrustedProject(cwd = process.cwd(), { home = homedir() } = {})
71
71
  *
72
72
  * 헬퍼 실행은 비용이 있으므로 갱신 경로(5분에 한 번 뜨는 detached 자식)에서만
73
73
  * 부릅니다. 수 초 간격으로 도는 렌더 경로에서 부르면 통계선이 그만큼 느려집니다.
74
+ *
75
+ * 헬퍼에는 APIKEY_HELPER_NONINTERACTIVE=1 을 넘겨, 캐시된 토큰이 없을 때 로그인
76
+ * 창을 띄우지 말고 바로 실패하라고 알립니다. 이 호출은 사용자 입력과 무관하게 5분마다
77
+ * 일어나므로, 헬퍼가 브라우저 인증을 시작하면 자리를 비운 동안 인증창이 쌓입니다.
78
+ * 게다가 HELPER_TIMEOUT_MS 안에 인증이 끝날 수 없어서 그 창은 어차피 쓸모가 없습니다.
79
+ * 이 변수를 모르는 헬퍼는 그대로 동작합니다.
74
80
  */
75
81
  export function keyFromApiKeyHelper(cwd = process.cwd()) {
76
82
  // 프로젝트 설정의 헬퍼는 그 저장소가 넣어 둔 셸 명령입니다. Claude Code 는 신뢰
@@ -96,6 +102,7 @@ export function keyFromApiKeyHelper(cwd = process.cwd()) {
96
102
  encoding: 'utf8',
97
103
  timeout: HELPER_TIMEOUT_MS,
98
104
  stdio: ['ignore', 'pipe', 'ignore'],
105
+ env: { ...process.env, APIKEY_HELPER_NONINTERACTIVE: '1' },
99
106
  });
100
107
  // 진행 로그를 함께 출력하는 헬퍼가 있으므로 마지막 비어 있지 않은 줄을 취합니다.
101
108
  const token = out.trim().split(/\r?\n/).filter(Boolean).pop();
@@ -263,7 +263,11 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
263
263
  '',
264
264
  '위임을 실행할 때는 사용자에게 다음 한 줄을 먼저 표시할 것',
265
265
  '(어떤 도구가 토큰을 아끼고 있는지 가시화):',
266
- '`🔀 [sprag] 모델 피팅: "<유형>" → <agent> 위임`',
266
+ '`🔀 [sprag] 모델 피팅: "<유형>" → <agent> (<모델> · effort <effort>) 위임`',
267
+ '',
268
+ '<모델>과 <effort>에는 룰의 에이전트 표기 괄호 안에 적힌 값(예: Sonnet 5.5 · effort medium)을',
269
+ '그대로 옮기고, 값이 적혀 있지 않으면 그 부분을 뺍니다. Claude Code 호출 행에는 에이전트',
270
+ '이름만 보이므로, Agent 도구의 description 맨 앞에도 `[<모델> · <effort>]` 를 붙이십시오.',
267
271
  '',
268
272
  '## 공통 조항 (룰마다 반복하지 않으므로 여기서 한 번만 읽으십시오)',
269
273
  '',
@@ -326,7 +330,12 @@ export function renderModelRatchet(rules, lang = userLanguage(), { root } = {})
326
330
  '',
327
331
  'When delegating under a rule below, show the user this line first so it is',
328
332
  'visible which tool is saving tokens:',
329
- '`🔀 [sprag] model fitting: "<category>" → delegated to <agent>`',
333
+ '`🔀 [sprag] model fitting: "<category>" → delegated to <agent> (<model> · effort <effort>)`',
334
+ '',
335
+ 'Fill <model> and <effort> from the parentheses after the agent in the rule',
336
+ '(e.g. Sonnet 5.5, effort medium), and leave out whichever the rule does not',
337
+ 'give. Claude Code shows only the agent name on the call row, so also start',
338
+ 'the Agent tool\'s description with `[<model> · <effort>]`.',
330
339
  '',
331
340
  '## Shared clauses (stated once here, not repeated per rule)',
332
341
  '',
@@ -296,9 +296,11 @@ export function routeHint(text, { rules, lang = userLanguage(), root, sessionRan
296
296
  // string whichever path decided to delegate.
297
297
  return ko
298
298
  ? `[sprag] 이 요청은 사용자가 이미 승인한 위임 룰 "${label}" 에 해당합니다: ${target} 로 위임하십시오 (${capText}). `
299
- + `되묻지 말고 위임하고, 위임할 때 \`🔀 [sprag] 모델 피팅: "${label}" → <agent> 위임\` 을 먼저 표시하십시오. `
299
+ + `되묻지 말고 위임하고, 위임할 때 \`🔀 [sprag] 모델 피팅: "${label}" → <agent> (<모델> · effort <effort>) 위임\` 을 먼저 표시하십시오. `
300
+ + `모델과 effort 값은 위 에이전트 표기에 적힌 그대로 쓰고, Agent 도구의 description 맨 앞에도 \`[<모델> · <effort>]\` 를 붙이십시오. `
300
301
  + `상한을 넘길 것 같거나 에러가 나면 그 자리에서 멈추고 메인 모델이 이어받습니다.`
301
302
  : `[sprag] This request matches a delegation rule the user already approved — "${label}": delegate to ${target} (${capText}). `
302
- + `Do not ask again; print \`🔀 [sprag] model fit: "${label}" → <agent>\` first. `
303
+ + `Do not ask again; print \`🔀 [sprag] model fit: "${label}" → <agent> (<model> · effort <effort>)\` first, `
304
+ + `with the model and effort the agent's phrase above gives, and start the Agent tool's description with \`[<model> · <effort>]\`. `
303
305
  + `If the run is likely to exceed its cap or hits an error, stop there and the main model takes over.`;
304
306
  }