@cspeach/cli 1.1.10 → 1.1.12
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/loop.js +25 -13
- package/dist/agent/providers/license-gate.js +1 -1
- package/dist/cost/display-mode.js +6 -2
- package/dist/projects/answer-blockers.js +5 -2
- package/dist/projects/workspace.js +41 -2
- package/dist/repl/at-picker.js +42 -5
- package/dist/sap/onboarding.js +1 -1
- package/dist/skills/bundled-skills.js +63 -63
- package/dist/standards/standards-init.js +93 -37
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -905,19 +905,31 @@ export async function runTurn(params) {
|
|
|
905
905
|
// carry the manifest. See ./turn-assistant-text.ts for details.
|
|
906
906
|
turnStatusEmitter.activity('finishing up');
|
|
907
907
|
const assistantText = collectTurnAssistantText(session.messages, turnStartMessageCount);
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
908
|
+
// 2026-09-17 — the save hook below asks the user things (save y/N,
|
|
909
|
+
// blocker pickers). Those are not tool dispatches, so nothing paused
|
|
910
|
+
// the status row: it kept showing "finishing up" with a running timer
|
|
911
|
+
// while the user was the one being waited on (Laeeq's dry run). Pause
|
|
912
|
+
// for the whole hook — the row hides while paused — and resume after,
|
|
913
|
+
// so any real work that follows (auto-compact) shows again.
|
|
914
|
+
turnStatusEmitter.pause();
|
|
915
|
+
try {
|
|
916
|
+
if (!params.suppressSaveHook) {
|
|
917
|
+
await maybeOfferSave({
|
|
918
|
+
skill: params.skill,
|
|
919
|
+
assistantText,
|
|
920
|
+
userMessage: userMessageForSave,
|
|
921
|
+
tokensUsed: Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens),
|
|
922
|
+
// A4 — record the actual turn model if an override ran the stream
|
|
923
|
+
// (unreachable today: plan-resume turns suppress this hook, and
|
|
924
|
+
// they are the only modelOverride caller — kept honest anyway).
|
|
925
|
+
model: params.modelOverride ?? resolveModelRole('session_default', cfg) ?? session.model ?? 'unknown',
|
|
926
|
+
emit,
|
|
927
|
+
promotedFrom: promotedFromForSave,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
finally {
|
|
932
|
+
turnStatusEmitter.resume();
|
|
921
933
|
}
|
|
922
934
|
// Phase 2 #9 (2026-05-16) — auto-compact end-of-turn hook.
|
|
923
935
|
// Runs only on the success path (after maybeOfferSave) so an interrupted
|
|
@@ -9,7 +9,7 @@ export class CspeachLicenseError extends Error {
|
|
|
9
9
|
super(`CSPeach requires a license to run in '${mode}' mode.\n` +
|
|
10
10
|
`In this mode your prompts go straight to your own LLM — but the CSPeach skills\n` +
|
|
11
11
|
`and CLI are licensed software. Run \`cspeach login\` to activate your license.\n` +
|
|
12
|
-
`Need an access key? Email
|
|
12
|
+
`Need an access key? Email info@cremencing.com.`);
|
|
13
13
|
this.name = 'CspeachLicenseError';
|
|
14
14
|
this.mode = mode;
|
|
15
15
|
}
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
export function costDisplayMode(cfg, balanceInfo) {
|
|
22
22
|
if (cfg.llm?.mode !== 'managed')
|
|
23
23
|
return 'usd';
|
|
24
|
-
if (balanceInfo?.metered !== true)
|
|
25
|
-
return 'usd';
|
|
26
24
|
if (cfg.show_cogs === true)
|
|
27
25
|
return 'usd';
|
|
26
|
+
// 2026-09-17 — a managed account NEVER falls back to dollars. Before, a
|
|
27
|
+
// balance lookup that failed or took over 2.5s at start (slow network on a
|
|
28
|
+
// customer laptop) dropped the display to USD — our cost on their screen,
|
|
29
|
+
// exactly what credits mode exists to prevent. Unknown or unmetered
|
|
30
|
+
// balances render "credits: updating…" until the next good sample.
|
|
31
|
+
void balanceInfo;
|
|
28
32
|
return 'credits';
|
|
29
33
|
}
|
|
@@ -87,8 +87,9 @@ export async function offerAnswerBlockers(args) {
|
|
|
87
87
|
const answers = new Map();
|
|
88
88
|
for (let i = 0; i < blockers.length; i++) {
|
|
89
89
|
const b = blockers[i];
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// 2026-09-17 — the "Blocker i/n: …" line is printed only on the free-text
|
|
91
|
+
// path below. When a picker handles the question it shows the same text
|
|
92
|
+
// itself, and printing it here too showed every blocker twice.
|
|
92
93
|
// Choices first, where the question has them and a picker is available.
|
|
93
94
|
// 'write' falls through to the text box; 'skip' leaves the blocker open.
|
|
94
95
|
let askText = true;
|
|
@@ -111,6 +112,8 @@ export async function offerAnswerBlockers(args) {
|
|
|
111
112
|
// c.kind === 'write' → askText stays true
|
|
112
113
|
}
|
|
113
114
|
if (askText) {
|
|
115
|
+
args.log('');
|
|
116
|
+
args.log(`Blocker ${i + 1}/${blockers.length}: ${b.text}`);
|
|
114
117
|
const a = (await args.prompt(' Your answer (Enter to skip): ')).trim();
|
|
115
118
|
if (a.length > 0)
|
|
116
119
|
answers.set(b.id, a);
|
|
@@ -373,6 +373,12 @@ export function resolveAtToken(args) {
|
|
|
373
373
|
if (matches.length === 0) {
|
|
374
374
|
return { kind: 'notFound', token, workspace };
|
|
375
375
|
}
|
|
376
|
+
// 2026-09-17 — an EXACT filename beats substring siblings: `@spec.md`
|
|
377
|
+
// must not be "ambiguous" just because `old-spec.md` sits beside it.
|
|
378
|
+
const exact = matches.filter((f) => f.filename.toLowerCase() === lower);
|
|
379
|
+
if (exact.length === 1) {
|
|
380
|
+
return { kind: 'path', path: exact[0].path };
|
|
381
|
+
}
|
|
376
382
|
return { kind: 'ambiguous', matches };
|
|
377
383
|
}
|
|
378
384
|
/**
|
|
@@ -442,6 +448,11 @@ export function resolveBareTokenInCwd(token, cwd) {
|
|
|
442
448
|
return { kind: 'path', path: matchedFiles[0].path };
|
|
443
449
|
}
|
|
444
450
|
if (matchedFiles.length > 1) {
|
|
451
|
+
// 2026-09-17 — exact filename wins over substring siblings (see resolveAtToken).
|
|
452
|
+
const exact = matchedFiles.filter((f) => f.filename.toLowerCase() === lower);
|
|
453
|
+
if (exact.length === 1) {
|
|
454
|
+
return { kind: 'path', path: exact[0].path };
|
|
455
|
+
}
|
|
445
456
|
return { kind: 'ambiguous', matches: matchedFiles };
|
|
446
457
|
}
|
|
447
458
|
return null;
|
|
@@ -545,10 +556,21 @@ export async function expandTextFileAttachments(userMessage, cwd, log) {
|
|
|
545
556
|
const matches = [];
|
|
546
557
|
let m;
|
|
547
558
|
while ((m = tokenRe.exec(userMessage)) !== null) {
|
|
548
|
-
|
|
559
|
+
let token = m[1] ?? m[2] ?? '';
|
|
560
|
+
let match = m[0];
|
|
549
561
|
if (token.length === 0)
|
|
550
562
|
continue;
|
|
551
|
-
|
|
563
|
+
// 2026-09-17 — an unquoted token keeps the sentence punctuation that
|
|
564
|
+
// follows it ("review @spec.docx, then …"); strip it so the file still
|
|
565
|
+
// resolves, and leave it in the message by shortening the match.
|
|
566
|
+
if (m[1] === undefined) {
|
|
567
|
+
const trail = /[,;:.!?)]+$/.exec(token);
|
|
568
|
+
if (trail && trail[0].length < token.length) {
|
|
569
|
+
token = token.slice(0, -trail[0].length);
|
|
570
|
+
match = match.slice(0, -trail[0].length);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
matches.push({ match, token, index: m.index });
|
|
552
574
|
}
|
|
553
575
|
if (matches.length === 0)
|
|
554
576
|
return userMessage;
|
|
@@ -557,6 +579,23 @@ export async function expandTextFileAttachments(userMessage, cwd, log) {
|
|
|
557
579
|
const substitutions = [];
|
|
558
580
|
for (const { match, token, index } of matches) {
|
|
559
581
|
const resolved = await resolveAtTokenAsync(token, cwd);
|
|
582
|
+
// 2026-09-17 — say so when a reference does not attach. Before, an
|
|
583
|
+
// ambiguous or missing @file was left as literal text with no message,
|
|
584
|
+
// and the model guessed. Missing-file notes only for tokens that look
|
|
585
|
+
// like a supported file, so `user@company.com` stays silent.
|
|
586
|
+
if (resolved.kind === 'ambiguous') {
|
|
587
|
+
if (log) {
|
|
588
|
+
const names = resolved.matches.map((m) => m.filename).join(', ');
|
|
589
|
+
log(`(@${token} matches ${resolved.matches.length} files: ${names} — use the full name)`);
|
|
590
|
+
}
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (resolved.kind === 'notFound') {
|
|
594
|
+
if (log && isAttachableFilename(token.split(/[\\/]/).pop() ?? token)) {
|
|
595
|
+
log(`(@${token} not found — looked in ${cwd} and the workspace)`);
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
560
599
|
if (resolved.kind !== 'path')
|
|
561
600
|
continue;
|
|
562
601
|
const path = resolved.path;
|
package/dist/repl/at-picker.js
CHANGED
|
@@ -24,6 +24,8 @@ import { withInquirer } from './inquirer-guard.js';
|
|
|
24
24
|
import { inquirerTheme } from './inquirer-theme.js';
|
|
25
25
|
import { ensureWorkspaceSync, listProjectFilesSync, isAttachableFilename } from '../projects/workspace.js';
|
|
26
26
|
const CANCEL_VALUE = '__cspeach_cancel__';
|
|
27
|
+
/** Subfolders never worth listing in the `@` picker (generated or vendored). */
|
|
28
|
+
const SKIPPED_DIRS = new Set(['node_modules', 'cspeach-out', 'dist', 'build', 'target']);
|
|
27
29
|
/**
|
|
28
30
|
* 2026-06-06 (Ink picker parity): workspace listing + substring filter,
|
|
29
31
|
* exported so the Ink FilePalette shares the same data + seeding
|
|
@@ -57,16 +59,15 @@ export function listWorkspaceFileChoices(opts) {
|
|
|
57
59
|
catch {
|
|
58
60
|
// fall through — still return whatever the cwd half yields
|
|
59
61
|
}
|
|
60
|
-
// cwd half —
|
|
62
|
+
// cwd half — .txt/.md/.docx/.pdf at the top level AND one level of
|
|
63
|
+
// subfolders (2026-09-17: a spec in `docs\` never showed in the picker).
|
|
64
|
+
// Subfolder entries are listed as `docs/spec.docx`, which the resolver
|
|
65
|
+
// already treats as a cwd-relative path. Never throws.
|
|
61
66
|
const cwd = opts?.cwd ?? process.cwd();
|
|
62
67
|
try {
|
|
63
68
|
for (const name of readdirSync(cwd)) {
|
|
64
69
|
if (name.startsWith('.'))
|
|
65
70
|
continue; // dotfiles/dirs (incl. .cspeach)
|
|
66
|
-
if (!isAttachableFilename(name))
|
|
67
|
-
continue;
|
|
68
|
-
if (seen.has(name))
|
|
69
|
-
continue; // workspace already has it
|
|
70
71
|
const full = join(cwd, name);
|
|
71
72
|
let stat;
|
|
72
73
|
try {
|
|
@@ -75,6 +76,42 @@ export function listWorkspaceFileChoices(opts) {
|
|
|
75
76
|
catch {
|
|
76
77
|
continue;
|
|
77
78
|
}
|
|
79
|
+
if (stat.isDirectory()) {
|
|
80
|
+
if (SKIPPED_DIRS.has(name.toLowerCase()))
|
|
81
|
+
continue;
|
|
82
|
+
let inner;
|
|
83
|
+
try {
|
|
84
|
+
inner = readdirSync(full);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const child of inner) {
|
|
90
|
+
if (child.startsWith('.'))
|
|
91
|
+
continue;
|
|
92
|
+
if (!isAttachableFilename(child))
|
|
93
|
+
continue;
|
|
94
|
+
const rel = `${name}/${child}`;
|
|
95
|
+
if (seen.has(rel))
|
|
96
|
+
continue;
|
|
97
|
+
let cstat;
|
|
98
|
+
try {
|
|
99
|
+
cstat = statSync(join(full, child));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!cstat.isFile())
|
|
105
|
+
continue;
|
|
106
|
+
seen.add(rel);
|
|
107
|
+
out.push({ filename: rel, mtimeMs: cstat.mtimeMs });
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!isAttachableFilename(name))
|
|
112
|
+
continue;
|
|
113
|
+
if (seen.has(name))
|
|
114
|
+
continue; // workspace already has it
|
|
78
115
|
if (!stat.isFile())
|
|
79
116
|
continue;
|
|
80
117
|
seen.add(name);
|
package/dist/sap/onboarding.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function runAddSapWizard() {
|
|
|
71
71
|
default: 'basic',
|
|
72
72
|
});
|
|
73
73
|
const username = await input({
|
|
74
|
-
message: 'SAP username (e.g.
|
|
74
|
+
message: 'SAP username (e.g. JSMITH):',
|
|
75
75
|
validate: (v) => v.length > 0 || 'Required',
|
|
76
76
|
});
|
|
77
77
|
let certPath;
|