@selesai/code 0.13.38 → 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 +7 -0
- 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/tokenin-onboarding.ts +138 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.39] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **`/tokenin usage` shows the account balance.** It queries `/tokenin/account/me/balance` and shows available credit (plan, PAYG, debt), active plan credit used, and plan expiry; a 404 falls back to the LiteLLM `/key/info` quota view.
|
|
9
|
+
- **The `researcher` subagent uses `web_explore`** from the bundled `pi-web-agent` extension instead of `pi-web-access` tools; `pi-web-agent` registers a builtin-agent augmentation so foreground `researcher` children load it.
|
|
10
|
+
- **Graft CLI floor raised to 0.21** (`MIN_GRAFT_VERSION` 0.21.0, install spec `@nanonets/graft@^0.21`).
|
|
11
|
+
|
|
5
12
|
## [0.13.38] - 2026-10-01
|
|
6
13
|
|
|
7
14
|
### Changed
|
|
@@ -68,8 +68,8 @@ describe("installGraft", () => {
|
|
|
68
68
|
it("runs the compatible global npm install without a shell off Windows", async () => {
|
|
69
69
|
const { pi } = makePi(async () => execReturning(OK));
|
|
70
70
|
const run = await installGraft(pi.exec as never, "/repo");
|
|
71
|
-
expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.
|
|
72
|
-
expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.
|
|
71
|
+
expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.21");
|
|
72
|
+
expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.21");
|
|
73
73
|
const [command, args, options] = pi.exec.mock.calls[0] as [string, string[], { cwd: string; timeout: number }];
|
|
74
74
|
expect(command).toBe(process.platform === "win32" ? "cmd" : "npm");
|
|
75
75
|
expect(args).toContain("install");
|
|
@@ -118,7 +118,7 @@ describe("version handling", () => {
|
|
|
118
118
|
|
|
119
119
|
it("accepts the verified range and rejects the boundaries", () => {
|
|
120
120
|
expect(isCompatibleVersion(MIN_GRAFT_VERSION)).toBe(true);
|
|
121
|
-
expect(isCompatibleVersion("0.
|
|
121
|
+
expect(isCompatibleVersion("0.21.1")).toBe(true);
|
|
122
122
|
expect(isCompatibleVersion("0.99.0")).toBe(true);
|
|
123
123
|
expect(isCompatibleVersion("0.17.9")).toBe(false);
|
|
124
124
|
expect(isCompatibleVersion(MAX_GRAFT_VERSION_EXCLUSIVE)).toBe(false);
|
|
@@ -128,9 +128,9 @@ describe("version handling", () => {
|
|
|
128
128
|
|
|
129
129
|
describe("probeGraft", () => {
|
|
130
130
|
it("accepts a supported version and reports it", async () => {
|
|
131
|
-
const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.
|
|
131
|
+
const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.21.1\n" }));
|
|
132
132
|
const probe = await probeGraft(pi.exec as never, "/repo");
|
|
133
|
-
expect(probe).toMatchObject({ kind: "ok", version: "0.
|
|
133
|
+
expect(probe).toMatchObject({ kind: "ok", version: "0.21.1" });
|
|
134
134
|
if (probe.kind === "ok") expect(probe.argv.at(-1)).toBe("--version");
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Selesai model for deep enrichment, otherwise stay structural and local.
|
|
16
16
|
* - Extension-spawned commands default to telemetry opt-out (`DO_NOT_TRACK=1`).
|
|
17
17
|
*
|
|
18
|
-
* Verified against @nanonets/graft 0.
|
|
18
|
+
* Verified against @nanonets/graft 0.21.1 (see {@link MIN_GRAFT_VERSION}).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { truncateHead, type ExecOptions, type ExecResult } from "@selesai/code";
|
|
@@ -27,8 +27,13 @@ export const GRAFT_PACKAGE = "@nanonets/graft";
|
|
|
27
27
|
* Lowest Graft CLI whose documented surface this adapter was verified against:
|
|
28
28
|
* `ask --source/--json`, `skeleton`, `callers --direction/-d`, `grep --in/-i/--fixed`,
|
|
29
29
|
* `map --max-dirs`, `check --json`, `build [--deep]`, and `--version`.
|
|
30
|
+
*
|
|
31
|
+
* The floor sits on the 0.21 minor line this adapter is verified against
|
|
32
|
+
* (0.21.1). Upgrades are deliberate: an executable below the floor reports
|
|
33
|
+
* `incompatible`, and the adapter never overwrites it — the user upgrades with
|
|
34
|
+
* {@link INSTALL_COMMAND}.
|
|
30
35
|
*/
|
|
31
|
-
export const MIN_GRAFT_VERSION = "0.
|
|
36
|
+
export const MIN_GRAFT_VERSION = "0.21.0";
|
|
32
37
|
/** Upper bound is exclusive: a 1.x CLI is allowed to change its contract. */
|
|
33
38
|
export const MAX_GRAFT_VERSION_EXCLUSIVE = "1.0.0";
|
|
34
39
|
|
|
@@ -45,8 +50,8 @@ export const DEEP_BUILD_TIMEOUT_MS = 1_800_000;
|
|
|
45
50
|
/** Global npm installs can be slow on a cold cache. */
|
|
46
51
|
export const INSTALL_TIMEOUT_MS = 180_000;
|
|
47
52
|
|
|
48
|
-
/** Kept on the verified 0.
|
|
49
|
-
export const GRAFT_INSTALL_SPEC = `${GRAFT_PACKAGE}@^0.
|
|
53
|
+
/** Kept on the verified 0.21 minor line; never install a floating latest CLI. */
|
|
54
|
+
export const GRAFT_INSTALL_SPEC = `${GRAFT_PACKAGE}@^0.21`;
|
|
50
55
|
export const INSTALL_COMMAND = `npm install -g ${GRAFT_INSTALL_SPEC}`;
|
|
51
56
|
export const TELEMETRY_ENV = "DO_NOT_TRACK";
|
|
52
57
|
|
|
@@ -399,7 +404,7 @@ export interface AskHit {
|
|
|
399
404
|
}
|
|
400
405
|
|
|
401
406
|
/**
|
|
402
|
-
* `graft ask --json` result, as emitted by `AskResult` upstream (0.
|
|
407
|
+
* `graft ask --json` result, as emitted by `AskResult` upstream (0.21.1).
|
|
403
408
|
*
|
|
404
409
|
* Every field is optional on read: a CLI that changes shape degrades to raw
|
|
405
410
|
* text rather than to a wrong summary, and callers must never treat the parse
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
import { MODE_ENTRY_TYPE, type GraftState } from "./state.ts";
|
|
16
16
|
import { notified, makeCtx, makePi, type CtxStub } from "./test-support.ts";
|
|
17
17
|
|
|
18
|
-
const HEALTHY: GraftState = { name: "fresh-structural", version: "0.
|
|
18
|
+
const HEALTHY: GraftState = { name: "fresh-structural", version: "0.21.1" };
|
|
19
19
|
|
|
20
20
|
function installOf(overrides: Partial<GraftInstallRun> = {}): GraftInstallRun {
|
|
21
21
|
return {
|
|
22
|
-
argv: ["npm", "install", "-g", "@nanonets/graft@^0.
|
|
22
|
+
argv: ["npm", "install", "-g", "@nanonets/graft@^0.21"],
|
|
23
23
|
cwd: "/repo",
|
|
24
24
|
code: 0,
|
|
25
25
|
stdout: "",
|
|
@@ -75,7 +75,7 @@ function makeRuntime(
|
|
|
75
75
|
const recheck = vi.fn(async () => {});
|
|
76
76
|
const installCli = vi.fn(async () => {
|
|
77
77
|
const result = overrides.installResult ?? installOf();
|
|
78
|
-
if (result.code === 0 && !result.killed) state = overrides.postInstallState ?? { name: "unbuilt", version: "0.
|
|
78
|
+
if (result.code === 0 && !result.killed) state = overrides.postInstallState ?? { name: "unbuilt", version: "0.21.1" };
|
|
79
79
|
return result;
|
|
80
80
|
});
|
|
81
81
|
const runBuild = vi.fn(async () => overrides.buildResult ?? runOf());
|
|
@@ -134,7 +134,7 @@ describe("formatDoctor", () => {
|
|
|
134
134
|
it("reports repository, executable, graph, state, mode, telemetry and provider boundary", () => {
|
|
135
135
|
const text = formatDoctor(base);
|
|
136
136
|
expect(text).toContain("/repo (git ✓)");
|
|
137
|
-
expect(text).toContain("graft 0.
|
|
137
|
+
expect(text).toContain("graft 0.21.1");
|
|
138
138
|
expect(text).toContain("structural only");
|
|
139
139
|
expect(text).toContain("fresh-structural");
|
|
140
140
|
expect(text).toContain("nothing is injected");
|
|
@@ -188,7 +188,7 @@ describe("formatStatus", () => {
|
|
|
188
188
|
providerEnv: [],
|
|
189
189
|
});
|
|
190
190
|
expect(text.split("\n")).toHaveLength(4);
|
|
191
|
-
expect(text).toContain("graft: ● structural v0.
|
|
191
|
+
expect(text).toContain("graft: ● structural v0.21.1");
|
|
192
192
|
expect(text).toContain("hybrid");
|
|
193
193
|
expect(text).toContain("root: /repo");
|
|
194
194
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* adapter against the real executable, and it skips itself when no compatible
|
|
7
7
|
* Graft CLI is on PATH — which is the state of ordinary CI.
|
|
8
8
|
*
|
|
9
|
-
* To run it for real: `npm install -g @nanonets/graft@^0.
|
|
9
|
+
* To run it for real: `npm install -g @nanonets/graft@^0.21` then
|
|
10
10
|
* npx vitest run src/extensions/pi-graft/graft.integration.test.ts
|
|
11
11
|
*
|
|
12
12
|
* A structural build is tree-sitter only: no model, no API key, no network. The
|
|
@@ -39,7 +39,7 @@ function writeProjectSettings(value: unknown): void {
|
|
|
39
39
|
function exec(graftOutput = ASK_JSON) {
|
|
40
40
|
return async (command: string, args: string[]) => {
|
|
41
41
|
if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${repo}\n` };
|
|
42
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
42
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
43
43
|
return { ...OK, stdout: graftOutput };
|
|
44
44
|
};
|
|
45
45
|
}
|
|
@@ -88,7 +88,7 @@ describe("session start", () => {
|
|
|
88
88
|
it("resolves the Git working tree, probes the executable, and publishes a status line", async () => {
|
|
89
89
|
const session = await bootSession();
|
|
90
90
|
expect(session.commandsRun()).toEqual(["git rev-parse --show-toplevel", "graft --version"]);
|
|
91
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
91
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1");
|
|
92
92
|
await session.reset();
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -163,7 +163,7 @@ describe("session start", () => {
|
|
|
163
163
|
}
|
|
164
164
|
if (args.includes("--version")) {
|
|
165
165
|
if (!installed) throw new Error("spawn graft ENOENT");
|
|
166
|
-
return { ...OK, stdout: "0.
|
|
166
|
+
return { ...OK, stdout: "0.21.1\n" };
|
|
167
167
|
}
|
|
168
168
|
return { ...OK };
|
|
169
169
|
});
|
|
@@ -171,8 +171,8 @@ describe("session start", () => {
|
|
|
171
171
|
const ctx = makeCtx({ cwd: repo });
|
|
172
172
|
const start = handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>;
|
|
173
173
|
await start({ type: "session_start", reason: "startup" }, ctx);
|
|
174
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
175
|
-
expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.
|
|
174
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
175
|
+
expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.21");
|
|
176
176
|
|
|
177
177
|
await start({ type: "session_start", reason: "reload" }, ctx);
|
|
178
178
|
expect(harness.commandsRun().filter((command) => command.startsWith("npm install"))).toHaveLength(1);
|
|
@@ -202,7 +202,7 @@ describe("session start", () => {
|
|
|
202
202
|
writeProjectSettings({});
|
|
203
203
|
const harness = makePi(async (command: string) => {
|
|
204
204
|
if (command === "git") return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
|
|
205
|
-
return { ...OK, stdout: "0.
|
|
205
|
+
return { ...OK, stdout: "0.21.1\n" };
|
|
206
206
|
});
|
|
207
207
|
graftExtension(harness.pi as never);
|
|
208
208
|
const ctx = makeCtx({ cwd: repo });
|
|
@@ -233,14 +233,14 @@ describe("mutation awareness", () => {
|
|
|
233
233
|
it("marks the graph changed after a successful edit", async () => {
|
|
234
234
|
const session = await bootSession();
|
|
235
235
|
await onToolResult(session, "edit", { path: "src/a.ts" });
|
|
236
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.
|
|
236
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
|
|
237
237
|
await session.reset();
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
it("marks the graph changed after a clearly mutating bash command", async () => {
|
|
241
241
|
const session = await bootSession();
|
|
242
242
|
await onToolResult(session, "bash", { command: "rm -rf dist" });
|
|
243
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.
|
|
243
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
|
|
244
244
|
await session.reset();
|
|
245
245
|
});
|
|
246
246
|
|
|
@@ -249,7 +249,7 @@ describe("mutation awareness", () => {
|
|
|
249
249
|
await onToolResult(session, "read", { path: "src/a.ts" });
|
|
250
250
|
await onToolResult(session, "bash", { command: "git status" });
|
|
251
251
|
await onToolResult(session, "edit", { path: "src/a.ts" }, true);
|
|
252
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
252
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1");
|
|
253
253
|
await session.reset();
|
|
254
254
|
});
|
|
255
255
|
});
|
|
@@ -270,7 +270,7 @@ describe("proactive refresh", () => {
|
|
|
270
270
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
271
271
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
272
272
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
273
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
273
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
274
274
|
if (args.includes("build")) {
|
|
275
275
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
276
276
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -283,7 +283,7 @@ describe("proactive refresh", () => {
|
|
|
283
283
|
{ type: "session_start", reason: "startup" },
|
|
284
284
|
ctx,
|
|
285
285
|
);
|
|
286
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
286
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
287
287
|
expect(harness.commandsRun()).toContain("graft build");
|
|
288
288
|
});
|
|
289
289
|
|
|
@@ -294,7 +294,7 @@ describe("proactive refresh", () => {
|
|
|
294
294
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
295
295
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
296
296
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
297
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
297
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
298
298
|
if (args.includes("build")) {
|
|
299
299
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
300
300
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -314,7 +314,7 @@ describe("proactive refresh", () => {
|
|
|
314
314
|
{ type: "session_start", reason: "startup" },
|
|
315
315
|
ctx,
|
|
316
316
|
);
|
|
317
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.
|
|
317
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.21.1"));
|
|
318
318
|
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
319
319
|
expect(build[1]).toEqual(["build", "--deep"]);
|
|
320
320
|
expect(build[2]).toMatchObject({
|
|
@@ -335,7 +335,7 @@ describe("proactive refresh", () => {
|
|
|
335
335
|
const models: string[] = [];
|
|
336
336
|
const harness = makePi(async (command: string, args: string[], options?: unknown) => {
|
|
337
337
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
338
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
338
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
339
339
|
if (args.includes("build")) {
|
|
340
340
|
const model = (options as { env?: Record<string, string> }).env?.GRAFT_MODEL ?? "";
|
|
341
341
|
models.push(model);
|
|
@@ -359,7 +359,7 @@ describe("proactive refresh", () => {
|
|
|
359
359
|
{ type: "session_start", reason: "startup" },
|
|
360
360
|
ctx,
|
|
361
361
|
);
|
|
362
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.
|
|
362
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.21.1"));
|
|
363
363
|
expect(models).toEqual(["deepseek-v4.1-flash", "glm-5.3-flash"]);
|
|
364
364
|
expect(notified(ctx)).toContain("retrying the deep pass");
|
|
365
365
|
});
|
|
@@ -371,7 +371,7 @@ describe("proactive refresh", () => {
|
|
|
371
371
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
372
372
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
373
373
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
374
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
374
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
375
375
|
if (args.includes("build")) {
|
|
376
376
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
377
377
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -388,7 +388,7 @@ describe("proactive refresh", () => {
|
|
|
388
388
|
{ type: "session_start", reason: "startup" },
|
|
389
389
|
ctx,
|
|
390
390
|
);
|
|
391
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
391
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
392
392
|
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
393
393
|
expect(build[1]).toEqual(["build"]);
|
|
394
394
|
});
|
|
@@ -403,11 +403,11 @@ describe("proactive refresh", () => {
|
|
|
403
403
|
{ type: "tool_result", toolName: "write", toolCallId: "c1", input: {}, content: [], isError: false },
|
|
404
404
|
session.ctx,
|
|
405
405
|
);
|
|
406
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.
|
|
406
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
|
|
407
407
|
|
|
408
408
|
await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
|
|
409
|
-
expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.
|
|
410
|
-
await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.
|
|
409
|
+
expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.21.1");
|
|
410
|
+
await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
|
|
411
411
|
expect(session.commandsRun()).toContain("graft build");
|
|
412
412
|
await session.reset();
|
|
413
413
|
});
|
|
@@ -434,7 +434,7 @@ describe("proactive refresh", () => {
|
|
|
434
434
|
writeProjectSettings({ refreshDebounceSeconds: 0 });
|
|
435
435
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
436
436
|
if (command === "git") return { ...OK, stdout: `${repo}\n` };
|
|
437
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
437
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
438
438
|
if (args.includes("build")) return { code: 1, stdout: "", stderr: "✗ index failed", killed: false };
|
|
439
439
|
return { ...OK, stdout: ASK_JSON };
|
|
440
440
|
});
|
|
@@ -449,7 +449,7 @@ describe("proactive refresh", () => {
|
|
|
449
449
|
ctx,
|
|
450
450
|
);
|
|
451
451
|
await (handlerFor(harness, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, ctx);
|
|
452
|
-
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.
|
|
452
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.21.1"));
|
|
453
453
|
});
|
|
454
454
|
});
|
|
455
455
|
|
|
@@ -541,7 +541,7 @@ describe("retrieval strategy", () => {
|
|
|
541
541
|
writeProjectSettings({ mode: "push" });
|
|
542
542
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
543
543
|
if (command === "git") return { ...OK, stdout: `${repo}\n` };
|
|
544
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
544
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
545
545
|
return { code: 1, stdout: "", stderr: "✗ graph missing", killed: false };
|
|
546
546
|
});
|
|
547
547
|
graftExtension(harness.pi as never);
|
|
@@ -565,7 +565,7 @@ describe("retrieval strategy", () => {
|
|
|
565
565
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: { mode: "push" } }), "utf-8");
|
|
566
566
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
567
567
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
568
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
568
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
569
569
|
if (args.includes("build")) {
|
|
570
570
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
571
571
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -642,7 +642,7 @@ describe("tool readiness", () => {
|
|
|
642
642
|
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
643
643
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
644
644
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
645
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
645
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
646
646
|
if (args.includes("build")) {
|
|
647
647
|
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
648
648
|
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
@@ -671,7 +671,7 @@ describe("tool readiness", () => {
|
|
|
671
671
|
gitCalls += 1;
|
|
672
672
|
return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
|
|
673
673
|
}
|
|
674
|
-
if (args.includes("--version")) return { ...OK, stdout: "0.
|
|
674
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
|
|
675
675
|
return { ...OK, stdout: ASK_JSON };
|
|
676
676
|
});
|
|
677
677
|
graftExtension(harness.pi as never);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./prompt.ts";
|
|
14
14
|
import { DEFAULT_MIN_COVERAGE, type GraftState } from "./state.ts";
|
|
15
15
|
|
|
16
|
-
const FRESH: GraftState = { name: "fresh-structural", version: "0.
|
|
16
|
+
const FRESH: GraftState = { name: "fresh-structural", version: "0.21.1" };
|
|
17
17
|
|
|
18
18
|
function payload(overrides: Partial<AskPayload> = {}): AskPayload {
|
|
19
19
|
return {
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
} from "./state.ts";
|
|
23
23
|
|
|
24
24
|
const UNBUILT: GraftState = { name: "unbuilt", detail: "no graph yet" };
|
|
25
|
-
const FRESH: GraftState = { name: "fresh-structural", version: "0.
|
|
26
|
-
const DEEP: GraftState = { name: "fresh-deep", version: "0.
|
|
25
|
+
const FRESH: GraftState = { name: "fresh-structural", version: "0.21.1", deep: false };
|
|
26
|
+
const DEEP: GraftState = { name: "fresh-deep", version: "0.21.1", deep: true };
|
|
27
27
|
|
|
28
28
|
describe("reduceGraftState", () => {
|
|
29
29
|
it("only claims a repository once root resolution says there is a Git working tree", () => {
|
|
@@ -36,16 +36,16 @@ describe("reduceGraftState", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("derives built/fresh/deep from what is actually on disk", () => {
|
|
39
|
-
expect(reduceGraftState(gitState(), { type: "probe-ok", version: "0.
|
|
39
|
+
expect(reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: ABSENT_GRAPH })).toEqual({
|
|
40
40
|
name: "unbuilt",
|
|
41
|
-
version: "0.
|
|
41
|
+
version: "0.21.1",
|
|
42
42
|
detail: expect.stringContaining("/graft build"),
|
|
43
43
|
});
|
|
44
44
|
expect(
|
|
45
|
-
reduceGraftState(gitState(), { type: "probe-ok", version: "0.
|
|
45
|
+
reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: { built: true, deep: false } }),
|
|
46
46
|
).toEqual(FRESH);
|
|
47
47
|
expect(
|
|
48
|
-
reduceGraftState(gitState(), { type: "probe-ok", version: "0.
|
|
48
|
+
reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: { built: true, deep: true } }),
|
|
49
49
|
).toMatchObject({ name: "fresh-deep", deep: true });
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -91,7 +91,7 @@ describe("reduceGraftState", () => {
|
|
|
91
91
|
expect(reduceGraftState(FRESH, { type: "build-started", deep: true })).toMatchObject({
|
|
92
92
|
name: "building",
|
|
93
93
|
deep: true,
|
|
94
|
-
version: "0.
|
|
94
|
+
version: "0.21.1",
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
@@ -140,8 +140,8 @@ describe("state predicates and rendering", () => {
|
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it("renders a compact status line with the version when it is known", () => {
|
|
143
|
-
expect(statusText(FRESH)).toBe("graft: ● structural v0.
|
|
144
|
-
expect(statusText(DEEP)).toBe("graft: ● deep v0.
|
|
143
|
+
expect(statusText(FRESH)).toBe("graft: ● structural v0.21.1");
|
|
144
|
+
expect(statusText(DEEP)).toBe("graft: ● deep v0.21.1");
|
|
145
145
|
expect(statusText({ name: "unavailable" })).toBe("graft: ✕ no cli");
|
|
146
146
|
expect(statusText({ name: "changed" })).toBe("graft: ◐ changed");
|
|
147
147
|
});
|
|
@@ -190,7 +190,7 @@ export function makeCtx(
|
|
|
190
190
|
*/
|
|
191
191
|
export function healthyExec(overrides: { gitRoot?: string; version?: string; queryOutput?: string } = {}): ExecImpl {
|
|
192
192
|
const gitRoot = overrides.gitRoot ?? "/repo";
|
|
193
|
-
const version = overrides.version ?? "0.
|
|
193
|
+
const version = overrides.version ?? "0.21.1";
|
|
194
194
|
return async (command, args) => {
|
|
195
195
|
if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${gitRoot}\n` };
|
|
196
196
|
if (args.includes("--version")) return { ...OK, stdout: `${version}\n` };
|
|
@@ -64,7 +64,7 @@ function makeHarness(
|
|
|
64
64
|
const runtime: GraftToolRuntime = {
|
|
65
65
|
run: run as never,
|
|
66
66
|
settings: () => options.settings ?? {},
|
|
67
|
-
state: () => options.state ?? { name: "fresh-structural", version: "0.
|
|
67
|
+
state: () => options.state ?? { name: "fresh-structural", version: "0.21.1" },
|
|
68
68
|
noteQuery: ((_ctx: unknown, result: GraftRun) => {
|
|
69
69
|
noted.push(result);
|
|
70
70
|
}) as never,
|
|
@@ -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/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": {
|