pi-runline 0.4.0 → 0.5.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Code mode for [pi](https://github.com/mariozechner/pi).
4
4
 
5
- An extension that plugs [runline](https://www.npmjs.com/package/runline) into coding agents. The agent gets two native tools, a fuzzy picker for choosing which of the 188 built-in plugins to expose, and a guided credential prompt for the ones it hasn't seen before.
5
+ An extension that plugs [runline](https://www.npmjs.com/package/runline) into coding agents. The agent gets one native tool, in-sandbox action discovery, a fuzzy picker for choosing which of the 188 built-in plugins to expose, and a guided credential prompt for the ones it hasn't seen before.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,13 +10,32 @@ An extension that plugs [runline](https://www.npmjs.com/package/runline) into co
10
10
  pi install pi-runline
11
11
  ```
12
12
 
13
- ## Tools
13
+ ## How the agent uses it
14
14
 
15
- On session start, if the current working directory has a `.runline/` (or one is configured globally — see below), the extension registers two tools and injects the selected plugin catalog into the agent's context:
15
+ On session start, if the current working directory has a `.runline/` (or one is configured globally — see below), the extension registers a single tool and injects a short primer naming the enabled plugins:
16
16
 
17
- - **`list_runline_actions`** — enumerate available actions with input schemas. Optional `plugin` filter.
18
17
  - **`execute_runline`** — run JavaScript in runline's QuickJS sandbox. Every enabled plugin is a top-level global; `return` surfaces the result; logs are captured.
19
18
 
19
+ Discovery happens **inside the sandbox**, not as a separate tool. The agent uses an `actions` object to explore the catalog without paying for a full schema dump in its system prompt:
20
+
21
+ ```js
22
+ actions.list() // every "plugin.action" path
23
+ actions.list("github") // filter to one plugin
24
+ actions.find("create issue") // ranked fuzzy search (MiniSearch)
25
+ actions.describe("github.issue.create")
26
+ // → { path, plugin, action, description, signature, inputs }
27
+ actions.check("github.issue.create", { owner: "a" })
28
+ // → { ok, missing, unknown, typeErrors, signature } (does NOT call the action)
29
+ ```
30
+
31
+ Unknown paths throw with did-you-mean suggestions, so typos are self-correcting. Recommended flow: `find` → `describe` → `check` → call.
32
+
33
+ Calling actions is unchanged — plugin globals and `actions.<plugin>.<action>(...)` are the same call:
34
+
35
+ ```js
36
+ return await github.issue.create({ owner: "acme", repo: "api", title: "Bug" });
37
+ ```
38
+
20
39
  ## `/runline-plugins` — the picker
21
40
 
22
41
  Typing `/runline-plugins` in a pi session opens a fuzzy multi-select over all 188 built-in plugins.
@@ -66,6 +85,6 @@ Fallback used when the current cwd has no `.runline/` anywhere up the tree. Usef
66
85
 
67
86
  ## How plugin allow-listing works
68
87
 
69
- The extension deliberately exposes nothing by default. That's on purpose — 2,410 actions is a lot of context budget. You pick the plugins that matter for a given project, commit the allowlist to `.runline/config.json`, and the agent only ever sees (and can only discover via `list_runline_actions`) the ones you enabled.
88
+ The extension deliberately exposes nothing by default. That's on purpose — 2,410 actions is a lot of context budget. You pick the plugins that matter for a given project, commit the allowlist to `.runline/config.json`, and the agent only ever sees the ones you enabled in its primer.
70
89
 
71
- Note that the QuickJS sandbox itself still registers every runline plugin as a global, so in principle an agent could guess and call a disabled plugin. In practice the catalog injection and `list_runline_actions` output are the only surface the agent knows about, and unconfigured plugins error out at first action call anyway. Plumbing the allowlist into the sandbox globals is on the roadmap.
90
+ Note that the QuickJS sandbox itself still registers every runline plugin as a global (and `actions.list()` will surface them all), so in principle an agent could guess and call a disabled plugin. In practice the primer only advertises the allowlisted ones, and unconfigured plugins error out at first action call anyway. Plumbing the allowlist into the sandbox registry is on the roadmap.
@@ -10,16 +10,6 @@ import {
10
10
  savePiPlugins,
11
11
  } from "../runline-resolve.js";
12
12
 
13
- type ActionEntry = {
14
- plugin: string;
15
- action: string;
16
- description?: string;
17
- inputSchema?: Record<
18
- string,
19
- { type: string; required?: boolean; description?: string }
20
- >;
21
- };
22
-
23
13
  function filterByAllowlist<T extends { name?: string; plugin?: string }>(
24
14
  items: T[],
25
15
  allow: string[] | undefined,
@@ -29,34 +19,6 @@ function filterByAllowlist<T extends { name?: string; plugin?: string }>(
29
19
  return items.filter((i) => set.has((i.name ?? i.plugin) as string));
30
20
  }
31
21
 
32
- function formatActions(actions: ActionEntry[]): string {
33
- const grouped = new Map<string, ActionEntry[]>();
34
- for (const a of actions) {
35
- const list = grouped.get(a.plugin) ?? [];
36
- list.push(a);
37
- grouped.set(a.plugin, list);
38
- }
39
-
40
- const lines: string[] = [];
41
- for (const [plugin, entries] of grouped) {
42
- lines.push(`### ${plugin}`);
43
- for (const a of entries) {
44
- const inputs = a.inputSchema
45
- ? Object.entries(a.inputSchema)
46
- .map(([k, v]) => `${k}: ${v.type}${v.required ? "" : "?"}`)
47
- .join(", ")
48
- : "";
49
- const sig = inputs
50
- ? `\`${plugin}.${a.action}({ ${inputs} })\``
51
- : `\`${plugin}.${a.action}()\``;
52
- const desc = a.description ? ` — ${a.description}` : "";
53
- lines.push(`- ${sig}${desc}`);
54
- }
55
- lines.push("");
56
- }
57
- return lines.join("\n");
58
- }
59
-
60
22
  const runlineCache = new Map<string, Promise<Runline>>();
61
23
 
62
24
  async function getRunline(cwd: string): Promise<Runline> {
@@ -157,21 +119,39 @@ export default function (pi: ExtensionAPI) {
157
119
  );
158
120
 
159
121
  if (!alreadyInjected) {
160
- const header =
161
- "## Runline actions\n\n" +
162
- "This project has runline installed. You have two tools:\n" +
163
- "- `list_runline_actions` — show the full action catalog with input schemas\n" +
164
- "- `execute_runline` — run JavaScript in a sandbox where each plugin is a top-level global. " +
122
+ const pluginList = plugins
123
+ .map((p) => `\`${p.name}\` (${p.actions.length})`)
124
+ .join(", ");
125
+
126
+ const content =
127
+ "## Runline\n\n" +
128
+ `This project has runline installed with **${plugins.length} plugins, ${actions.length} actions**. ` +
129
+ "Use the `execute_runline` tool to run JavaScript in a sandbox where each plugin is a top-level global. " +
165
130
  "Chain actions, await results, return a value.\n\n" +
166
- `**${plugins.length} plugins, ${actions.length} actions available.**\n\n` +
167
- "Example:\n" +
131
+ `**Enabled plugins:** ${pluginList}\n\n` +
132
+ "### Discovering actions\n\n" +
133
+ "Inside the sandbox, an `actions` object lets you explore the catalog without leaving `execute_runline`. " +
134
+ "Prefer this over guessing — it's how you find the right action and verify call shapes before invoking.\n\n" +
135
+ "```js\n" +
136
+ 'actions.list() // every "plugin.action" path\n' +
137
+ 'actions.list("github") // just one plugin\n' +
138
+ 'actions.find("create issue") // ranked fuzzy search — [{path, description, score}]\n' +
139
+ 'actions.describe("github.issue.create")\n' +
140
+ "// → { path, plugin, action, description, signature, inputs }\n" +
141
+ 'actions.check("github.issue.create", { owner: "a" })\n' +
142
+ "// → { ok, missing, unknown, typeErrors, signature } (does NOT call the action)\n" +
143
+ "```\n\n" +
144
+ "Unknown paths throw with did-you-mean suggestions, so typos are self-correcting. " +
145
+ "Recommended flow: `find` → `describe` → `check` → call.\n\n" +
146
+ "### Calling actions\n\n" +
168
147
  "```js\n" +
169
148
  'return await github.issue.create({ owner: "acme", repo: "api", title: "Bug" })\n' +
170
- "```\n\n";
149
+ "```\n\n" +
150
+ "Plugin globals (`github`, `slack`, ...) and `actions.<plugin>.<action>(...)` both work — same call.\n";
171
151
 
172
152
  pi.sendMessage({
173
153
  customType: "runline-context",
174
- content: header + formatActions(actions),
154
+ content,
175
155
  display: true,
176
156
  });
177
157
  }
@@ -205,9 +185,10 @@ export default function (pi: ExtensionAPI) {
205
185
  const rl = await getRunline(ctx.cwd);
206
186
  // Note: the sandbox currently exposes every registered plugin as a
207
187
  // global. The allowlist drives what the agent is told about in its
208
- // injected context and list_runline_actions output, which is the only
209
- // practical route for the agent to know what exists. Plumbing the
210
- // allowlist through to the sandbox globals is a future improvement.
188
+ // injected context (and what `actions.list()` surfaces in practice,
189
+ // since the agent only knows to look for what was advertised).
190
+ // Plumbing the allowlist through to the sandbox registry is a
191
+ // future improvement.
211
192
  const result = await rl.execute(params.code);
212
193
 
213
194
  const logs = result.logs?.length
@@ -234,38 +215,6 @@ export default function (pi: ExtensionAPI) {
234
215
  },
235
216
  });
236
217
 
237
- pi.registerTool({
238
- name: "list_runline_actions",
239
- label: "Runline Actions",
240
- description:
241
- "List every available runline action with its plugin, description, and input schema.",
242
- promptSnippet:
243
- "Discover runline plugin actions and their input shapes before calling execute_runline",
244
- parameters: Type.Object({
245
- plugin: Type.Optional(
246
- Type.String({
247
- description: "Filter to a single plugin (e.g. 'github')",
248
- }),
249
- ),
250
- }),
251
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
252
- const rl = await getRunline(ctx.cwd);
253
- const runlineDir = findRunlineDir(ctx.cwd);
254
- const allow = runlineDir
255
- ? loadExtConfig(runlineDir).piPlugins
256
- : undefined;
257
- let actions = filterByAllowlist(rl.actions(), allow);
258
- if (params.plugin) {
259
- actions = actions.filter((a) => a.plugin === params.plugin);
260
- }
261
- const text = formatActions(actions);
262
- return {
263
- content: [{ type: "text", text: text || "No actions enabled." }],
264
- details: { actions },
265
- };
266
- },
267
- });
268
-
269
218
  // ── Commands ────────────────────────────────────────────────────
270
219
 
271
220
  pi.registerCommand("runline-plugins", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-runline",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Code mode for pi",
5
5
  "type": "commonjs",
6
6
  "keywords": [
@@ -22,7 +22,7 @@
22
22
  "lint:fix": "biome check --write extensions/"
23
23
  },
24
24
  "dependencies": {
25
- "runline": "^0.4.0"
25
+ "runline": "^0.5.1"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@mariozechner/pi-coding-agent": "*",