@cspeach/cli 1.1.17 → 1.1.19
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.md +3 -4
- package/dist/agent/loop.js +143 -25
- package/dist/agent/providers/local-provider.js +4 -1
- package/dist/agent/repair-partial.js +76 -19
- package/dist/agent/tool-dispatch.js +80 -22
- package/dist/agent/turn-error-ux.js +77 -26
- package/dist/agent/turn-stream.js +25 -3
- package/dist/approvals/adt-type.js +27 -0
- package/dist/approvals/advisory-prompt.js +48 -0
- package/dist/approvals/advisory-render.js +25 -11
- package/dist/approvals/approval-prompt.js +74 -24
- package/dist/approvals/jwt.js +2 -1
- package/dist/approvals/render.js +64 -36
- package/dist/approvals/risk-floor.js +53 -2
- package/dist/auth/api-key.js +30 -16
- package/dist/auth/keychain.js +0 -0
- package/dist/auth/me.js +25 -7
- package/dist/cli.js +25 -6
- package/dist/commands/compact.js +12 -9
- package/dist/commands/config-set.js +6 -2
- package/dist/commands/config-show.js +7 -5
- package/dist/commands/cost.js +41 -34
- package/dist/commands/help.js +88 -50
- package/dist/commands/login.js +28 -19
- package/dist/commands/logout.js +7 -3
- package/dist/commands/plan-chain.js +71 -77
- package/dist/commands/plan-continue.js +1 -0
- package/dist/commands/plan-resume.js +20 -12
- package/dist/commands/whoami.js +23 -9
- package/dist/config/loader.js +15 -3
- package/dist/cost/pricing.js +6 -5
- package/dist/doctor/checks/forge-rules.js +13 -3
- package/dist/doctor/checks/keychain.js +6 -4
- package/dist/doctor/run.js +101 -20
- package/dist/index.js +5 -2
- package/dist/lib/graceful-exit.js +56 -0
- package/dist/lib/piped-prompt.js +65 -0
- package/dist/lock-contention.js +2 -2
- package/dist/one-shot.js +135 -40
- package/dist/projects/answer-blockers.js +6 -1
- package/dist/projects/handover-md.js +15 -14
- package/dist/projects/plan-run.js +97 -54
- package/dist/projects/promote-command.js +11 -2
- package/dist/projects/save-command.js +3 -2
- package/dist/projects/status.js +2 -1
- package/dist/projects/yes-no.js +12 -0
- package/dist/renderer/abap-inline.js +18 -14
- package/dist/renderer/answer-trim.js +69 -0
- package/dist/renderer/banners.js +8 -8
- package/dist/renderer/brand-settled.js +19 -0
- package/dist/renderer/change-card.js +156 -0
- package/dist/renderer/checklist-format.js +130 -0
- package/dist/renderer/color-mode.js +45 -0
- package/dist/renderer/error-detail.js +104 -0
- package/dist/renderer/fence-state.js +46 -0
- package/dist/renderer/footer-line.js +96 -0
- package/dist/renderer/glyphs.js +25 -0
- package/dist/renderer/goodbye.js +38 -0
- package/dist/renderer/legacy-palette.js +41 -0
- package/dist/renderer/look.js +30 -0
- package/dist/renderer/markdown.js +299 -92
- package/dist/renderer/notice-log.js +72 -0
- package/dist/renderer/notice-shape.js +48 -0
- package/dist/renderer/notices.js +40 -8
- package/dist/renderer/panel-rows.js +16 -0
- package/dist/renderer/pipeline.js +66 -0
- package/dist/renderer/progress-chatter.js +23 -17
- package/dist/renderer/rendering-mode.js +34 -0
- package/dist/renderer/routing-line.js +14 -0
- package/dist/renderer/sanitize.js +12 -0
- package/dist/renderer/severity.js +2 -7
- package/dist/renderer/startup-lines.js +152 -0
- package/dist/renderer/status-footer.js +52 -34
- package/dist/renderer/steering-echo.js +41 -0
- package/dist/renderer/syntax.js +31 -12
- package/dist/renderer/tables.js +5 -1
- package/dist/renderer/theme.js +44 -0
- package/dist/renderer/thinking-heartbeat.js +33 -2
- package/dist/renderer/todo-block.js +10 -49
- package/dist/renderer/tool-labels.js +238 -0
- package/dist/renderer/tool-widget.js +245 -51
- package/dist/renderer/trace.js +42 -0
- package/dist/renderer/transcript-flow.js +132 -0
- package/dist/renderer/tty.js +34 -1
- package/dist/renderer/ui-width.js +42 -0
- package/dist/renderer/verify-chain.js +4 -2
- package/dist/renderer/widget-fallback.js +58 -65
- package/dist/repl/bracketed-paste.js +7 -1
- package/dist/repl/builtin-commands.js +19 -8
- package/dist/repl/credential-handover-gate.js +28 -0
- package/dist/repl/early-line-buffer.js +5 -2
- package/dist/repl/file-picker.js +54 -12
- package/dist/repl/ink-stdin-guard.js +66 -0
- package/dist/repl/inquirer-guard.js +59 -16
- package/dist/repl/inquirer-theme.js +27 -33
- package/dist/repl/paste-marker.js +19 -0
- package/dist/repl/plan-turn-end.js +20 -0
- package/dist/repl/post-turn-status.js +36 -11
- package/dist/repl/reroute-turn.js +21 -0
- package/dist/repl/reset-tty-stdin.js +44 -0
- package/dist/repl/restore-guard.js +22 -0
- package/dist/repl/resume-standalone.js +63 -0
- package/dist/repl/rule8-detector.js +11 -3
- package/dist/repl/safety-confirm.js +161 -93
- package/dist/repl/session-spend-line.js +8 -4
- package/dist/repl/themed-prompts.js +19 -0
- package/dist/repl/ui-look-command.js +75 -0
- package/dist/repl/update-method-preview-hook.js +48 -5
- package/dist/repl.js +795 -332
- package/dist/rewind/cli.js +3 -2
- package/dist/rewind/format.js +14 -9
- package/dist/rewind/restore.js +11 -0
- package/dist/router/classifier.js +47 -4
- package/dist/router/intent-extractor.js +24 -8
- package/dist/router/piped-routing.js +23 -0
- package/dist/router/resume-routing.js +16 -0
- package/dist/router/routing-failure.js +57 -0
- package/dist/sap/first-run-choice.js +1 -1
- package/dist/sap/onboarding.js +3 -2
- package/dist/sap/standalone-onboarding.js +1 -1
- package/dist/sap/system-info.js +4 -2
- package/dist/sap/unreachable.js +28 -0
- package/dist/sap-errors/clean-error-text.js +182 -0
- package/dist/session/interrupt-reason.js +39 -0
- package/dist/session/recap.js +36 -24
- package/dist/session/resume.js +104 -36
- package/dist/session/store.js +69 -27
- package/dist/session/user-prompt.js +21 -0
- package/dist/skill-catalog.js +7 -0
- package/dist/skills/bundled-skills.js +76 -69
- package/dist/skills/signing-public-key.js +1 -1
- package/dist/standards/standards-init.js +1 -1
- package/dist/test-helpers/answer-any.js +46 -0
- package/dist/tools/approval.js +27 -11
- package/dist/tools/ask-question.js +47 -5
- package/dist/tools/filesystem/file-write.js +19 -2
- package/dist/tools/fiori/fe-extend.js +16 -1
- package/dist/tools/fiori/fe-scaffold.js +105 -3
- package/dist/tools/fiori/html-escapes.js +28 -0
- package/dist/tools/fiori/metadata/audit.js +605 -0
- package/dist/tools/fiori/metadata/references.js +104 -0
- package/dist/tools/fiori/metadata/types.js +8 -0
- package/dist/tools/fiori/preview/app-guard.js +215 -0
- package/dist/tools/fiori/preview/env.js +193 -0
- package/dist/tools/fiori/preview/readiness.js +127 -0
- package/dist/tools/fiori/preview/registry.js +412 -0
- package/dist/tools/fiori/preview/start.js +469 -0
- package/dist/tools/fiori/samples/loader.js +20 -5
- package/dist/tools/fiori/scaffold.js +18 -6
- package/dist/tools/fiori/smoke/app-driver-page.js +372 -0
- package/dist/tools/fiori/smoke/app-driver.js +100 -0
- package/dist/tools/fiori/smoke/assertions.js +161 -24
- package/dist/tools/fiori/smoke/audit-columns.js +21 -0
- package/dist/tools/fiori/smoke/batch.js +273 -0
- package/dist/tools/fiori/smoke/draft-safety.js +241 -0
- package/dist/tools/fiori/smoke/draft-smoke.js +473 -0
- package/dist/tools/fiori/smoke/driver.js +171 -4
- package/dist/tools/fiori/smoke/evidence.js +35 -0
- package/dist/tools/fiori/smoke/labels-codes.js +207 -0
- package/dist/tools/fiori/smoke/nav-error.js +26 -0
- package/dist/tools/fiori/smoke/run-smoke.js +155 -12
- package/dist/tools/fiori/tools.js +651 -7
- package/dist/tools/fiori/ui5-version.js +16 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/local-build.js +6 -0
- package/dist/tools/sap-read.js +16 -1
- package/dist/tools/sap-write.js +122 -10
- package/dist/tools/shell/shell_exec.js +30 -12
- package/dist/tools/syntax-state.js +67 -0
- package/dist/tools/todo.js +23 -18
- package/dist/tools/transport-fit.js +462 -0
- package/dist/tools/transport-resolution.js +3 -1
- package/dist/tools/transport.js +33 -7
- package/dist/tools/verify.js +184 -8
- package/dist/ui/app.js +284 -146
- package/dist/ui/approval-modal.js +41 -15
- package/dist/ui/ask-answer-text.js +17 -0
- package/dist/ui/body.js +25 -6
- package/dist/ui/card-slot.js +98 -0
- package/dist/ui/checklist.js +9 -0
- package/dist/ui/coaching-picker-classic.js +8 -5
- package/dist/ui/command-palette.js +6 -4
- package/dist/ui/confirm-request.js +18 -0
- package/dist/ui/context-grid.js +2 -1
- package/dist/ui/file-palette.js +6 -4
- package/dist/ui/files-card-emitter.js +19 -0
- package/dist/ui/footer-line.js +16 -0
- package/dist/ui/footer.js +105 -104
- package/dist/ui/input-wrap.js +122 -0
- package/dist/ui/interrupt.js +103 -0
- package/dist/ui/key-burst.js +117 -0
- package/dist/ui/line-resolution.js +4 -3
- package/dist/ui/live-area.js +71 -0
- package/dist/ui/login-banner.js +54 -43
- package/dist/ui/rewind-panel.js +76 -24
- package/dist/ui/role-props.js +47 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +49 -3
- package/dist/ui/skill-picker.js +5 -47
- package/dist/ui/text-input.js +128 -24
- package/dist/ui/todo-emitter.js +19 -0
- package/dist/ui/turn-status-emitter.js +28 -2
- package/dist/ui/turn-status.js +16 -19
- package/dist/ui/use-card-request.js +44 -0
- package/dist/ui/widgets/ask-card.js +521 -0
- package/dist/ui/widgets/bar-chart.js +11 -5
- package/dist/ui/widgets/coaching-picker.js +50 -55
- package/dist/ui/widgets/confirm-card.js +193 -0
- package/dist/ui/widgets/dep-graph.js +3 -2
- package/dist/ui/widgets/diff-viewer.js +6 -5
- package/dist/ui/widgets/files-card.js +97 -0
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/reclaim-raw-stdin.js +24 -0
- package/dist/ui/widgets/shortcuts-sheet.js +25 -0
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/upgrade-check.js +39 -8
- package/package.json +11 -2
- package/test-harness/fe-draft/app.ts +188 -0
- package/test-harness/fe-draft/fe-draft.e2e.test.ts +461 -0
- package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +51 -0
- package/test-harness/fe-draft/routes.ts +224 -0
- package/test-harness/fe-draft/routes.unit.test.ts +45 -0
- package/test-harness/fe-draft/server.ts +225 -0
- package/test-harness/fe-draft/variants.ts +196 -0
- package/vitest.smoke-e2e.config.ts +20 -0
|
@@ -28,6 +28,10 @@ import { decideNextPhaseGate, getAndResetPlanPhaseSubagentDispatches, getAndRese
|
|
|
28
28
|
import { runPhaseAudit } from './plan-audit.js';
|
|
29
29
|
import { applyAuditResult, applyPhaseResolution, buildResumeCommand, offerNextPhaseAutoRun, } from './plan-resume.js';
|
|
30
30
|
import { appendCostLine, buildEntry } from '../cost/cost-log.js';
|
|
31
|
+
import { getGlyphs } from '../renderer/glyphs.js';
|
|
32
|
+
import { formatFailure, formatAttention, formatInfo } from '../renderer/notice-shape.js';
|
|
33
|
+
import { paint } from '../renderer/theme.js';
|
|
34
|
+
import { clean } from '../renderer/tool-labels.js';
|
|
31
35
|
import { startThinkingHeartbeat, AUDIT_HEARTBEAT_THRESHOLDS } from '../renderer/thinking-heartbeat.js';
|
|
32
36
|
import { computeCost, formatCost } from '../cost/pricing.js';
|
|
33
37
|
import { creditsActive } from '../cost/credits.js';
|
|
@@ -47,7 +51,7 @@ export async function runPostPhaseChain(args) {
|
|
|
47
51
|
return null;
|
|
48
52
|
let phase = loaded.content.phases.find((p) => p.id === args.executedPhaseId);
|
|
49
53
|
if (!phase) {
|
|
50
|
-
args.log(
|
|
54
|
+
args.log(formatFailure({ what: 'Plan phase not found', object: args.executedPhaseId, detail: [`${basename(path)} · follow-up steps skipped`] }).trimEnd());
|
|
51
55
|
return path;
|
|
52
56
|
}
|
|
53
57
|
// Per-run flags survive the chain: an explicitly flagged run keeps its mode
|
|
@@ -89,7 +93,7 @@ export async function runPostPhaseChain(args) {
|
|
|
89
93
|
clearAudit: true,
|
|
90
94
|
log: args.log,
|
|
91
95
|
});
|
|
92
|
-
args.log(
|
|
96
|
+
args.log(formatFailure({ what: 'Plan deviation', object: `phase ${phase.id}`, detail: [deviationDetail] }).trimEnd(), stoppedBanner(`plan deviation — phase ${phase.id} blocked (undeclared write denied)`, resumeCmd(path)));
|
|
93
97
|
ringBell();
|
|
94
98
|
return path;
|
|
95
99
|
}
|
|
@@ -244,7 +248,7 @@ export async function runPostPhaseChain(args) {
|
|
|
244
248
|
// finding against the phase, whose own exit gate (activation + ATC)
|
|
245
249
|
// already passed. So we do NOT hard-stop verified work: exactly like a
|
|
246
250
|
// warn/pass, render a LOUD, DISTINCT "could not run / NOT verified" note,
|
|
247
|
-
// keep the infra_failed state recorded (tracker keeps
|
|
251
|
+
// keep the infra_failed state recorded (tracker keeps "! audit not run";
|
|
248
252
|
// the final summary lists it as unverified) and CONTINUE to the gate +
|
|
249
253
|
// offer below. isAuditResolved treats infra_failed as resolved, so
|
|
250
254
|
// dependants unlock and the failure menu is never shown.
|
|
@@ -253,7 +257,7 @@ export async function runPostPhaseChain(args) {
|
|
|
253
257
|
else if (verdict.state === 'failed') {
|
|
254
258
|
renderFindings(args.log, phase.id, verdict.findings);
|
|
255
259
|
if (forceBlock) {
|
|
256
|
-
args.log(
|
|
260
|
+
args.log(formatFailure({ what: `Phase ${phase.id} failed its audit ${countedFailures} times in a row`, detail: ['marked blocked'] }).trimEnd(), stoppedBanner('two consecutive failed audits — phase blocked', resumeCmd(path)));
|
|
257
261
|
ringBell();
|
|
258
262
|
return path;
|
|
259
263
|
}
|
|
@@ -291,7 +295,7 @@ export async function runPostPhaseChain(args) {
|
|
|
291
295
|
summary: `audit ${phase.id}: failed — user chose re-run (status → todo)`,
|
|
292
296
|
log: args.log,
|
|
293
297
|
});
|
|
294
|
-
args.log(
|
|
298
|
+
args.log(formatInfo(`Re-running phase ${phase.id} · it will be re-audited after execution.`).trimEnd());
|
|
295
299
|
args.queueDispatch(resumeCmd(path));
|
|
296
300
|
return path;
|
|
297
301
|
}
|
|
@@ -308,7 +312,7 @@ export async function runPostPhaseChain(args) {
|
|
|
308
312
|
if (!loaded)
|
|
309
313
|
return path;
|
|
310
314
|
phase = loaded.content.phases.find((p) => p.id === args.executedPhaseId);
|
|
311
|
-
args.log(
|
|
315
|
+
args.log(paint('dim', `${getGlyphs().info} audit waived (${category}) for ${phase.id} — reason recorded in the envelope.`));
|
|
312
316
|
}
|
|
313
317
|
else if (verdict.state === 'warn') {
|
|
314
318
|
// Non-blocking tier: ALWAYS shown, concise (Open-Q decision — warns are
|
|
@@ -317,7 +321,7 @@ export async function runPostPhaseChain(args) {
|
|
|
317
321
|
renderWarnings(args.log, phase.id, verdict.findings);
|
|
318
322
|
}
|
|
319
323
|
else {
|
|
320
|
-
args.log(
|
|
324
|
+
args.log(paint('ok', `${getGlyphs().done} audit passed · ${phase.id} verified against the evidence.`));
|
|
321
325
|
}
|
|
322
326
|
}
|
|
323
327
|
/* ── 2. gate + offer ──────────────────────────────────────────────────── */
|
|
@@ -383,6 +387,7 @@ export async function runPostPhaseChain(args) {
|
|
|
383
387
|
nextDelegateTo: next.delegateTo,
|
|
384
388
|
},
|
|
385
389
|
prompt: args.prompt,
|
|
390
|
+
confirm: args.confirm,
|
|
386
391
|
queueDispatch: args.queueDispatch,
|
|
387
392
|
log: args.log,
|
|
388
393
|
gate: decision,
|
|
@@ -439,8 +444,11 @@ export async function runReauditPass(args) {
|
|
|
439
444
|
if (queued) {
|
|
440
445
|
const remaining = phaseIds.slice(i + 1);
|
|
441
446
|
if (remaining.length > 0) {
|
|
442
|
-
args.log(
|
|
443
|
-
|
|
447
|
+
args.log(formatAttention({
|
|
448
|
+
text: `${remaining.length} hand-edited phase(s) not re-audited yet`,
|
|
449
|
+
fact: remaining.join(', '),
|
|
450
|
+
next: 'they are not auto-detected after this write; verify them (or re-run their phases) once the queued dispatch completes',
|
|
451
|
+
}).trimEnd());
|
|
444
452
|
}
|
|
445
453
|
break;
|
|
446
454
|
}
|
|
@@ -452,18 +460,18 @@ function loadPlan(path, log) {
|
|
|
452
460
|
try {
|
|
453
461
|
const envelope = readProjectFile(path);
|
|
454
462
|
if (envelope.artefactType !== 'plan') {
|
|
455
|
-
log(
|
|
463
|
+
log(formatFailure({ what: 'Not a plan file', object: basename(path), detail: ['follow-up steps skipped'] }).trimEnd());
|
|
456
464
|
return null;
|
|
457
465
|
}
|
|
458
466
|
const pc = parsePlanContent(envelope.content);
|
|
459
467
|
if (!pc.ok) {
|
|
460
|
-
log(
|
|
468
|
+
log(formatFailure({ what: 'Plan file failed validation', object: basename(path), detail: [pc.errors[0] ?? '', 'follow-up steps skipped'] }).trimEnd());
|
|
461
469
|
return null;
|
|
462
470
|
}
|
|
463
471
|
return { envelope, content: pc.content };
|
|
464
472
|
}
|
|
465
473
|
catch (e) {
|
|
466
|
-
log(
|
|
474
|
+
log(formatFailure({ what: 'Could not load the plan file', object: basename(path), detail: [e instanceof Error ? e.message : String(e)] }).trimEnd());
|
|
467
475
|
return null;
|
|
468
476
|
}
|
|
469
477
|
}
|
|
@@ -491,7 +499,7 @@ function clampInfra(msg) {
|
|
|
491
499
|
return flat.length > INFRA_RENDER_CHARS ? flat.slice(0, INFRA_RENDER_CHARS - 1).trimEnd() + '…' : flat;
|
|
492
500
|
}
|
|
493
501
|
function renderFindings(log, phaseId, findings) {
|
|
494
|
-
log('',
|
|
502
|
+
log('', paint('bad', `${getGlyphs().failed} audit FAILED for phase ${phaseId}:`));
|
|
495
503
|
const shown = findings.length > 0 ? findings : ['(auditor returned no findings text)'];
|
|
496
504
|
shown.forEach((f, i) => log(` ${i + 1}. ${f}`));
|
|
497
505
|
}
|
|
@@ -503,7 +511,7 @@ function renderFindings(log, phaseId, findings) {
|
|
|
503
511
|
*/
|
|
504
512
|
function renderWarnings(log, phaseId, findings) {
|
|
505
513
|
const n = findings.length;
|
|
506
|
-
log('',
|
|
514
|
+
log('', paint('warn', `${getGlyphs().attention} audit passed with ${n} note${n === 1 ? '' : 's'} for phase ${phaseId}:`));
|
|
507
515
|
const shown = n > 0 ? findings : ['(auditor returned a warn verdict with no note detail)'];
|
|
508
516
|
shown.forEach((f, i) => log(` ${i + 1}. ${f}`));
|
|
509
517
|
}
|
|
@@ -517,51 +525,36 @@ function renderWarnings(log, phaseId, findings) {
|
|
|
517
525
|
* nothing else surfaces this).
|
|
518
526
|
*/
|
|
519
527
|
function renderAuditNotRun(log, phaseId, reason, resumeCommand) {
|
|
520
|
-
log('',
|
|
528
|
+
log('', paint('warn', `${getGlyphs().attention} audit could not run for phase ${phaseId} (audit infrastructure failed after 3 attempts — the model call returned no output).`), paint('warn', ' This phase is NOT audit-verified; its own exit gate (activation + ATC) passed. Proceeding.'), paint('dim', ` reason: ${reason}`), paint('dim', ` If the auditor keeps failing, run with --no-audit (or set audit off in config) to stop retrying; resume as usual: ${resumeCommand}`));
|
|
521
529
|
}
|
|
522
530
|
/**
|
|
523
531
|
* Shared menu surface for the audit-resolution selects. Mirrors
|
|
524
532
|
* safety-confirm.ts: headless → no decision (fail-closed, nothing written);
|
|
525
|
-
* Ink →
|
|
533
|
+
* Ink → confirm card via askConfirm (piece 2 §5); classic → inquirer select.
|
|
526
534
|
*/
|
|
527
535
|
async function selectAuditAction(args) {
|
|
528
536
|
const { isHeadless, shouldUseInk } = await import('../renderer/tty.js');
|
|
529
537
|
if (isHeadless())
|
|
530
538
|
return 'none';
|
|
531
539
|
if (shouldUseInk()) {
|
|
532
|
-
// D-C — this
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const res = await askQuestionEmitter.request({
|
|
551
|
-
id: args.id,
|
|
552
|
-
question: args.question,
|
|
553
|
-
context: args.context,
|
|
554
|
-
kind: 'choice',
|
|
555
|
-
choices: args.choices,
|
|
556
|
-
});
|
|
557
|
-
if (res.cancelled || res.answer == null)
|
|
558
|
-
return 'none';
|
|
559
|
-
return coerceAction(res.answer, args.choices);
|
|
560
|
-
}
|
|
561
|
-
finally {
|
|
562
|
-
if (statusWasActive)
|
|
563
|
-
turnStatusEmitter.resume();
|
|
564
|
-
}
|
|
540
|
+
// D-C — this card opens in the POST-PHASE chain. A leaked keystroke used
|
|
541
|
+
// to auto-select choice 1 ("Re-run phase") and silently re-run the
|
|
542
|
+
// phase. The card slot claims input focus (spinner stopped, status row
|
|
543
|
+
// paused, restored after), digits only move the cursor, and typed-ahead
|
|
544
|
+
// keys are dropped — so no stray key can pick a choice.
|
|
545
|
+
const { askConfirm } = await import('../ui/confirm-request.js');
|
|
546
|
+
const res = await askConfirm({
|
|
547
|
+
id: args.id,
|
|
548
|
+
tag: 'SAFETY CHECK',
|
|
549
|
+
title: args.question,
|
|
550
|
+
facts: args.facts,
|
|
551
|
+
choices: args.choices,
|
|
552
|
+
initial: 0,
|
|
553
|
+
footnote: 'Plan audit',
|
|
554
|
+
});
|
|
555
|
+
if (res.cancelled || res.value == null)
|
|
556
|
+
return 'none';
|
|
557
|
+
return coerceAction(res.value, args.choices);
|
|
565
558
|
}
|
|
566
559
|
const { select } = await import('@inquirer/prompts');
|
|
567
560
|
const { withInquirer } = await import('../repl/inquirer-guard.js');
|
|
@@ -583,12 +576,23 @@ async function selectAuditAction(args) {
|
|
|
583
576
|
function coerceAction(answer, choices) {
|
|
584
577
|
return choices.some((c) => c.value === answer) ? answer : 'none';
|
|
585
578
|
}
|
|
586
|
-
/** Production failed-verdict menu (see selectAuditAction for the surface).
|
|
587
|
-
|
|
579
|
+
/** Production failed-verdict menu (see selectAuditAction for the surface).
|
|
580
|
+
* The findings are already printed above by renderFindings; the card
|
|
581
|
+
* repeats only the count and the first three. */
|
|
582
|
+
export async function defaultSelectFailureAction(a) {
|
|
583
|
+
const n = a.findings.length;
|
|
584
|
+
const facts = [
|
|
585
|
+
`${n} ${n === 1 ? 'finding' : 'findings'}, listed above`,
|
|
586
|
+
// Findings are model text: display-clean them at the source (ANSI and
|
|
587
|
+
// control characters out, one line each), like the safety-card rows.
|
|
588
|
+
...a.findings.slice(0, 3).map((f, i) => `${i + 1}. ${clean(f)}`),
|
|
589
|
+
];
|
|
590
|
+
if (n > 3)
|
|
591
|
+
facts.push(`${getGlyphs().more} ${n - 3} more`);
|
|
588
592
|
return selectAuditAction({
|
|
589
593
|
id: 'plan-audit-failed',
|
|
590
594
|
question: `Audit failed for phase ${a.phaseId} — what now?`,
|
|
591
|
-
|
|
595
|
+
facts,
|
|
592
596
|
choices: [
|
|
593
597
|
{ value: 'rerun', label: 'Re-run phase (re-audited after execution)' },
|
|
594
598
|
{ value: 'waive', label: 'Waive audit (record a one-line reason)' },
|
|
@@ -604,7 +608,8 @@ async function defaultSelectFailureAction(a) {
|
|
|
604
608
|
* a waive no human explicitly labelled a false-positive is recorded as
|
|
605
609
|
* accepted-risk, so the measured FP rate never over-counts). Never hangs.
|
|
606
610
|
*/
|
|
607
|
-
|
|
611
|
+
/** Exported for the piece-2 behaviour pins (plan-chain-focus-pin.test.ts). */
|
|
612
|
+
export async function defaultSelectWaiveCategory(a) {
|
|
608
613
|
const { isHeadless, shouldUseInk } = await import('../renderer/tty.js');
|
|
609
614
|
if (isHeadless())
|
|
610
615
|
return 'accepted-risk';
|
|
@@ -615,30 +620,19 @@ async function defaultSelectWaiveCategory(a) {
|
|
|
615
620
|
];
|
|
616
621
|
const coerce = (answer) => choices.some((c) => c.value === answer) ? answer : 'accepted-risk';
|
|
617
622
|
if (shouldUseInk()) {
|
|
618
|
-
// Same input-focus discipline as selectAuditAction
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
kind: 'choice',
|
|
632
|
-
choices,
|
|
633
|
-
});
|
|
634
|
-
if (res.cancelled || res.answer == null)
|
|
635
|
-
return 'accepted-risk';
|
|
636
|
-
return coerce(res.answer);
|
|
637
|
-
}
|
|
638
|
-
finally {
|
|
639
|
-
if (statusWasActive)
|
|
640
|
-
turnStatusEmitter.resume();
|
|
641
|
-
}
|
|
623
|
+
// Same input-focus discipline as selectAuditAction (askConfirm).
|
|
624
|
+
const { askConfirm } = await import('../ui/confirm-request.js');
|
|
625
|
+
const res = await askConfirm({
|
|
626
|
+
id: 'plan-waive-category',
|
|
627
|
+
tag: 'QUESTION',
|
|
628
|
+
title: question,
|
|
629
|
+
choices,
|
|
630
|
+
initial: 0,
|
|
631
|
+
footnote: 'Plan audit',
|
|
632
|
+
});
|
|
633
|
+
if (res.cancelled || res.value == null)
|
|
634
|
+
return 'accepted-risk';
|
|
635
|
+
return coerce(res.value);
|
|
642
636
|
}
|
|
643
637
|
const { select } = await import('@inquirer/prompts');
|
|
644
638
|
const { withInquirer } = await import('../repl/inquirer-guard.js');
|
|
@@ -173,6 +173,7 @@ export async function runContinuationChain(args) {
|
|
|
173
173
|
attemptStartedAt: args.marker.attemptStartedAt,
|
|
174
174
|
subagentDispatches: args.subagentDispatches,
|
|
175
175
|
prompt: args.prompt,
|
|
176
|
+
confirm: args.confirm,
|
|
176
177
|
queueDispatch: args.queueDispatch,
|
|
177
178
|
log: args.log,
|
|
178
179
|
});
|
|
@@ -52,6 +52,8 @@ import { collectTurnAssistantText } from '../agent/turn-assistant-text.js';
|
|
|
52
52
|
import { getAuthorIdentity } from '../agent/loop.js';
|
|
53
53
|
import { FLOORPLAN_TO_TEMPLATE } from '../tools/fiori/floorplan-map.js';
|
|
54
54
|
import { getCurrentTransport, setCurrentTransport } from '../repl/current-transport.js';
|
|
55
|
+
import { askYesNo, isYes } from '../projects/yes-no.js';
|
|
56
|
+
import { formatAttention, formatInfo } from '../renderer/notice-shape.js';
|
|
55
57
|
/**
|
|
56
58
|
* Parse and STRIP the per-run chaining-mode (`--guarded` / `--step`) and
|
|
57
59
|
* audit off-switch (`--audit` / `--no-audit`) flags from a plan-resume body.
|
|
@@ -182,7 +184,7 @@ export async function preparePlanResume(args) {
|
|
|
182
184
|
// the auto-fire dispatch) should never have to name a version number.
|
|
183
185
|
const newestOfFamily = pickNewestPlanVersion(resolved.matches.map((m) => m.path));
|
|
184
186
|
if (newestOfFamily) {
|
|
185
|
-
args.log(
|
|
187
|
+
args.log(formatInfo(`Resolved @${tokens[0]} to the latest version ${basename(newestOfFamily)}`).trimEnd());
|
|
186
188
|
path = newestOfFamily;
|
|
187
189
|
}
|
|
188
190
|
else {
|
|
@@ -206,7 +208,7 @@ export async function preparePlanResume(args) {
|
|
|
206
208
|
// next to the picked file, silently redirect to it with a note.
|
|
207
209
|
const newest = findNewestVersion(path, envelope);
|
|
208
210
|
if (newest) {
|
|
209
|
-
args.log(
|
|
211
|
+
args.log(formatInfo(`Newer version found · resuming ${basename(newest.path)} (you picked ${basename(path)})`).trimEnd());
|
|
210
212
|
path = newest.path;
|
|
211
213
|
envelope = newest.envelope;
|
|
212
214
|
}
|
|
@@ -252,11 +254,13 @@ export async function preparePlanResume(args) {
|
|
|
252
254
|
const lease = acquireRunLease(path, args.sessionId, { log: (line) => args.log(line) });
|
|
253
255
|
if (!lease.acquired && lease.holder) {
|
|
254
256
|
const holder = lease.holder;
|
|
255
|
-
args.log(
|
|
256
|
-
const answer =
|
|
257
|
-
|
|
258
|
-
:
|
|
259
|
-
|
|
257
|
+
args.log(formatAttention({ text: 'Another session is already resuming this plan', fact: `session ${holder.sessionId} (pid ${holder.pid}${holder.at ? `, since ${holder.at}` : ''})` }).trimEnd());
|
|
258
|
+
const answer = await askYesNo({
|
|
259
|
+
question: 'Take over its run lease and resume here anyway?',
|
|
260
|
+
confirm: args.confirm,
|
|
261
|
+
prompt: args.prompt,
|
|
262
|
+
});
|
|
263
|
+
if (!isYes(answer)) {
|
|
260
264
|
args.log('Resume refused — the other session keeps the lease. Re-run when it finishes (a dead session\'s lease is taken over automatically).');
|
|
261
265
|
return null;
|
|
262
266
|
}
|
|
@@ -300,7 +304,7 @@ export async function preparePlanResume(args) {
|
|
|
300
304
|
const names = unresolved.map((p) => p.audit?.state === 'failed'
|
|
301
305
|
? `${p.id} (FAILED audit — ${p.audit.findings?.length ?? 0} finding${(p.audit.findings?.length ?? 0) === 1 ? '' : 's'} waived)`
|
|
302
306
|
: `${p.id} (${p.audit?.state ?? 'unresolved'})`);
|
|
303
|
-
args.log(
|
|
307
|
+
args.log(formatInfo(`audits off (--no-audit): waived ${unresolved.length} unresolved audit${unresolved.length === 1 ? '' : 's'} from an earlier run — ${names.join(', ')} (reason: audits turned off).`).trimEnd());
|
|
304
308
|
}
|
|
305
309
|
}
|
|
306
310
|
// Task 6 — hand-edit detection: the newest history entry was NOT authored
|
|
@@ -322,7 +326,7 @@ export async function preparePlanResume(args) {
|
|
|
322
326
|
? { ...p, audit: { ...(p.audit ?? {}), state: 'not_audited_hand_edited' } }
|
|
323
327
|
: p),
|
|
324
328
|
};
|
|
325
|
-
args.log(
|
|
329
|
+
args.log(formatAttention({ text: `Plan was last edited outside CSPeach (by ${lastHistory?.by?.name ?? 'unknown'})`, fact: 'satisfied phases without an audit show as hand-edited · file not rewritten' }).trimEnd());
|
|
326
330
|
}
|
|
327
331
|
// Task 6 — re-audit-on-resume: an unresolved persisted audit (a crash
|
|
328
332
|
// between phase save and verdict save leaves 'pending'; an auditor outage
|
|
@@ -636,12 +640,16 @@ export async function offerNextPhaseAutoRun(args) {
|
|
|
636
640
|
return false;
|
|
637
641
|
const delegate = args.outcome.nextDelegateTo ? ` (${args.outcome.nextDelegateTo})` : '';
|
|
638
642
|
if (args.gate?.action === 'auto') {
|
|
639
|
-
args.log(
|
|
643
|
+
args.log(formatInfo(`Auto-running next phase ${args.outcome.nextPhaseId}${delegate} · guarded mode: declared non-write phase, prior audit clean.`).trimEnd());
|
|
640
644
|
args.queueDispatch(args.outcome.resumeCommand);
|
|
641
645
|
return true;
|
|
642
646
|
}
|
|
643
|
-
const answer =
|
|
644
|
-
|
|
647
|
+
const answer = await askYesNo({
|
|
648
|
+
question: `Run next phase ${args.outcome.nextPhaseId}${delegate} now in a fresh context?`,
|
|
649
|
+
confirm: args.confirm,
|
|
650
|
+
prompt: args.prompt,
|
|
651
|
+
});
|
|
652
|
+
if (!isYes(answer)) {
|
|
645
653
|
args.log(`Exiting — resume later with: ${args.outcome.resumeCommand}`);
|
|
646
654
|
return false;
|
|
647
655
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -6,30 +6,44 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readAuthFile } from '../auth/auth-file.js';
|
|
8
8
|
import { fetchMe } from '../auth/me.js';
|
|
9
|
+
import { formatFailure, formatInfo } from '../renderer/notice-shape.js';
|
|
10
|
+
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
11
|
+
import { paint } from '../renderer/theme.js';
|
|
12
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
13
|
+
/** Server-supplied text (email, plan, trial fields) is one line with no control characters. */
|
|
14
|
+
const serverText = (v) => sanitizeControlChars(String(v ?? '')).replace(/\s*\r?\n\s*/g, ' ').trim();
|
|
9
15
|
export async function runWhoamiCommand(deps) {
|
|
10
16
|
const auth = await readAuthFile();
|
|
11
17
|
if (!auth) {
|
|
12
|
-
deps.print('Not signed in
|
|
13
|
-
deps.print('
|
|
18
|
+
deps.print(formatInfo('Not signed in with cspeach login (no ~/.cspeach/auth.json)').trimEnd());
|
|
19
|
+
deps.print(` ${paint('dim', 'Next:')} cspeach login (the REPL may still use a keychain key — run cspeach to check)`);
|
|
14
20
|
return 0;
|
|
15
21
|
}
|
|
16
22
|
const me = await fetchMe({ proxyUrl: deps.proxyUrl, apiKey: auth.apiKey });
|
|
17
23
|
if (!me.ok) {
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// Status 0 = the request never got an answer (network error or the /v1/me
|
|
25
|
+
// timeout): signing in again would not help. Any HTTP answer keeps the
|
|
26
|
+
// log-in hint, as before.
|
|
27
|
+
const unreachable = me.status === 0;
|
|
28
|
+
deps.print(formatFailure({
|
|
29
|
+
what: 'Could not read your account',
|
|
30
|
+
detail: [unreachable ? `could not reach the server (${me.error})` : `the server answered ${me.status} (${me.error})`],
|
|
31
|
+
next: unreachable ? 'check your network or VPN, then run cspeach whoami again' : 'cspeach login',
|
|
32
|
+
}).trimEnd());
|
|
20
33
|
return 1;
|
|
21
34
|
}
|
|
22
35
|
const p = me.profile;
|
|
23
|
-
deps.print(
|
|
24
|
-
deps.print(
|
|
36
|
+
deps.print(panelHeading('Account'));
|
|
37
|
+
deps.print(panelRow('Email', serverText(p.email), 8));
|
|
38
|
+
deps.print(panelRow('Plan', serverText(p.plan), 8));
|
|
25
39
|
if (p.trial_mode) {
|
|
26
|
-
deps.print(
|
|
40
|
+
deps.print(panelRow('Trial', serverText(`${p.trial_mode}, ends ${p.trial_ends_at}`), 8));
|
|
27
41
|
if (p.trial_token_cap_input) {
|
|
28
|
-
deps.print(
|
|
42
|
+
deps.print(panelRow('Tokens', serverText(`${p.usage_input_tokens}/${p.trial_token_cap_input} in, ${p.usage_output_tokens}/${p.trial_token_cap_output ?? '∞'} out`), 8));
|
|
29
43
|
}
|
|
30
44
|
}
|
|
31
45
|
if (p.entitlements.enabled_skills.length > 0) {
|
|
32
|
-
deps.print(
|
|
46
|
+
deps.print(panelRow('Skills', serverText(p.entitlements.enabled_skills.join(', ')), 8));
|
|
33
47
|
}
|
|
34
48
|
return 0;
|
|
35
49
|
}
|
package/dist/config/loader.js
CHANGED
|
@@ -22,9 +22,15 @@ export const DEFAULT_CONFIG = {
|
|
|
22
22
|
// Default `auto` — Ink inline+Static is the production path as of 0.8.0.
|
|
23
23
|
// Smoke-verified 2026-06-06 (A1, post-Hajj plan): smooth streaming on a
|
|
24
24
|
// big skill turn, no stutter/jumping/picker re-mount, phantom cursor #25
|
|
25
|
-
// root-caused and fixed (485604b).
|
|
26
|
-
// to
|
|
27
|
-
|
|
25
|
+
// root-caused and fixed (485604b). Piped/CI/one-shot invocations fall back
|
|
26
|
+
// to plain output automatically. A stored 'classic' is retiring (2026-09-27):
|
|
27
|
+
// it is read as 'auto' — see renderer/rendering-mode.ts (CSPEACH_UI=classic
|
|
28
|
+
// is the one-launch escape hatch).
|
|
29
|
+
// 2026-09-26 — `look` = the conversation look (spec
|
|
30
|
+
// docs/superpowers/specs/2026-09-26-cli-look-piece1-design.md). 'panels' is
|
|
31
|
+
// the default; 'legacy' reproduces the pre-overhaul output exactly.
|
|
32
|
+
// `background` is left unset: getBackground() then reads COLORFGBG, else dark.
|
|
33
|
+
ui: { rendering: 'auto', look: 'panels' },
|
|
28
34
|
// Default `approval-gated` — preserves current UX (user approves each write).
|
|
29
35
|
write_mode: 'approval-gated',
|
|
30
36
|
// Default `managed` — lowest-friction entry point; uses the cspeach.dev proxy.
|
|
@@ -218,6 +224,12 @@ export async function loadConfig() {
|
|
|
218
224
|
// `config show` and saveConfig output clean — no `local_files = undefined`.
|
|
219
225
|
if (merged.local_files === undefined)
|
|
220
226
|
delete merged.local_files;
|
|
227
|
+
// 2026-09-26 — unknown values fall back instead of reaching the renderer.
|
|
228
|
+
if (merged.ui.look !== 'panels' && merged.ui.look !== 'legacy')
|
|
229
|
+
merged.ui.look = DEFAULT_CONFIG.ui.look;
|
|
230
|
+
if (merged.ui.background !== undefined && merged.ui.background !== 'dark' && merged.ui.background !== 'light') {
|
|
231
|
+
delete merged.ui.background;
|
|
232
|
+
}
|
|
221
233
|
// Same for `local_build`: when the key is absent from disk we coerce it to
|
|
222
234
|
// `undefined` (so resolveLocalBuild's legacy fallback can fire). Delete the
|
|
223
235
|
// key so it doesn't serialise as `local_build = undefined` and so callers
|
package/dist/cost/pricing.js
CHANGED
|
@@ -22,10 +22,12 @@
|
|
|
22
22
|
* Unknown models → returns null + records the model name to a process-level
|
|
23
23
|
* Set so we surface them as an obvious warning rather than silently zeroing.
|
|
24
24
|
*/
|
|
25
|
+
import { recordFullNotice } from '../renderer/notice-log.js';
|
|
25
26
|
import { getServerModelConfig } from '../models/server-config.js';
|
|
26
27
|
const PRICING = {
|
|
27
28
|
// Claude 5 family (2026-09-15) — Opus 5 keeps the Opus 4.5+ rate ($5/$25);
|
|
28
29
|
// Sonnet 5 is $2/$10. Cache read = 10% of input, cache write = 1.25x input.
|
|
30
|
+
'claude-opus-5-5': { inputPer1M: 4.00, outputPer1M: 20.00, cacheReadPer1M: 0.20, cacheCreatePer1M: 5.00 },
|
|
29
31
|
'claude-opus-5': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheCreatePer1M: 6.25 },
|
|
30
32
|
'claude-sonnet-5': { inputPer1M: 2.00, outputPer1M: 10.00, cacheReadPer1M: 0.20, cacheCreatePer1M: 2.50 },
|
|
31
33
|
// Claude Opus 4.5+ — input 5, output 25, cache_read 0.50, cache_write 6.25 (1.25x input).
|
|
@@ -90,11 +92,10 @@ export function getRate(model) {
|
|
|
90
92
|
return PRICING[stripDate];
|
|
91
93
|
if (!seenUnknownModels.has(normalized)) {
|
|
92
94
|
seenUnknownModels.add(normalized);
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
catch { /* noop */ }
|
|
95
|
+
// Diagnostics never reach the user's screen (fix round 3, item 1): a raw
|
|
96
|
+
// console.warn bypassed the Ink/classic writers and printed JSON into the
|
|
97
|
+
// chat, spinner, goodbye and one-shot stderr. Log file only.
|
|
98
|
+
recordFullNotice('debug', JSON.stringify({ msg: 'pricing_unknown_model', model: normalized }));
|
|
98
99
|
}
|
|
99
100
|
return null;
|
|
100
101
|
}
|
|
@@ -19,6 +19,7 @@ import { loadConfig } from '../../config/loader.js';
|
|
|
19
19
|
import { getConnection } from '../../sap/connection-manager.js';
|
|
20
20
|
import { AdtClient, snapshots } from '@cspeach/sap-client';
|
|
21
21
|
import { sapChecksSkipped, standaloneSkip } from './_standalone-skip.js';
|
|
22
|
+
import { verifySyntax } from '../../tools/verify.js';
|
|
22
23
|
const PROBE_NAME = 'Z_CSPEACH_DOCTOR_PROBE';
|
|
23
24
|
const PROBE_PACKAGE = '$TMP';
|
|
24
25
|
const PROBE_SOURCE = `REPORT ${PROBE_NAME}.\nWRITE / 'cspeach doctor probe — safe to delete'.`;
|
|
@@ -84,12 +85,21 @@ export async function checkForgeRules() {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
// Step 4: Verify Rule 10 fired — syntax was checked. Re-run syntaxCheck and confirm clean.
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// Re-review item 2 — through verifySyntax (fail closed): a check that did
|
|
89
|
+
// not run is ok:false with its reason, never a throw out of collectChecks.
|
|
90
|
+
const syntax = await verifySyntax(adt, PROBE_NAME, 'PROG');
|
|
91
|
+
if (syntax.didNotRun !== undefined) {
|
|
89
92
|
return {
|
|
90
93
|
name: 'forge-rules',
|
|
91
94
|
ok: false,
|
|
92
|
-
message: `Rule 10: syntax check
|
|
95
|
+
message: `Rule 10: syntax check did not run: ${syntax.didNotRun.replace(/^Syntax check did not run:?\s*/i, '')}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!syntax.ok) {
|
|
99
|
+
return {
|
|
100
|
+
name: 'forge-rules',
|
|
101
|
+
ok: false,
|
|
102
|
+
message: `Rule 10: syntax check returned errors on probe: ${syntax.errors[0] ?? 'unknown'}`,
|
|
93
103
|
};
|
|
94
104
|
}
|
|
95
105
|
return {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import keychain from '../../auth/keychain.js';
|
|
1
|
+
import keychain, { newProbeAccount } from '../../auth/keychain.js';
|
|
2
2
|
export async function checkKeychain() {
|
|
3
3
|
try {
|
|
4
|
+
// Round 4 item 7 — per-process probe account (see newProbeAccount).
|
|
5
|
+
const account = newProbeAccount();
|
|
4
6
|
const probe = `probe-${Date.now()}`;
|
|
5
|
-
await keychain.setPassword('cspeach',
|
|
6
|
-
const ok = (await keychain.getPassword('cspeach',
|
|
7
|
-
await keychain.deletePassword('cspeach',
|
|
7
|
+
await keychain.setPassword('cspeach', account, probe);
|
|
8
|
+
const ok = (await keychain.getPassword('cspeach', account)) === probe;
|
|
9
|
+
await keychain.deletePassword('cspeach', account);
|
|
8
10
|
if (ok)
|
|
9
11
|
return { name: 'keychain', ok: true, message: 'OS keychain working' };
|
|
10
12
|
return { name: 'keychain', ok: false, message: 'probe write/read mismatch', hint: 'Per-session fallback mode will be used' };
|