pi-gortex 0.3.0 → 0.4.1
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 +15 -12
- package/docs/architecture.md +49 -12
- package/docs/tests.md +4 -4
- package/package.json +3 -3
- package/src/config.ts +73 -22
- package/src/diff.ts +404 -0
- package/src/hook.ts +4 -3
- package/src/index.ts +127 -52
- package/src/mcp-client.ts +159 -80
- package/src/native.ts +1 -1
- package/src/render.ts +65 -8
- package/src/runtime.ts +3 -0
- package/src/state.ts +7 -0
- package/src/tools.ts +48 -8
package/README.md
CHANGED
|
@@ -13,13 +13,16 @@ need instead of reading whole files around it, which is where the token
|
|
|
13
13
|
savings come from. It is a single static binary with no dependency chain,
|
|
14
14
|
and it covers 257 languages through tree-sitter.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
This extension connects the two. It hands `gortex mcp` to Pi's built-in MCP,
|
|
17
|
+
or runs its own MCP client with `native_mcp` off. Read discipline goes through
|
|
18
|
+
`gortex hook` on both.
|
|
17
19
|
|
|
18
20
|
## What it does
|
|
19
21
|
|
|
20
|
-
- **Registers Gortex's graph tools as native Pi tools**,
|
|
21
|
-
`gortex mcp` child per session. The
|
|
22
|
-
daemon's own `tools_search`, and
|
|
22
|
+
- **Registers Gortex's graph tools as native Pi tools**, through Pi's
|
|
23
|
+
built-in MCP or over the extension's own `gortex mcp` child per session. The
|
|
24
|
+
deferred catalogue is reached through the daemon's own `tools_search`, and
|
|
25
|
+
promoted tools appear in Pi's registry.
|
|
23
26
|
- **Applies read discipline.** Pi's tool calls are forwarded to
|
|
24
27
|
`gortex hook --agent=pi`, which answers block / soft guidance / nothing
|
|
25
28
|
according to the posture you configured. Gortex owns that logic, so the
|
|
@@ -31,7 +34,7 @@ See [docs/architecture.md](docs/architecture.md) for how it works.
|
|
|
31
34
|
|
|
32
35
|
## Requirements
|
|
33
36
|
|
|
34
|
-
- **Node 24+**, **Pi 0
|
|
37
|
+
- **Node 24+**, **Pi 1.0+**
|
|
35
38
|
- **The `gortex` binary** on `PATH`, **v0.61.2 or newer**. See
|
|
36
39
|
[Gortex installation](https://github.com/zzet/gortex/blob/main/docs/installation.md).
|
|
37
40
|
The extension starts the shared daemon itself and never blocks Pi's startup
|
|
@@ -57,16 +60,16 @@ the absolute path, or a single run with `pi -e /path/to/pi-gortex`.
|
|
|
57
60
|
## Configure
|
|
58
61
|
|
|
59
62
|
Zero configuration required. Every value resolves at runtime, first hit wins:
|
|
60
|
-
environment -> `<cwd>/.pi/gortex.json` -> `<agent dir>/extensions/gortex.json`
|
|
63
|
+
environment -> `<session cwd>/.pi/gortex.json` -> `<agent dir>/extensions/gortex.json`
|
|
61
64
|
(agent dir is `$PI_CODING_AGENT_DIR`, else `~/.pi/agent`) -> defaults.
|
|
62
65
|
|
|
63
66
|
| Sidecar key | Environment | Default | Meaning |
|
|
64
67
|
|----------------|--------------------|--------------------|---------|
|
|
65
|
-
| `bin` | `GORTEX_BIN` | `gortex` on `PATH` |
|
|
66
|
-
| `hook_mode` | `GORTEX_HOOK_MODE` | `deny` | Posture: `deny`, `enrich`, `consult-unlock`, `nudge
|
|
68
|
+
| `bin` | `GORTEX_BIN` | `gortex` on `PATH` | The gortex binary. Read from the global sidecar only; a project file cannot set it. `~` expands, and a relative path resolves against the sidecar's directory (the launch directory for `GORTEX_BIN`). |
|
|
69
|
+
| `hook_mode` | `GORTEX_HOOK_MODE` | `deny` | Posture: `deny`, `enrich`, `consult-unlock`, `nudge` (`adaptive-nudge` is an alias). |
|
|
67
70
|
| `enforce` | `GORTEX_ENFORCE` | `true` | `false` keeps the graph tools and the briefing, and wires no enforcement. |
|
|
68
|
-
| `tools_preset` | `GORTEX_TOOLS` |
|
|
69
|
-
| `native_mcp` | `GORTEX_NATIVE_MCP` | `true` |
|
|
71
|
+
| `tools_preset` | `GORTEX_TOOLS` | daemon's default | Eager MCP surface: a Gortex preset (`core`, `full`, `agent`, `edit`, `nav`, `readonly`, `localization`), optionally followed by comma-separated tool names and `+tool`/`-tool` changes, e.g. `nav,+edit_file`. A value that does not open with a preset is ignored. |
|
|
72
|
+
| `native_mcp` | `GORTEX_NATIVE_MCP` | `true` | Register `gortex mcp` with Pi's built-in MCP. `false` keeps the extension's own client. |
|
|
70
73
|
|
|
71
74
|
```json
|
|
72
75
|
{
|
|
@@ -76,8 +79,8 @@ environment -> `<cwd>/.pi/gortex.json` -> `<agent dir>/extensions/gortex.json`
|
|
|
76
79
|
}
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
The extension reads its configuration
|
|
80
|
-
effect
|
|
82
|
+
The extension reads its configuration at every session start, from the
|
|
83
|
+
directory the session runs in, so a change takes effect on `/reload` or `/new`.
|
|
81
84
|
|
|
82
85
|
**Pi's own `mcp.json` wins.** With `native_mcp` on, the extension registers a
|
|
83
86
|
server named `gortex`. A `gortex` entry in `~/.pi/agent/mcp.json` or
|
package/docs/architecture.md
CHANGED
|
@@ -68,15 +68,15 @@ implementation forwards the call down that channel.
|
|
|
68
68
|
arrive in bursts, so the re-sync is debounced; a search call awaits its own
|
|
69
69
|
re-sync directly, so every tool its reply names is callable by the time the
|
|
70
70
|
model reads it.
|
|
71
|
-
- **Framing.**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
call respawns it.
|
|
71
|
+
- **Framing.** Frames are cut at the newline byte and decoded whole, so a
|
|
72
|
+
character split across chunks arrives intact. A frame past a generous byte
|
|
73
|
+
cap kills the child, because a stream cannot be resynced mid-frame, and the
|
|
74
|
+
next call respawns it.
|
|
76
75
|
- **Timeouts.** Every request is bounded: a short cap for ordinary requests, a
|
|
77
76
|
longer one for the handshake, the longest for tool calls. The last is generous
|
|
78
77
|
enough for analyzers that legitimately run for minutes, and finite so a wedged
|
|
79
|
-
daemon cannot hang an agent turn forever.
|
|
78
|
+
daemon cannot hang an agent turn forever. A call dropped on timeout or on
|
|
79
|
+
Pi's abort tells the daemon to cancel it.
|
|
80
80
|
|
|
81
81
|
### Tool-name aliasing
|
|
82
82
|
|
|
@@ -88,6 +88,21 @@ built-ins Pi may grow later as well as today's collisions. Each aliased tool
|
|
|
88
88
|
front-loads the rename into its own description, because Gortex's guidance and
|
|
89
89
|
denial messages name the bare tool and the model has to map it.
|
|
90
90
|
|
|
91
|
+
### Edit diffs
|
|
92
|
+
|
|
93
|
+
The daemon reports an applied edit without its content, so the bridge reads the
|
|
94
|
+
files a write names before and after the call and stores their diffs in the
|
|
95
|
+
result's `details`, shaped like Pi's own edit details (see `src/diff.ts`). The
|
|
96
|
+
model still reads only Gortex's reply.
|
|
97
|
+
|
|
98
|
+
On Pi's built-in MCP the reads ride the `tool_call` and `tool_result` events.
|
|
99
|
+
Pi prepares a whole parallel batch before running it, so no lock can span a
|
|
100
|
+
call; a file two calls in flight write gets no diff instead.
|
|
101
|
+
|
|
102
|
+
A wrong diff is worse than none, so where one could mislead or stall Pi (a
|
|
103
|
+
shell command running alongside, or a change a line diff cannot show cheaply) a
|
|
104
|
+
one-line note stands in for it.
|
|
105
|
+
|
|
91
106
|
## Channel 2: read discipline over the hook bridge
|
|
92
107
|
|
|
93
108
|
Every tool call Pi is about to run is forwarded to Gortex's hook with a
|
|
@@ -100,7 +115,8 @@ keys. That translation lives on this side of the bridge, so the Go side keeps
|
|
|
100
115
|
one vocabulary for every agent it serves.
|
|
101
116
|
|
|
102
117
|
A call to a Gortex tool is flagged as a graph call and sent under the daemon's
|
|
103
|
-
own tool name, which is what the postures key off.
|
|
118
|
+
own tool name, which is what the postures key off. On Pi's built-in MCP that
|
|
119
|
+
means the `mcp__gortex__` prefix is stripped. It skips the translation, so
|
|
104
120
|
Gortex's `read` never passes for Pi's. On Pi's built-in MCP the same hook also
|
|
105
121
|
sees each tool a codemode script calls.
|
|
106
122
|
|
|
@@ -109,11 +125,14 @@ behaviour identical across hosts.
|
|
|
109
125
|
|
|
110
126
|
## Orientation injection
|
|
111
127
|
|
|
112
|
-
The session briefing
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
The session briefing is stored once in the session history as a hidden message,
|
|
129
|
+
so every LLM call carries it. It stays out of the system prompt, because a
|
|
130
|
+
change there invalidates prefix prompt caching.
|
|
131
|
+
|
|
132
|
+
## Withdrawn tools
|
|
133
|
+
|
|
134
|
+
Pi cannot unregister a tool, so one the daemon withdraws leaves Pi's active tool
|
|
135
|
+
set instead.
|
|
117
136
|
|
|
118
137
|
## The readiness barrier
|
|
119
138
|
|
|
@@ -174,3 +193,21 @@ Every value resolves at runtime, layered from the environment down through a
|
|
|
174
193
|
project file and a global one to defaults. See the table in the
|
|
175
194
|
[README](../README.md#configure). Runtime resolution is what lets one artifact
|
|
176
195
|
serve both the npm package and a directory install.
|
|
196
|
+
|
|
197
|
+
Resolution runs on every `session_start`, against the session's own directory.
|
|
198
|
+
A resumed session runs where it was created, which need not be where Pi was
|
|
199
|
+
launched. So the native server is registered on `session_start` rather than
|
|
200
|
+
when Pi loads the extension, and again only when the session's preset or
|
|
201
|
+
channel differ, since Pi reconnects a re-registered server. Pi reads a
|
|
202
|
+
registration made before its own `session_start` handler and connects one made
|
|
203
|
+
after it. The extension's own `gortex mcp` is spawned in the session's
|
|
204
|
+
directory, which is what the proxy scopes the graph to.
|
|
205
|
+
|
|
206
|
+
A tool preset reaches `gortex mcp` only when it opens with a preset Gortex
|
|
207
|
+
knows. Gortex reads anything else as a bare allow-list, so a typo would strand
|
|
208
|
+
the session; it falls through to the next layer instead. The resolved value
|
|
209
|
+
always overrides an inherited `GORTEX_TOOLS`.
|
|
210
|
+
|
|
211
|
+
The project file sits in whatever repository the session runs in, so it cannot
|
|
212
|
+
set `bin`. Otherwise a cloned repository could choose the executable the
|
|
213
|
+
extension spawns. The binary comes from `GORTEX_BIN`, the global file or `PATH`.
|
package/docs/tests.md
CHANGED
|
@@ -88,15 +88,15 @@ point in the directory when the manifest resolves to nothing.
|
|
|
88
88
|
## Where the double stops
|
|
89
89
|
|
|
90
90
|
The double models what the suites need and no more. Behaviour past that edge,
|
|
91
|
-
such as promotion re-sync under real notification traffic,
|
|
92
|
-
|
|
91
|
+
such as promotion re-sync under real notification traffic, a real model turn or
|
|
92
|
+
a live daemon, is unverified here. A change
|
|
93
93
|
in that territory is verified by hand against a running Gortex, and the suite
|
|
94
94
|
says as much rather than implying coverage it does not have.
|
|
95
95
|
|
|
96
96
|
## Pinning
|
|
97
97
|
|
|
98
|
-
The Pi devDependency is pinned tight on purpose. Pi
|
|
99
|
-
|
|
98
|
+
The Pi devDependency is pinned tight on purpose. Pi's runner's method surface
|
|
99
|
+
has been renamed without a changelog entry before, so a tight pin
|
|
100
100
|
turns that into a red build rather than a silently skipped suite. The runtime
|
|
101
101
|
peer dependency stays wide.
|
|
102
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-gortex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Gortex for Pi: high-performance code-intelligence engine for AI agents, based on graph. Exposes only the information the agent needs, cutting token usage up to 50x. 100% local.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"prepublishOnly": "npm run check"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@earendil-works/pi-coding-agent": "
|
|
39
|
-
"@earendil-works/pi-tui": "
|
|
38
|
+
"@earendil-works/pi-coding-agent": "1.0.2",
|
|
39
|
+
"@earendil-works/pi-tui": "1.0.2",
|
|
40
40
|
"@types/node": "^24.19.1",
|
|
41
41
|
"jiti": "^2.7.0",
|
|
42
42
|
"typescript": "^7.0.2"
|
package/src/config.ts
CHANGED
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
10
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
/** The sidecar file's on-disk shape. `gortex init` is its writer. */
|
|
13
13
|
export interface SidecarConfig {
|
|
14
|
-
/**
|
|
14
|
+
/** The gortex binary. Global sidecar only; relative to the sidecar's directory. Default: resolved from PATH. */
|
|
15
15
|
bin?: string;
|
|
16
16
|
/** Hook posture: deny | enrich | consult-unlock | nudge. */
|
|
17
17
|
hook_mode?: string;
|
|
18
18
|
/** false mirrors `gortex init --no-hooks`: orientation only, no enforcement. */
|
|
19
19
|
enforce?: boolean;
|
|
20
|
-
/** Eager MCP tool preset
|
|
20
|
+
/** Eager MCP tool preset (core | full | agent | edit | nav | readonly | localization) or a tool spec. */
|
|
21
21
|
tools_preset?: string;
|
|
22
22
|
/** false keeps the extension's own MCP client on a Pi that has built-in MCP. */
|
|
23
23
|
native_mcp?: boolean;
|
|
@@ -31,7 +31,7 @@ export interface GortexConfig {
|
|
|
31
31
|
hookArgv: string[];
|
|
32
32
|
/** Whether to wire read-discipline enforcement onto tool_call. */
|
|
33
33
|
enforce: boolean;
|
|
34
|
-
/**
|
|
34
|
+
/** GORTEX_TOOLS for `gortex mcp`; empty leaves the daemon's default surface. */
|
|
35
35
|
toolsPreset: string;
|
|
36
36
|
/** Whether to hand the tool channel to Pi's built-in MCP when Pi has one. */
|
|
37
37
|
nativeMcp: boolean;
|
|
@@ -47,7 +47,15 @@ export interface ResolveConfigOptions {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export const DEFAULT_BIN = "gortex";
|
|
50
|
-
|
|
50
|
+
/** No preset: `gortex mcp` serves the daemon's default surface. */
|
|
51
|
+
export const DEFAULT_TOOLS_PRESET = "";
|
|
52
|
+
|
|
53
|
+
/** Expands a leading `~`, `~/` or `~\` to the home directory. */
|
|
54
|
+
export function expandHome(path: string, home: string = homedir()): string {
|
|
55
|
+
if (path === "~") return home;
|
|
56
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
51
59
|
|
|
52
60
|
/**
|
|
53
61
|
* Pi's agent directory, resolved the way Pi itself resolves it
|
|
@@ -59,15 +67,46 @@ export const DEFAULT_TOOLS_PRESET = "core";
|
|
|
59
67
|
*/
|
|
60
68
|
export function defaultAgentDir(env: Record<string, string | undefined> = process.env): string {
|
|
61
69
|
const override = env.PI_CODING_AGENT_DIR;
|
|
62
|
-
if (override && override.trim() !== "")
|
|
63
|
-
const path = override.trim();
|
|
64
|
-
if (path === "~") return homedir();
|
|
65
|
-
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
66
|
-
return path;
|
|
67
|
-
}
|
|
70
|
+
if (override && override.trim() !== "") return expandHome(override.trim());
|
|
68
71
|
return join(homedir(), ".pi", "agent");
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/**
|
|
75
|
+
* A `bin` setting as spawn needs it: `~` expanded, a relative path anchored at
|
|
76
|
+
* `baseDir`, a bare name left for PATH lookup.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveBin(bin: string, baseDir: string): string {
|
|
79
|
+
const expanded = expandHome(bin);
|
|
80
|
+
if (!/[\\/]/.test(expanded) || isAbsolute(expanded)) return expanded;
|
|
81
|
+
return resolve(baseDir, expanded);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Preset names and aliases Gortex accepts (internal/mcp/tool_presets.go).
|
|
85
|
+
const KNOWN_PRESETS = new Set([
|
|
86
|
+
"full", "all",
|
|
87
|
+
"core", "default", "classic",
|
|
88
|
+
"agent", "coding-agent",
|
|
89
|
+
"facade-v1", "agent-v2", "compact", "facade",
|
|
90
|
+
"readonly", "read-only", "read_only",
|
|
91
|
+
"edit", "editor", "edit-harness",
|
|
92
|
+
"nav", "navigate", "explore",
|
|
93
|
+
"localization", "locate", "find",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A tools_preset / GORTEX_TOOLS value Gortex should see, or undefined. Gortex
|
|
98
|
+
* reads a spec that does not open with a preset as a bare allow-list, so a
|
|
99
|
+
* typo (or `+tool` alone) would strip the session down to a tool or two. Only
|
|
100
|
+
* a spec opening with a known preset passes, optionally followed by
|
|
101
|
+
* comma-separated tool names and `+tool`/`-tool` changes.
|
|
102
|
+
*/
|
|
103
|
+
export function toolsSpec(value: string | undefined): string | undefined {
|
|
104
|
+
const tokens = (value ?? "").split(",").map((token) => token.trim()).filter(Boolean);
|
|
105
|
+
const preset = tokens[0]?.toLowerCase();
|
|
106
|
+
if (preset === undefined || !KNOWN_PRESETS.has(preset)) return undefined;
|
|
107
|
+
return [preset, ...tokens.slice(1)].join(",");
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
/** Sidecar paths, nearest first. A project file wins over the global one. */
|
|
72
111
|
export function sidecarPaths(cwd: string, agentDir: string): string[] {
|
|
73
112
|
return [join(cwd, ".pi", "gortex.json"), join(agentDir, "extensions", "gortex.json")];
|
|
@@ -139,29 +178,41 @@ export function resolveConfig(opts: ResolveConfigOptions = {}): GortexConfig {
|
|
|
139
178
|
const agentDir = opts.agentDir ?? defaultAgentDir(env);
|
|
140
179
|
const readFile = opts.readFile ?? ((path: string) => readFileSync(path, "utf8"));
|
|
141
180
|
|
|
142
|
-
const [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
181
|
+
const [projectPath, globalPath] = sidecarPaths(cwd, agentDir) as [string, string];
|
|
182
|
+
const project = readSidecar(projectPath, readFile);
|
|
183
|
+
const global = readSidecar(globalPath, readFile);
|
|
184
|
+
|
|
185
|
+
// A checked-out repository must never pick the executable, so the project
|
|
186
|
+
// sidecar has no say in `bin`. `gortex` unqualified is resolved against PATH.
|
|
187
|
+
const envBin = firstString(env.GORTEX_BIN);
|
|
188
|
+
const globalBin = firstString(global.bin);
|
|
189
|
+
const bin = envBin
|
|
190
|
+
? resolveBin(envBin, process.cwd())
|
|
191
|
+
: globalBin
|
|
192
|
+
? resolveBin(globalBin, dirname(globalPath))
|
|
193
|
+
: DEFAULT_BIN;
|
|
194
|
+
|
|
195
|
+
const mode = normalizeMode(firstString(env.GORTEX_HOOK_MODE, project.hook_mode, global.hook_mode));
|
|
148
196
|
const hookArgv = [bin, "hook", "--agent=pi"];
|
|
149
197
|
// "deny" is the hook's own default, so it rides as an absent flag.
|
|
150
198
|
if (mode !== "deny") hookArgv.push(`--mode=${mode}`);
|
|
151
199
|
|
|
152
200
|
const enforce =
|
|
153
201
|
parseBoolean(env.GORTEX_ENFORCE) ??
|
|
154
|
-
(typeof project
|
|
155
|
-
(typeof global
|
|
202
|
+
(typeof project.enforce === "boolean" ? project.enforce : undefined) ??
|
|
203
|
+
(typeof global.enforce === "boolean" ? global.enforce : undefined) ??
|
|
156
204
|
true;
|
|
157
205
|
|
|
158
206
|
const toolsPreset =
|
|
159
|
-
|
|
207
|
+
toolsSpec(env.GORTEX_TOOLS) ??
|
|
208
|
+
toolsSpec(firstString(project.tools_preset)) ??
|
|
209
|
+
toolsSpec(firstString(global.tools_preset)) ??
|
|
210
|
+
DEFAULT_TOOLS_PRESET;
|
|
160
211
|
|
|
161
212
|
const nativeMcp =
|
|
162
213
|
parseBoolean(env.GORTEX_NATIVE_MCP) ??
|
|
163
|
-
(typeof project
|
|
164
|
-
(typeof global
|
|
214
|
+
(typeof project.native_mcp === "boolean" ? project.native_mcp : undefined) ??
|
|
215
|
+
(typeof global.native_mcp === "boolean" ? global.native_mcp : undefined) ??
|
|
165
216
|
true;
|
|
166
217
|
|
|
167
218
|
return { bin, hookArgv, enforce, toolsPreset, nativeMcp };
|