@cspeach/cli 1.1.33 → 1.1.35
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/dist/agent/anthropic-provider.js +2 -0
- package/dist/agent/azure-profile.js +7 -0
- package/dist/agent/connection-profile.js +49 -0
- package/dist/agent/loop.js +182 -39
- package/dist/agent/provider-shape.js +36 -0
- package/dist/agent/providers/ai-hub-provider.js +6 -5
- package/dist/agent/providers/azure-provider.js +125 -0
- package/dist/agent/providers/byok-provider.js +8 -7
- package/dist/agent/providers/factory.js +8 -3
- package/dist/agent/providers/local-provider.js +18 -22
- package/dist/agent/risk-gate.js +202 -0
- package/dist/agent/summarise-via-provider.js +55 -8
- package/dist/agent/tool-dispatch.js +19 -0
- package/dist/agent/turn-error-ux.js +22 -0
- package/dist/auth/api-key.js +10 -5
- package/dist/auth/auth-file.js +35 -6
- package/dist/auth/azure/azure-key.js +21 -0
- package/dist/auth/azure/azure-log.js +45 -0
- package/dist/auth/azure/cache-store.js +171 -0
- package/dist/auth/azure/clear.js +28 -0
- package/dist/auth/azure/loopback.js +192 -0
- package/dist/auth/azure/msal-client.js +41 -0
- package/dist/auth/azure/sign-in.js +219 -0
- package/dist/auth/azure/startup-sign-in.js +64 -0
- package/dist/auth/azure/token-provider.js +116 -0
- package/dist/auth/key-resolver.js +258 -0
- package/dist/auth/me.js +11 -5
- package/dist/cli.js +21 -2
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +74 -3
- package/dist/commands/config-show.js +25 -0
- package/dist/commands/cost.js +19 -6
- package/dist/commands/help.js +2 -2
- package/dist/commands/login-azure.js +33 -0
- package/dist/commands/login.js +56 -8
- package/dist/commands/logout.js +34 -10
- package/dist/commands/plan-boundary.js +4 -4
- package/dist/commands/plan-model-tier.js +8 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/whoami.js +113 -9
- package/dist/config/loader.js +5 -0
- package/dist/connection/azure-auth.js +135 -0
- package/dist/connection/azure-errors.js +103 -0
- package/dist/connection/azure-facts.js +21 -0
- package/dist/connection/azure-gate.js +40 -0
- package/dist/connection/azure-last-call.js +5 -0
- package/dist/connection/azure-turn-error.js +25 -0
- package/dist/connection/cache.js +70 -0
- package/dist/connection/fetch.js +33 -0
- package/dist/connection/guard.js +94 -0
- package/dist/connection/limit-gate.js +26 -0
- package/dist/connection/local-usage.js +71 -0
- package/dist/connection/login-step.js +62 -0
- package/dist/connection/startup.js +67 -0
- package/dist/connection/state.js +68 -0
- package/dist/connection/types.js +1 -0
- package/dist/cost/cost-log.js +3 -1
- package/dist/cost/credits-wire.js +1 -1
- package/dist/cost/credits.js +4 -0
- package/dist/cost/display-mode.js +7 -0
- package/dist/cost/router-spend.js +3 -0
- package/dist/cost/session-cost.js +4 -0
- package/dist/doctor/checks/azure.js +100 -0
- package/dist/doctor/checks/credits.js +4 -3
- package/dist/doctor/checks/usage-reporting.js +22 -0
- package/dist/doctor/run.js +7 -0
- package/dist/lib/graceful-exit.js +17 -0
- package/dist/licence/licence-check.js +205 -0
- package/dist/licence/paths.js +6 -0
- package/dist/licence/turn-gate.js +66 -0
- package/dist/models/model-identity.js +65 -0
- package/dist/models/resolve.js +8 -0
- package/dist/models/server-config.js +3 -2
- package/dist/one-shot.js +86 -2
- package/dist/projects/answer-blockers.js +25 -12
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/renderer/tool-labels.js +8 -2
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- package/dist/repl/ask-or-no.js +18 -0
- package/dist/repl/builtin-commands.js +10 -2
- package/dist/repl/post-turn-status.js +3 -2
- package/dist/repl/safety-confirm.js +47 -2
- package/dist/repl/session-spend-line.js +4 -1
- package/dist/repl/startup-account.js +51 -0
- package/dist/repl.js +217 -48
- package/dist/router/byok-classifier.js +41 -14
- package/dist/router/classifier.js +5 -16
- package/dist/router/router-target.js +41 -0
- package/dist/sap/connection-manager.js +15 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/sap/session-end.js +66 -0
- package/dist/session/audit-export.js +11 -4
- package/dist/skills/manifest-client.js +37 -4
- package/dist/skills/skills-cache.js +115 -0
- package/dist/skills/source-authed.js +276 -0
- package/dist/tools/ask-question.js +1 -1
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- package/dist/tools/sap-read.js +22 -22
- package/dist/tools/sap-write.js +249 -18
- package/dist/tools/snapshot.js +26 -0
- package/dist/tools/tool-loading.js +9 -10
- package/dist/ui/app.js +19 -9
- package/dist/ui/approval-modal.js +37 -5
- package/dist/ui/live-area.js +14 -1
- package/dist/ui/widgets/ask-card.js +183 -107
- package/dist/ui/widgets/coaching-picker.js +22 -1
- package/dist/ui/widgets/confirm-card.js +102 -76
- package/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +4 -3
- package/dist/skills/bundled-skills.js +0 -286
- package/dist/skills/source-bundled.js +0 -20
- package/dist/skills/source-manifest.js +0 -36
package/dist/auth/me.js
CHANGED
|
@@ -18,20 +18,26 @@ export async function fetchMe(opts) {
|
|
|
18
18
|
try {
|
|
19
19
|
return await Promise.race([
|
|
20
20
|
(async () => {
|
|
21
|
-
|
|
21
|
+
// No extra headers → exactly `{ Authorization }`, as before the batch.
|
|
22
|
+
const r = await fetch(url, { headers: { ...(opts.headers ?? {}), Authorization: `Bearer ${opts.apiKey}` }, signal });
|
|
23
|
+
const serverDate = r.headers.get('date');
|
|
22
24
|
if (!r.ok) {
|
|
23
25
|
const body = await r.json().catch(() => ({}));
|
|
24
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
ok: false, status: r.status, error: body?.error ?? 'unknown',
|
|
28
|
+
...(typeof body?.reason === 'string' ? { reason: body.reason } : {}),
|
|
29
|
+
serverDate,
|
|
30
|
+
};
|
|
25
31
|
}
|
|
26
32
|
const profile = await r.json();
|
|
27
|
-
return { ok: true, profile };
|
|
33
|
+
return { ok: true, profile, serverDate };
|
|
28
34
|
})(),
|
|
29
35
|
timedOut,
|
|
30
36
|
]);
|
|
31
37
|
}
|
|
32
38
|
catch (err) {
|
|
33
39
|
if (signal.aborted)
|
|
34
|
-
return { ok: false, status: 0, error: `timed out after ${timeoutMs} ms
|
|
35
|
-
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
|
|
40
|
+
return { ok: false, status: 0, error: `timed out after ${timeoutMs} ms`, serverDate: null };
|
|
41
|
+
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err), serverDate: null };
|
|
36
42
|
}
|
|
37
43
|
}
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,15 @@ export async function main(argv) {
|
|
|
27
27
|
const legacyNotice = legacyByokKeyNotice(await migrateLegacyByokKey().catch(() => 'failed'));
|
|
28
28
|
if (legacyNotice)
|
|
29
29
|
console.error(legacyNotice);
|
|
30
|
+
// Security batch item 6 — move the CSPeach key from auth.json into this
|
|
31
|
+
// home's OS credential store entry (copy → read back → only then remove).
|
|
32
|
+
// Once; idempotent; one notice when it cannot.
|
|
33
|
+
{
|
|
34
|
+
const { migrateSignIn, signInMigrationNotice } = await import('./auth/key-resolver.js');
|
|
35
|
+
const notice = signInMigrationNotice(await migrateSignIn().catch(() => 'failed'));
|
|
36
|
+
if (notice)
|
|
37
|
+
console.error(notice);
|
|
38
|
+
}
|
|
30
39
|
let args = argv.slice(2);
|
|
31
40
|
// Standalone mode (2026-09-16) — `--no-sap` is accepted ANYWHERE in the args
|
|
32
41
|
// for both the REPL and one-shot, and stripped like `--sap-alias` below.
|
|
@@ -162,6 +171,14 @@ export async function main(argv) {
|
|
|
162
171
|
const { runDoctor } = await import('./doctor/run.js');
|
|
163
172
|
return runDoctor();
|
|
164
173
|
}
|
|
174
|
+
// Azure tenant mode (Task 6b) — the Microsoft sign-in alone, with the
|
|
175
|
+
// company settings already on this machine. Same opener as `cspeach login`:
|
|
176
|
+
// no shell, the authorize URL (full of &) as one argument.
|
|
177
|
+
if (args[0] === 'login' && args[1] === 'azure') {
|
|
178
|
+
const { runLoginAzureCommand } = await import('./commands/login-azure.js');
|
|
179
|
+
const { loginOpener } = await import('./util/open-in-browser.js');
|
|
180
|
+
return runLoginAzureCommand({ deviceCode: args.includes('--device-code'), print: (s) => console.log(s), open: loginOpener() });
|
|
181
|
+
}
|
|
165
182
|
if (args[0] === 'login') {
|
|
166
183
|
const { runLoginCommand } = await import('./commands/login.js');
|
|
167
184
|
const { input } = await import('@inquirer/prompts');
|
|
@@ -176,6 +193,7 @@ export async function main(argv) {
|
|
|
176
193
|
open,
|
|
177
194
|
print: (s) => console.log(s),
|
|
178
195
|
prompt: (q) => input({ message: q }),
|
|
196
|
+
deviceCode: args.includes('--device-code'),
|
|
179
197
|
});
|
|
180
198
|
}
|
|
181
199
|
if (args[0] === 'logout') {
|
|
@@ -186,7 +204,7 @@ export async function main(argv) {
|
|
|
186
204
|
const { runWhoamiCommand } = await import('./commands/whoami.js');
|
|
187
205
|
const { loadConfig } = await import('./config/loader.js');
|
|
188
206
|
const cfg = await loadConfig();
|
|
189
|
-
return runWhoamiCommand({ proxyUrl: cfg.proxy_url, print: (s) => console.log(s) });
|
|
207
|
+
return runWhoamiCommand({ proxyUrl: cfg.proxy_url, print: (s) => console.log(s), mode: cfg.llm.mode });
|
|
190
208
|
}
|
|
191
209
|
// A team project — many laptops, one picture (spec 2026-09-18).
|
|
192
210
|
if (args[0] === 'team') {
|
|
@@ -297,7 +315,8 @@ Usage:
|
|
|
297
315
|
cspeach sap link [alias] [--project dir] Let a local CAP app reach its S/4 services (Ctrl+C stops)
|
|
298
316
|
cspeach doctor Diagnose install + connectivity
|
|
299
317
|
cspeach login Sign in via device-code flow
|
|
300
|
-
cspeach
|
|
318
|
+
cspeach login azure Sign in to Microsoft for your company's Azure (--device-code: use a code instead of the browser)
|
|
319
|
+
cspeach logout Sign out (removes the CSPeach key from this home)
|
|
301
320
|
cspeach whoami Print current account info
|
|
302
321
|
cspeach team discover Read the whole estate from SAP into this folder (read only)
|
|
303
322
|
cspeach team start <packages> Start a team project in this folder: object list, first check, you own it all
|
package/dist/commands/compact.js
CHANGED
|
@@ -33,6 +33,7 @@ import { paint } from '../renderer/theme.js';
|
|
|
33
33
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
34
34
|
import { saveSession } from '../session/store.js';
|
|
35
35
|
import { isUserPrompt } from '../session/user-prompt.js';
|
|
36
|
+
import { getActiveDeployments } from '../models/model-identity.js';
|
|
36
37
|
/** Keep this many recent user-prompt anchored turns verbatim. */
|
|
37
38
|
export const KEEP_RECENT_TURNS = 5;
|
|
38
39
|
/** Don't bother compacting if there's only a handful of older turns. */
|
|
@@ -200,6 +201,16 @@ export function renderTodoSection(todos) {
|
|
|
200
201
|
* turns are where the list was built, and Haiku isn't trusted to
|
|
201
202
|
* reproduce it verbatim.
|
|
202
203
|
*/
|
|
204
|
+
/**
|
|
205
|
+
* Azure tenant mode, Task 5a — the model the summary line names. With a
|
|
206
|
+
* company deployment table (azure only) compaction runs on the small
|
|
207
|
+
* deployment, so the line names it and its family; every other mode keeps
|
|
208
|
+
* the COMPACTION_MODEL text byte for byte.
|
|
209
|
+
*/
|
|
210
|
+
function compactionModelLabel() {
|
|
211
|
+
const table = getActiveDeployments();
|
|
212
|
+
return table ? `${table.small.name} (${table.small.model})` : COMPACTION_MODEL;
|
|
213
|
+
}
|
|
203
214
|
export function buildSummaryMessage(summary, droppedTurns, todos) {
|
|
204
215
|
const ts = new Date().toISOString();
|
|
205
216
|
const todoSection = renderTodoSection(todos);
|
|
@@ -207,7 +218,7 @@ export function buildSummaryMessage(summary, droppedTurns, todos) {
|
|
|
207
218
|
role: 'user',
|
|
208
219
|
content: [
|
|
209
220
|
`[Context summary auto-generated by /compact at ${ts}.`,
|
|
210
|
-
`Original turns (${droppedTurns}) were summarised by ${
|
|
221
|
+
`Original turns (${droppedTurns}) were summarised by ${compactionModelLabel()} to`,
|
|
211
222
|
`reduce cache cost on subsequent turns. The original session is backed up`,
|
|
212
223
|
`to ~/.cspeach/sessions/${'<session-id>'}.pre-compact-<timestamp>.json]`,
|
|
213
224
|
'',
|
|
@@ -20,6 +20,7 @@ const MUTABLE_KEYS = new Set([
|
|
|
20
20
|
'write_mode',
|
|
21
21
|
'plan_mode',
|
|
22
22
|
'plan_audit',
|
|
23
|
+
'usage_reporting',
|
|
23
24
|
'approvals',
|
|
24
25
|
'no_sap',
|
|
25
26
|
'llm.mode',
|
|
@@ -31,6 +32,7 @@ const MUTABLE_KEYS = new Set([
|
|
|
31
32
|
// Virtual keys that route to keychain (not saved to config.toml):
|
|
32
33
|
'llm.byok_key',
|
|
33
34
|
'llm.ai_hub_token',
|
|
35
|
+
'llm.azure_key',
|
|
34
36
|
]);
|
|
35
37
|
// SAP-scoped key pattern: sap.<alias>.auto_approve
|
|
36
38
|
const SAP_AUTO_APPROVE_RE = /^sap\.([A-Z0-9_-]{2,32})\.auto_approve$/;
|
|
@@ -70,9 +72,10 @@ const EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
|
70
72
|
const TELEMETRY_VALUES = ['minimal', 'full'];
|
|
71
73
|
const AUTO_APPROVE_VALUES = ['never', 'low', 'medium'];
|
|
72
74
|
const SYSTEM_ROLE_VALUES = ['dev', 'qas', 'prd'];
|
|
73
|
-
const LLM_MODE_VALUES = ['managed', 'byok', 'ai-hub', 'local'];
|
|
75
|
+
const LLM_MODE_VALUES = ['managed', 'byok', 'ai-hub', 'local', 'azure'];
|
|
74
76
|
const PLAN_MODE_VALUES = ['step', 'guarded'];
|
|
75
77
|
const PLAN_AUDIT_VALUES = ['on', 'off'];
|
|
78
|
+
const USAGE_REPORTING_VALUES = ['on', 'off'];
|
|
76
79
|
const APPROVALS_VALUES = ['plan', 'per-write'];
|
|
77
80
|
/**
|
|
78
81
|
* Thrown on validation failure. The outer CLI dispatcher converts to
|
|
@@ -105,7 +108,7 @@ const INTERNAL_KEYS = new Set(['default_model_set', 'effort_set', 'file_keys']);
|
|
|
105
108
|
* unset` cannot remove them — that is a keychain operation — so they are
|
|
106
109
|
* rejected with a pointer rather than silently no-op'd.
|
|
107
110
|
*/
|
|
108
|
-
const VIRTUAL_KEYCHAIN_KEYS = new Set(['llm.byok_key', 'llm.ai_hub_token']);
|
|
111
|
+
const VIRTUAL_KEYCHAIN_KEYS = new Set(['llm.byok_key', 'llm.ai_hub_token', 'llm.azure_key']);
|
|
109
112
|
/**
|
|
110
113
|
* MUTABLE_KEYS that `config unset` can act on (excludes keychain virtuals and
|
|
111
114
|
* the deprecated `local_files` alias — the canonical name is `local_build`).
|
|
@@ -392,6 +395,38 @@ export async function runConfigSet(args) {
|
|
|
392
395
|
console.log(chalk.green(`Set ${key} = ${value}`));
|
|
393
396
|
return;
|
|
394
397
|
}
|
|
398
|
+
// Azure tenant mode (Task 5a) — the admin-issued Azure key, only when the
|
|
399
|
+
// company allows keys. Stored in the OS credential store per CSPEACH_HOME.
|
|
400
|
+
if (key === 'llm.azure_key') {
|
|
401
|
+
const { readConnectionCache } = await import('../connection/cache.js');
|
|
402
|
+
const company = await readConnectionCache();
|
|
403
|
+
if (!company || company.removed)
|
|
404
|
+
err('Your company has no Azure settings in CSPeach. Run cspeach login first.');
|
|
405
|
+
if (!company.connection.sign_in.includes('key'))
|
|
406
|
+
err('Your company does not allow Azure keys in CSPeach. Use cspeach login azure.');
|
|
407
|
+
let secret = value;
|
|
408
|
+
if (secret === '-') {
|
|
409
|
+
if (process.stdin.isTTY) {
|
|
410
|
+
const { password } = await import('@inquirer/prompts');
|
|
411
|
+
secret = await password({ message: 'Azure key (input masked):', mask: '*' });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const chunks = [];
|
|
415
|
+
for await (const c of process.stdin)
|
|
416
|
+
chunks.push(c);
|
|
417
|
+
secret = Buffer.concat(chunks).toString('utf8').trim();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
console.log(chalk.yellow('WARN: the key was passed as a shell argument and is now in your shell history. Prefer: cspeach config set llm.azure_key -'));
|
|
422
|
+
}
|
|
423
|
+
if (!secret || !secret.trim())
|
|
424
|
+
err('No key was given.');
|
|
425
|
+
const { setAzureKey } = await import('../auth/azure/azure-key.js');
|
|
426
|
+
await setAzureKey(secret.trim());
|
|
427
|
+
console.log(chalk.green('✓ Azure key stored in your OS credential store'));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
395
430
|
// Virtual keys that store secrets in the OS keychain instead of config.toml.
|
|
396
431
|
if (key === 'llm.byok_key' || key === 'llm.ai_hub_token') {
|
|
397
432
|
// Support stdin piping for secrets: `echo "key" | cspeach config set llm.byok_key -`
|
|
@@ -557,6 +592,25 @@ export async function runConfigSet(args) {
|
|
|
557
592
|
}
|
|
558
593
|
cfg.plan_audit = value;
|
|
559
594
|
break;
|
|
595
|
+
case 'usage_reporting':
|
|
596
|
+
// Security batch item 3 — usage counts from non-managed turns.
|
|
597
|
+
if (!USAGE_REPORTING_VALUES.includes(value)) {
|
|
598
|
+
err(`Invalid usage_reporting "${value}". Allowed: ${USAGE_REPORTING_VALUES.join(', ')}`);
|
|
599
|
+
}
|
|
600
|
+
// Azure tenant mode, Task 8b (plan D11) — the company decides: when its
|
|
601
|
+
// settings require usage counts, or this seat has a monthly limit (which
|
|
602
|
+
// needs the counts; fix round 1, ruling 1), "off" is refused and nothing
|
|
603
|
+
// changes. Every other mode keeps today's switch exactly.
|
|
604
|
+
if (value === 'off' && cfg.llm?.mode === 'azure') {
|
|
605
|
+
const { companyUsageRule } = await import('../usage/usage-reporter.js');
|
|
606
|
+
const rule = await companyUsageRule(cfg);
|
|
607
|
+
if (rule === 'required')
|
|
608
|
+
err('Your company requires usage counts for CSPeach on Azure');
|
|
609
|
+
if (rule === 'limit')
|
|
610
|
+
err('Your company sets a monthly CSPeach limit for you, so usage counts stay on.');
|
|
611
|
+
}
|
|
612
|
+
cfg.usage_reporting = value;
|
|
613
|
+
break;
|
|
560
614
|
case 'approvals':
|
|
561
615
|
// 'plan' = the plan's one approval covers its listed objects (default);
|
|
562
616
|
// 'per-write' = a card before every write.
|
|
@@ -565,12 +619,21 @@ export async function runConfigSet(args) {
|
|
|
565
619
|
}
|
|
566
620
|
cfg.approvals = value;
|
|
567
621
|
break;
|
|
568
|
-
case 'llm.mode':
|
|
622
|
+
case 'llm.mode': {
|
|
569
623
|
if (!LLM_MODE_VALUES.includes(value)) {
|
|
570
624
|
err(`Invalid llm.mode "${value}". Allowed: ${LLM_MODE_VALUES.join(', ')}`);
|
|
571
625
|
}
|
|
626
|
+
// Azure tenant mode — the org setting wins (spec "How settings reach the
|
|
627
|
+
// CLI"). The cache is this CSPeach home's connection.json.
|
|
628
|
+
const { readConnectionCache } = await import('../connection/cache.js');
|
|
629
|
+
const company = await readConnectionCache();
|
|
630
|
+
if (value !== 'azure' && company && !company.removed)
|
|
631
|
+
err('Your company set CSPeach to use your Azure. Run cspeach logout to leave it.');
|
|
632
|
+
if (value === 'azure' && !company)
|
|
633
|
+
err('Your company has no Azure settings on this machine. Run cspeach login to load them.');
|
|
572
634
|
cfg.llm.mode = value;
|
|
573
635
|
break;
|
|
636
|
+
}
|
|
574
637
|
case 'llm.ai_hub_base_url':
|
|
575
638
|
try {
|
|
576
639
|
new URL(value);
|
|
@@ -613,6 +676,11 @@ export async function runConfigSet(args) {
|
|
|
613
676
|
// value-diff can't see — e.g. `config set default_model claude-opus-4-8`).
|
|
614
677
|
markExplicit(cfg, key);
|
|
615
678
|
await saveConfig(cfg);
|
|
679
|
+
// F15 — off means off: the counts already waiting are deleted too.
|
|
680
|
+
if (key === 'usage_reporting' && value === 'off') {
|
|
681
|
+
const { deleteUsageQueue } = await import('../usage/usage-queue.js');
|
|
682
|
+
await deleteUsageQueue().catch(() => { });
|
|
683
|
+
}
|
|
616
684
|
console.log(chalk.green(`Set ${key} = ${value}`));
|
|
617
685
|
}
|
|
618
686
|
/**
|
|
@@ -689,6 +757,8 @@ export function validateValue(key, value) {
|
|
|
689
757
|
return PLAN_MODE_VALUES.includes(value);
|
|
690
758
|
case 'plan_audit':
|
|
691
759
|
return PLAN_AUDIT_VALUES.includes(value);
|
|
760
|
+
case 'usage_reporting':
|
|
761
|
+
return USAGE_REPORTING_VALUES.includes(value);
|
|
692
762
|
case 'approvals':
|
|
693
763
|
return APPROVALS_VALUES.includes(value);
|
|
694
764
|
case 'no_sap':
|
|
@@ -711,6 +781,7 @@ export function validateValue(key, value) {
|
|
|
711
781
|
return isModelAlias(value);
|
|
712
782
|
case 'llm.byok_key':
|
|
713
783
|
case 'llm.ai_hub_token':
|
|
784
|
+
case 'llm.azure_key':
|
|
714
785
|
return value.length > 0; // Secrets pass through if non-empty; no format check
|
|
715
786
|
default:
|
|
716
787
|
return false;
|
|
@@ -51,6 +51,7 @@ const LLM_REQUIRED_KEYS_BY_MODE = {
|
|
|
51
51
|
byok: [],
|
|
52
52
|
'ai-hub': ['ai_hub_base_url', 'ai_hub_model_alias'],
|
|
53
53
|
local: ['local_base_url', 'local_model'],
|
|
54
|
+
azure: [],
|
|
54
55
|
};
|
|
55
56
|
function printLlmMissingHints(llm) {
|
|
56
57
|
if (typeof llm !== 'object' || llm === null)
|
|
@@ -134,6 +135,24 @@ function resolveDottedKey(cfg, keyPath) {
|
|
|
134
135
|
}
|
|
135
136
|
return current;
|
|
136
137
|
}
|
|
138
|
+
/** Azure tenant mode (Task 7) — the company's settings, read-only, never a secret. */
|
|
139
|
+
export function formatAzureBlock(c) {
|
|
140
|
+
const d = c.connection.deployments;
|
|
141
|
+
const entry = (e) => (e ? `${e.name} (${e.model})` : '<none: plan reviews run on main>');
|
|
142
|
+
return [
|
|
143
|
+
'',
|
|
144
|
+
`${chalk.bold('[azure]')} ${chalk.gray('# set by your company · read-only · change it through your CSPeach admin')}`,
|
|
145
|
+
`endpoint = "${c.connection.endpoint}"`,
|
|
146
|
+
`tenant = "${c.connection.entra_tenant_id}"`,
|
|
147
|
+
`main = "${entry(d.main)}"`,
|
|
148
|
+
`small = "${entry(d.small)}"`,
|
|
149
|
+
`review = "${entry(d.review)}"`,
|
|
150
|
+
`sign_in = "${c.connection.sign_in.join(', ')}"`,
|
|
151
|
+
`usage_counts = "${c.connection.usage_counts}"`,
|
|
152
|
+
...(c.pending ? [chalk.yellow(`# a changed endpoint is waiting for you: ${c.pending.endpoint} (start cspeach to review it)`)] : []),
|
|
153
|
+
...(c.removed ? [chalk.yellow('# your company removed these settings: run cspeach login')] : []),
|
|
154
|
+
];
|
|
155
|
+
}
|
|
137
156
|
/**
|
|
138
157
|
* Executes `cspeach config show [key]`.
|
|
139
158
|
* Without argument: prints full config as TOML (secrets redacted).
|
|
@@ -153,6 +172,12 @@ export async function runConfigShow(args) {
|
|
|
153
172
|
if (customer?.model && customer.enforced) {
|
|
154
173
|
console.log(chalk.gray(`# model set by your account admin: ${customer.model} (enforced)`));
|
|
155
174
|
}
|
|
175
|
+
if (cfg.llm.mode === 'azure') {
|
|
176
|
+
const { readConnectionCache } = await import('../connection/cache.js');
|
|
177
|
+
const c = await readConnectionCache();
|
|
178
|
+
for (const l of c ? formatAzureBlock(c) : ['', chalk.yellow("# [azure] your company's settings are not on this machine: run cspeach login")])
|
|
179
|
+
console.log(l);
|
|
180
|
+
}
|
|
156
181
|
return;
|
|
157
182
|
}
|
|
158
183
|
const keyPath = args[0];
|
package/dist/commands/cost.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { readCostLog, summarise, turnRows } from '../cost/cost-log.js';
|
|
13
13
|
import { formatCost } from '../cost/pricing.js';
|
|
14
|
-
import { creditsActive, formatCredits, globalCredits, CREDITS_PENDING, } from '../cost/credits.js';
|
|
14
|
+
import { creditsActive, tokensOnlyActive, formatCredits, globalCredits, CREDITS_PENDING, } from '../cost/credits.js';
|
|
15
15
|
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
16
16
|
import { formatInfo } from '../renderer/notice-shape.js';
|
|
17
17
|
import { countLabel } from '../renderer/checklist-format.js';
|
|
@@ -54,6 +54,9 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
54
54
|
// 2026-10-02 (owner): tokens go too. Not a COGS leak on their own, but beside
|
|
55
55
|
// credits they invite "credits per token" maths — credits only for managed.
|
|
56
56
|
const inCredits = creditsActive();
|
|
57
|
+
// Azure (Task 5b): tokens only — CSPeach has no price for the customer's
|
|
58
|
+
// deployments, so no formatCost() figure is printed anywhere below.
|
|
59
|
+
const tokensOnly = tokensOnlyActive();
|
|
57
60
|
const perTurn = summariseCredits(entries);
|
|
58
61
|
const turnsNote = `${s.turns} turn${s.turns === 1 ? '' : 's'}`;
|
|
59
62
|
emit('');
|
|
@@ -66,6 +69,9 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
66
69
|
if (credits?.balance != null)
|
|
67
70
|
emit(panelRow('Balance', formatCredits(credits.balance, unit)));
|
|
68
71
|
}
|
|
72
|
+
else if (tokensOnly) {
|
|
73
|
+
emit(panelHeading('Session tokens so far', turnsNote));
|
|
74
|
+
}
|
|
69
75
|
else {
|
|
70
76
|
emit(panelHeading(`Session cost so far: ${formatCost(s.totalCost)}`, turnsNote));
|
|
71
77
|
}
|
|
@@ -75,13 +81,19 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
75
81
|
emit(panelRow('input', s.totalTokens.input.toLocaleString().padStart(12)));
|
|
76
82
|
emit(panelRow('output', s.totalTokens.output.toLocaleString().padStart(12)));
|
|
77
83
|
emit(panelRow('cache read', s.totalTokens.cacheRead.toLocaleString().padStart(12)));
|
|
78
|
-
|
|
84
|
+
// Azure (Task 7, ruling 7): the spec's wording; other modes unchanged.
|
|
85
|
+
emit(panelRow(tokensOnly ? 'cache write' : 'cache create', s.totalTokens.cacheCreate.toLocaleString().padStart(12)));
|
|
86
|
+
}
|
|
87
|
+
if (tokensOnly) {
|
|
88
|
+
emit('');
|
|
89
|
+
emit(panelRow('Cost', 'billed on your Azure invoice · CSPeach has no price for your deployments'));
|
|
79
90
|
}
|
|
80
91
|
// Task 18 fix M3 — reader subagent spend (already inside the total above).
|
|
81
92
|
// USD only: in credits mode the balance delta already carries it.
|
|
82
93
|
if (s.readerCalls > 0 && !inCredits) {
|
|
83
94
|
emit('');
|
|
84
|
-
|
|
95
|
+
const readerTail = tokensOnly ? '' : ` ${formatCost(s.readerCost)}`;
|
|
96
|
+
emit(panelRow('Readers', `${s.readerCalls} run${s.readerCalls === 1 ? '' : 's'}${readerTail}`));
|
|
85
97
|
}
|
|
86
98
|
if (s.byModel.length > 1 && !inCredits) {
|
|
87
99
|
emit('');
|
|
@@ -89,7 +101,7 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
89
101
|
for (const m of s.byModel) {
|
|
90
102
|
// Credits mode: turn counts only. A per-model dollar split is a direct
|
|
91
103
|
// read on our per-model COGS.
|
|
92
|
-
const tail = inCredits ? '' : ` ${formatCost(m.cost)}`;
|
|
104
|
+
const tail = inCredits || tokensOnly ? '' : ` ${formatCost(m.cost)}`;
|
|
93
105
|
emit(panelRow(modelText(m.model), `${m.turns} turn${m.turns === 1 ? '' : 's'}${tail}`, 24));
|
|
94
106
|
}
|
|
95
107
|
}
|
|
@@ -117,8 +129,9 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
117
129
|
? (typeof e.credits === 'number'
|
|
118
130
|
? formatCredits(e.credits, credits?.unitLabel ?? 'credits')
|
|
119
131
|
: CREDITS_PENDING)
|
|
120
|
-
: formatCost(e.cost);
|
|
121
|
-
|
|
132
|
+
: tokensOnly ? '' : formatCost(e.cost);
|
|
133
|
+
const row = inCredits ? tail : tokensOnly ? `${tok.padStart(10)} tok` : `${tok.padStart(10)} tok ${tail}`;
|
|
134
|
+
emit(panelRow(`turn ${String(e.turn).padStart(3)}`, row, 8));
|
|
122
135
|
}
|
|
123
136
|
if (inCredits && perTurn.unknown > 0) {
|
|
124
137
|
emit('');
|
package/dist/commands/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { SKILL_CATALOG } from '../skill-catalog.js';
|
|
3
|
-
import { costCommandDescription,
|
|
3
|
+
import { costCommandDescription, usageCommandDescription } from '../repl/builtin-commands.js';
|
|
4
4
|
import { panelHeading } from '../renderer/panel-rows.js';
|
|
5
5
|
import { paint } from '../renderer/theme.js';
|
|
6
6
|
import { uiWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
@@ -52,7 +52,7 @@ export const HELP_GROUPS = [
|
|
|
52
52
|
{ name: '/new', description: fixed('Next prompt picks a skill afresh (also /reset)') },
|
|
53
53
|
{ name: '/recap', description: fixed('Show the recap (last session, open tasks, plan)') },
|
|
54
54
|
{ name: '/cost', description: costCommandDescription },
|
|
55
|
-
{ name: '/usage', description:
|
|
55
|
+
{ name: '/usage', description: usageCommandDescription },
|
|
56
56
|
{ name: '/context', description: fixed("Show what fills the model's context window") },
|
|
57
57
|
{ name: '/compact', description: fixed('Shrink a long session and keep the facts') },
|
|
58
58
|
{ name: '/help', description: fixed('This screen (also /skills)') },
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cspeach login azure [--device-code]` — the Microsoft sign-in alone (token
|
|
3
|
+
* expired, password changed, a new policy). Uses the company settings already
|
|
4
|
+
* on this machine; `cspeach login` fetches them.
|
|
5
|
+
*/
|
|
6
|
+
import { formatFailure } from '../renderer/notice-shape.js';
|
|
7
|
+
import { paint } from '../renderer/theme.js';
|
|
8
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
9
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
10
|
+
import { readConnectionCache } from '../connection/cache.js';
|
|
11
|
+
import { signInAzure } from '../auth/azure/sign-in.js';
|
|
12
|
+
export async function runLoginAzureCommand(deps) {
|
|
13
|
+
const cached = await (deps.read ?? readConnectionCache)();
|
|
14
|
+
if (!cached || cached.removed) {
|
|
15
|
+
deps.print(formatFailure({ what: "Your company's Azure settings are not on this machine.", next: 'cspeach login' }).trimEnd());
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
const conn = cached.connection;
|
|
19
|
+
if (!conn.sign_in.includes('entra')) {
|
|
20
|
+
deps.print(formatFailure({ what: 'Your company uses Azure keys, not Microsoft sign-in.', next: 'ask IT for your key, then cspeach config set llm.azure_key -' }).trimEnd());
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
const r = await (deps.signIn ?? signInAzure)(conn, { deviceCode: deps.deviceCode }, { open: deps.open, print: deps.print });
|
|
24
|
+
if (!r.ok) {
|
|
25
|
+
deps.print(formatFailure({ what: r.what, next: r.next }).trimEnd());
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
const until = r.expiresOn ? ` · token valid until ${r.expiresOn.toISOString()}` : '';
|
|
29
|
+
// The account name comes from Microsoft: one line, no control characters.
|
|
30
|
+
const who = sanitizeControlChars(r.username).replace(/\s+/g, ' ').trim();
|
|
31
|
+
deps.print(`${paint('ok', getGlyphs().done)} Signed in to Microsoft as ${who}${until}`);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -6,23 +6,30 @@
|
|
|
6
6
|
* 2) Open the browser to verification_url_complete (or print URL if open fails)
|
|
7
7
|
* 3) Print user_code prominently so customer can read it
|
|
8
8
|
* 4) Poll /v1/auth/device/poll every interval until verified or maxAttempts
|
|
9
|
-
* 5) Save api_key
|
|
9
|
+
* 5) Save api_key to the OS credential store (this CSPeach home's account);
|
|
10
|
+
* auth.json keeps customer_id + savedAt (the key only without a store)
|
|
10
11
|
* 6) Call GET /v1/me — if trial_mode === 'byok', pull the org's Anthropic key,
|
|
11
12
|
* or prompt for the user's own
|
|
12
13
|
* 7) Store that key in the OS keychain (cspeach.anthropic/api-key) — never
|
|
13
14
|
* in auth.json (2026-10-02)
|
|
15
|
+
* 8) GET /v1/me/connection — Azure tenant mode: the company's Azure
|
|
16
|
+
* settings are shown and saved only on a yes (a no exits 6)
|
|
17
|
+
* 9) Azure with Entra: the Microsoft sign-in (browser, or a device code
|
|
18
|
+
* with --device-code); a failure exits 7 and keeps the CSPeach sign-in
|
|
14
19
|
*
|
|
15
20
|
* Dependencies (open, print, prompt) are injected so the command is
|
|
16
21
|
* unit-testable without stubbing platform globals.
|
|
17
22
|
*/
|
|
18
23
|
import keychain from '../auth/keychain.js';
|
|
19
|
-
import {
|
|
24
|
+
import { saveSignIn } from '../auth/key-resolver.js';
|
|
20
25
|
import { fetchOrgAnthropicKey } from '../auth/org-anthropic-key.js';
|
|
21
26
|
import { formatFailure, formatAttention, formatInfo } from '../renderer/notice-shape.js';
|
|
22
27
|
import { panelRow } from '../renderer/panel-rows.js';
|
|
23
28
|
import { paint } from '../renderer/theme.js';
|
|
24
29
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
25
30
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
31
|
+
import { runConnectionStep } from '../connection/login-step.js';
|
|
32
|
+
import { clientIdConfigured } from '../connection/azure-facts.js';
|
|
26
33
|
const ANTHROPIC_KEYCHAIN_SERVICE = 'cspeach.anthropic';
|
|
27
34
|
const ANTHROPIC_KEYCHAIN_ACCOUNT = 'api-key';
|
|
28
35
|
/** Server-supplied text (user code, URL, email, plan) is one line with no control characters. */
|
|
@@ -92,12 +99,13 @@ export async function runLoginCommand(deps) {
|
|
|
92
99
|
deps.print(formatFailure({ what: 'Timed out waiting for the browser sign-in', next: 'cspeach login' }).trimEnd());
|
|
93
100
|
return 5;
|
|
94
101
|
}
|
|
95
|
-
// 5) Save (non-BYOK part).
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
// 5) Save (non-BYOK part). Security batch item 6 — the OS credential store
|
|
103
|
+
// first; auth.json keeps customerId + savedAt, and the key only when no
|
|
104
|
+
// credential store works on this machine.
|
|
105
|
+
const savedTo = await (deps.saveSignIn ?? saveSignIn)({ apiKey: polled.api_key, customerId: polled.customer_id });
|
|
106
|
+
deps.print(done(savedTo === 'store'
|
|
107
|
+
? 'Saved your CSPeach key to your OS credential store'
|
|
108
|
+
: 'Saved your CSPeach key to auth.json in your CSPeach home (no OS credential store is available)'));
|
|
101
109
|
// 6) /v1/me — figure out if BYOK is required.
|
|
102
110
|
try {
|
|
103
111
|
const r = await fetch(`${proxy}/v1/me`, { headers: { Authorization: `Bearer ${polled.api_key}` } });
|
|
@@ -138,6 +146,46 @@ export async function runLoginCommand(deps) {
|
|
|
138
146
|
catch (err) {
|
|
139
147
|
deps.print(formatInfo(`Could not read your account details (${err instanceof Error ? err.message : String(err)}) · sign-in still worked`).trimEnd());
|
|
140
148
|
}
|
|
149
|
+
// 8) Azure tenant mode — the company's connection settings (no secrets).
|
|
150
|
+
// { kind: 'none' } changes nothing; an Azure answer is confirmed on screen.
|
|
151
|
+
const step = await (deps.connectionStep ?? runConnectionStep)({
|
|
152
|
+
proxyUrl: proxy, apiKey: polled.api_key, print: deps.print,
|
|
153
|
+
confirm: async (q) => /^y(es)?$/i.test((await deps.prompt(`${q} [y/n] `)).trim()),
|
|
154
|
+
});
|
|
155
|
+
if (step.result === 'declined')
|
|
156
|
+
return 6;
|
|
157
|
+
// Azure tenant mode (Task 6b) — with Entra the Microsoft sign-in follows
|
|
158
|
+
// the settings. A failed one keeps the CSPeach sign-in (exit 7).
|
|
159
|
+
if (step.result === 'azure') {
|
|
160
|
+
if (step.connection.sign_in.includes('entra')) {
|
|
161
|
+
// Only when this build can sign in to Microsoft at all; otherwise the
|
|
162
|
+
// sign-in's own refusal is the one line.
|
|
163
|
+
if (clientIdConfigured())
|
|
164
|
+
deps.print(formatInfo("Now sign in to Microsoft for your company's Azure").trimEnd());
|
|
165
|
+
const signIn = deps.azureSignIn ?? (await import('../auth/azure/sign-in.js')).signInAzure;
|
|
166
|
+
const r = await signIn(step.connection, { deviceCode: deps.deviceCode === true }, { open: deps.open, print: deps.print });
|
|
167
|
+
if (!r.ok) {
|
|
168
|
+
deps.print(formatFailure({ what: r.what, next: r.next }).trimEnd());
|
|
169
|
+
deps.print(formatInfo('Your CSPeach sign-in worked · when the Microsoft sign-in is sorted out, run cspeach login azure').trimEnd());
|
|
170
|
+
return 7;
|
|
171
|
+
}
|
|
172
|
+
deps.print(done(`Signed in to Microsoft as ${serverText(r.username)}`));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
deps.print(formatInfo('Your company uses Azure keys · ask IT for yours, then run cspeach config set llm.azure_key -').trimEnd());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Fix round 1 (I3): still set to azure (e.g. after cspeach logout) but the
|
|
179
|
+
// company has no settings. Never suggest managed: that would move prompts
|
|
180
|
+
// to our service without the company's say.
|
|
181
|
+
// Ruling 1 (Task 7): only a real { kind: 'none' } answer says so; a service
|
|
182
|
+
// without the settings route ('no_route') stays silent.
|
|
183
|
+
if (step.result === 'none') {
|
|
184
|
+
const { loadConfig } = await import('../config/loader.js');
|
|
185
|
+
const mode = await loadConfig().then((c) => c.llm?.mode).catch(() => undefined);
|
|
186
|
+
if (mode === 'azure')
|
|
187
|
+
deps.print(formatInfo('Your company has no Azure settings in CSPeach · ask your CSPeach admin which setup to use').trimEnd());
|
|
188
|
+
}
|
|
141
189
|
deps.print(` ${paint('dim', 'Next:')} run cspeach to start`);
|
|
142
190
|
return 0;
|
|
143
191
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cspeach logout` —
|
|
3
|
-
*
|
|
2
|
+
* `cspeach logout` — security batch item 6: delete this home's credential
|
|
3
|
+
* store entry, auth.json and licence.json; Azure tenant mode adds the
|
|
4
|
+
* company's Azure entries in this home (clearAzureSignIn). Other CSPeach
|
|
5
|
+
* homes are untouched.
|
|
4
6
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
+
import { clearSignIn } from '../auth/key-resolver.js';
|
|
8
|
+
import { clearAzureSignIn } from '../auth/azure/clear.js';
|
|
9
|
+
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
7
10
|
import { paint } from '../renderer/theme.js';
|
|
8
11
|
import { getGlyphs } from '../renderer/glyphs.js';
|
|
9
12
|
export async function runLogoutCommand(deps) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
deps.print(
|
|
14
|
-
|
|
13
|
+
const { hadSignIn, cleared } = await (deps.clear ?? (() => clearSignIn()))();
|
|
14
|
+
// Fix round 1 (F2): never say "removed" while a key is still in this home.
|
|
15
|
+
if (cleared === false) {
|
|
16
|
+
deps.print(formatFailure({
|
|
17
|
+
what: 'Could not sign out completely',
|
|
18
|
+
detail: ['a CSPeach key is still in this CSPeach home (OS credential store or auth.json)'],
|
|
19
|
+
next: 'close other cspeach windows, then run cspeach logout again',
|
|
20
|
+
}).trimEnd());
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const az = await (deps.clearAzure ?? (() => clearAzureSignIn()))()
|
|
24
|
+
.catch(() => ({ removed: [], failed: ['your Azure sign-in details'] }));
|
|
25
|
+
if (hadSignIn) {
|
|
26
|
+
deps.print(`${paint('ok', getGlyphs().done)} Signed out · removed your CSPeach key from this CSPeach home`);
|
|
15
27
|
deps.print(` ${paint('dim', 'Next:')} cspeach login (to sign in again)`);
|
|
16
28
|
}
|
|
17
29
|
else {
|
|
18
|
-
deps.print(formatInfo('Already signed out · no
|
|
30
|
+
deps.print(formatInfo('Already signed out · no CSPeach key in this CSPeach home').trimEnd());
|
|
31
|
+
}
|
|
32
|
+
for (const what of az.removed)
|
|
33
|
+
deps.print(`${paint('ok', getGlyphs().done)} Removed ${what} from this CSPeach home`);
|
|
34
|
+
// Fix round 1: outside azure mode an Azure clean-up error (e.g. EPERM on a
|
|
35
|
+
// stale connection.json) neither prints nor changes the exit code.
|
|
36
|
+
const mode = az.failed.length > 0
|
|
37
|
+
? await (deps.mode ?? (async () => (await (await import('../config/loader.js')).loadConfig()).llm?.mode))().catch(() => undefined)
|
|
38
|
+
: undefined;
|
|
39
|
+
// An unreadable mode still reports the failure.
|
|
40
|
+
if (az.failed.length > 0 && (mode === undefined || mode === 'azure')) {
|
|
41
|
+
deps.print(formatFailure({ what: `Could not remove ${az.failed.join(', ')}`, next: 'close other cspeach windows, then run cspeach logout again' }).trimEnd());
|
|
42
|
+
return 1;
|
|
19
43
|
}
|
|
20
44
|
return 0;
|
|
21
45
|
}
|
|
@@ -67,12 +67,12 @@ const COMPACT_CALL = {
|
|
|
67
67
|
maxTokens: 4000,
|
|
68
68
|
headers: { 'X-CSForge-Skill': 'compact', 'X-CSPeach-Model-Role': 'compact' },
|
|
69
69
|
};
|
|
70
|
-
export function makePhaseBoundarySummariser(provider, model) {
|
|
71
|
-
return (msgs) => summariseViaProvider(provider, buildSummarisationPrompt(), buildPhaseBoundaryPrompt() + '\n\n' + serialiseForSummariser(msgs), { model, maxTokens: COMPACT_CALL.maxTokens, headers: { ...COMPACT_CALL.headers } });
|
|
70
|
+
export function makePhaseBoundarySummariser(provider, model, sessionId) {
|
|
71
|
+
return (msgs) => summariseViaProvider(provider, buildSummarisationPrompt(), buildPhaseBoundaryPrompt() + '\n\n' + serialiseForSummariser(msgs), { model, maxTokens: COMPACT_CALL.maxTokens, headers: { ...COMPACT_CALL.headers }, ...(sessionId ? { sessionId } : {}) });
|
|
72
72
|
}
|
|
73
73
|
/** The standard /compact summarisation (the compact skill carries the instructions). */
|
|
74
|
-
export function makeAutoCompactSummariser(provider, model) {
|
|
75
|
-
return (msgs) => summariseViaProvider(provider, buildSummarisationPrompt(), serialiseForSummariser(msgs), { model, maxTokens: COMPACT_CALL.maxTokens, headers: { ...COMPACT_CALL.headers } });
|
|
74
|
+
export function makeAutoCompactSummariser(provider, model, sessionId) {
|
|
75
|
+
return (msgs) => summariseViaProvider(provider, buildSummarisationPrompt(), serialiseForSummariser(msgs), { model, maxTokens: COMPACT_CALL.maxTokens, headers: { ...COMPACT_CALL.headers }, ...(sessionId ? { sessionId } : {}) });
|
|
76
76
|
}
|
|
77
77
|
/** Auto-compact after a plan turn whose revision is already saved and that queued no next step. */
|
|
78
78
|
export async function maybeAutoCompactAfterPlanTurn(a) {
|