@selesai/code 0.13.37 → 0.13.39
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/CHANGELOG.md +15 -0
- package/dist/extensions/jev/decisions.test.ts +18 -0
- package/dist/extensions/jev/decisions.ts +54 -6
- package/dist/extensions/jev-ask-tool.test.ts +371 -25
- package/dist/extensions/jev-ask-tool.ts +826 -167
- package/dist/extensions/jev-find-source.ts +190 -0
- package/dist/extensions/package.json +1 -1
- package/dist/extensions/pi-graft/cli.test.ts +5 -5
- package/dist/extensions/pi-graft/cli.ts +10 -5
- package/dist/extensions/pi-graft/commands.test.ts +5 -5
- package/dist/extensions/pi-graft/graft.integration.test.ts +1 -1
- package/dist/extensions/pi-graft/lifecycle.test.ts +26 -26
- package/dist/extensions/pi-graft/prompt.test.ts +1 -1
- package/dist/extensions/pi-graft/state.test.ts +9 -9
- package/dist/extensions/pi-graft/test-support.ts +1 -1
- package/dist/extensions/pi-graft/tools.test.ts +1 -1
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/docs/agents.md +1 -7
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +3 -4
- package/dist/extensions/pi-web-agent/src/extension.ts +11 -0
- package/dist/extensions/tokenin-onboarding.ts +138 -1
- package/docs/settings.md +8 -6
- package/package.json +1 -1
|
@@ -73,13 +73,7 @@ Native `oracle` runs inside Pi and can use its configured read tools. The Claude
|
|
|
73
73
|
| `external-job-requests/` and `external-job-responses/` | Host-mediated provider bridge | pending request, terminal response | Host process writes a matching response and removes the request | Bridge timeout or malformed request response | Requests are operation-scoped. Recovery sends `reattach`/`result`, not `start` or `follow-up`, when job metadata exists. `start` and `follow-up` use durable dispatch claims | Provider not registered, host bridge not loaded, malformed request, provider exception, ambiguous dispatch without a provider job id |
|
|
74
74
|
| Provider artifact path | External provider | provider-defined terminal artifact | Provider returns `artifactPath`, or Pi writes returned text to `external-job-<index>.result.md` | Provider reports failure or no result | Existing artifact path is retained in `status.json` | Missing artifact with no text output returns a terminal message instead of inventing content |
|
|
75
75
|
|
|
76
|
-
The `researcher` builtin uses `
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
pi install npm:pi-web-access
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The loaded provider must register all four tools, including `source_check`, before launch; a missing required tool prevents a successful run. Fetched-source inspection is a fallback for a registered `source_check` call failing, not for missing registration.
|
|
76
|
+
The `researcher` builtin uses `web_explore` from the bundled `pi-web-agent` extension. Because the `tools` field is a strict allowlist, `web_explore` must be registered in the child; run `researcher` as a background child (`async: true`) so ambient extensions load.
|
|
83
77
|
|
|
84
78
|
## Overriding builtins and custom agents
|
|
85
79
|
|
|
@@ -1904,7 +1904,7 @@ Do work
|
|
|
1904
1904
|
delegate: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
|
|
1905
1905
|
reviewer: ["read", "grep", "find", "ls", "contact_supervisor"],
|
|
1906
1906
|
scout: ["read", "grep", "find", "ls", "bash", "write", "contact_supervisor"],
|
|
1907
|
-
researcher: ["read", "write", "
|
|
1907
|
+
researcher: ["read", "write", "web_explore"],
|
|
1908
1908
|
};
|
|
1909
1909
|
for (const [name, tools] of Object.entries(expectedTools)) {
|
|
1910
1910
|
const agent = agents.find((candidate) => candidate.name === name);
|
|
@@ -1914,12 +1914,11 @@ Do work
|
|
|
1914
1914
|
|
|
1915
1915
|
const researcherPrompt = agents.find((candidate) => candidate.name === "researcher")?.systemPrompt ?? "";
|
|
1916
1916
|
assert.match(researcherPrompt, /search-result summaries as discovery aids, not final evidence/);
|
|
1917
|
-
assert.match(researcherPrompt, /
|
|
1917
|
+
assert.match(researcherPrompt, /Verify decision-critical or disputed claims/);
|
|
1918
1918
|
assert.match(researcherPrompt, /direct evidence, source interpretation, and researcher inference distinctly/);
|
|
1919
1919
|
assert.match(researcherPrompt, /Record contradictions.*Record missing evidence/);
|
|
1920
1920
|
assert.match(researcherPrompt, /Never invent dates, quotations, citations, or unsupported precision/);
|
|
1921
|
-
assert.match(researcherPrompt, /`
|
|
1922
|
-
assert.match(researcherPrompt, /If a registered `source_check` call fails, continue/);
|
|
1921
|
+
assert.match(researcherPrompt, /`web_explore`/);
|
|
1923
1922
|
assert.match(researcherPrompt, /\*\*Support:\*\* direct evidence \| interpretation\. \*\*Confidence:\*\* high \| medium \| low/);
|
|
1924
1923
|
} finally {
|
|
1925
1924
|
if (previousHome === undefined) delete process.env.HOME;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
1
2
|
import type { ExtensionAPI } from '@selesai/code';
|
|
2
3
|
import { getDefaultBackendConfig, type BackendConfig } from './backends/config.js';
|
|
3
4
|
import { Type } from 'typebox';
|
|
@@ -71,6 +72,16 @@ async function getEffectiveBackendConfig(pi: ExtensionAPI): Promise<BackendConfi
|
|
|
71
72
|
export default function extension(pi: ExtensionAPI) {
|
|
72
73
|
registerWebAgentConfigCommands(pi);
|
|
73
74
|
|
|
75
|
+
// Lets foreground `researcher` children load this extension (see pi-subagents builtin augmentations).
|
|
76
|
+
pi.events.on('pi-subagents:request-builtin-agent-augmentations', (request: unknown) => {
|
|
77
|
+
(request as { register?: (a: unknown) => void } | undefined)?.register?.({
|
|
78
|
+
id: 'pi-web-agent',
|
|
79
|
+
agentNames: ['researcher'],
|
|
80
|
+
addTools: ['web_explore'],
|
|
81
|
+
subagentOnlyExtensions: [fileURLToPath(import.meta.url)]
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
74
85
|
const injectedWebExplore = (pi as ExtensionAPI & { __webExploreTool?: ReturnType<typeof createWebExploreTool> }).__webExploreTool;
|
|
75
86
|
let cachedBackendKey: string | undefined;
|
|
76
87
|
let cachedWebExplore: ReturnType<typeof createWebExploreTool> | undefined;
|
|
@@ -791,9 +791,141 @@ export function formatTokenInUsage(usage: TokenInUsage): string {
|
|
|
791
791
|
].join("\n");
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
+
export interface TokenInBalanceBucket {
|
|
795
|
+
kind: string;
|
|
796
|
+
planId?: string;
|
|
797
|
+
amountUsd: number;
|
|
798
|
+
usedUsd: number;
|
|
799
|
+
remainingUsd: number;
|
|
800
|
+
state: "active" | "upcoming" | "expired";
|
|
801
|
+
expiresAt?: string;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export interface TokenInBalance {
|
|
805
|
+
availableUsd: number;
|
|
806
|
+
fixedAvailableUsd: number;
|
|
807
|
+
paygAvailableUsd: number;
|
|
808
|
+
debtUsd: number;
|
|
809
|
+
asOf?: string;
|
|
810
|
+
buckets: TokenInBalanceBucket[];
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const finiteNumber = (value: unknown): number | undefined => {
|
|
814
|
+
const n = typeof value === "number" ? value : Number.NaN;
|
|
815
|
+
return Number.isFinite(n) ? n : undefined;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
/** Parse a /tokenin/account/me/balance payload. Exported for tests. */
|
|
819
|
+
export function parseTokenInBalance(payload: unknown): TokenInBalance | undefined {
|
|
820
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
821
|
+
const obj = payload as Record<string, unknown>;
|
|
822
|
+
const availableUsd = finiteNumber(obj.available_usd);
|
|
823
|
+
if (availableUsd === undefined || !Array.isArray(obj.buckets)) return undefined;
|
|
824
|
+
const buckets: TokenInBalanceBucket[] = [];
|
|
825
|
+
for (const raw of obj.buckets) {
|
|
826
|
+
if (!raw || typeof raw !== "object") continue;
|
|
827
|
+
const b = raw as Record<string, unknown>;
|
|
828
|
+
const state = b.state;
|
|
829
|
+
if (state !== "active" && state !== "upcoming" && state !== "expired") continue;
|
|
830
|
+
const bucket: TokenInBalanceBucket = {
|
|
831
|
+
kind: typeof b.kind === "string" ? b.kind : "unknown",
|
|
832
|
+
amountUsd: finiteNumber(b.amount_usd) ?? 0,
|
|
833
|
+
usedUsd: finiteNumber(b.used_usd) ?? 0,
|
|
834
|
+
remainingUsd: finiteNumber(b.remaining_usd) ?? 0,
|
|
835
|
+
state,
|
|
836
|
+
};
|
|
837
|
+
if (typeof b.plan_id === "string") bucket.planId = b.plan_id;
|
|
838
|
+
if (typeof b.expires_at === "string") bucket.expiresAt = b.expires_at;
|
|
839
|
+
buckets.push(bucket);
|
|
840
|
+
}
|
|
841
|
+
const balance: TokenInBalance = {
|
|
842
|
+
availableUsd,
|
|
843
|
+
fixedAvailableUsd: finiteNumber(obj.fixed_available_usd) ?? 0,
|
|
844
|
+
paygAvailableUsd: finiteNumber(obj.payg_available_usd) ?? 0,
|
|
845
|
+
debtUsd: finiteNumber(obj.debt_usd) ?? 0,
|
|
846
|
+
buckets,
|
|
847
|
+
};
|
|
848
|
+
if (typeof obj.as_of === "string") balance.asOf = obj.as_of;
|
|
849
|
+
return balance;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function formatTimeUntil(iso: string): string | undefined {
|
|
853
|
+
const d = new Date(iso);
|
|
854
|
+
if (Number.isNaN(d.getTime())) return undefined;
|
|
855
|
+
const ms = d.getTime() - Date.now();
|
|
856
|
+
if (ms <= 0) return undefined;
|
|
857
|
+
const days = Math.floor(ms / 86_400_000);
|
|
858
|
+
const hours = Math.floor((ms % 86_400_000) / 3_600_000);
|
|
859
|
+
return days > 0 ? `${days}d ${hours}h` : `${Math.max(1, Math.ceil(ms / 3_600_000))}h`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Format an account balance as a boxed display. Exported for tests. */
|
|
863
|
+
export function formatTokenInBalance(balance: TokenInBalance): string {
|
|
864
|
+
const money = (v: number) => `$${v.toFixed(2)}`;
|
|
865
|
+
const active = balance.buckets.filter((b) => b.state === "active");
|
|
866
|
+
const total = active.reduce((sum, b) => sum + b.amountUsd, 0);
|
|
867
|
+
const used = active.reduce((sum, b) => sum + b.usedUsd, 0);
|
|
868
|
+
const pct = total > 0 ? Math.min(100, (used / total) * 100) : 0;
|
|
869
|
+
const barWidth = 40;
|
|
870
|
+
const filled = total > 0 ? Math.min(barWidth, Math.round((pct / 100) * barWidth)) : 0;
|
|
871
|
+
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
|
872
|
+
const pctText = total > 0 ? `${pct.toFixed(1)}%` : "—";
|
|
873
|
+
|
|
874
|
+
const summary = `Available ${money(balance.availableUsd)}`;
|
|
875
|
+
let split = `Plan ${money(balance.fixedAvailableUsd)} · PAYG ${money(balance.paygAvailableUsd)}`;
|
|
876
|
+
if (balance.debtUsd > 0) split += ` · Debt ${money(balance.debtUsd)}`;
|
|
877
|
+
const usedLine = total > 0 ? `Used ${money(used)} of ${money(total)} active credit` : "No active credit";
|
|
878
|
+
|
|
879
|
+
const expiries = active
|
|
880
|
+
.map((b) => b.expiresAt)
|
|
881
|
+
.filter((v): v is string => typeof v === "string")
|
|
882
|
+
.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
|
883
|
+
const until = expiries.length > 0 ? formatTimeUntil(expiries[0]) : undefined;
|
|
884
|
+
const footer = until ? `Plan credit expires in ${until}` : "";
|
|
885
|
+
|
|
886
|
+
const W = 50;
|
|
887
|
+
const inner = W - 4;
|
|
888
|
+
const pad = (s: string) => s + " ".repeat(Math.max(0, inner - s.length));
|
|
889
|
+
const title = "╭─ Token-In balance ";
|
|
890
|
+
const top = `${title}${"─".repeat(W - title.length - 1)}╮`;
|
|
891
|
+
const bottom = `╰${"─".repeat(W - 2)}╯`;
|
|
892
|
+
const lines = [top, `│ ${pad(summary)} │`, `│ ${pad(split)} │`, `│ ${pad(usedLine)} │`, `│ ${bar} ${pctText.padStart(5)} │`];
|
|
893
|
+
if (footer) lines.push(`│ ${pad(footer)} │`);
|
|
894
|
+
lines.push(bottom);
|
|
895
|
+
return lines.join("\n");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function tokenInProxyRoot(baseUrl: string): string {
|
|
899
|
+
return baseUrl.replace(/\/v1\/?$/, "");
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Query the self-scoped account balance with the user's own key. Returns undefined when
|
|
904
|
+
* the gateway has no account balance for this key (404: older gateway, accounts disabled,
|
|
905
|
+
* or a legacy key) or answers with something that is not a balance, so callers can fall
|
|
906
|
+
* back to /key/info. Other errors throw. Exported for tests.
|
|
907
|
+
*/
|
|
908
|
+
export async function fetchTokenInBalance(apiKey: string, baseUrl: string): Promise<TokenInBalance | undefined> {
|
|
909
|
+
const response = await fetch(`${tokenInProxyRoot(baseUrl)}/tokenin/account/me/balance`, {
|
|
910
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
911
|
+
signal: AbortSignal.timeout(5_000),
|
|
912
|
+
});
|
|
913
|
+
if (response.status === 404) return undefined;
|
|
914
|
+
if (!response.ok) {
|
|
915
|
+
throw new Error(`Token-In usage endpoint returned HTTP ${response.status}`);
|
|
916
|
+
}
|
|
917
|
+
let payload: unknown;
|
|
918
|
+
try {
|
|
919
|
+
payload = await response.json();
|
|
920
|
+
} catch {
|
|
921
|
+
return undefined;
|
|
922
|
+
}
|
|
923
|
+
return parseTokenInBalance(payload);
|
|
924
|
+
}
|
|
925
|
+
|
|
794
926
|
/** Query LiteLLM /key/info at the proxy root with the account key. Exported for tests. */
|
|
795
927
|
export async function fetchTokenInUsage(apiKey: string, baseUrl: string): Promise<TokenInUsage> {
|
|
796
|
-
const root = baseUrl
|
|
928
|
+
const root = tokenInProxyRoot(baseUrl);
|
|
797
929
|
const response = await fetch(`${root}/key/info`, {
|
|
798
930
|
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
799
931
|
signal: AbortSignal.timeout(5_000),
|
|
@@ -906,6 +1038,11 @@ async function handleTokenInUsage(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
906
1038
|
const account = auth.accounts.find((a) => a.id === activeId);
|
|
907
1039
|
const baseUrl = account?.baseUrl ?? getTokenInBaseUrl(readModelsJson()) ?? TOKEN_IN_DEFAULT_BASE_URL;
|
|
908
1040
|
try {
|
|
1041
|
+
const balance = await fetchTokenInBalance(apiKey, baseUrl);
|
|
1042
|
+
if (balance) {
|
|
1043
|
+
ctx.ui.notify(formatTokenInBalance(balance), "info");
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
909
1046
|
const usage = await fetchTokenInUsage(apiKey, baseUrl);
|
|
910
1047
|
ctx.ui.notify(formatTokenInUsage(usage), "info");
|
|
911
1048
|
} catch (err) {
|
package/docs/settings.md
CHANGED
|
@@ -67,12 +67,14 @@ routing. The host-side routes stay off until they are enabled in `jevAdvisory`;
|
|
|
67
67
|
of the agent's loadout before each run (it returns after `/tokenin add`, no reload needed), and a call
|
|
68
68
|
that slips through reads no file and runs no command.
|
|
69
69
|
|
|
70
|
-
The same route also offers `jev_find`, a
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
The same route also offers `jev_find`, a behavior finder: ripgrep gathers candidate files (those
|
|
71
|
+
matching `pattern`, or every file under `path` filtered by `glob`); on large trees Jev first judges
|
|
72
|
+
directories and descends only into relevant ones, then judges files, then the declarations (source
|
|
73
|
+
units) inside relevant files. The agent gets a ranked file list with reading leads plus the accepted
|
|
74
|
+
units verbatim with original line numbers (at most 12 Jev requests and 16 KiB of source per call).
|
|
75
|
+
It respects `.gitignore`, stays inside the working directory, skips secret-named files, retries a
|
|
76
|
+
transport failure once, and still returns ripgrep-ranked candidates with keyword-window source when
|
|
77
|
+
Jev is unavailable.
|
|
76
78
|
- `subagent` — sets up a single-child `subagent` call from the parent agent (off by default). Jev only
|
|
77
79
|
fills what the parent left open, and can only narrow: (1) when `agent` is omitted or generic
|
|
78
80
|
(`genericAgents`, default `["delegate"]`), it picks an agent by function from enabled native agents
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.39",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|