@ory/pi 0.10.0

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.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Setup CLI for the Ory Pi plugin.
5
+ *
6
+ * Pi auto-discovers in-process extensions from `<cwd>/.pi/extensions/` (project
7
+ * scope) and `~/.pi/agent/extensions/` (global scope): every `*.ts`/`*.js` file
8
+ * placed there is loaded via jiti and its default export is invoked as the
9
+ * extension factory. A bare module id in `settings.json`'s `extensions` array is
10
+ * NOT loaded — those entries are resolved as filesystem paths.
11
+ *
12
+ * So this writer drops a tiny loader file that re-exports the installed
13
+ * `@ory/pi` package's default factory:
14
+ *
15
+ * // .pi/extensions/ory.js
16
+ * export { default } from "@ory/pi";
17
+ *
18
+ * Pi discovers the file, jiti resolves `@ory/pi` from `node_modules`, and the
19
+ * factory runs. Project scope writes `<projectDir>/.pi/extensions/ory.js`;
20
+ * global scope (`--global`) writes `~/.pi/agent/extensions/ory.js`. No
21
+ * `settings.json` entry is required — discovery alone enables the extension.
22
+ *
23
+ * Pi excludes MCP by design, so there is NO MCP server block.
24
+ *
25
+ * Usage:
26
+ * npx ory-pi-setup # Install loader into project .pi/extensions/
27
+ * npx ory-pi-setup --global # Install loader into ~/.pi/agent/extensions/
28
+ * npx ory-pi-setup --project-dir /path/to/repo
29
+ * npx ory-pi-setup --print # Print the loader contents to stdout
30
+ * npx ory-pi-setup --uninstall # Remove the Ory loader
31
+ *
32
+ * Verified against @earendil-works/pi-coding-agent v0.80.2 (the extension
33
+ * loader's `discoverAndLoadExtensions` / `discoverExtensionsInDir`).
34
+ */
35
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ var desc = Object.getOwnPropertyDescriptor(m, k);
38
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
39
+ desc = { enumerable: true, get: function() { return m[k]; } };
40
+ }
41
+ Object.defineProperty(o, k2, desc);
42
+ }) : (function(o, m, k, k2) {
43
+ if (k2 === undefined) k2 = k;
44
+ o[k2] = m[k];
45
+ }));
46
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
47
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
48
+ }) : function(o, v) {
49
+ o["default"] = v;
50
+ });
51
+ var __importStar = (this && this.__importStar) || (function () {
52
+ var ownKeys = function(o) {
53
+ ownKeys = Object.getOwnPropertyNames || function (o) {
54
+ var ar = [];
55
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
56
+ return ar;
57
+ };
58
+ return ownKeys(o);
59
+ };
60
+ return function (mod) {
61
+ if (mod && mod.__esModule) return mod;
62
+ var result = {};
63
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
64
+ __setModuleDefault(result, mod);
65
+ return result;
66
+ };
67
+ })();
68
+ Object.defineProperty(exports, "__esModule", { value: true });
69
+ const path = __importStar(require("node:path"));
70
+ const argus_1 = require("@ory/argus");
71
+ const assets_js_1 = require("./assets.js");
72
+ const loader_js_1 = require("./loader.js");
73
+ function main() {
74
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
75
+ (0, argus_1.printSetupHelp)("ory-pi-setup", "Pi");
76
+ process.exit(0);
77
+ }
78
+ const args = (0, argus_1.parseSetupArgs)({ supportsGlobal: true });
79
+ if (args.print) {
80
+ console.log(loader_js_1.LOADER_CONTENTS);
81
+ return;
82
+ }
83
+ if (args.uninstall) {
84
+ const removed = (0, loader_js_1.removeOryLoader)(args);
85
+ console.log(removed
86
+ ? "Removed Ory extension loader."
87
+ : "No Ory extension loader found. Nothing to uninstall.");
88
+ (0, assets_js_1.uninstallPiOryAssets)(args.projectDir);
89
+ console.log(`Removed Ory skills from ${path.join(args.projectDir, ".pi", "skills")}`);
90
+ return;
91
+ }
92
+ const loaderPath = (0, loader_js_1.writeOryLoader)(args);
93
+ (0, assets_js_1.installPiOryAssets)(args.projectDir);
94
+ console.log(`Ory extension loader installed at ${loaderPath}`);
95
+ console.log(`Ory skills installed to ${path.join(args.projectDir, ".pi", "skills")}`);
96
+ (0, argus_1.printNextSteps)("Pi", "npx ory-pi-setup --uninstall");
97
+ }
98
+ main();
@@ -0,0 +1,6 @@
1
+ import { createOryPlugin } from "./plugin.js";
2
+ export { createOryPlugin };
3
+ export type { CreateOryPluginDeps } from "./plugin.js";
4
+ export type { PiExtension, PiExtensionApi, PiEvent, PiEventHandlers, PiToolCallContext, PiToolResultContext, PiToolCallResult, PiHookRuntimeContext, } from "./types.js";
5
+ declare const _default: import("./types.js").PiExtension;
6
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOryPlugin = void 0;
4
+ const plugin_js_1 = require("./plugin.js");
5
+ Object.defineProperty(exports, "createOryPlugin", { enumerable: true, get: function () { return plugin_js_1.createOryPlugin; } });
6
+ // Pi loads in-process extensions via jiti and expects a default-exported
7
+ // factory function `(pi) => void | Promise<void>`. CJS output (module:
8
+ // nodenext → CJS) surfaces `module.exports` as the ESM `default`, so a single
9
+ // default export is what Pi's loader receives. Instantiate the plugin from env.
10
+ // eslint-disable-next-line import/no-default-export
11
+ exports.default = (0, plugin_js_1.createOryPlugin)();
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pi in-process extension (auth + blocking permission gate + tracing).
3
+ *
4
+ * Pi loads a default-exported factory `(pi) => void | Promise<void>` from a
5
+ * TypeScript extension module (via jiti) and awaits it before startup
6
+ * continues. We treat the factory body as the session-start hook: it runs the
7
+ * user gate (advisory — there is no block channel for the session itself), the
8
+ * agent gate, and writes the user→agent delegation tuple.
9
+ *
10
+ * The blocking permission decision lives in the `tool_call` hook, which CAN
11
+ * block by returning `{ block: true, reason }` (like OpenClaw's
12
+ * `before_tool_call`). The `tool_result` hook records the `tool.complete`
13
+ * audit span after a tool runs.
14
+ *
15
+ * Pi excludes MCP and sub-agents by design, so there is NO MCP tool parsing
16
+ * and NO sub-agent identity path.
17
+ *
18
+ * Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
19
+ * core/extensions/types.d.ts and the extension loader): the `tool_call`
20
+ * handler returns `ToolCallEventResult { block?: boolean; reason? }`,
21
+ * `tool_result` carries `isError`/`content`, and Pi awaits the async factory
22
+ * default export.
23
+ */
24
+ import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
25
+ import type { PiExtension } from "./types.js";
26
+ export interface CreateOryPluginDeps {
27
+ /** Test injection point for the user login flow. */
28
+ userLogin?: typeof ensureUserAuthenticated;
29
+ /** Test injection point for the agent identity gate. */
30
+ agentGate?: typeof ensureAgentIdentity;
31
+ }
32
+ /**
33
+ * Create the Ory extension factory for Pi.
34
+ *
35
+ * Returns a function matching Pi's `PiExtension` contract: it receives the
36
+ * ExtensionAPI, runs the session-start auth gates in the body, and registers
37
+ * the `tool_call` (blocking) and `tool_result` (tracing) hooks.
38
+ */
39
+ export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
40
+ projectUrl: string;
41
+ apiKey?: string;
42
+ }, deps?: CreateOryPluginDeps): PiExtension;
package/dist/plugin.js ADDED
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ /**
3
+ * Pi in-process extension (auth + blocking permission gate + tracing).
4
+ *
5
+ * Pi loads a default-exported factory `(pi) => void | Promise<void>` from a
6
+ * TypeScript extension module (via jiti) and awaits it before startup
7
+ * continues. We treat the factory body as the session-start hook: it runs the
8
+ * user gate (advisory — there is no block channel for the session itself), the
9
+ * agent gate, and writes the user→agent delegation tuple.
10
+ *
11
+ * The blocking permission decision lives in the `tool_call` hook, which CAN
12
+ * block by returning `{ block: true, reason }` (like OpenClaw's
13
+ * `before_tool_call`). The `tool_result` hook records the `tool.complete`
14
+ * audit span after a tool runs.
15
+ *
16
+ * Pi excludes MCP and sub-agents by design, so there is NO MCP tool parsing
17
+ * and NO sub-agent identity path.
18
+ *
19
+ * Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
20
+ * core/extensions/types.d.ts and the extension loader): the `tool_call`
21
+ * handler returns `ToolCallEventResult { block?: boolean; reason? }`,
22
+ * `tool_result` carries `isError`/`content`, and Pi awaits the async factory
23
+ * default export.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createOryPlugin = createOryPlugin;
27
+ const argus_1 = require("@ory/argus");
28
+ /**
29
+ * Create the Ory extension factory for Pi.
30
+ *
31
+ * Returns a function matching Pi's `PiExtension` contract: it receives the
32
+ * ExtensionAPI, runs the session-start auth gates in the body, and registers
33
+ * the `tool_call` (blocking) and `tool_result` (tracing) hooks.
34
+ */
35
+ function createOryPlugin(clientOrConfig, deps = {}) {
36
+ return async (pi) => {
37
+ const client = clientOrConfig instanceof argus_1.OryAgentClient
38
+ ? clientOrConfig
39
+ : clientOrConfig
40
+ ? new argus_1.OryAgentClient({ ...clientOrConfig, harness: "pi" })
41
+ : argus_1.OryAgentClient.fromEnv("pi");
42
+ // The factory body IS the session-start hook: Pi awaits async extension
43
+ // factories before startup continues (verified against
44
+ // @earendil-works/pi-coding-agent v0.80.2). Pi also exposes a dedicated
45
+ // `session_start` event, but the factory body is sufficient and runs
46
+ // exactly once per process.
47
+ await runSessionStart(client, deps);
48
+ pi.on("tool_call", createToolCallHandler(client));
49
+ pi.on("tool_result", createToolResultHandler(client));
50
+ };
51
+ }
52
+ // ─── session start (factory body) ──────────────────────────────────
53
+ async function runSessionStart(client, deps) {
54
+ client.logger.info("lifecycle.session_start", { harness: "pi" });
55
+ client.tracer.setContext({ traceId: (0, argus_1.deriveTraceId)("pi"), sessionId: "pi" });
56
+ client.tracer.record("session.start", "ok", { attributes: {} });
57
+ // Run the user login. Pi's factory body has no return-value channel to
58
+ // block the session, so allowBlock is false — the flow emits the user.auth
59
+ // audit span, refreshes tokens, and may prompt when interactive, but never
60
+ // prevents the session from starting. When ORY_USER_LOGIN is unset it is a
61
+ // no-op (mode === "disabled") and we fall through to legacy verification.
62
+ const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
63
+ const decision = await userGate(client, {
64
+ binName: "ory-pi",
65
+ harness: "pi",
66
+ allowBlock: false,
67
+ });
68
+ // Resolve the agent identity (machine credentials). Never blocks; attaches
69
+ // the agent's bearer token to outgoing Ory API calls.
70
+ const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
71
+ await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "pi" });
72
+ // Once both principals are populated, write the user→agent delegation tuple.
73
+ // Idempotent + fail-open: audit-trail data only.
74
+ await recordUserDelegatesAgent(client);
75
+ if (decision.mode !== "disabled") {
76
+ return;
77
+ }
78
+ const resolved = (0, argus_1.resolveConfig)();
79
+ if (resolved.auditOnly) {
80
+ client.logger.info("config.audit_only", {
81
+ message: "Audit-only mode enabled. Auth and permission checks are disabled.",
82
+ });
83
+ return;
84
+ }
85
+ if (!resolved.projectUrl) {
86
+ client.logger.warn("config.not_configured", {
87
+ message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
88
+ "Run 'npx ory-pi configure' to connect to an Ory project.",
89
+ });
90
+ return;
91
+ }
92
+ const sessionToken = process.env.ORY_SESSION_TOKEN;
93
+ const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
94
+ if (sessionToken) {
95
+ await verifySessionToken(client, sessionToken);
96
+ return;
97
+ }
98
+ if (oauth2Token) {
99
+ await verifyOAuth2Token(client, oauth2Token);
100
+ return;
101
+ }
102
+ client.logger.warn("session.no_credentials", {
103
+ message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
104
+ "Skipping authentication.",
105
+ });
106
+ }
107
+ async function verifySessionToken(client, token) {
108
+ try {
109
+ const session = await client.verifySession(token);
110
+ if (!session.active) {
111
+ client.logger.warn("session.inactive", {
112
+ message: "Ory session is not active. Re-authenticate to enable auth checks.",
113
+ });
114
+ }
115
+ }
116
+ catch (err) {
117
+ client.logger.warn("session.verify_failed", {
118
+ code: isOryError(err) ? err.code : "unknown",
119
+ message: err instanceof Error ? err.message : String(err),
120
+ });
121
+ }
122
+ }
123
+ async function verifyOAuth2Token(client, token) {
124
+ try {
125
+ const tokenInfo = await client.introspectToken(token);
126
+ if (!tokenInfo.active) {
127
+ client.logger.warn("oauth2.token_inactive", {
128
+ message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
129
+ });
130
+ return;
131
+ }
132
+ client.logger.info("oauth2.session_authenticated", {
133
+ clientId: tokenInfo.clientId,
134
+ subject: tokenInfo.subject,
135
+ });
136
+ }
137
+ catch (err) {
138
+ client.logger.warn("oauth2.introspect_failed", {
139
+ code: isOryError(err) ? err.code : "unknown",
140
+ message: err instanceof Error ? err.message : String(err),
141
+ });
142
+ }
143
+ }
144
+ // ─── tool_call (pre-execution, blocking) ───────────────────────────
145
+ function createToolCallHandler(client) {
146
+ return async (event) => {
147
+ const toolName = event.toolName ?? "unknown";
148
+ // Pi does not put a session id on the tool_call event; the per-process
149
+ // trace context set at session start ("pi") carries correlation. We tag
150
+ // the span with the toolCallId so a tool_call lines up with its later
151
+ // tool_result.
152
+ client.logger.info("lifecycle.tool_call", { toolName, toolCallId: event.toolCallId });
153
+ client.tracer.setContext({ traceId: (0, argus_1.deriveTraceId)("pi"), sessionId: "pi" });
154
+ const inputSummary = (0, argus_1.summarizeToolInput)(toolName, event.input);
155
+ // In audit-only mode, log the invocation but skip permission checks.
156
+ if ((0, argus_1.resolveConfig)().auditOnly) {
157
+ client.tracer.record("tool.invoke", "ok", {
158
+ attributes: { toolName, ...inputSummary },
159
+ });
160
+ return;
161
+ }
162
+ // If Ory is not configured, pass through.
163
+ if (!(0, argus_1.resolveConfig)().projectUrl) {
164
+ client.tracer.record("tool.invoke", "skipped", {
165
+ attributes: { toolName, reason: "not_configured", ...inputSummary },
166
+ });
167
+ return;
168
+ }
169
+ const subject = (0, argus_1.resolveUserSubject)(client, "agent:pi");
170
+ const subjectId = (0, argus_1.subjectLabel)(subject);
171
+ const namespace = resolveNamespace();
172
+ try {
173
+ const outcome = await (0, argus_1.gateToolCall)(client, {
174
+ harness: "pi",
175
+ toolName,
176
+ check: { namespace, object: toolName, relation: "use", ...subject },
177
+ spanAttributes: { toolName },
178
+ });
179
+ // Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
180
+ // the user.interaction span is already recorded; pass through.
181
+ if (outcome.kind === "interactive") {
182
+ return;
183
+ }
184
+ const decision = outcome;
185
+ if (decision.kind === "fail_open") {
186
+ handlePermissionError(decision.error, toolName, client);
187
+ return;
188
+ }
189
+ const attrs = { toolName, ...inputSummary };
190
+ const decisionAttrs = decision.spanAttributes;
191
+ if (decision.kind === "allow") {
192
+ client.tracer.record("tool.invoke", "ok", {
193
+ attributes: { ...attrs, ...decisionAttrs, allowed: true },
194
+ });
195
+ return;
196
+ }
197
+ if (decision.kind === "observe") {
198
+ client.tracer.record("tool.block", "denied", {
199
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
200
+ });
201
+ client.tracer.record("tool.invoke", "ok", {
202
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
203
+ });
204
+ return;
205
+ }
206
+ // decision.kind === "deny" — enforce: block the tool call.
207
+ client.tracer.record("tool.block", "denied", {
208
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
209
+ });
210
+ const reason = (0, argus_1.formatDenialMessage)({
211
+ tool: toolName,
212
+ subjectId,
213
+ namespace,
214
+ });
215
+ client.logger.warn("tool.denied", {
216
+ toolName,
217
+ subjectId,
218
+ message: reason,
219
+ });
220
+ return { block: true, reason };
221
+ }
222
+ catch (err) {
223
+ handlePermissionError(err, toolName, client);
224
+ // Fail open — allow the tool call.
225
+ }
226
+ };
227
+ }
228
+ // ─── tool_result (post-execution, tracing) ─────────────────────────
229
+ function createToolResultHandler(client) {
230
+ return async (event) => {
231
+ const toolName = event.toolName ?? "unknown";
232
+ client.tracer.setContext({ traceId: (0, argus_1.deriveTraceId)("pi"), sessionId: "pi" });
233
+ // Pi reports failure via the `isError` boolean and the result payload via
234
+ // `content` (verified shape), not `error`/`result`.
235
+ const isError = event.isError === true;
236
+ client.logger.info("lifecycle.tool_result", {
237
+ toolName,
238
+ toolCallId: event.toolCallId,
239
+ isError,
240
+ });
241
+ client.tracer.record("tool.complete", isError ? "error" : "ok", {
242
+ attributes: {
243
+ toolName,
244
+ ...(0, argus_1.summarizeToolInput)(toolName, event.input),
245
+ ...(0, argus_1.summarizeToolOutput)(toolName, event.content),
246
+ ...(isError ? { error: true } : {}),
247
+ },
248
+ });
249
+ };
250
+ }
251
+ // ─── Helpers ───────────────────────────────────────────────────────
252
+ function resolveNamespace() {
253
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
254
+ }
255
+ function isOryError(err) {
256
+ return (typeof err === "object" &&
257
+ err !== null &&
258
+ "code" in err &&
259
+ "message" in err);
260
+ }
261
+ function handlePermissionError(err, toolName, client) {
262
+ if (isOryError(err)) {
263
+ if (err.code === "network_error" || err.code === "rate_limited") {
264
+ client.logger.warn("permission.fallback", {
265
+ toolName,
266
+ code: err.code,
267
+ message: "Failing open",
268
+ });
269
+ return;
270
+ }
271
+ client.logger.error("permission.check.error", {
272
+ toolName,
273
+ code: err.code,
274
+ message: err.message,
275
+ });
276
+ }
277
+ else {
278
+ client.logger.error("permission.check.error", {
279
+ toolName,
280
+ error: err instanceof Error ? err.message : String(err),
281
+ });
282
+ }
283
+ // Fail-open is log-and-allow: no tool.invoke span, matching the core
284
+ // gate() vocabulary. The errored permission.check span carries the audit
285
+ // trail for the failed check itself.
286
+ }
287
+ /**
288
+ * Write the user→agent delegation tuple. Idempotent and fail-open: requires
289
+ * both principal subjects to be populated; any error (including an
290
+ * unconfigured projectUrl, which manifests as a network_error) is logged and
291
+ * swallowed. Purely audit-trail data — does not affect enforcement.
292
+ */
293
+ async function recordUserDelegatesAgent(client) {
294
+ const user = client.userPrincipal.subject;
295
+ const agent = client.agentPrincipal.subject;
296
+ if (!user || !agent) {
297
+ client.logger.debug("delegation.skip", {
298
+ reason: "missing principal",
299
+ hasUser: !!user,
300
+ hasAgent: !!agent,
301
+ });
302
+ return;
303
+ }
304
+ try {
305
+ await client.createRelationship({
306
+ namespace: resolveNamespace(),
307
+ object: `agent:${agent}`,
308
+ relation: "delegate",
309
+ subjectId: `user:${user}`,
310
+ }, { spanAttributes: { delegation: "user-to-agent" } });
311
+ }
312
+ catch (err) {
313
+ const oryErr = err;
314
+ client.logger.warn("delegation.user_to_agent.failed", {
315
+ code: oryErr.code,
316
+ message: oryErr.message,
317
+ });
318
+ }
319
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Pi (the minimal coding agent — pi.dev / github.com/badlogic/pi-mono) plugin
3
+ * types.
4
+ *
5
+ * Pi loads in-process TypeScript extensions via jiti. The module's DEFAULT
6
+ * EXPORT is a factory function `(pi: ExtensionAPI) => void | Promise<void>`.
7
+ * Pi awaits async factories before startup continues, so the factory body is
8
+ * the de-facto session-start hook: we run the auth gates inline there.
9
+ *
10
+ * Tool hooks are registered with `pi.on(event, handler)`. The pre-execution
11
+ * `tool_call` hook BLOCKS a tool by returning `{ block: true, reason }`;
12
+ * returning nothing/undefined allows. A best-effort `tool_result` hook fires
13
+ * after a tool completes and is used for audit tracing.
14
+ *
15
+ * Pi deliberately excludes MCP and sub-agents, so there is no MCP tool
16
+ * parsing and no sub-agent identity path.
17
+ *
18
+ * Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
19
+ * core/extensions/types.d.ts). The `tool_call` event "Fired before a tool
20
+ * executes. Can block." and a handler returns
21
+ * `ToolCallEventResult { block?: boolean; reason?: string }`; the
22
+ * `tool_result` event "Fired after a tool executes." carries `isError` and
23
+ * `content`. The factory default export receives an `ExtensionAPI`.
24
+ */
25
+ /**
26
+ * Context delivered to a `tool_call` handler (pre-execution).
27
+ *
28
+ * Verified shape: `{ type: "tool_call", toolCallId, toolName, input }`.
29
+ * `input` is mutable in place to patch tool arguments; we only read it. Pi
30
+ * does not put a session id on this event, so the plugin derives the
31
+ * permission subject from a stable per-process fallback.
32
+ */
33
+ export interface PiToolCallContext {
34
+ /** Discriminant: always "tool_call". */
35
+ type?: "tool_call";
36
+ /** Correlates the call with its later `tool_result`. */
37
+ toolCallId?: string;
38
+ /** Tool being invoked. */
39
+ toolName?: string;
40
+ /** Tool parameters (mutable in place per Pi; the gate only reads them). */
41
+ input?: Record<string, unknown>;
42
+ [key: string]: unknown;
43
+ }
44
+ /**
45
+ * Context delivered to a `tool_result` handler (post-execution).
46
+ *
47
+ * Verified shape: `{ type: "tool_result", toolCallId, toolName, input,
48
+ * content, isError, details }`. Note Pi reports failure via the `isError`
49
+ * boolean and the result payload via `content` (not `error`/`result`).
50
+ */
51
+ export interface PiToolResultContext {
52
+ /** Discriminant: always "tool_result". */
53
+ type?: "tool_result";
54
+ /** Correlates with the originating `tool_call`. */
55
+ toolCallId?: string;
56
+ toolName?: string;
57
+ input?: Record<string, unknown>;
58
+ /** Tool output blocks (text / images). */
59
+ content?: unknown;
60
+ /** True when the tool returned an error result. */
61
+ isError?: boolean;
62
+ /** Tool-specific structured details, when present. */
63
+ details?: unknown;
64
+ [key: string]: unknown;
65
+ }
66
+ /**
67
+ * The ExtensionContext Pi passes as the second argument to a hook (`ctx`).
68
+ * Modeled minimally; the plugin does not depend on it beyond optional UI.
69
+ */
70
+ export interface PiHookRuntimeContext {
71
+ ui?: {
72
+ confirm?: (message: string) => boolean | Promise<boolean>;
73
+ [key: string]: unknown;
74
+ };
75
+ [key: string]: unknown;
76
+ }
77
+ /**
78
+ * The result a `tool_call` handler returns to block a tool (Pi's
79
+ * `ToolCallEventResult`). Returning nothing/undefined allows the tool
80
+ * through. Verified: `{ block?: boolean; reason?: string }`.
81
+ */
82
+ export interface PiToolCallResult {
83
+ block: true;
84
+ reason?: string;
85
+ }
86
+ /**
87
+ * Map of Pi extension event name → handler signature. Handlers may be async.
88
+ * Verified against Pi's `ExtensionAPI.on(...)` overloads.
89
+ */
90
+ export interface PiEventHandlers {
91
+ tool_call: (event: PiToolCallContext, ctx?: PiHookRuntimeContext) => void | PiToolCallResult | Promise<void | PiToolCallResult>;
92
+ tool_result: (event: PiToolResultContext, ctx?: PiHookRuntimeContext) => void | Promise<void>;
93
+ }
94
+ export type PiEvent = keyof PiEventHandlers;
95
+ /**
96
+ * The ExtensionAPI object Pi passes to the default-exported factory.
97
+ * Verified against Pi's exported `ExtensionAPI` interface (a superset — we
98
+ * model only the members this plugin uses).
99
+ */
100
+ export interface PiExtensionApi {
101
+ /** Subscribe to a lifecycle event. */
102
+ on<E extends PiEvent>(event: E, handler: PiEventHandlers[E]): void;
103
+ /** Register a custom tool. Unused by this plugin; modeled for completeness. */
104
+ registerTool?: (name: string, def: unknown) => void;
105
+ /** Register a custom command. Unused by this plugin; modeled for completeness. */
106
+ registerCommand?: (name: string, def: unknown) => void;
107
+ [key: string]: unknown;
108
+ }
109
+ /**
110
+ * The default-exported extension factory Pi invokes at load time. Pi awaits
111
+ * the returned promise before startup continues, so it is safe to run the
112
+ * session-start auth gates in the body.
113
+ */
114
+ export type PiExtension = (pi: PiExtensionApi) => void | Promise<void>;
package/dist/types.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * Pi (the minimal coding agent — pi.dev / github.com/badlogic/pi-mono) plugin
4
+ * types.
5
+ *
6
+ * Pi loads in-process TypeScript extensions via jiti. The module's DEFAULT
7
+ * EXPORT is a factory function `(pi: ExtensionAPI) => void | Promise<void>`.
8
+ * Pi awaits async factories before startup continues, so the factory body is
9
+ * the de-facto session-start hook: we run the auth gates inline there.
10
+ *
11
+ * Tool hooks are registered with `pi.on(event, handler)`. The pre-execution
12
+ * `tool_call` hook BLOCKS a tool by returning `{ block: true, reason }`;
13
+ * returning nothing/undefined allows. A best-effort `tool_result` hook fires
14
+ * after a tool completes and is used for audit tracing.
15
+ *
16
+ * Pi deliberately excludes MCP and sub-agents, so there is no MCP tool
17
+ * parsing and no sub-agent identity path.
18
+ *
19
+ * Contract verified against @earendil-works/pi-coding-agent v0.80.2 (dist
20
+ * core/extensions/types.d.ts). The `tool_call` event "Fired before a tool
21
+ * executes. Can block." and a handler returns
22
+ * `ToolCallEventResult { block?: boolean; reason?: string }`; the
23
+ * `tool_result` event "Fired after a tool executes." carries `isError` and
24
+ * `content`. The factory default export receives an `ExtensionAPI`.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });