@herbertgao/pi-extensions 2026.9.7 → 2026.9.9
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 +8 -4
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +6 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
- package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
- package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
- package/node_modules/grok-mermaid/CHANGELOG.md +46 -0
- package/node_modules/grok-mermaid/LICENSE +205 -0
- package/node_modules/grok-mermaid/README.md +191 -0
- package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
- package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/ansi.js +23 -0
- package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
- package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
- package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/canvas.js +366 -0
- package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
- package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
- package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/graph.js +91 -0
- package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
- package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
- package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/index.js +100 -0
- package/node_modules/grok-mermaid/dist/index.js.map +1 -0
- package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
- package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/labels.js +324 -0
- package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
- package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
- package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
- package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
- package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
- package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/layout.js +881 -0
- package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
- package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
- package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/parse.js +1151 -0
- package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
- package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
- package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/source-box.js +78 -0
- package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
- package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
- package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/types.js +1 -0
- package/node_modules/grok-mermaid/dist/types.js.map +1 -0
- package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
- package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/width-data.js +994 -0
- package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
- package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
- package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/width.js +76 -0
- package/node_modules/grok-mermaid/dist/width.js.map +1 -0
- package/node_modules/grok-mermaid/package.json +49 -0
- package/node_modules/grok-mermaid/src/ansi.ts +34 -0
- package/node_modules/grok-mermaid/src/canvas.ts +373 -0
- package/node_modules/grok-mermaid/src/graph.ts +142 -0
- package/node_modules/grok-mermaid/src/index.ts +104 -0
- package/node_modules/grok-mermaid/src/labels.ts +326 -0
- package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
- package/node_modules/grok-mermaid/src/layout.ts +1015 -0
- package/node_modules/grok-mermaid/src/parse.ts +1189 -0
- package/node_modules/grok-mermaid/src/source-box.ts +89 -0
- package/node_modules/grok-mermaid/src/types.ts +43 -0
- package/node_modules/grok-mermaid/src/width-data.ts +993 -0
- package/node_modules/grok-mermaid/src/width.ts +74 -0
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
- package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
- package/node_modules/pi-jev-auto-mode/README.md +285 -0
- package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
- package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
- package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
- package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
- package/node_modules/pi-jev-auto-mode/index.ts +1 -0
- package/node_modules/pi-jev-auto-mode/package.json +72 -0
- package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
- package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
- package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
- package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
- package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
- package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
- package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
- package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
- package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
- package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
- package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
- package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
- package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
- package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
- package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
- package/node_modules/pi-lens/CHANGELOG.md +45 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
- package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
- package/node_modules/pi-lens/dist/index.js +729 -344
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-typesafe/LICENSE +21 -0
- package/node_modules/pi-typesafe/README.md +153 -0
- package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
- package/node_modules/pi-typesafe/dist/ask.js +25 -0
- package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
- package/node_modules/pi-typesafe/dist/auth.js +114 -0
- package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
- package/node_modules/pi-typesafe/dist/batch.js +116 -0
- package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
- package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
- package/node_modules/pi-typesafe/dist/client.js +188 -0
- package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
- package/node_modules/pi-typesafe/dist/credentials.js +112 -0
- package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
- package/node_modules/pi-typesafe/dist/errors.js +31 -0
- package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/extension.js +222 -0
- package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
- package/node_modules/pi-typesafe/dist/index.js +9 -0
- package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
- package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
- package/node_modules/pi-typesafe/dist/login.js +38 -0
- package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
- package/node_modules/pi-typesafe/dist/schema.js +152 -0
- package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/ui.js +3 -0
- package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
- package/node_modules/pi-typesafe/dist/usage.js +182 -0
- package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
- package/node_modules/pi-typesafe/extensions/index.js +2 -0
- package/node_modules/pi-typesafe/package.json +89 -0
- package/package.json +16 -8
|
@@ -45,8 +45,9 @@ import * as path from "node:path";
|
|
|
45
45
|
import { Type } from "../clients/deps/typebox.js";
|
|
46
46
|
import { markDisposition, normalizeMessage, } from "../clients/diagnostic-dispositions.js";
|
|
47
47
|
import { insertSuppressComment } from "../clients/dispatch/suppress-writer.js";
|
|
48
|
-
import {
|
|
48
|
+
import { normalizeMapKey } from "../clients/path-utils.js";
|
|
49
49
|
import { resolveLensToolName } from "../clients/tool-config.js";
|
|
50
|
+
import { getFileDiagnostics, } from "../clients/widget-state.js";
|
|
50
51
|
const DISPOSITIONS = [
|
|
51
52
|
"false-positive",
|
|
52
53
|
"suppress",
|
|
@@ -70,11 +71,22 @@ function isBlank(text) {
|
|
|
70
71
|
* violation) are ambiguous; per #802 this picks the match CLOSEST to the
|
|
71
72
|
* caller's line rather than guessing — the conservative choice for suppress,
|
|
72
73
|
* which must never place a comment above the wrong site.
|
|
74
|
+
*
|
|
75
|
+
* #3160: `getFileDiagnostics` keys with `normalizeEphemeralMapKey` — case-
|
|
76
|
+
* preserving on POSIX by design (no filesystem I/O on that hot path) — but
|
|
77
|
+
* the widget's own writers key from `ctx.filePath`, which
|
|
78
|
+
* `createDispatchContext` already canonicalizes to the on-disk casing
|
|
79
|
+
* (#2016/#3098). `absPath` here is the caller's RAW spelling, so on a
|
|
80
|
+
* case-insensitive filesystem a mis-cased mark call must derive the same
|
|
81
|
+
* canonical key the writers used or it silently misses the live diagnostic
|
|
82
|
+
* and falls through to the fuzzy fallback below — do NOT case-fold
|
|
83
|
+
* `normalizeEphemeralMapKey` itself for this: on a case-sensitive filesystem
|
|
84
|
+
* that would merge two genuinely different files.
|
|
73
85
|
*/
|
|
74
86
|
function widgetCrossCheck(absPath, tool, rule, message, callerLine) {
|
|
75
87
|
let diagnostics;
|
|
76
88
|
try {
|
|
77
|
-
diagnostics = getFileDiagnostics(absPath);
|
|
89
|
+
diagnostics = getFileDiagnostics(normalizeMapKey(absPath));
|
|
78
90
|
}
|
|
79
91
|
catch {
|
|
80
92
|
return undefined;
|
|
@@ -18,13 +18,13 @@ import { DEPENDENCY_DRIFT_MAX_DELIVERIES } from "../clients/blocker-freshness.js
|
|
|
18
18
|
import { freshnessFromMtime } from "../clients/freshness.js";
|
|
19
19
|
import { applyFindingPolicy, loadProjectRulePolicyMap, } from "../clients/dispatch/finding-policy.js";
|
|
20
20
|
import { gateFindingsByPathFreshness } from "../clients/advisory-provenance.js";
|
|
21
|
-
import { markUnreconciledFindings } from "../clients/finding-delivery-gate.js";
|
|
21
|
+
import { formatCacheAgeLabel, markUnreconciledFindings, } from "../clients/finding-delivery-gate.js";
|
|
22
22
|
import { normalizeRuleId } from "../clients/dispatch/rule-id-normalize.js";
|
|
23
23
|
import { applyRulePolicy, } from "../clients/dispatch/rule-policy.js";
|
|
24
24
|
import { compactRenderResult } from "./render-compact.js";
|
|
25
25
|
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
26
26
|
import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
|
|
27
|
-
import { isAtOrAboveHomeDir, normalizeEphemeralMapKey, normalizeFilePath, realpathOrResolve, } from "../clients/path-utils.js";
|
|
27
|
+
import { isAtOrAboveHomeDir, normalizeEphemeralMapKey, normalizeFilePath, normalizeMapKey, realpathOrResolve, } from "../clients/path-utils.js";
|
|
28
28
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
29
29
|
import { retireInlineBlockerAndResyncGuard } from "../clients/git-guard.js";
|
|
30
30
|
import { primaryServerId, resolveLspCwdForFile, } from "../clients/lsp/config.js";
|
|
@@ -508,7 +508,7 @@ function formatProjectDeltaDiagnostic(diagnostic, stale) {
|
|
|
508
508
|
* rule/message), live -> unchanged. See `clients/finding-delivery-gate.ts`
|
|
509
509
|
* surface `lens-diagnostics:mode-delta`.
|
|
510
510
|
*/
|
|
511
|
-
function appendProjectDiagnosticsDeltaLines(
|
|
511
|
+
function appendProjectDiagnosticsDeltaLines(groups, cwd, report, severity, includeFile) {
|
|
512
512
|
const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
|
|
513
513
|
matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
|
|
514
514
|
const gated = gateFindingsByPathFreshness({
|
|
@@ -532,11 +532,9 @@ function appendProjectDiagnosticsDeltaLines(lines, cwd, report, severity, includ
|
|
|
532
532
|
byFile.set(filePath, bucket);
|
|
533
533
|
}
|
|
534
534
|
for (const [filePath, fileDiagnostics] of byFile) {
|
|
535
|
-
const
|
|
536
|
-
if (!lines.includes(rel))
|
|
537
|
-
lines.push(rel);
|
|
535
|
+
const group = getDeltaFileGroup(groups, cwd, filePath);
|
|
538
536
|
for (const diagnostic of fileDiagnostics) {
|
|
539
|
-
|
|
537
|
+
group.projectLines.push(formatProjectDeltaDiagnostic(diagnostic, staleSet.has(diagnostic)));
|
|
540
538
|
}
|
|
541
539
|
}
|
|
542
540
|
return diagnostics.length;
|
|
@@ -723,7 +721,15 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
|
|
|
723
721
|
}
|
|
724
722
|
for (const f of gated.stale) {
|
|
725
723
|
const arr = byFile.get(f.filePath) ?? [];
|
|
726
|
-
|
|
724
|
+
// Fix B (#3167): carry the stamp the row was judged against so the render
|
|
725
|
+
// can emit one age label per file group — the row's own observation stamp,
|
|
726
|
+
// not the report-level one (the #2504 r4 multi-stamp case).
|
|
727
|
+
arr.push({
|
|
728
|
+
...f.warning,
|
|
729
|
+
stale: true,
|
|
730
|
+
line: undefined,
|
|
731
|
+
staleAsOf: effectiveAt,
|
|
732
|
+
});
|
|
727
733
|
byFile.set(f.filePath, arr);
|
|
728
734
|
}
|
|
729
735
|
return files
|
|
@@ -733,10 +739,34 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
|
|
|
733
739
|
}))
|
|
734
740
|
.filter((file) => file.warnings.length > 0);
|
|
735
741
|
}
|
|
742
|
+
function getDeltaFileGroup(groups, cwd, filePath) {
|
|
743
|
+
const rel = path.relative(cwd, filePath);
|
|
744
|
+
let group = groups.get(rel);
|
|
745
|
+
if (!group) {
|
|
746
|
+
group = {
|
|
747
|
+
rel,
|
|
748
|
+
actionableLines: [],
|
|
749
|
+
qualityLines: [],
|
|
750
|
+
projectLines: [],
|
|
751
|
+
incomplete: false,
|
|
752
|
+
};
|
|
753
|
+
groups.set(rel, group);
|
|
754
|
+
}
|
|
755
|
+
return group;
|
|
756
|
+
}
|
|
736
757
|
// @delivery-surface: lens-diagnostics:mode-delta
|
|
737
758
|
function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
|
|
738
759
|
const actionableEntry = cacheManager.readCache("actionable-warnings", cwd);
|
|
739
760
|
const qualityEntry = cacheManager.readCache("code-quality-warnings", cwd);
|
|
761
|
+
// #3170: files whose re-verify pass could not complete inside its budget —
|
|
762
|
+
// their rows render as carried, plus this explicit gap label (never a
|
|
763
|
+
// false clean). Computed from the RAW actionable-warnings cache entries
|
|
764
|
+
// because the freshness pipeline below rebuilds the file shape. The
|
|
765
|
+
// re-verify only ever writes the actionable-warnings cache — the quality
|
|
766
|
+
// report's file shape carries no such marker.
|
|
767
|
+
const reverifyIncompletePaths = new Set((actionableEntry?.data?.files ?? [])
|
|
768
|
+
.filter((file) => file.reVerifyIncomplete)
|
|
769
|
+
.map((file) => normalizeMapKey(file.filePath)));
|
|
740
770
|
const actionable = actionableEntry?.data;
|
|
741
771
|
const quality = qualityEntry?.data;
|
|
742
772
|
// Project rule policy (`.pi-lens.json` `rules.<id>.disable` /
|
|
@@ -778,32 +808,69 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
|
|
|
778
808
|
warnings: matchingWarnings(file.warnings),
|
|
779
809
|
}))
|
|
780
810
|
.filter((file) => file.warnings.length > 0);
|
|
781
|
-
|
|
811
|
+
// #3196: one grouping pass keyed by file — every tier below appends into
|
|
812
|
+
// the SAME per-file group (see `DeltaFileGroup`/`getDeltaFileGroup`), so a
|
|
813
|
+
// file present in more than one report always renders its rows under its
|
|
814
|
+
// own header, wherever that header ends up in the final buffer.
|
|
815
|
+
const groups = new Map();
|
|
782
816
|
// Fixable warnings from actionable-warnings and quality cache entries retain
|
|
783
817
|
// their own severity tier. Apply the same threshold semantics as the LSP path.
|
|
818
|
+
// F5/F10 (#3168): at most ONE label group (age and/or #3170's re-verify
|
|
819
|
+
// gap) per file. The actionable tier is folded first, so its stale row (if
|
|
820
|
+
// any) wins the group's age label; the quality tier only supplies one when
|
|
821
|
+
// the actionable tier had none for that file (#3168 F10(c)'s quality-only
|
|
822
|
+
// shape). The `(re-verify incomplete)` flag is a fact about the file (from
|
|
823
|
+
// the raw actionable cache, independent of which tier's rows happen to
|
|
824
|
+
// render it), so it is recorded on the group the first time either tier
|
|
825
|
+
// touches that file and never predicted.
|
|
784
826
|
if (filteredActionableFiles.length > 0) {
|
|
785
827
|
for (const file of filteredActionableFiles) {
|
|
786
|
-
const
|
|
787
|
-
lines.push(`${rel}`);
|
|
828
|
+
const group = getDeltaFileGroup(groups, cwd, file.filePath);
|
|
788
829
|
for (const w of file.warnings) {
|
|
789
830
|
const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
|
|
790
|
-
|
|
831
|
+
group.actionableLines.push(` ⚠ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
|
|
832
|
+
}
|
|
833
|
+
const key = normalizeMapKey(file.filePath);
|
|
834
|
+
if (reverifyIncompletePaths.has(key))
|
|
835
|
+
group.incomplete = true;
|
|
836
|
+
if (!group.staleRow) {
|
|
837
|
+
const staleWarning = file.warnings.find((w) => w.stale);
|
|
838
|
+
if (staleWarning)
|
|
839
|
+
group.staleRow = { staleAsOf: staleWarning.staleAsOf };
|
|
791
840
|
}
|
|
792
841
|
}
|
|
793
842
|
}
|
|
794
843
|
// Quality issues
|
|
795
844
|
if (filteredQualityFiles.length > 0) {
|
|
796
845
|
for (const file of filteredQualityFiles) {
|
|
797
|
-
const
|
|
798
|
-
if (!lines.includes(rel))
|
|
799
|
-
lines.push(rel);
|
|
846
|
+
const group = getDeltaFileGroup(groups, cwd, file.filePath);
|
|
800
847
|
for (const w of file.warnings) {
|
|
801
848
|
const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
|
|
802
|
-
|
|
849
|
+
group.qualityLines.push(` ℹ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
|
|
850
|
+
}
|
|
851
|
+
const key = normalizeMapKey(file.filePath);
|
|
852
|
+
if (reverifyIncompletePaths.has(key))
|
|
853
|
+
group.incomplete = true;
|
|
854
|
+
if (!group.staleRow) {
|
|
855
|
+
const staleWarning = file.warnings.find((w) => w.stale);
|
|
856
|
+
if (staleWarning)
|
|
857
|
+
group.staleRow = { staleAsOf: staleWarning.staleAsOf };
|
|
803
858
|
}
|
|
804
859
|
}
|
|
805
860
|
}
|
|
806
|
-
const projectDeltaCount = appendProjectDiagnosticsDeltaLines(
|
|
861
|
+
const projectDeltaCount = appendProjectDiagnosticsDeltaLines(groups, cwd, projectDelta, severity, includeFile);
|
|
862
|
+
const lines = [];
|
|
863
|
+
for (const group of groups.values()) {
|
|
864
|
+
lines.push(group.rel);
|
|
865
|
+
lines.push(...group.actionableLines);
|
|
866
|
+
lines.push(...group.qualityLines);
|
|
867
|
+
lines.push(...group.projectLines);
|
|
868
|
+
if (group.staleRow) {
|
|
869
|
+
lines.push(` (${formatCacheAgeLabel(group.staleRow.staleAsOf)})`);
|
|
870
|
+
}
|
|
871
|
+
if (group.incomplete)
|
|
872
|
+
lines.push(" (re-verify incomplete)");
|
|
873
|
+
}
|
|
807
874
|
const selectedActionableFiles = filteredActionableFiles;
|
|
808
875
|
const selectedQualityFiles = filteredQualityFiles;
|
|
809
876
|
const aw = selectedActionableFiles.reduce((count, file) => count + file.warnings.length, 0);
|
|
@@ -23,7 +23,8 @@ import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
|
|
|
23
23
|
import { attemptTsserverSyncDiagnostics } from "../clients/lsp/tsserver-sync.js";
|
|
24
24
|
import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
|
|
25
25
|
import { demoteInferredProjectDiagnostics } from "../clients/lsp/inferred-project.js";
|
|
26
|
-
import {
|
|
26
|
+
import { normalizeMapKey } from "../clients/path-utils.js";
|
|
27
|
+
import { countRetainedSuppressedRows, isBlocking, reconcileScanDiagnostics, } from "../clients/widget-state.js";
|
|
27
28
|
import { makeProgressReporter } from "./scan-progress.js";
|
|
28
29
|
import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
|
|
29
30
|
import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
|
|
@@ -239,7 +240,18 @@ onConfirmedNoBlockers, getService = getLSPService) {
|
|
|
239
240
|
// Preserve input order (including duplicate entries); normalize each
|
|
240
241
|
// path before grouping so Windows separators and dot segments cannot
|
|
241
242
|
// change cache/group identity. Explicit lists never enter the walker.
|
|
242
|
-
|
|
243
|
+
// #3160/#3182: `path.resolve(cwd, entry)` folds dot segments (and
|
|
244
|
+
// separators) structurally — it ignores `cwd` whenever `entry` is
|
|
245
|
+
// already absolute, so the old `path.isAbsolute` ternary is
|
|
246
|
+
// redundant. `normalizeMapKey` then adopts on-disk CASING where the
|
|
247
|
+
// path exists — an agent-typed, possibly mis-cased `paths` entry
|
|
248
|
+
// must key `reconcileScanDiagnostics`'s widget-state write the same
|
|
249
|
+
// way a canonical writer (clients/pipeline.ts's ctx.filePath) or the
|
|
250
|
+
// #3160-fixed lens_diagnostic_mark reader would. `path.resolve`
|
|
251
|
+
// stays required for the relative→absolute step: `normalizeMapKey`
|
|
252
|
+
// folds dot segments itself since #3184, but never resolves against
|
|
253
|
+
// `cwd` (a cwd fold there broke every monorepo, #2490).
|
|
254
|
+
const absPaths = rawPaths.map((entry) => normalizeMapKey(path.resolve(cwd, entry)));
|
|
243
255
|
return runBatchFileDiagnostics(absPaths, severity, lspService, {
|
|
244
256
|
concurrency,
|
|
245
257
|
waitMs,
|
|
@@ -264,9 +276,12 @@ onConfirmedNoBlockers, getService = getLSPService) {
|
|
|
264
276
|
details: {},
|
|
265
277
|
};
|
|
266
278
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
// #3184: the single-`path` sibling of the `paths` batch above, and
|
|
280
|
+
// keyed the same way — `runFileDiagnostics` hands this straight to
|
|
281
|
+
// `reconcileScanDiagnostics`, whose `normalizeEphemeralMapKey` key
|
|
282
|
+
// derivation folds neither dot segments nor casing. Same expression as
|
|
283
|
+
// :444 so both modes of this tool write under ONE key per file.
|
|
284
|
+
const absPath = normalizeMapKey(path.resolve(cwd, rawPath));
|
|
270
285
|
let stat;
|
|
271
286
|
try {
|
|
272
287
|
stat = fs.statSync(absPath);
|
|
@@ -616,6 +631,17 @@ severity) {
|
|
|
616
631
|
// otherwise have SEEN, never one per finding (AGENTS.md "bounded
|
|
617
632
|
// observability"). `total` is the in-scope population for the same
|
|
618
633
|
// reason the count is, so the record and the rendered line agree.
|
|
634
|
+
//
|
|
635
|
+
// #3158 round 2 F4: `retainedSuppressed` is the widget store's
|
|
636
|
+
// SUCCESS-path number — how many suppressed rows the store is carrying
|
|
637
|
+
// INTO this scan, written by the scans before it. Until this, retention
|
|
638
|
+
// was observable only when its per-file cap truncated
|
|
639
|
+
// (`widget-suppressed-retention-capped`), so a healthy footer chip had no
|
|
640
|
+
// record behind it at all. Folded into the record this lane already emits
|
|
641
|
+
// rather than added as a second one: same cardinality (one per filtered
|
|
642
|
+
// file per scan, never one per row), no new sink, and the two numbers are
|
|
643
|
+
// read together — `suppressed` is what THIS scan dropped,
|
|
644
|
+
// `retainedSuppressed` is what the footer chip is still counting.
|
|
619
645
|
logLatency({
|
|
620
646
|
type: "phase",
|
|
621
647
|
toolName: "lsp_diagnostics",
|
|
@@ -625,6 +651,7 @@ severity) {
|
|
|
625
651
|
metadata: {
|
|
626
652
|
suppressed,
|
|
627
653
|
total: inScopeBefore,
|
|
654
|
+
retainedSuppressed: countRetainedSuppressedRows(file),
|
|
628
655
|
},
|
|
629
656
|
});
|
|
630
657
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Gapac
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# pi-typesafe
|
|
2
|
+
|
|
3
|
+
[Jev](https://typesafe.ai) inside [Pi](https://pi.dev). Jev is TypeSafe's judgment model: send it some state and typed questions and it returns probabilities instead of prose, in well under a second, for a fraction of a cent. This package gives Pi three things built on it:
|
|
4
|
+
|
|
5
|
+
- **A tool for the agent.** `typesafe_evaluate` hands small structured judgments (classify, triage, compare, score) to Jev and returns calibrated numbers in one batched call.
|
|
6
|
+
- **A playground.** `/typesafe test` and `/typesafe playground` run requests from the terminal without touching the model's context.
|
|
7
|
+
- **A typed API for other extensions.** One client, one key store, one login prompt, so extensions such as [pi-warden](https://github.com/DevMortimer/pi-warden) do not each ask for a key — and so a long run can tell you whether Jev was reachable and what it cost.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
Independent project. Not affiliated with TypeSafe AI or the Pi authors.
|
|
12
|
+
|
|
13
|
+
## In one minute
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:pi-typesafe
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then, inside Pi:
|
|
20
|
+
|
|
21
|
+
1. `/typesafe login` and paste a key from [console.typesafe.ai](https://console.typesafe.ai). Input is hidden; the key is verified against the API and saved to `~/.pi/agent/pi-typesafe/auth.json` with owner-only permissions.
|
|
22
|
+
2. `/typesafe test` sends one built-in sample request and shows the answers.
|
|
23
|
+
3. `/typesafe enable` lets the agent call the tool for this session, after you confirm the data notice.
|
|
24
|
+
|
|
25
|
+
Requires Pi 0.85 or newer and Node.js 22.19 or newer. Usage is billed to your TypeSafe account. For CI or scripts set `TYPESAFE_API_KEY` in the environment instead; it takes precedence over the stored key. Do not paste the key into chat, command arguments, or project files.
|
|
26
|
+
|
|
27
|
+
## The three question types
|
|
28
|
+
|
|
29
|
+
Jev answers three kinds of question about the state you send. Every question in a request runs in parallel and in isolation, so adding questions barely changes the latency.
|
|
30
|
+
|
|
31
|
+
| Type | Asks | Returns |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| **Choice** | Which of these options fits? | the chosen key, a probability per option, confidence |
|
|
34
|
+
| **Score** | Where on this ordered rubric does it sit? | a position (may be fractional), probabilities, confidence |
|
|
35
|
+
| **Noul** | Is this statement true? | a probability from 0 to 1 |
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"state": { "message": "I was charged twice. Please help today." },
|
|
40
|
+
"questions": {
|
|
41
|
+
"team": { "type": "choice", "instructions": "Which team should handle this?",
|
|
42
|
+
"criteria": { "billing": "Charges and payments", "technical": "Software failures", "other": "None of these" } },
|
|
43
|
+
"urgent": { "type": "noul", "instructions": "Does the sender request help today?" },
|
|
44
|
+
"frustration": { "type": "score", "instructions": "How frustrated does the sender sound?",
|
|
45
|
+
"criteria": ["Neutral request", "Frustrated but civil", "Explicit anger or threats"] }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Read probabilities and confidence alongside the answer. Confidence describes how concentrated the distribution is; it is not proof of correctness or permission to act.
|
|
51
|
+
|
|
52
|
+
## The agent tool
|
|
53
|
+
|
|
54
|
+
`typesafe_evaluate` is registered at startup but **disabled until you run `/typesafe enable`** in the session. For automated or headless runs, set `PI_TYPESAFE_ENABLED=1` explicitly.
|
|
55
|
+
|
|
56
|
+
The tool accepts JSON `state` plus 1 to 32 questions and returns answers, model, token usage, and elapsed time. Good uses: triaging a list of issues in one call, deciding which of several files a change belongs to, checking whether a reply answers the question that was asked, scoring candidates against a rubric you wrote. Bad uses: anything that needs reasoning across steps, or a single question that mixes several judgments.
|
|
57
|
+
|
|
58
|
+
## Writing questions that work
|
|
59
|
+
|
|
60
|
+
The question text is the whole program. Jev answers exactly what is asked, so ambiguity shows up as a middling probability rather than an error.
|
|
61
|
+
|
|
62
|
+
- **Ask about what the state says, not what you would conclude.** A report that says "happens every time" scored `P(yes) = 0.36` for `Is the bug reproducible from the text?` because that can also mean "could a reader reproduce it using only this text?". `Does the reporter state that the problem occurs consistently?` is the intended question.
|
|
63
|
+
- **Describe situations in Score levels, not degrees.** `"Workaround exists"` is checkable; `"medium"` is not.
|
|
64
|
+
- **Include a no-match option** in a Choice (`other`, `unclear`) when nothing may fit; the model cannot pick an option you omitted.
|
|
65
|
+
- **One judgment per question.** Split independent dimensions into separate questions and batch them in one request; they run in parallel and cannot see each other.
|
|
66
|
+
- **Name the state fields you mean** with backticks (`` `report.body` ``) when the state has several parts.
|
|
67
|
+
|
|
68
|
+
The [TypeSafe docs](https://docs.typesafe.ai/primitives) cover each primitive in detail.
|
|
69
|
+
|
|
70
|
+
## Availability and spend
|
|
71
|
+
|
|
72
|
+
An enabled extension with no usable key looks exactly like a working one — one evaluation ran keyless for hours before anyone noticed. Three answers exist now, and `/typesafe status` prints all of them:
|
|
73
|
+
|
|
74
|
+
- **Is Jev reachable?** `authState()` reports the key source, whether it has been accepted, and the last failure that degraded it; `describeAuth()` turns that into a level and one safe line. The extension calls both at session start and after an authentication rejection, so a headless run says judgments are skipped instead of quietly falling back to its offline path.
|
|
75
|
+
- **What has this run cost?** `getSpend()` returns session counters, today's persisted counters, and the cap currently reached. Cost is estimated from input tokens only, because output is free.
|
|
76
|
+
- **When does it stop?** `maxRequests` bounds a client instance. Three caps bound a local day, survive restarts, and stop a request before it is submitted:
|
|
77
|
+
|
|
78
|
+
| Option | Environment | Bounds |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `maxRequestsPerDay` | `PI_TYPESAFE_MAX_REQUESTS_PER_DAY` | requests |
|
|
81
|
+
| `maxInputTokensPerDay` | `PI_TYPESAFE_MAX_INPUT_TOKENS_PER_DAY` | input tokens |
|
|
82
|
+
| `maxUsdPerDay` | `PI_TYPESAFE_MAX_USD_PER_DAY` | estimated spend |
|
|
83
|
+
|
|
84
|
+
The environment may lower an explicit cap but never raise it. A reached cap raises a `budget` error naming the cap, the amount used, and the day.
|
|
85
|
+
|
|
86
|
+
Need more than one request? `evaluateAll(request)` asks any number of questions about one state (over 32 are chunked and fanned out), and `evaluateMany(requests)` runs several requests at once. Both preserve order, bound concurrency, never throw, and stop submitting once the budget is gone. [`pi-typesafe/calibrate`](docs/api.md#calibration-pi-typesafecalibrate) turns labelled cases into thresholds with AUC, a sweep, and a replay runner.
|
|
87
|
+
|
|
88
|
+
## Commands
|
|
89
|
+
|
|
90
|
+
| Command | Effect |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `/typesafe login` | Enter and verify an API key (hidden input), then store it |
|
|
93
|
+
| `/typesafe logout` | Delete the stored key and disable the tool |
|
|
94
|
+
| `/typesafe setup` | Check which key is in use; starts login if none |
|
|
95
|
+
| `/typesafe status` | Opt-in state, key state, session and today's counters, cost estimate, any reached cap |
|
|
96
|
+
| `/typesafe enable` | Confirm the data notice and allow agent tool calls this session |
|
|
97
|
+
| `/typesafe disable` | Stop future agent tool calls |
|
|
98
|
+
| `/typesafe test` | Send one built-in sample request |
|
|
99
|
+
| `/typesafe playground` | Edit request JSON in Pi's editor, confirm, view results |
|
|
100
|
+
|
|
101
|
+
Playground and test results are shown in the terminal only; they do not enter the model's context.
|
|
102
|
+
|
|
103
|
+
## Questions people ask
|
|
104
|
+
|
|
105
|
+
**Do I need a key to install?**
|
|
106
|
+
Install works without one; nothing is sent until you log in and enable the tool. Jev is new and access may be limited at the moment. Keys come from [console.typesafe.ai](https://console.typesafe.ai).
|
|
107
|
+
|
|
108
|
+
**What does a request cost?**
|
|
109
|
+
Whatever TypeSafe bills for the input tokens of your state and questions; output is free. At the listed rate ($42 per billion input tokens at the time of writing) a few hundred tokens cost well under a hundredth of a cent. The per-session cap is 20 attempts, and `maxUsdPerDay` stops a long run at a number you choose.
|
|
110
|
+
|
|
111
|
+
**Why not just ask the main model?**
|
|
112
|
+
The main model can answer any of these questions in prose. It is slower, costs more per call, and grades its own work. Jev returns a calibrated number your code or the agent can branch on, in a quarter of a second, from a separate model. That matters most when the same question is asked many times: every tool call, every file, every issue in a list.
|
|
113
|
+
|
|
114
|
+
**What is sent?**
|
|
115
|
+
Only the state and questions you (or the agent, once enabled) submit, to `https://api.typesafe.ai` only. No files, conversation history, or telemetry. Error messages never include upstream response bodies, headers, keys, or your submitted state.
|
|
116
|
+
|
|
117
|
+
**Limits?**
|
|
118
|
+
Per request: 32 questions and 64 KiB of JSON. Per session: 20 attempts, 15-second timeout, no automatic retries. Per day: no cap unless you set one. Session limits reset when a session starts or reloads; daily counters live in `~/.pi/agent/pi-typesafe/usage.json` and roll over at local midnight. The SDK's `TYPESAFE_BASE_URL` and `TYPESAFE_LOG_LEVEL` overrides are ignored.
|
|
119
|
+
|
|
120
|
+
## For extension authors
|
|
121
|
+
|
|
122
|
+
Import the library from your own extension. It has no dependency on Pi and is safe in tests.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { ask, createTypeSafe, choice, noul, score } from "pi-typesafe";
|
|
126
|
+
|
|
127
|
+
const typesafe = createTypeSafe({ maxRequests: 5, maxUsdPerDay: 1 }); // key: TYPESAFE_API_KEY, else the login store
|
|
128
|
+
const answer = await ask(typesafe, {
|
|
129
|
+
state: { title: "Login fails after update", body: "..." },
|
|
130
|
+
questions: {
|
|
131
|
+
area: choice("Which area does this report concern?", { auth: "Sign-in", ui: "Layout", other: null }),
|
|
132
|
+
duplicate: noul("Does the report describe the same defect as `known_issue`?"),
|
|
133
|
+
severity: score("How severe is the defect?", ["Cosmetic", "Workaround exists", "Blocking"]),
|
|
134
|
+
},
|
|
135
|
+
}, { timeoutMs: 5_000 });
|
|
136
|
+
if (!answer.ok) return { skipped: answer.errorCode === "budget" }; // never throws
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Your extension owns its own user consent and budget; `/typesafe enable` applies only to this package's tool. Check `authState()` rather than your own consent flag before you report that judgments are on. Every export — the client, `ask`, batching, the usage ledger, auth state, and the `pi-typesafe/calibrate` and `pi-typesafe/ui` entry points — is in [docs/api.md](docs/api.md).
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm install
|
|
145
|
+
npm run check # typecheck, offline tests, build
|
|
146
|
+
cp .env.example .env # add your key locally; .env is git-ignored
|
|
147
|
+
npm run test:live # one billable sample request
|
|
148
|
+
npm run dev:pi # start Pi with only this working tree as extension (.env optional)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Questions, SystemOneRequest } from "@typesafe-ai/sdk";
|
|
2
|
+
import type { Evaluation, TypeSafe } from "./client.js";
|
|
3
|
+
import type { IntegrationErrorCode } from "./errors.js";
|
|
4
|
+
/** Anything with pi-typesafe's `evaluate`: the real client in a session, a stub in tests. */
|
|
5
|
+
export type Judge = Pick<TypeSafe, "evaluate">;
|
|
6
|
+
/** Default per-ask deadline; matches the client's own request timeout. */
|
|
7
|
+
export declare const DEFAULT_ASK_TIMEOUT_MS = 15000;
|
|
8
|
+
export interface AskOptions {
|
|
9
|
+
/** Per-ask deadline, merged with the caller's own signal. Default: DEFAULT_ASK_TIMEOUT_MS. */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export type AskAnswer<Q extends Questions> = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly answers: Evaluation<Q>["answers"];
|
|
16
|
+
readonly model: string;
|
|
17
|
+
readonly usage: Evaluation<Q>["usage"];
|
|
18
|
+
readonly elapsedMs: number;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ok: false;
|
|
21
|
+
readonly error: string;
|
|
22
|
+
readonly errorCode?: IntegrationErrorCode;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* One typed Jev request that never throws: a failure comes back as `{ ok: false }` with pi-typesafe's own message
|
|
26
|
+
* (which carries no upstream body, header, key, or submitted state) and its code, so a caller can stop asking after a
|
|
27
|
+
* `budget` error. The per-ask timeout is merged into the caller's signal, so either can cancel the request.
|
|
28
|
+
*
|
|
29
|
+
* This is the author-facing "ask Jev" seam. Agents get the same admission through the `typesafe_evaluate` tool; the two
|
|
30
|
+
* share `prepareEvaluationRequest`, so what one accepts the other accepts.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ask<Q extends Questions>(judge: Judge, request: SystemOneRequest<Q>, options?: AskOptions): Promise<AskAnswer<Q>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
2
|
+
/** Default per-ask deadline; matches the client's own request timeout. */
|
|
3
|
+
export const DEFAULT_ASK_TIMEOUT_MS = 15_000;
|
|
4
|
+
const FALLBACK_MESSAGE = "TypeSafe request failed.";
|
|
5
|
+
/**
|
|
6
|
+
* One typed Jev request that never throws: a failure comes back as `{ ok: false }` with pi-typesafe's own message
|
|
7
|
+
* (which carries no upstream body, header, key, or submitted state) and its code, so a caller can stop asking after a
|
|
8
|
+
* `budget` error. The per-ask timeout is merged into the caller's signal, so either can cancel the request.
|
|
9
|
+
*
|
|
10
|
+
* This is the author-facing "ask Jev" seam. Agents get the same admission through the `typesafe_evaluate` tool; the two
|
|
11
|
+
* share `prepareEvaluationRequest`, so what one accepts the other accepts.
|
|
12
|
+
*/
|
|
13
|
+
export async function ask(judge, request, options = {}) {
|
|
14
|
+
const timeout = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS);
|
|
15
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
16
|
+
try {
|
|
17
|
+
const result = await judge.evaluate(request, { signal });
|
|
18
|
+
return { ok: true, answers: result.answers, model: result.model, usage: result.usage, elapsedMs: result.elapsedMs };
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof TypeSafeIntegrationError)
|
|
22
|
+
return { ok: false, error: error.message, errorCode: error.code };
|
|
23
|
+
return { ok: false, error: FALLBACK_MESSAGE };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { KeySource } from "./credentials.js";
|
|
2
|
+
import { TypeSafeIntegrationError } from "./errors.js";
|
|
3
|
+
import type { IntegrationErrorCode } from "./errors.js";
|
|
4
|
+
/** The last request that degraded TypeSafe, with no upstream body, header, key, or submitted state. */
|
|
5
|
+
export interface AuthFailure {
|
|
6
|
+
readonly code: IntegrationErrorCode;
|
|
7
|
+
readonly status?: number;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly at: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The whole answer to "is Jev actually available right now": which key is in effect, whether it has been accepted, and
|
|
13
|
+
* the last failure that degraded it. Consumers must consult this instead of treating their own consent flag as proof
|
|
14
|
+
* that judgments will happen — an enabled extension with no key used to look identical to a working one.
|
|
15
|
+
*/
|
|
16
|
+
export interface AuthState {
|
|
17
|
+
/** Same kinds as KeySituation: where the key in effect comes from. */
|
|
18
|
+
readonly kind: "environment" | "stored" | "missing" | "unusable";
|
|
19
|
+
readonly source?: KeySource;
|
|
20
|
+
/** Where the key would be read from. */
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** Why a stored key cannot be used, when that is the case. */
|
|
23
|
+
readonly reason?: string;
|
|
24
|
+
/** Short human label for the key source: `TYPESAFE_API_KEY`, `/typesafe login`, `no key`, `unusable key`. */
|
|
25
|
+
readonly keyName: string;
|
|
26
|
+
/** True when the key in effect was accepted by api.typesafe.ai (login verifies it; a successful request proves it). */
|
|
27
|
+
readonly verified: boolean;
|
|
28
|
+
readonly verifiedAt?: string;
|
|
29
|
+
/** The last failure, cleared by the next successful request. */
|
|
30
|
+
readonly lastFailure?: AuthFailure;
|
|
31
|
+
/** A key is present and the last authentication outcome was not a rejection. False means judgments are skipped. */
|
|
32
|
+
readonly usable: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** The auth-state file: one small, owner-only record that outlives the process that wrote it. */
|
|
35
|
+
export declare function authStatePath(): string;
|
|
36
|
+
/** What the key situation, the last outcome, and the clock add up to. Never throws. */
|
|
37
|
+
export declare function authState(options?: {
|
|
38
|
+
path?: string;
|
|
39
|
+
}): AuthState;
|
|
40
|
+
/** Record that the key was accepted: login verification, or any successful request. Clears the last failure. */
|
|
41
|
+
export declare function recordAuthVerified(at?: Date): void;
|
|
42
|
+
/** Record the failure that degraded TypeSafe. The verification timestamp is kept so a recovered key stays known. */
|
|
43
|
+
export declare function recordAuthFailure(error: TypeSafeIntegrationError, at?: Date): void;
|
|
44
|
+
/** Forget verification and degradation: used when the key itself changes (login or logout). */
|
|
45
|
+
export declare function clearAuthState(): void;
|
|
46
|
+
export interface AuthReport {
|
|
47
|
+
/** `error` when judgments are skipped or were rejected, `warning` when the key is unverified, otherwise `ok`. */
|
|
48
|
+
readonly level: "ok" | "warning" | "error";
|
|
49
|
+
/** One line naming the key source and, when degraded, the reason. Safe to display. */
|
|
50
|
+
readonly text: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One line plus a level, so a status command, a headless log, or a consumer's own status line can call out a degraded
|
|
54
|
+
* state instead of reporting "enabled".
|
|
55
|
+
*/
|
|
56
|
+
export declare function describeAuth(state?: AuthState): AuthReport;
|