@selesai/code 0.13.23 → 0.13.24
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 +11 -0
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.js +1 -0
- package/dist/extensions/pi-graft/cli.test.ts +23 -0
- package/dist/extensions/pi-graft/cli.ts +6 -5
- package/dist/extensions/pi-graft/commands.test.ts +16 -14
- package/dist/extensions/pi-graft/commands.ts +17 -17
- package/dist/extensions/pi-graft/index.ts +114 -14
- package/dist/extensions/pi-graft/lifecycle.test.ts +54 -10
- package/dist/extensions/pi-graft/state.test.ts +2 -2
- package/dist/extensions/pi-graft/state.ts +1 -1
- package/dist/extensions/pi-graft/tools.ts +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.24] - 2026-09-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Self-building Graft graph.** Graft now creates the initial graph itself instead of waiting for a manual build: the first session start, the first read-only tool call, and a completed CLI install each trigger one build for the repository. A compatible active Selesai model (TokenIn, LiteLLM, OpenAI, Anthropic, or OrcaRouter) adds the deep tier; otherwise the build stays local and structural.
|
|
9
|
+
- **Active-model deep builds.** Deep enrichment now runs on the active Selesai model instead of Graft's own provider configuration. The credential is resolved per build and passed only to the Graft child process, which never inherits it through the host environment.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Graft's default retrieval mode is `hybrid`.** Eligible coding turns start from a smaller orientation pack while precise Graft tools stay available; set `graft.mode: "push"` for the previous full-pack behavior.
|
|
13
|
+
- **Deep is the default build.** `/graft build` and `/graft setup` now run the provider-backed deep build, with the new `/graft build --structural` as the explicit local, offline escape hatch.
|
|
14
|
+
- **Graft doctor and recovery hints.** Provider reporting now describes active-model deep builds and the child-process credential boundary instead of asking for Graft's own provider environment variables.
|
|
15
|
+
|
|
5
16
|
## [0.13.23] - 2026-09-13
|
|
6
17
|
|
|
7
18
|
### Added
|
package/dist/core/exec.d.ts
CHANGED
package/dist/core/exec.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { execCommand } from "../../core/exec.ts";
|
|
2
3
|
import {
|
|
3
4
|
applyTelemetryDefault,
|
|
4
5
|
askReferences,
|
|
@@ -32,6 +33,19 @@ function execReturning(result: StubExecResult) {
|
|
|
32
33
|
return { ...result };
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
describe("execCommand environment", () => {
|
|
37
|
+
it("passes a supplied credential only to its child process", async () => {
|
|
38
|
+
const result = await execCommand(
|
|
39
|
+
process.execPath,
|
|
40
|
+
["-e", "process.stdout.write(process.env.GRAFT_TEST_TOKEN ?? '')"],
|
|
41
|
+
process.cwd(),
|
|
42
|
+
{ env: { ...process.env, GRAFT_TEST_TOKEN: "child-only" } },
|
|
43
|
+
);
|
|
44
|
+
expect(result).toMatchObject({ code: 0, stdout: "child-only" });
|
|
45
|
+
expect(process.env.GRAFT_TEST_TOKEN).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
35
49
|
describe("graftCommand", () => {
|
|
36
50
|
it("invokes the executable directly off Windows", () => {
|
|
37
51
|
const original = process.platform;
|
|
@@ -153,6 +167,15 @@ describe("probeGraft", () => {
|
|
|
153
167
|
});
|
|
154
168
|
});
|
|
155
169
|
|
|
170
|
+
describe("runGraft", () => {
|
|
171
|
+
it("forwards a private provider environment only to the Graft child", async () => {
|
|
172
|
+
const { pi } = makePi(async () => execReturning(OK));
|
|
173
|
+
const env = { ...process.env, GRAFT_PROVIDER: "litellm", GRAFT_MODEL: "deepseek-v4-flash", GRAFT_API_KEY: "secret" };
|
|
174
|
+
await runGraft(pi.exec as never, "/repo", { kind: "build", deep: true }, { env });
|
|
175
|
+
expect(pi.exec.mock.calls[0]![2]).toMatchObject({ env });
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
156
179
|
describe("graftArgs", () => {
|
|
157
180
|
it("maps every read-only operation to its documented CLI invocation", () => {
|
|
158
181
|
const cases: [GraftOp, string[]][] = [
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* paths, regexes, symbols, and question text cannot become shell injection.
|
|
12
12
|
* - Graft's upstream agent-wiring command (`init`) is never invoked. A missing
|
|
13
13
|
* compatible CLI is installed once through npm; incompatible CLIs are left alone.
|
|
14
|
-
* - Semantic tools are read-only.
|
|
15
|
-
*
|
|
14
|
+
* - Semantic tools are read-only. Automatic builds use the active compatible
|
|
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
18
|
* Verified against @nanonets/graft 0.18.0 (see {@link MIN_GRAFT_VERSION}).
|
|
@@ -144,8 +144,8 @@ export async function probeGraft(exec: ExecLike, cwd: string, signal?: AbortSign
|
|
|
144
144
|
// ---------------------------------------------------------------------------
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* The read-only operations this adapter exposes, plus
|
|
148
|
-
*
|
|
147
|
+
* The read-only operations this adapter exposes, plus structural and deep
|
|
148
|
+
* builds. Every other Graft subcommand (including `init`, `viz`, and
|
|
149
149
|
* `uninstall`) is deliberately unreachable from tool calls.
|
|
150
150
|
*/
|
|
151
151
|
export type GraftOp =
|
|
@@ -286,7 +286,7 @@ export async function runGraft(
|
|
|
286
286
|
exec: ExecLike,
|
|
287
287
|
repoRoot: string,
|
|
288
288
|
op: GraftOp,
|
|
289
|
-
options: { signal?: AbortSignal; timeout?: number } = {},
|
|
289
|
+
options: { signal?: AbortSignal; timeout?: number; env?: NodeJS.ProcessEnv } = {},
|
|
290
290
|
): Promise<GraftRun> {
|
|
291
291
|
const args = graftArgs(op);
|
|
292
292
|
const { command, args: argv } = graftCommand(args);
|
|
@@ -297,6 +297,7 @@ export async function runGraft(
|
|
|
297
297
|
cwd: repoRoot,
|
|
298
298
|
timeout: options.timeout ?? timeoutFor(op),
|
|
299
299
|
signal: options.signal,
|
|
300
|
+
env: options.env,
|
|
300
301
|
});
|
|
301
302
|
} catch (error) {
|
|
302
303
|
return {
|
|
@@ -139,14 +139,14 @@ describe("formatDoctor", () => {
|
|
|
139
139
|
expect(text).toContain("fresh-structural");
|
|
140
140
|
expect(text).toContain("nothing is injected");
|
|
141
141
|
expect(text).toContain("DO_NOT_TRACK=1 set by this extension");
|
|
142
|
-
expect(text).toContain("deep builds
|
|
142
|
+
expect(text).toContain("automatic deep builds use Selesai's active compatible model");
|
|
143
143
|
expect(text).toContain("graft deep adds provider-backed summaries");
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
it("shows only which provider variables are set, never their values", () => {
|
|
147
147
|
const text = formatDoctor({ ...base, providerEnv: ["GRAFT_PROVIDER", "GRAFT_MODEL"] });
|
|
148
148
|
expect(text).toContain("GRAFT_PROVIDER, GRAFT_MODEL set");
|
|
149
|
-
expect(text).toContain("values never
|
|
149
|
+
expect(text).toContain("values never shown");
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
it("reports whether this session actually applied the telemetry default", () => {
|
|
@@ -205,8 +205,8 @@ describe("buildEffects", () => {
|
|
|
205
205
|
|
|
206
206
|
it("states the provider boundary and the cache for a deep build", () => {
|
|
207
207
|
const text = buildEffects(true);
|
|
208
|
-
expect(text).toContain("
|
|
209
|
-
expect(text).toContain("Graft
|
|
208
|
+
expect(text).toContain("Selesai's active compatible model");
|
|
209
|
+
expect(text).toContain("Graft child process");
|
|
210
210
|
expect(text).toContain("caches those summaries under graft/");
|
|
211
211
|
});
|
|
212
212
|
});
|
|
@@ -289,7 +289,7 @@ describe("mutation-capable commands", () => {
|
|
|
289
289
|
}
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
it("installs a missing CLI, rechecks it, then asks
|
|
292
|
+
it("installs a missing CLI, rechecks it, then asks before the default deep build", async () => {
|
|
293
293
|
const harness = makeRuntime({ state: { name: "unavailable", detail: "ENOENT" }, built: false });
|
|
294
294
|
const ctx = makeCtx();
|
|
295
295
|
await invoke(harness.runtime, "setup", ctx);
|
|
@@ -297,8 +297,8 @@ describe("mutation-capable commands", () => {
|
|
|
297
297
|
expect(harness.installCli).toHaveBeenCalledWith(ctx);
|
|
298
298
|
expect(harness.recheck).toHaveBeenCalledTimes(3);
|
|
299
299
|
expect(ctx.ui.confirm).toHaveBeenCalledTimes(1);
|
|
300
|
-
expect(ctx.ui.confirm.mock.calls[0]![0]).toBe("
|
|
301
|
-
expect(harness.runBuild).toHaveBeenCalledWith(
|
|
300
|
+
expect(ctx.ui.confirm.mock.calls[0]![0]).toBe("Run the provider-backed deep build?");
|
|
301
|
+
expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
it("does not build when installing the missing CLI fails", async () => {
|
|
@@ -345,14 +345,16 @@ describe("mutation-capable commands", () => {
|
|
|
345
345
|
expect(message).toContain(".gitignore");
|
|
346
346
|
expect(message).toContain("init");
|
|
347
347
|
expect(message).toContain("never run");
|
|
348
|
-
expect(harness.runBuild).toHaveBeenCalledWith(
|
|
348
|
+
expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
|
|
349
349
|
expect(notified(ctx)).toContain("Done.");
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
-
it("
|
|
352
|
+
it("defaults build to deep, with an explicit structural escape hatch", async () => {
|
|
353
353
|
const harness = makeRuntime();
|
|
354
354
|
await invoke(harness.runtime, "build", makeCtx());
|
|
355
|
-
expect(harness.runBuild).toHaveBeenCalledWith(
|
|
355
|
+
expect(harness.runBuild).toHaveBeenCalledWith(true, expect.anything());
|
|
356
|
+
await invoke(harness.runtime, "build --structural", makeCtx());
|
|
357
|
+
expect(harness.runBuild).toHaveBeenLastCalledWith(false, expect.anything());
|
|
356
358
|
});
|
|
357
359
|
|
|
358
360
|
it("gives `build --deep` the same provider consent as `/graft deep`", async () => {
|
|
@@ -371,10 +373,10 @@ describe("mutation-capable commands", () => {
|
|
|
371
373
|
expect(harness.runBuild).toHaveBeenNthCalledWith(2, true, viaDeep);
|
|
372
374
|
});
|
|
373
375
|
|
|
374
|
-
it("treats setup as the
|
|
376
|
+
it("treats setup as the default deep build", async () => {
|
|
375
377
|
const harness = makeRuntime();
|
|
376
378
|
await invoke(harness.runtime, "setup", makeCtx());
|
|
377
|
-
expect(harness.runBuild).toHaveBeenCalledWith(
|
|
379
|
+
expect(harness.runBuild).toHaveBeenCalledWith(true, expect.anything());
|
|
378
380
|
});
|
|
379
381
|
|
|
380
382
|
it("shows the provider boundary before a deep build and only sends source off-machine after consent", async () => {
|
|
@@ -385,7 +387,7 @@ describe("mutation-capable commands", () => {
|
|
|
385
387
|
const [title, message] = ctx.ui.confirm.mock.calls[0] as [string, string];
|
|
386
388
|
expect(title).toContain("provider-backed");
|
|
387
389
|
expect(message).toContain("leaves this machine");
|
|
388
|
-
expect(message).toContain("
|
|
390
|
+
expect(message).toContain("credential is passed only to this Graft child process");
|
|
389
391
|
expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
|
|
390
392
|
});
|
|
391
393
|
|
|
@@ -472,7 +474,7 @@ describe("runtime contract", () => {
|
|
|
472
474
|
const ctx = makeCtx();
|
|
473
475
|
await invoke(harness.runtime, "status", ctx);
|
|
474
476
|
expect(PROVIDER_ENV_NAMES).toContain("GRAFT_API_KEY");
|
|
475
|
-
expect(DEEP_PROVIDER_HINT).toContain("
|
|
477
|
+
expect(DEEP_PROVIDER_HINT).toContain("active Selesai model");
|
|
476
478
|
expect(DOCTOR_USAGE).toContain("mode [pull|push|hybrid]");
|
|
477
479
|
});
|
|
478
480
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `/graft` command surface: setup, build, deep, refresh, status, doctor, mode.
|
|
3
3
|
*
|
|
4
|
-
* Command handlers own
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and prints the exact manual command instead.
|
|
4
|
+
* Command handlers own reporting and the explicit deep-build confirmation.
|
|
5
|
+
* They say exactly which repository files may change; with no dialog UI a
|
|
6
|
+
* manual deep build refuses and prints the exact command instead.
|
|
8
7
|
*
|
|
9
8
|
* Nothing here ever invokes Graft's upstream agent-wiring command. Selesai's
|
|
10
9
|
* setup installs a missing CLI automatically, explains local effects, and builds
|
|
@@ -61,7 +60,7 @@ export interface GraftCommandRuntime {
|
|
|
61
60
|
graphPresence(): GraphPresence;
|
|
62
61
|
/** Install the compatible CLI when probing found none. */
|
|
63
62
|
installCli(ctx: ExtensionContext): Promise<GraftInstallRun>;
|
|
64
|
-
/** Structural or deep build.
|
|
63
|
+
/** Structural or deep build. Deep calls only after consent. */
|
|
65
64
|
runBuild(deep: boolean, ctx: ExtensionContext): Promise<GraftRun>;
|
|
66
65
|
/** Proactive refresh of an existing graph; undefined when there is nothing to refresh. */
|
|
67
66
|
runRefresh(ctx: ExtensionContext): Promise<GraftRun | undefined>;
|
|
@@ -75,8 +74,8 @@ export const DOCTOR_USAGE = `/graft <command>
|
|
|
75
74
|
|
|
76
75
|
status current availability, graph state, and retrieval mode
|
|
77
76
|
doctor full health report with a recovery step for each problem
|
|
78
|
-
setup install the CLI if needed, then
|
|
79
|
-
build
|
|
77
|
+
setup install the CLI if needed, then build the deep graph with the active model
|
|
78
|
+
build [--structural] build graft/ (deep by default; structural stays local)
|
|
80
79
|
deep provider-backed build: adds summaries and per-symbol crux
|
|
81
80
|
refresh re-index an existing graph after edits
|
|
82
81
|
mode [pull|push|hybrid] show or set the retrieval strategy
|
|
@@ -145,8 +144,8 @@ export function formatDoctor(input: DoctorInput): string {
|
|
|
145
144
|
row(
|
|
146
145
|
"provider env",
|
|
147
146
|
input.providerEnv.length > 0
|
|
148
|
-
? `${input.providerEnv.join(", ")} set (values never
|
|
149
|
-
: `none of ${PROVIDER_ENV_NAMES.join(", ")} set — deep builds
|
|
147
|
+
? `${input.providerEnv.join(", ")} set (values never shown) — manual Graft CLI configuration is available`
|
|
148
|
+
: `none of ${PROVIDER_ENV_NAMES.join(", ")} set — automatic deep builds use Selesai's active compatible model`,
|
|
150
149
|
);
|
|
151
150
|
|
|
152
151
|
const recovery = recoveryFor(input.state);
|
|
@@ -185,7 +184,7 @@ export function buildEffects(deep: boolean): string {
|
|
|
185
184
|
];
|
|
186
185
|
if (deep) {
|
|
187
186
|
effects.push(
|
|
188
|
-
" • sends source-derived file summaries and symbol cruxes through
|
|
187
|
+
" • sends source-derived file summaries and symbol cruxes through Selesai's active compatible model; credentials are passed only to the Graft child process",
|
|
189
188
|
" • caches those summaries under graft/ so later deep builds are incremental",
|
|
190
189
|
);
|
|
191
190
|
}
|
|
@@ -259,9 +258,10 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
|
|
|
259
258
|
const items: ArgumentCompletion[] = [
|
|
260
259
|
{ value: "status", label: "status", description: "availability, graph state, retrieval mode" },
|
|
261
260
|
{ value: "doctor", label: "doctor", description: "full health report and recovery steps" },
|
|
262
|
-
{ value: "setup", label: "setup", description: "install CLI if needed, then build
|
|
263
|
-
{ value: "build", label: "build", description: "build graft/
|
|
264
|
-
{ value: "
|
|
261
|
+
{ value: "setup", label: "setup", description: "install CLI if needed, then build the deep graph" },
|
|
262
|
+
{ value: "build", label: "build", description: "build graft/ with the active model (default deep)" },
|
|
263
|
+
{ value: "build --structural", label: "build --structural", description: "build graft/ locally without model enrichment" },
|
|
264
|
+
{ value: "deep", label: "deep", description: "provider-backed build with the active model" },
|
|
265
265
|
{ value: "refresh", label: "refresh", description: "re-index an existing graph after edits" },
|
|
266
266
|
{ value: "mode", label: "mode", description: "show or set pull | push | hybrid" },
|
|
267
267
|
{ value: "help", label: "help", description: "usage" },
|
|
@@ -317,7 +317,7 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
|
|
|
317
317
|
case "setup":
|
|
318
318
|
case "build":
|
|
319
319
|
case "deep": {
|
|
320
|
-
const deep = subcommand === "deep" || rest.includes("--
|
|
320
|
+
const deep = subcommand === "deep" || !rest.includes("--structural");
|
|
321
321
|
await runtime.recheck(ctx);
|
|
322
322
|
if (!(await ensureCli(ctx, runtime))) return;
|
|
323
323
|
const repo = runtime.repoRoot() ?? "this repository";
|
|
@@ -330,7 +330,7 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
|
|
|
330
330
|
? "Set up Graft for this repository?"
|
|
331
331
|
: "Build the Graft graph?",
|
|
332
332
|
deep
|
|
333
|
-
? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass
|
|
333
|
+
? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass summarizes each changed file and extracts per-symbol cruxes using Selesai's active compatible model, so source-derived content leaves this machine. Its credential is passed only to this Graft child process.\n\nFiles it can write:\n${buildEffects(true)}`
|
|
334
334
|
: `This runs \`graft build\` in ${repo} and can write:\n\n${buildEffects(false)}`,
|
|
335
335
|
manualCommand,
|
|
336
336
|
);
|
|
@@ -388,5 +388,5 @@ async function runAndReport(
|
|
|
388
388
|
|
|
389
389
|
/** Where the deep-build guidance points a user whose provider is not configured. */
|
|
390
390
|
export const DEEP_PROVIDER_HINT =
|
|
391
|
-
`
|
|
392
|
-
`${GRAFT_PACKAGE}
|
|
391
|
+
`Select a compatible active Selesai model (TokenIn, LiteLLM, OpenAI, Anthropic, or OrcaRouter), then re-run /graft deep. ` +
|
|
392
|
+
`${GRAFT_PACKAGE} receives its credential only in that child process.`;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Graft's upstream agent-wiring command (`init`) is never invoked.
|
|
13
13
|
* - A missing compatible CLI is automatically installed once through npm;
|
|
14
14
|
* incompatible user installations are never overwritten.
|
|
15
|
-
* -
|
|
15
|
+
* - The first build uses Selesai's active compatible model for deep enrichment; otherwise it stays local and structural.
|
|
16
16
|
* - Every tool is read-only; the graph is a local cache, not a committed artifact.
|
|
17
17
|
* - Graft processes default to telemetry opt-out without touching Graft's settings.
|
|
18
18
|
*
|
|
@@ -175,11 +175,47 @@ export function isMutatingResult(event: { toolName: string; input: Record<string
|
|
|
175
175
|
return false;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
const GRAFT_PROVIDER_FOR_SELESAI_PROVIDER: Record<string, "openai" | "anthropic" | "litellm" | "orcarouter"> = {
|
|
179
|
+
anthropic: "anthropic",
|
|
180
|
+
litellm: "litellm",
|
|
181
|
+
openai: "openai",
|
|
182
|
+
orcarouter: "orcarouter",
|
|
183
|
+
tokenin: "litellm",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Resolve the active Selesai model into the isolated environment Graft needs. */
|
|
187
|
+
async function activeModelGraftEnvironment(ctx: ExtensionContext): Promise<NodeJS.ProcessEnv | undefined> {
|
|
188
|
+
const model = ctx.model;
|
|
189
|
+
const provider = model ? GRAFT_PROVIDER_FOR_SELESAI_PROVIDER[model.provider] : undefined;
|
|
190
|
+
if (!provider) return undefined;
|
|
191
|
+
|
|
192
|
+
let auth: Awaited<ReturnType<typeof ctx.modelRegistry.getProviderAuth>>;
|
|
193
|
+
try {
|
|
194
|
+
auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
195
|
+
} catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const apiKey = auth?.auth.apiKey;
|
|
199
|
+
const baseUrl = auth?.auth.baseUrl;
|
|
200
|
+
if (!apiKey || (provider === "litellm" && !baseUrl)) return undefined;
|
|
201
|
+
|
|
202
|
+
const { GRAFT_PROVIDER: _provider, GRAFT_MODEL: _model, GRAFT_API_KEY: _apiKey, GRAFT_BASE_URL: _baseUrl, ...env } = process.env;
|
|
203
|
+
return {
|
|
204
|
+
...env,
|
|
205
|
+
GRAFT_PROVIDER: provider,
|
|
206
|
+
GRAFT_MODEL: model.id,
|
|
207
|
+
GRAFT_API_KEY: apiKey,
|
|
208
|
+
...(baseUrl ? { GRAFT_BASE_URL: baseUrl } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
178
212
|
export default function graftExtension(pi: ExtensionAPI): void {
|
|
179
213
|
registerBuiltinAgentAugmentation(pi);
|
|
180
214
|
const exec: ExecLike = (command, args, options) => pi.exec(command, args, options);
|
|
181
215
|
let session = freshSession();
|
|
182
216
|
let installPromise: Promise<GraftInstallRun> | undefined;
|
|
217
|
+
let structuralBuildPromise: Promise<GraftRun> | undefined;
|
|
218
|
+
let structuralBuildSession: GraftSession | undefined;
|
|
183
219
|
const guard = createInjectionGuard();
|
|
184
220
|
|
|
185
221
|
const applyEvent = (ctx: ExtensionContext | undefined, event: GraftEvent): void => {
|
|
@@ -241,6 +277,44 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
241
277
|
}
|
|
242
278
|
};
|
|
243
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Start the graph once. Compatible active Selesai models add the deep tier;
|
|
282
|
+
* otherwise the free local structural tier remains usable.
|
|
283
|
+
*/
|
|
284
|
+
const autoBuild = (ctx: ExtensionContext, activeSession: GraftSession): Promise<GraftRun> | undefined => {
|
|
285
|
+
if (structuralBuildPromise && structuralBuildSession === activeSession) return structuralBuildPromise;
|
|
286
|
+
if (
|
|
287
|
+
!activeSession.repoRoot ||
|
|
288
|
+
activeSession.probe?.kind !== "ok" ||
|
|
289
|
+
activeSession.presence.built ||
|
|
290
|
+
activeSession.state.name === "failed"
|
|
291
|
+
) return undefined;
|
|
292
|
+
|
|
293
|
+
const pending = activeModelGraftEnvironment(ctx).then(async (env) => {
|
|
294
|
+
const deep = env !== undefined;
|
|
295
|
+
if (session === activeSession && !activeSession.disposed) applyEvent(ctx, { type: "build-started", deep });
|
|
296
|
+
return runGraft(exec, activeSession.repoRoot!, { kind: "build", deep }, { env });
|
|
297
|
+
});
|
|
298
|
+
structuralBuildPromise = pending;
|
|
299
|
+
structuralBuildSession = activeSession;
|
|
300
|
+
void pending.then(
|
|
301
|
+
(run) => {
|
|
302
|
+
if (session === activeSession && !activeSession.disposed) recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
303
|
+
},
|
|
304
|
+
(error) => {
|
|
305
|
+
if (session === activeSession && !activeSession.disposed) {
|
|
306
|
+
applyEvent(ctx, { type: "build-failed", detail: error instanceof Error ? error.message : String(error) });
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
).finally(() => {
|
|
310
|
+
if (structuralBuildPromise === pending && structuralBuildSession === activeSession) {
|
|
311
|
+
structuralBuildPromise = undefined;
|
|
312
|
+
structuralBuildSession = undefined;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return pending;
|
|
316
|
+
};
|
|
317
|
+
|
|
244
318
|
/** Install a missing CLI without blocking session startup or racing another session. */
|
|
245
319
|
const autoInstall = (ctx: ExtensionContext, activeSession: GraftSession): void => {
|
|
246
320
|
if (ctx.hasUI) ctx.ui.notify("Installing Graft CLI…", "info");
|
|
@@ -258,6 +332,7 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
258
332
|
if (ctx.hasUI) ctx.ui.notify("Graft CLI installed, but is not yet usable. Restart Selesai and run /graft doctor.", "warning");
|
|
259
333
|
return;
|
|
260
334
|
}
|
|
335
|
+
autoBuild(ctx, activeSession);
|
|
261
336
|
if (ctx.hasUI) ctx.ui.notify("Graft CLI installed.", "info");
|
|
262
337
|
})();
|
|
263
338
|
};
|
|
@@ -285,11 +360,14 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
285
360
|
throw new Error(`Graft is not usable here: ${detail} Run /graft doctor for a recovery step.`);
|
|
286
361
|
}
|
|
287
362
|
if (!session.presence.built && op.kind !== "check-freshness" && op.kind !== "build") {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
363
|
+
await autoBuild(ctx, session);
|
|
364
|
+
if (!session.presence.built) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`There is no ${GRAPH_DIR_NAME}/ graph in ${session.repoRoot} yet. ${
|
|
367
|
+
recoveryFor(session.state) ?? "Run /graft build to retry the structural index."
|
|
368
|
+
}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
293
371
|
}
|
|
294
372
|
};
|
|
295
373
|
|
|
@@ -337,6 +415,7 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
337
415
|
|
|
338
416
|
await recheck(ctx);
|
|
339
417
|
if (session.state.name === "unavailable") autoInstall(ctx, session);
|
|
418
|
+
else autoBuild(ctx, session);
|
|
340
419
|
});
|
|
341
420
|
|
|
342
421
|
pi.on("session_shutdown", (_event: SessionShutdownEvent, ctx: ExtensionContext) => {
|
|
@@ -450,9 +529,8 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
450
529
|
* Debounced, idle-aware proactive refresh.
|
|
451
530
|
*
|
|
452
531
|
* `agent_end` is the runtime's own idle boundary, so this needs no timer: a
|
|
453
|
-
* burst of edits inside one turn costs at most one rebuild
|
|
454
|
-
*
|
|
455
|
-
* repository stays unbuilt until the user consents to a build.
|
|
532
|
+
* burst of edits inside one turn costs at most one rebuild. A compatible
|
|
533
|
+
* active Selesai model adds the deep tier; otherwise refresh stays local.
|
|
456
534
|
*/
|
|
457
535
|
pi.on("agent_end", (_event, ctx) => {
|
|
458
536
|
if (!session.enabled || session.disposed) return;
|
|
@@ -466,9 +544,11 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
466
544
|
|
|
467
545
|
void (async () => {
|
|
468
546
|
try {
|
|
469
|
-
const
|
|
547
|
+
const env = await activeModelGraftEnvironment(ctx);
|
|
548
|
+
const deep = env !== undefined;
|
|
549
|
+
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
470
550
|
if (session.disposed) return;
|
|
471
|
-
recordBuild(ctx, run,
|
|
551
|
+
recordBuild(ctx, run, deep);
|
|
472
552
|
} catch (error) {
|
|
473
553
|
if (session.disposed) return;
|
|
474
554
|
applyEvent(ctx, {
|
|
@@ -501,8 +581,26 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
501
581
|
installCli,
|
|
502
582
|
runBuild: async (deep, ctx) => {
|
|
503
583
|
await ensureReady(ctx, { kind: "build", deep });
|
|
584
|
+
if (!deep) {
|
|
585
|
+
const pending = autoBuild(ctx, session);
|
|
586
|
+
if (pending) return pending;
|
|
587
|
+
}
|
|
588
|
+
const env = deep ? await activeModelGraftEnvironment(ctx) : undefined;
|
|
589
|
+
if (deep && !env) {
|
|
590
|
+
return {
|
|
591
|
+
op: { kind: "build", deep },
|
|
592
|
+
argv: ["graft", "build", "--deep"],
|
|
593
|
+
cwd: session.repoRoot!,
|
|
594
|
+
code: 1,
|
|
595
|
+
stdout: "",
|
|
596
|
+
stderr: "The active Selesai model cannot be used for Graft deep builds.",
|
|
597
|
+
killed: false,
|
|
598
|
+
cancelled: false,
|
|
599
|
+
timedOut: false,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
504
602
|
applyEvent(ctx, { type: "build-started", deep });
|
|
505
|
-
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep });
|
|
603
|
+
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
506
604
|
recordBuild(ctx, run, deep);
|
|
507
605
|
return run;
|
|
508
606
|
},
|
|
@@ -511,8 +609,10 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
511
609
|
await ensureReady(ctx, { kind: "build", deep: false });
|
|
512
610
|
session.lastRefreshAt = Date.now();
|
|
513
611
|
applyEvent(ctx, { type: "refresh-started" });
|
|
514
|
-
const
|
|
515
|
-
|
|
612
|
+
const env = await activeModelGraftEnvironment(ctx);
|
|
613
|
+
const deep = env !== undefined;
|
|
614
|
+
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
615
|
+
recordBuild(ctx, run, deep);
|
|
516
616
|
return run;
|
|
517
617
|
},
|
|
518
618
|
providerEnvNames: () => PROVIDER_ENV_NAMES.filter((name) => Boolean(process.env[name])),
|
|
@@ -263,7 +263,7 @@ describe("proactive refresh", () => {
|
|
|
263
263
|
await session.reset();
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
it("
|
|
266
|
+
it("automatically creates the initial local structural graph", async () => {
|
|
267
267
|
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
268
268
|
const bare = join(root, "bare-repo");
|
|
269
269
|
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
@@ -271,6 +271,10 @@ describe("proactive refresh", () => {
|
|
|
271
271
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
272
272
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
273
273
|
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
274
|
+
if (args.includes("build")) {
|
|
275
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
276
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
277
|
+
}
|
|
274
278
|
return { ...OK, stdout: ASK_JSON };
|
|
275
279
|
});
|
|
276
280
|
graftExtension(harness.pi as never);
|
|
@@ -279,10 +283,41 @@ describe("proactive refresh", () => {
|
|
|
279
283
|
{ type: "session_start", reason: "startup" },
|
|
280
284
|
ctx,
|
|
281
285
|
);
|
|
282
|
-
expect(ctx.statuses.get("graft")).toBe("graft:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
|
|
287
|
+
expect(harness.commandsRun()).toContain("graft build");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("uses the active TokenIn model for the default deep build", async () => {
|
|
291
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
292
|
+
const bare = join(root, "deep-repo");
|
|
293
|
+
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
294
|
+
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
295
|
+
const harness = makePi(async (command: string, args: string[]) => {
|
|
296
|
+
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
297
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
298
|
+
if (args.includes("build")) {
|
|
299
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
300
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
301
|
+
writeFileSync(join(bare, "graft", "concept.md"), "# Concept", "utf-8");
|
|
302
|
+
}
|
|
303
|
+
return { ...OK, stdout: ASK_JSON };
|
|
304
|
+
});
|
|
305
|
+
graftExtension(harness.pi as never);
|
|
306
|
+
const ctx = makeCtx({ cwd: bare });
|
|
307
|
+
Object.assign(ctx, {
|
|
308
|
+
model: { provider: "tokenin", id: "deepseek-v4-flash" },
|
|
309
|
+
modelRegistry: {
|
|
310
|
+
getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token", baseUrl: "https://lite.andlet.me/v1" } })),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
314
|
+
{ type: "session_start", reason: "startup" },
|
|
315
|
+
ctx,
|
|
316
|
+
);
|
|
317
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
|
|
318
|
+
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
319
|
+
expect(build[1]).toEqual(["build", "--deep"]);
|
|
320
|
+
expect(build[2]).toMatchObject({ env: { GRAFT_PROVIDER: "litellm", GRAFT_MODEL: "deepseek-v4-flash" } });
|
|
286
321
|
});
|
|
287
322
|
|
|
288
323
|
it("re-indexes after edits, shows the sync phase, and returns to fresh", async () => {
|
|
@@ -352,7 +387,7 @@ describe("retrieval strategy", () => {
|
|
|
352
387
|
session.ctx,
|
|
353
388
|
);
|
|
354
389
|
|
|
355
|
-
it("injects a source-backed pack in
|
|
390
|
+
it("injects a source-backed orientation pack in the default hybrid mode", async () => {
|
|
356
391
|
const session = await bootSession();
|
|
357
392
|
const result = await beforeAgentStart(session, "Fix the login crash");
|
|
358
393
|
expect(result).toBeDefined();
|
|
@@ -450,7 +485,7 @@ describe("retrieval strategy", () => {
|
|
|
450
485
|
expect(notified(ctx)).toContain("Graft context unavailable");
|
|
451
486
|
});
|
|
452
487
|
|
|
453
|
-
it("
|
|
488
|
+
it("waits for the initial structural build before injecting context", async () => {
|
|
454
489
|
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
455
490
|
const bare = join(root, "unbuilt-repo");
|
|
456
491
|
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
@@ -458,6 +493,10 @@ describe("retrieval strategy", () => {
|
|
|
458
493
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
459
494
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
460
495
|
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
496
|
+
if (args.includes("build")) {
|
|
497
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
498
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
499
|
+
}
|
|
461
500
|
return { ...OK, stdout: ASK_JSON };
|
|
462
501
|
});
|
|
463
502
|
graftExtension(harness.pi as never);
|
|
@@ -471,7 +510,7 @@ describe("retrieval strategy", () => {
|
|
|
471
510
|
{ type: "before_agent_start", prompt: "Fix the login crash" },
|
|
472
511
|
ctx,
|
|
473
512
|
),
|
|
474
|
-
).
|
|
513
|
+
).toMatchObject({ message: { customType: "graft-context" } });
|
|
475
514
|
});
|
|
476
515
|
});
|
|
477
516
|
|
|
@@ -523,7 +562,7 @@ describe("tool readiness", () => {
|
|
|
523
562
|
).rejects.toThrow(/Run \/graft doctor/);
|
|
524
563
|
});
|
|
525
564
|
|
|
526
|
-
it("
|
|
565
|
+
it("builds before answering from a graph that is not there", async () => {
|
|
527
566
|
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
528
567
|
const bare = join(root, "unbuilt-tools");
|
|
529
568
|
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
@@ -531,6 +570,10 @@ describe("tool readiness", () => {
|
|
|
531
570
|
const harness = makePi(async (command: string, args: string[]) => {
|
|
532
571
|
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
533
572
|
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
573
|
+
if (args.includes("build")) {
|
|
574
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
575
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
576
|
+
}
|
|
534
577
|
return { ...OK, stdout: ASK_JSON };
|
|
535
578
|
});
|
|
536
579
|
graftExtension(harness.pi as never);
|
|
@@ -542,7 +585,8 @@ describe("tool readiness", () => {
|
|
|
542
585
|
const tool = toolFor(harness, "graft_find_code");
|
|
543
586
|
await expect(
|
|
544
587
|
(tool.execute as (...args: unknown[]) => Promise<unknown>)("c", { question: "x" }, undefined, undefined, ctx),
|
|
545
|
-
).
|
|
588
|
+
).resolves.toBeDefined();
|
|
589
|
+
expect(harness.commandsRun()).toContain("graft build");
|
|
546
590
|
});
|
|
547
591
|
|
|
548
592
|
it("re-resolves lazily when an earlier attempt found no repository", async () => {
|
|
@@ -170,8 +170,8 @@ describe("retrieval mode", () => {
|
|
|
170
170
|
expect(isRetrievalMode(undefined)).toBe(false);
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
it("defaults to
|
|
174
|
-
expect(DEFAULT_RETRIEVAL_MODE).toBe("
|
|
173
|
+
it("defaults to hybrid, the relevance-gated context mode", () => {
|
|
174
|
+
expect(DEFAULT_RETRIEVAL_MODE).toBe("hybrid");
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
it("restores the newest mode chosen on this branch, ignoring other custom entries", () => {
|
|
@@ -30,7 +30,7 @@ import { INSTALL_COMMAND } from "./cli.ts";
|
|
|
30
30
|
export type RetrievalMode = "pull" | "push" | "hybrid";
|
|
31
31
|
|
|
32
32
|
export const RETRIEVAL_MODES: readonly RetrievalMode[] = ["pull", "push", "hybrid"];
|
|
33
|
-
export const DEFAULT_RETRIEVAL_MODE: RetrievalMode = "
|
|
33
|
+
export const DEFAULT_RETRIEVAL_MODE: RetrievalMode = "hybrid";
|
|
34
34
|
|
|
35
35
|
export function isRetrievalMode(value: unknown): value is RetrievalMode {
|
|
36
36
|
return typeof value === "string" && (RETRIEVAL_MODES as readonly string[]).includes(value);
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* graft_check_freshness → graft check --json
|
|
13
13
|
*
|
|
14
14
|
* Every one of them is read-only under Selesai's capability model. Nothing here
|
|
15
|
-
* can build, mutate, install, or configure anything — the
|
|
16
|
-
*
|
|
15
|
+
* can build, mutate, install, or configure anything — the extension builds the
|
|
16
|
+
* graph during session setup, using the active compatible Selesai model for the
|
|
17
|
+
* optional deep tier.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { StringEnum } from "@earendil-works/pi-ai";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.24",
|
|
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
|
"repository": {
|