codiedev 0.9.70 → 0.9.71
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/dist/captureSignals.js +4 -0
- package/dist/cli.js +4 -2
- package/dist/codie.js +25 -0
- package/dist/commands/doctor.js +11 -0
- package/dist/commands/pack.d.ts +1 -1
- package/dist/commands/pack.js +1 -1
- package/dist/connect.js +9 -1
- package/dist/harnessDirs.d.ts +3 -0
- package/dist/harnessDirs.js +6 -0
- package/dist/hook.js +25 -8
- package/dist/limitEventReporter.d.ts +1 -1
- package/dist/limitEventReporter.js +1 -1
- package/dist/managedPi.d.ts +47 -0
- package/dist/managedPi.js +284 -0
- package/dist/packs/harnesses.d.ts +4 -1
- package/dist/packs/harnesses.js +44 -3
- package/dist/piHooks.d.ts +22 -0
- package/dist/piHooks.js +257 -0
- package/dist/utils.d.ts +24 -2
- package/dist/utils.js +185 -4
- package/package.json +4 -4
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ManagedPiArgError = exports.GUARD_SOURCE = exports.BLOCKED_BASE_URL = exports.CAPTURE_FILE = exports.GUARD_FILE = exports.PI_PROVIDER_ENV = exports.PI_PROVIDER_ID = void 0;
|
|
37
|
+
exports.managedPiAgentDir = managedPiAgentDir;
|
|
38
|
+
exports.piProviderConfig = piProviderConfig;
|
|
39
|
+
exports.prepareManagedPiDir = prepareManagedPiDir;
|
|
40
|
+
exports.managedPiEnv = managedPiEnv;
|
|
41
|
+
exports.managedPiArgs = managedPiArgs;
|
|
42
|
+
exports.runManagedPi = runManagedPi;
|
|
43
|
+
// `codie --pi [args]`: pi as a Codiedev harness, on the company's approved
|
|
44
|
+
// gateway models (the same catalog and key the OpenCode engine uses).
|
|
45
|
+
//
|
|
46
|
+
// The managed launch closes every path to a model that skips the gateway
|
|
47
|
+
// instead of trusting the user's ~/.pi:
|
|
48
|
+
// - pi runs from a Codiedev-owned agent dir (PI_CODING_AGENT_DIR), rewritten
|
|
49
|
+
// on every launch: only the `codiedev` provider, no packages, no
|
|
50
|
+
// extensions and no auth.json (so no stored provider credentials).
|
|
51
|
+
// - provider API keys pi would read from the environment are removed.
|
|
52
|
+
// - extension discovery is off (`--no-extensions`, which also skips
|
|
53
|
+
// project-local .pi/extensions); the only extensions are the guard below
|
|
54
|
+
// and the Codiedev capture extension.
|
|
55
|
+
// - flags that bring their own credentials or code (--api-key, --provider,
|
|
56
|
+
// -e) and the package subcommands are refused.
|
|
57
|
+
// - the guard points every other provider at a dead loopback address and
|
|
58
|
+
// keeps the session on an approved model, which covers an in-session
|
|
59
|
+
// /login too. Throwing from pi's before_provider_request does NOT stop a
|
|
60
|
+
// request (pi logs it and sends anyway), hence the base-URL override.
|
|
61
|
+
//
|
|
62
|
+
// Sessions, skills, prompts, themes and context files keep working; they
|
|
63
|
+
// don't reach a model on their own. Ported from #1783, minus the harness-pack
|
|
64
|
+
// proxy: pi talks to the gateway directly, like `codie`.
|
|
65
|
+
const child_process_1 = require("child_process");
|
|
66
|
+
const fs = __importStar(require("fs"));
|
|
67
|
+
const path = __importStar(require("path"));
|
|
68
|
+
const codieLauncher_1 = require("./codieLauncher");
|
|
69
|
+
const utils_1 = require("./utils");
|
|
70
|
+
exports.PI_PROVIDER_ID = "codiedev";
|
|
71
|
+
/** Env vars pi reads provider credentials from (`pi --help`, pi 0.73). */
|
|
72
|
+
exports.PI_PROVIDER_ENV = [
|
|
73
|
+
"ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "OPENAI_API_KEY", "AZURE_OPENAI_API_KEY",
|
|
74
|
+
"AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_RESOURCE_NAME", "DEEPSEEK_API_KEY", "GEMINI_API_KEY",
|
|
75
|
+
"GROQ_API_KEY", "CEREBRAS_API_KEY", "XAI_API_KEY", "FIREWORKS_API_KEY", "OPENROUTER_API_KEY",
|
|
76
|
+
"AI_GATEWAY_API_KEY", "ZAI_API_KEY", "MISTRAL_API_KEY", "MINIMAX_API_KEY", "MOONSHOT_API_KEY",
|
|
77
|
+
"OPENCODE_API_KEY", "KIMI_API_KEY", "CLOUDFLARE_API_KEY", "XIAOMI_API_KEY",
|
|
78
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
79
|
+
"AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN",
|
|
80
|
+
"AWS_BEARER_TOKEN_BEDROCK", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_API_KEY",
|
|
81
|
+
"COPILOT_GITHUB_TOKEN",
|
|
82
|
+
];
|
|
83
|
+
/** pi subcommands that install or change extension sources. */
|
|
84
|
+
const BLOCKED_SUBCOMMANDS = new Set(["install", "remove", "uninstall", "config"]);
|
|
85
|
+
exports.GUARD_FILE = "codie-guard.ts";
|
|
86
|
+
exports.CAPTURE_FILE = "codiedev-capture.ts";
|
|
87
|
+
/** Where the guard points every other provider: a loopback port nothing
|
|
88
|
+
* listens on, so a request (and whatever key it carries) never leaves the
|
|
89
|
+
* machine. */
|
|
90
|
+
exports.BLOCKED_BASE_URL = "http://127.0.0.1:9/blocked-by-codiedev";
|
|
91
|
+
/** The pi extension that keeps a managed session on Codiedev models and tags
|
|
92
|
+
* each gateway request with the session, repo and skills, so gateway billing
|
|
93
|
+
* joins the captured pi session (same headers as the OpenCode metadata
|
|
94
|
+
* plugin, assets/git-refs.mjs). */
|
|
95
|
+
exports.GUARD_SOURCE = `// Written by Codiedev on every \`codie --pi\` launch. Edits are overwritten.
|
|
96
|
+
import { execFileSync } from "node:child_process";
|
|
97
|
+
import { getProviders } from "@mariozechner/pi-ai";
|
|
98
|
+
|
|
99
|
+
const PROVIDER = ${JSON.stringify(exports.PI_PROVIDER_ID)};
|
|
100
|
+
const BLOCKED = ${JSON.stringify(exports.BLOCKED_BASE_URL)};
|
|
101
|
+
const ours = (m) => Boolean(m && m.provider === PROVIDER);
|
|
102
|
+
const label = (m) => (m ? m.provider + "/" + m.id : "that model");
|
|
103
|
+
// pi runs a header value starting with "!" as a shell command, so no value
|
|
104
|
+
// may start with one; branch and skill names are user-controlled.
|
|
105
|
+
const clean = (v, max) => String(v ?? "").replace(/[\\r\\n]/g, "").trim().replace(/^!+/, "").slice(0, max);
|
|
106
|
+
const git = (cwd, args) => { try { return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1500 }).trim(); } catch { return ""; } };
|
|
107
|
+
|
|
108
|
+
export default function (pi) {
|
|
109
|
+
for (const p of getProviders()) if (p !== PROVIDER) pi.registerProvider(p, { baseUrl: BLOCKED });
|
|
110
|
+
|
|
111
|
+
let last;
|
|
112
|
+
const skills = new Set();
|
|
113
|
+
const fallback = (ctx) => last ?? ctx.modelRegistry.getAll().find(ours);
|
|
114
|
+
const pin = async (model, ctx) => {
|
|
115
|
+
const back = fallback(ctx);
|
|
116
|
+
if (back) await pi.setModel(back);
|
|
117
|
+
if (ctx.hasUI) ctx.ui.notify("codie: " + label(model) + " isn't a Codiedev-approved model" + (back ? "; using " + back.id : "") + ".", "warning");
|
|
118
|
+
};
|
|
119
|
+
const tag = (ctx) => {
|
|
120
|
+
const headers = {};
|
|
121
|
+
const session = clean(ctx.sessionManager?.getSessionId?.(), 200);
|
|
122
|
+
if (session) headers["X-Codiedev-Session-Id"] = session;
|
|
123
|
+
const repo = clean(git(ctx.cwd, ["remote", "get-url", "origin"]).replace(/\\/\\/[^/@]*@/, "//"), 2048);
|
|
124
|
+
if (repo) headers["X-Codiedev-Repo-Url"] = repo;
|
|
125
|
+
const branch = clean(git(ctx.cwd, ["branch", "--show-current"]), 200);
|
|
126
|
+
if (branch) headers["X-Codiedev-Branch"] = branch;
|
|
127
|
+
const sha = clean(git(ctx.cwd, ["rev-parse", "HEAD"]), 40);
|
|
128
|
+
if (/^[0-9a-f]{7,40}$/i.test(sha)) headers["X-Codiedev-Head-Sha"] = sha;
|
|
129
|
+
const list = clean([...skills].join(","), 4096);
|
|
130
|
+
if (list) headers["X-Codiedev-Skills-Used"] = list;
|
|
131
|
+
// pi replaces the provider's whole request config here, so the key name
|
|
132
|
+
// goes with the headers every time.
|
|
133
|
+
pi.registerProvider(PROVIDER, { apiKey: "CODIEDEV_GATEWAY_KEY", headers });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
137
|
+
skills.clear();
|
|
138
|
+
for (const m of ctx.modelRegistry.getAll()) if (!ours(m)) pi.registerProvider(m.provider, { baseUrl: BLOCKED });
|
|
139
|
+
if (ours(ctx.model)) last = ctx.model;
|
|
140
|
+
else await pin(ctx.model, ctx);
|
|
141
|
+
tag(ctx);
|
|
142
|
+
});
|
|
143
|
+
pi.on("model_select", async (event, ctx) => {
|
|
144
|
+
if (ours(event.model)) last = event.model;
|
|
145
|
+
else await pin(event.model, ctx);
|
|
146
|
+
});
|
|
147
|
+
pi.on("input", async (event) => {
|
|
148
|
+
const m = String(event?.text ?? "").match(/^\\/skill:([a-z0-9][a-z0-9_.:-]{0,119})(?:\\s|$)/i);
|
|
149
|
+
if (m && skills.size < 50) skills.add(m[1]);
|
|
150
|
+
});
|
|
151
|
+
pi.on("tool_result", async (event) => {
|
|
152
|
+
const p = String(event?.input?.path ?? "").replace(/\\\\/g, "/").match(/\\/([a-z0-9][a-z0-9_.:-]{0,119})\\/SKILL\\.md$/i);
|
|
153
|
+
if (event?.toolName === "read" && !event.isError && p && skills.size < 50) skills.add(p[1]);
|
|
154
|
+
});
|
|
155
|
+
// Before every model call in the turn: the agent may have switched branch.
|
|
156
|
+
pi.on("turn_start", async (_event, ctx) => tag(ctx));
|
|
157
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
158
|
+
// Belt and braces: the request is already headed for BLOCKED.
|
|
159
|
+
if (!ours(ctx.model)) ctx.abort();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
function managedPiAgentDir(stateDir) {
|
|
164
|
+
return path.join(stateDir, "managed", "pi", "agent");
|
|
165
|
+
}
|
|
166
|
+
/** pi's models.json entry for the gateway. The key is read from
|
|
167
|
+
* CODIEDEV_GATEWAY_KEY at request time (pi resolves an env var name). */
|
|
168
|
+
function piProviderConfig(gatewayUrl, catalog) {
|
|
169
|
+
return {
|
|
170
|
+
baseUrl: gatewayUrl.replace(/\/+$/, ""),
|
|
171
|
+
api: "openai-completions",
|
|
172
|
+
apiKey: "CODIEDEV_GATEWAY_KEY",
|
|
173
|
+
compat: { supportsDeveloperRole: false, supportsReasoningEffort: false },
|
|
174
|
+
models: catalog.map((model) => ({
|
|
175
|
+
id: (0, codieLauncher_1.bareModel)(model.id),
|
|
176
|
+
name: model.name || (0, codieLauncher_1.bareModel)(model.id),
|
|
177
|
+
...(model.limit ? { contextWindow: model.limit.context, maxTokens: model.limit.output } : {}),
|
|
178
|
+
// pi shows a session's spend from this. The gateway publishes prompt and
|
|
179
|
+
// completion prices only, so cache is unpriced; gateway settlement is
|
|
180
|
+
// the true figure.
|
|
181
|
+
...(model.cost ? { cost: { input: model.cost.input, output: model.cost.output, cacheRead: 0, cacheWrite: 0 } } : {}),
|
|
182
|
+
})),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function writeJsonAtomic(file, value) {
|
|
186
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
187
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 });
|
|
188
|
+
fs.renameSync(tmp, file);
|
|
189
|
+
}
|
|
190
|
+
/** Rewrite everything Codiedev owns in the managed agent dir. Sessions (and
|
|
191
|
+
* anything else pi keeps there) are left alone. Returns the extensions to load. */
|
|
192
|
+
function prepareManagedPiDir(opts) {
|
|
193
|
+
const { agentDir, gatewayUrl, catalog } = opts;
|
|
194
|
+
if (!catalog.length)
|
|
195
|
+
throw new Error("No company-approved models were returned by Codiedev");
|
|
196
|
+
const ids = catalog.map((model) => (0, codieLauncher_1.bareModel)(model.id));
|
|
197
|
+
const requested = (0, codieLauncher_1.bareModel)(opts.model ?? "");
|
|
198
|
+
if (requested && !ids.includes(requested)) {
|
|
199
|
+
throw new Error(`Primary agent model is not approved for this Codiedev account: ${requested}`);
|
|
200
|
+
}
|
|
201
|
+
fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
|
202
|
+
writeJsonAtomic(path.join(agentDir, "models.json"), { providers: { [exports.PI_PROVIDER_ID]: piProviderConfig(gatewayUrl, catalog) } });
|
|
203
|
+
writeJsonAtomic(path.join(agentDir, "settings.json"), {
|
|
204
|
+
defaultProvider: exports.PI_PROVIDER_ID,
|
|
205
|
+
defaultModel: requested || ids[0],
|
|
206
|
+
enabledModels: ids.map((id) => `${exports.PI_PROVIDER_ID}/${id}`),
|
|
207
|
+
packages: [],
|
|
208
|
+
extensions: [],
|
|
209
|
+
skills: opts.skillPaths ?? [],
|
|
210
|
+
enableInstallTelemetry: false,
|
|
211
|
+
});
|
|
212
|
+
// Stored provider credentials (a /login from an earlier session) and any
|
|
213
|
+
// extensions dropped into the dir go away on every launch.
|
|
214
|
+
fs.rmSync(path.join(agentDir, "auth.json"), { force: true });
|
|
215
|
+
fs.rmSync(path.join(agentDir, "extensions"), { recursive: true, force: true });
|
|
216
|
+
const guard = path.join(agentDir, exports.GUARD_FILE);
|
|
217
|
+
fs.writeFileSync(guard, exports.GUARD_SOURCE, { mode: 0o600 });
|
|
218
|
+
const capture = path.join(agentDir, exports.CAPTURE_FILE);
|
|
219
|
+
fs.writeFileSync(capture, (0, utils_1.piExtensionSource)(opts.agentSecurityEnabled), { mode: 0o600 });
|
|
220
|
+
return [guard, capture];
|
|
221
|
+
}
|
|
222
|
+
/** The child env: no provider credentials, pi pointed at the managed dir. */
|
|
223
|
+
function managedPiEnv(env, agentDir, key, platform = process.platform) {
|
|
224
|
+
const blocked = new Set([...exports.PI_PROVIDER_ENV, "PI_CODING_AGENT_DIR"]);
|
|
225
|
+
const out = {};
|
|
226
|
+
for (const [k, v] of Object.entries(env)) {
|
|
227
|
+
// Windows env names are case-insensitive.
|
|
228
|
+
if (blocked.has(platform === "win32" ? k.toUpperCase() : k))
|
|
229
|
+
continue;
|
|
230
|
+
out[k] = v;
|
|
231
|
+
}
|
|
232
|
+
out.PI_CODING_AGENT_DIR = agentDir;
|
|
233
|
+
out.PI_TELEMETRY = "0";
|
|
234
|
+
out.CODIEDEV_GATEWAY_KEY = key;
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
class ManagedPiArgError extends Error {
|
|
238
|
+
}
|
|
239
|
+
exports.ManagedPiArgError = ManagedPiArgError;
|
|
240
|
+
/** Refuse what would bring other credentials or code into the session, then
|
|
241
|
+
* pin extension loading to the managed extensions. */
|
|
242
|
+
function managedPiArgs(args, extensions) {
|
|
243
|
+
if (args.length && BLOCKED_SUBCOMMANDS.has(args[0])) {
|
|
244
|
+
throw new ManagedPiArgError(`\`pi ${args[0]}\` changes extension sources, which codie --pi manages. Ask your Codiedev admin to approve a package instead.`);
|
|
245
|
+
}
|
|
246
|
+
for (let i = 0; i < args.length; i++) {
|
|
247
|
+
const a = args[i];
|
|
248
|
+
if (a === "--")
|
|
249
|
+
break;
|
|
250
|
+
const [flag, inline] = a.startsWith("--") && a.includes("=") ? [a.slice(0, a.indexOf("=")), a.slice(a.indexOf("=") + 1)] : [a, undefined];
|
|
251
|
+
if (flag === "--api-key")
|
|
252
|
+
throw new ManagedPiArgError("--api-key isn't allowed in codie --pi: models come from Codiedev.");
|
|
253
|
+
if (flag === "-e" || flag === "--extension")
|
|
254
|
+
throw new ManagedPiArgError(`${flag} isn't allowed in codie --pi: extensions are managed by Codiedev.`);
|
|
255
|
+
if (flag === "--provider") {
|
|
256
|
+
const value = inline ?? args[i + 1];
|
|
257
|
+
if (value !== exports.PI_PROVIDER_ID)
|
|
258
|
+
throw new ManagedPiArgError(`--provider ${value ?? "(none)"} isn't allowed in codie --pi: only Codiedev-approved models are available.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return ["--no-extensions", ...extensions.flatMap((file) => ["-e", file]), ...args];
|
|
262
|
+
}
|
|
263
|
+
function runManagedPi(opts) {
|
|
264
|
+
const env = opts.env ?? process.env;
|
|
265
|
+
const agentDir = managedPiAgentDir(opts.stateDir);
|
|
266
|
+
const extensions = prepareManagedPiDir({
|
|
267
|
+
agentDir, gatewayUrl: opts.gatewayUrl, catalog: opts.catalog, model: env.CODIEDEV_MODEL,
|
|
268
|
+
skillPaths: opts.skillPaths, agentSecurityEnabled: opts.agentSecurityEnabled,
|
|
269
|
+
});
|
|
270
|
+
const args = managedPiArgs(opts.args, extensions);
|
|
271
|
+
const bin = opts.bin ?? env.CODIEDEV_PI_BIN ?? "pi";
|
|
272
|
+
const result = (0, child_process_1.spawnSync)(bin, args, {
|
|
273
|
+
stdio: "inherit",
|
|
274
|
+
env: managedPiEnv(env, agentDir, opts.key),
|
|
275
|
+
// pi's npm launcher is a .cmd shim on Windows.
|
|
276
|
+
shell: process.platform === "win32",
|
|
277
|
+
});
|
|
278
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
279
|
+
throw new Error("pi isn't installed. Install it with `npm install -g @mariozechner/pi-coding-agent`, then run `codie --pi` again.");
|
|
280
|
+
}
|
|
281
|
+
if (result.error)
|
|
282
|
+
throw result.error;
|
|
283
|
+
return result.status === null ? 1 : result.status;
|
|
284
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PackManifest } from "./manifest";
|
|
2
2
|
import { RegistryEntry } from "./store";
|
|
3
|
-
export declare const HARNESSES: readonly ["claude", "copilot", "codex", "cursor", "codie"];
|
|
3
|
+
export declare const HARNESSES: readonly ["claude", "copilot", "codex", "cursor", "codie", "pi"];
|
|
4
4
|
export type HarnessName = (typeof HARNESSES)[number];
|
|
5
5
|
export interface RunResult {
|
|
6
6
|
status: number | null;
|
|
@@ -67,5 +67,8 @@ export declare function installCursorPlugin(home: string, slug: string, sourceDi
|
|
|
67
67
|
export declare function removeCursorPlugin(home: string, slug: string): boolean;
|
|
68
68
|
export declare const cursorAdapter: Adapter;
|
|
69
69
|
export declare const codieAdapter: Adapter;
|
|
70
|
+
/** pi has no plugin system; it loads extra skill dirs from settings.json
|
|
71
|
+
* `skills`. Point it at the portable copy (no Claude model aliases). */
|
|
72
|
+
export declare const piAdapter: Adapter;
|
|
70
73
|
export declare const ADAPTERS: Adapter[];
|
|
71
74
|
export declare function parseHarnessList(value: string | undefined): HarnessName[] | undefined;
|
package/dist/packs/harnesses.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ADAPTERS = exports.codieAdapter = exports.cursorAdapter = exports.CURSOR_MARKER = exports.codexAdapter = exports.copilotAdapter = exports.claudeAdapter = exports.HARNESSES = void 0;
|
|
36
|
+
exports.ADAPTERS = exports.piAdapter = exports.codieAdapter = exports.cursorAdapter = exports.CURSOR_MARKER = exports.codexAdapter = exports.copilotAdapter = exports.claudeAdapter = exports.HARNESSES = void 0;
|
|
37
37
|
exports.defaultRunner = defaultRunner;
|
|
38
38
|
exports.onPath = onPath;
|
|
39
39
|
exports.copyCursorSkills = copyCursorSkills;
|
|
@@ -51,7 +51,7 @@ const path = __importStar(require("path"));
|
|
|
51
51
|
const child_process_1 = require("child_process");
|
|
52
52
|
const harnessDirs_1 = require("../harnessDirs");
|
|
53
53
|
const paths_1 = require("./paths");
|
|
54
|
-
exports.HARNESSES = ["claude", "copilot", "codex", "cursor", "codie"];
|
|
54
|
+
exports.HARNESSES = ["claude", "copilot", "codex", "cursor", "codie", "pi"];
|
|
55
55
|
function defaultRunner(env) {
|
|
56
56
|
return (command, args, options = {}) => {
|
|
57
57
|
const result = (0, child_process_1.spawnSync)(command, args, {
|
|
@@ -395,7 +395,48 @@ exports.codieAdapter = {
|
|
|
395
395
|
plan: () => "inject skills/agents/hook shim at next codie launch",
|
|
396
396
|
uninstall: () => "removed from next `codie` launch",
|
|
397
397
|
};
|
|
398
|
-
|
|
398
|
+
function piSettingsPath(env, home) {
|
|
399
|
+
return path.join((0, harnessDirs_1.piAgentDir)(env, home), "settings.json");
|
|
400
|
+
}
|
|
401
|
+
/** Rewrite pi's `skills` list, keeping every other setting. Refuses a
|
|
402
|
+
* settings file it can't parse rather than replacing the user's config. */
|
|
403
|
+
function updatePiSkillPaths(file, update) {
|
|
404
|
+
let settings = {};
|
|
405
|
+
if (fs.existsSync(file)) {
|
|
406
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
407
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
408
|
+
throw new Error(`${file} is not a JSON object; leaving it alone`);
|
|
409
|
+
settings = parsed;
|
|
410
|
+
}
|
|
411
|
+
const current = Array.isArray(settings.skills) ? settings.skills.filter((p) => typeof p === "string") : [];
|
|
412
|
+
const next = update(current);
|
|
413
|
+
if (next.length === current.length && next.every((p, i) => p === current[i]))
|
|
414
|
+
return false;
|
|
415
|
+
settings.skills = next;
|
|
416
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
417
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
418
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
419
|
+
fs.renameSync(tmp, file);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
/** pi has no plugin system; it loads extra skill dirs from settings.json
|
|
423
|
+
* `skills`. Point it at the portable copy (no Claude model aliases). */
|
|
424
|
+
exports.piAdapter = {
|
|
425
|
+
name: "pi",
|
|
426
|
+
detect: (ctx) => onPath("pi", ctx.env) || fs.existsSync((0, harnessDirs_1.piAgentDir)(ctx.env, ctx.home)),
|
|
427
|
+
install: (ctx) => {
|
|
428
|
+
const skills = path.join(ctx.portablePluginDir, "skills");
|
|
429
|
+
const changed = updatePiSkillPaths(piSettingsPath(ctx.env, ctx.home), (paths) => paths.includes(skills) ? paths : [...paths, skills]);
|
|
430
|
+
return changed ? "installed" : "already installed";
|
|
431
|
+
},
|
|
432
|
+
plan: (ctx) => `add ${path.join(ctx.portablePluginDir, "skills")} to ${piSettingsPath(ctx.env, ctx.home)} skills`,
|
|
433
|
+
uninstall: (ctx) => {
|
|
434
|
+
const dir = (0, paths_1.pluginDir)((0, paths_1.portableRoot)(ctx.root), ctx.slug);
|
|
435
|
+
const changed = updatePiSkillPaths(piSettingsPath(ctx.env, ctx.home), (paths) => paths.filter((p) => p !== dir && !p.startsWith(dir + path.sep)));
|
|
436
|
+
return changed ? "uninstalled" : "already removed";
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
exports.ADAPTERS = [exports.claudeAdapter, exports.copilotAdapter, exports.codexAdapter, exports.cursorAdapter, exports.codieAdapter, exports.piAdapter];
|
|
399
440
|
function parseHarnessList(value) {
|
|
400
441
|
if (value === undefined)
|
|
401
442
|
return undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** customType of the context message the Codiedev extension injects. */
|
|
2
|
+
export declare const PI_CONTEXT_CUSTOM_TYPE = "codiedev";
|
|
3
|
+
/**
|
|
4
|
+
* pi session JSONL → Claude Code-shaped JSONL.
|
|
5
|
+
*
|
|
6
|
+
* Every message entry is kept in file order, abandoned /tree branches
|
|
7
|
+
* included: they were real model calls and real tokens. Usage is pi's own
|
|
8
|
+
* per-call record (`input` is already the uncached part). Skill runs, user
|
|
9
|
+
* interrupts and provider errors become the records the shared parsers read:
|
|
10
|
+
* `pi.skill_invoked`, Claude's interrupt marker and a native `agent.error`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizePiTranscript(content: string): string;
|
|
13
|
+
/** pi writes the session header at startup; no message yet means nothing to capture. */
|
|
14
|
+
export declare function isPiTranscriptEmpty(content: string): boolean;
|
|
15
|
+
/** How pi was launched, from its own argv (the extension runs in pi's process). */
|
|
16
|
+
export declare function piLaunchFromArgv(argv: string[]): string;
|
|
17
|
+
export declare function piExtensionPath(agentDir?: string): string;
|
|
18
|
+
/** What `doctor` and the security install check report for pi. */
|
|
19
|
+
export declare function inspectPiExtension(file?: string, security?: boolean): {
|
|
20
|
+
status: "pass" | "fail" | "not-installed";
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
package/dist/piHooks.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PI_CONTEXT_CUSTOM_TYPE = void 0;
|
|
37
|
+
exports.normalizePiTranscript = normalizePiTranscript;
|
|
38
|
+
exports.isPiTranscriptEmpty = isPiTranscriptEmpty;
|
|
39
|
+
exports.piLaunchFromArgv = piLaunchFromArgv;
|
|
40
|
+
exports.piExtensionPath = piExtensionPath;
|
|
41
|
+
exports.inspectPiExtension = inspectPiExtension;
|
|
42
|
+
// pi (@mariozechner/pi-coding-agent). pi has no hook config; it loads
|
|
43
|
+
// TypeScript extensions from <agentDir>/extensions/, so Codiedev installs one
|
|
44
|
+
// there (see installPiExtension in utils.ts) that forwards pi's events to the
|
|
45
|
+
// same hook subcommands every other harness uses.
|
|
46
|
+
//
|
|
47
|
+
// pi writes its own session JSONL (<agentDir>/sessions/--<cwd>--/<ts>_<id>.jsonl)
|
|
48
|
+
// and the extension passes that path as transcript_path. This file turns it
|
|
49
|
+
// into the Claude Code shape the shared capture pipeline parses.
|
|
50
|
+
//
|
|
51
|
+
// Imports only node built-ins and harnessDirs (no utils.ts) so hook.ts,
|
|
52
|
+
// utils.ts and securityInstall.ts can all import it without a cycle.
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const harnessDirs_1 = require("./harnessDirs");
|
|
56
|
+
/** customType of the context message the Codiedev extension injects. */
|
|
57
|
+
exports.PI_CONTEXT_CUSTOM_TYPE = "codiedev";
|
|
58
|
+
const SKILL_EXPANSION = /^<skill name="([^"]{1,200})" location="[^"]*">/;
|
|
59
|
+
const num = (n) => (typeof n === "number" && Number.isFinite(n) ? Math.max(0, n) : 0);
|
|
60
|
+
function textOf(content) {
|
|
61
|
+
if (typeof content === "string")
|
|
62
|
+
return content;
|
|
63
|
+
if (!Array.isArray(content))
|
|
64
|
+
return "";
|
|
65
|
+
return content
|
|
66
|
+
.map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join("\n");
|
|
69
|
+
}
|
|
70
|
+
function userBlocks(content) {
|
|
71
|
+
if (typeof content === "string")
|
|
72
|
+
return [{ type: "text", text: content }];
|
|
73
|
+
if (!Array.isArray(content))
|
|
74
|
+
return [];
|
|
75
|
+
return content.flatMap((block) => block?.type === "text" && typeof block.text === "string"
|
|
76
|
+
? [{ type: "text", text: block.text }]
|
|
77
|
+
// Image bytes never leave the machine; keep that one was attached.
|
|
78
|
+
: block?.type === "image" ? [{ type: "text", text: "[image]" }] : []);
|
|
79
|
+
}
|
|
80
|
+
/** `/x/skills/review/SKILL.md` → "review". pi's agent loads a listed skill by
|
|
81
|
+
* reading its SKILL.md; that read IS the invocation (docs/skills.md). */
|
|
82
|
+
function skillFromPath(value) {
|
|
83
|
+
if (typeof value !== "string")
|
|
84
|
+
return null;
|
|
85
|
+
const m = value.replace(/\\/g, "/").match(/\/([^/]+)\/SKILL\.md$/);
|
|
86
|
+
return m ? m[1] : null;
|
|
87
|
+
}
|
|
88
|
+
/** A provider error's HTTP status, when pi kept it at the front ("429 …"). */
|
|
89
|
+
function errorCode(message) {
|
|
90
|
+
return message.match(/^\s*(\d{3})\b/)?.[1] ?? "";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* pi session JSONL → Claude Code-shaped JSONL.
|
|
94
|
+
*
|
|
95
|
+
* Every message entry is kept in file order, abandoned /tree branches
|
|
96
|
+
* included: they were real model calls and real tokens. Usage is pi's own
|
|
97
|
+
* per-call record (`input` is already the uncached part). Skill runs, user
|
|
98
|
+
* interrupts and provider errors become the records the shared parsers read:
|
|
99
|
+
* `pi.skill_invoked`, Claude's interrupt marker and a native `agent.error`.
|
|
100
|
+
*/
|
|
101
|
+
function normalizePiTranscript(content) {
|
|
102
|
+
const out = [];
|
|
103
|
+
const skillReads = new Map();
|
|
104
|
+
// pi retries a failed provider call on its own (and fires agent_end after
|
|
105
|
+
// each try). Every error in one retry streak carries the streak's first
|
|
106
|
+
// time, so limit events, which dedupe on time, count the hit once.
|
|
107
|
+
let errorStreakAt;
|
|
108
|
+
const push = (record) => out.push(JSON.stringify(record));
|
|
109
|
+
for (const line of content.split("\n")) {
|
|
110
|
+
if (!line.trim())
|
|
111
|
+
continue;
|
|
112
|
+
let entry;
|
|
113
|
+
try {
|
|
114
|
+
entry = JSON.parse(line);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue; // pi may be mid-append
|
|
118
|
+
}
|
|
119
|
+
if (!entry || typeof entry !== "object")
|
|
120
|
+
continue;
|
|
121
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
|
|
122
|
+
const uuid = typeof entry.id === "string" ? entry.id : undefined;
|
|
123
|
+
if (entry.type !== "message" || !entry.message || typeof entry.message !== "object")
|
|
124
|
+
continue;
|
|
125
|
+
const message = entry.message;
|
|
126
|
+
if (!(message.role === "assistant" && message.stopReason === "error"))
|
|
127
|
+
errorStreakAt = undefined;
|
|
128
|
+
if (message.role === "user") {
|
|
129
|
+
const text = textOf(message.content);
|
|
130
|
+
const skill = text.match(SKILL_EXPANSION)?.[1];
|
|
131
|
+
if (skill)
|
|
132
|
+
push({ type: "pi.skill_invoked", timestamp, skill });
|
|
133
|
+
push({ type: "user", uuid, timestamp, message: { role: "user", content: userBlocks(message.content) } });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (message.role === "assistant") {
|
|
137
|
+
const blocks = [];
|
|
138
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
139
|
+
if (block?.type === "text" && typeof block.text === "string")
|
|
140
|
+
blocks.push({ type: "text", text: block.text });
|
|
141
|
+
else if (block?.type === "thinking" && typeof block.thinking === "string")
|
|
142
|
+
blocks.push({ type: "thinking", thinking: block.thinking });
|
|
143
|
+
else if (block?.type === "toolCall") {
|
|
144
|
+
const input = block.arguments && typeof block.arguments === "object" ? block.arguments : {};
|
|
145
|
+
blocks.push({ type: "tool_use", id: block.id, name: block.name ?? "tool", input });
|
|
146
|
+
const skill = block.name === "read" ? skillFromPath(input.path) : null;
|
|
147
|
+
if (skill && typeof block.id === "string")
|
|
148
|
+
skillReads.set(block.id, skill);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const usage = message.usage ?? {};
|
|
152
|
+
const tokens = num(usage.input) + num(usage.output) + num(usage.cacheRead) + num(usage.cacheWrite);
|
|
153
|
+
// A failed call records all-zero usage; it was not a billed request.
|
|
154
|
+
const measured = tokens > 0 || message.stopReason !== "error";
|
|
155
|
+
push({
|
|
156
|
+
type: "assistant",
|
|
157
|
+
uuid,
|
|
158
|
+
timestamp,
|
|
159
|
+
message: {
|
|
160
|
+
id: typeof message.responseId === "string" ? message.responseId : uuid,
|
|
161
|
+
role: "assistant",
|
|
162
|
+
model: typeof message.model === "string" ? message.model : undefined,
|
|
163
|
+
...(measured ? {
|
|
164
|
+
usage: {
|
|
165
|
+
input_tokens: num(usage.input),
|
|
166
|
+
output_tokens: num(usage.output),
|
|
167
|
+
cache_read_input_tokens: num(usage.cacheRead),
|
|
168
|
+
cache_creation_input_tokens: num(usage.cacheWrite),
|
|
169
|
+
},
|
|
170
|
+
} : {}),
|
|
171
|
+
content: blocks,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
if (message.stopReason === "aborted") {
|
|
175
|
+
push({ type: "user", timestamp, message: { role: "user", content: [{ type: "text", text: "[Request interrupted by user]" }] } });
|
|
176
|
+
}
|
|
177
|
+
if (message.stopReason === "error" && typeof message.errorMessage === "string") {
|
|
178
|
+
// The call's own time, not the entry's write time, so interim and
|
|
179
|
+
// final captures agree.
|
|
180
|
+
errorStreakAt ??= typeof message.timestamp === "number" ? new Date(message.timestamp).toISOString() : timestamp;
|
|
181
|
+
push({
|
|
182
|
+
type: "agent.error",
|
|
183
|
+
timestamp: errorStreakAt,
|
|
184
|
+
data: { errorContext: "model_call", error: { code: errorCode(message.errorMessage), message: message.errorMessage } },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (message.role === "toolResult") {
|
|
190
|
+
const id = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
191
|
+
const skill = id ? skillReads.get(id) : undefined;
|
|
192
|
+
if (skill && message.isError !== true)
|
|
193
|
+
push({ type: "pi.skill_invoked", timestamp, skill });
|
|
194
|
+
push({
|
|
195
|
+
type: "user",
|
|
196
|
+
uuid,
|
|
197
|
+
timestamp,
|
|
198
|
+
message: {
|
|
199
|
+
role: "user",
|
|
200
|
+
content: [{ type: "tool_result", tool_use_id: id, content: textOf(message.content), is_error: message.isError === true }],
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// `!cmd` typed by the user: shown to the model like any user turn.
|
|
206
|
+
if (message.role === "bashExecution" && typeof message.command === "string") {
|
|
207
|
+
push({ type: "user", uuid, timestamp, message: { role: "user", content: [{ type: "text", text: `<bash-input>${message.command}</bash-input>` }] } });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out.join("\n") + (out.length ? "\n" : "");
|
|
211
|
+
}
|
|
212
|
+
/** pi writes the session header at startup; no message yet means nothing to capture. */
|
|
213
|
+
function isPiTranscriptEmpty(content) {
|
|
214
|
+
return !content.split("\n").some((line) => line.includes('"type":"message"'));
|
|
215
|
+
}
|
|
216
|
+
/** How pi was launched, from its own argv (the extension runs in pi's process). */
|
|
217
|
+
function piLaunchFromArgv(argv) {
|
|
218
|
+
for (let i = 0; i < argv.length; i++) {
|
|
219
|
+
const a = argv[i];
|
|
220
|
+
if (a === "-p" || a === "--print")
|
|
221
|
+
return "pi:print";
|
|
222
|
+
if (a === "--mode") {
|
|
223
|
+
const mode = argv[i + 1];
|
|
224
|
+
if (mode === "json" || mode === "rpc")
|
|
225
|
+
return `pi:${mode}`;
|
|
226
|
+
}
|
|
227
|
+
if (a === "--mode=json" || a === "--mode=rpc")
|
|
228
|
+
return `pi:${a.slice(7)}`;
|
|
229
|
+
}
|
|
230
|
+
return "pi:tui";
|
|
231
|
+
}
|
|
232
|
+
function piExtensionPath(agentDir = (0, harnessDirs_1.piAgentDir)()) {
|
|
233
|
+
return path.join(agentDir, "extensions", "codiedev.ts");
|
|
234
|
+
}
|
|
235
|
+
/** What `doctor` and the security install check report for pi. */
|
|
236
|
+
function inspectPiExtension(file = piExtensionPath(), security = false) {
|
|
237
|
+
if (!fs.existsSync(file))
|
|
238
|
+
return { status: "not-installed", detail: `${file} not found` };
|
|
239
|
+
let source;
|
|
240
|
+
try {
|
|
241
|
+
source = fs.readFileSync(file, "utf8");
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return { status: "fail", detail: `${file} unreadable` };
|
|
245
|
+
}
|
|
246
|
+
const expected = security
|
|
247
|
+
? ["discover-pi", "security-pre-pi", "security-post-pi"]
|
|
248
|
+
: ["session-start-pi", "backup-pi", "capture-pi-final"];
|
|
249
|
+
// Quoted: "backup-pi" must not match inside some longer subcommand.
|
|
250
|
+
const missing = expected.filter((subcommand) => !source.includes(JSON.stringify(subcommand)));
|
|
251
|
+
if (missing.length)
|
|
252
|
+
return { status: "fail", detail: `missing ${missing.join(", ")} — re-run \`codiedev connect\`` };
|
|
253
|
+
if (security && source.includes("const AGENT_SECURITY_ENABLED = false;")) {
|
|
254
|
+
return { status: "fail", detail: "installed with agent security off — re-run `codiedev connect`" };
|
|
255
|
+
}
|
|
256
|
+
return { status: "pass", detail: security ? "prompt guard and tool telemetry installed" : `session start, capture and backup events (${(0, harnessDirs_1.displayPath)(file)})` };
|
|
257
|
+
}
|