@smithers-orchestrator/pi-plugin 0.16.9

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 William Cory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@smithers-orchestrator/pi-plugin",
3
+ "version": "0.16.9",
4
+ "description": "PI coding agent integration for Smithers workflow inspection",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "import": "./src/index.ts",
11
+ "default": "./src/index.ts"
12
+ },
13
+ "./extension": {
14
+ "types": "./src/extension.ts",
15
+ "import": "./src/extension.ts",
16
+ "default": "./src/extension.ts"
17
+ },
18
+ "./runtime/*": {
19
+ "types": "./src/runtime/*.ts",
20
+ "import": "./src/runtime/*.ts",
21
+ "default": "./src/runtime/*.ts"
22
+ },
23
+ "./views/*": {
24
+ "types": "./src/views/*.ts",
25
+ "import": "./src/views/*.ts",
26
+ "default": "./src/views/*.ts"
27
+ },
28
+ "./api/*": {
29
+ "types": "./src/api/*.ts",
30
+ "import": "./src/api/*.ts",
31
+ "default": "./src/api/*.ts"
32
+ },
33
+ "./*": {
34
+ "types": "./src/*.ts",
35
+ "import": "./src/*.ts",
36
+ "default": "./src/*.ts"
37
+ }
38
+ },
39
+ "files": [
40
+ "src/"
41
+ ],
42
+ "dependencies": {
43
+ "@mariozechner/pi-coding-agent": "^0.70.2",
44
+ "@mariozechner/pi-tui": "^0.70.2",
45
+ "@modelcontextprotocol/sdk": "^1.29.0",
46
+ "@sinclair/typebox": "^0.34.49",
47
+ "ws": "^8.20.0",
48
+ "@smithers-orchestrator/agents": "0.16.9",
49
+ "@smithers-orchestrator/cli": "0.16.9",
50
+ "@smithers-orchestrator/devtools": "0.16.9",
51
+ "@smithers-orchestrator/errors": "0.16.9",
52
+ "@smithers-orchestrator/protocol": "0.16.9"
53
+ },
54
+ "devDependencies": {
55
+ "@types/bun": "latest",
56
+ "@types/ws": "^8.18.1",
57
+ "typescript": "~5.9.3"
58
+ },
59
+ "scripts": {
60
+ "test": "bun test tests",
61
+ "typecheck": "tsc -p tsconfig.json --noEmit",
62
+ "build": "tsup --dts-only"
63
+ }
64
+ }
@@ -0,0 +1,7 @@
1
+ export type SmithersPiRunContext = {
2
+ runId: string;
3
+ workflowName: string;
4
+ status: string;
5
+ nodeStates: Array<{ nodeId: string; state: string }>;
6
+ errors: string[];
7
+ };
@@ -0,0 +1,86 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+
3
+ type RequestOptions = {
4
+ baseUrl?: string;
5
+ apiKey?: string;
6
+ };
7
+
8
+ type FetchOptions = {
9
+ method?: string;
10
+ body?: unknown;
11
+ };
12
+
13
+ const DEFAULT_BASE = "http://127.0.0.1:7331";
14
+
15
+ function buildHeaders(opts: RequestOptions, withJson: boolean) {
16
+ const headers: Record<string, string> = {};
17
+ if (withJson) {
18
+ headers["Content-Type"] = "application/json";
19
+ }
20
+ if (opts.apiKey) {
21
+ headers.Authorization = `Bearer ${opts.apiKey}`;
22
+ }
23
+ return headers;
24
+ }
25
+
26
+ export class SmithersPiHttpClient {
27
+ readonly baseUrl: string;
28
+ readonly apiKey: string | undefined;
29
+
30
+ constructor(opts: RequestOptions = {}) {
31
+ this.baseUrl = opts.baseUrl ?? DEFAULT_BASE;
32
+ this.apiKey = opts.apiKey;
33
+ }
34
+
35
+ async json(path: string, opts: FetchOptions = {}) {
36
+ const res = await fetch(`${this.baseUrl}${path}`, {
37
+ method: opts.method ?? "GET",
38
+ headers: buildHeaders(this, opts.body !== undefined),
39
+ body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
40
+ });
41
+ if (!res.ok) {
42
+ const text = await res.text().catch(() => "");
43
+ throw new SmithersError(
44
+ "PI_HTTP_ERROR",
45
+ `Smithers HTTP ${res.status}${text ? `: ${text}` : ""}`,
46
+ {
47
+ baseUrl: this.baseUrl,
48
+ path,
49
+ status: res.status,
50
+ },
51
+ );
52
+ }
53
+ return res.json();
54
+ }
55
+
56
+ async *events(path: string) {
57
+ const res = await fetch(`${this.baseUrl}${path}`, {
58
+ headers: buildHeaders(this, false),
59
+ });
60
+ if (!res.ok || !res.body) {
61
+ return;
62
+ }
63
+
64
+ const reader = res.body.getReader();
65
+ const decoder = new TextDecoder();
66
+ let buffer = "";
67
+
68
+ while (true) {
69
+ const { done, value } = await reader.read();
70
+ if (done) {
71
+ break;
72
+ }
73
+
74
+ buffer += decoder.decode(value, { stream: true });
75
+ const parts = buffer.split("\n\n");
76
+ buffer = parts.pop() ?? "";
77
+
78
+ for (const part of parts) {
79
+ const line = part.split("\n").find((item) => item.startsWith("data: "));
80
+ if (line) {
81
+ yield JSON.parse(line.slice(6));
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,23 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type ApproveArgs = {
4
+ runId: string;
5
+ nodeId: string;
6
+ iteration?: number;
7
+ note?: string;
8
+ baseUrl?: string;
9
+ apiKey?: string;
10
+ };
11
+
12
+ export async function approve(args: ApproveArgs) {
13
+ return new SmithersPiHttpClient(args).json(
14
+ `/v1/runs/${args.runId}/nodes/${args.nodeId}/approve`,
15
+ {
16
+ method: "POST",
17
+ body: {
18
+ iteration: args.iteration ?? 0,
19
+ note: args.note,
20
+ },
21
+ },
22
+ );
23
+ }
@@ -0,0 +1,14 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type CancelArgs = {
4
+ runId: string;
5
+ baseUrl?: string;
6
+ apiKey?: string;
7
+ };
8
+
9
+ export async function cancel(args: CancelArgs) {
10
+ return new SmithersPiHttpClient(args).json(`/v1/runs/${args.runId}/cancel`, {
11
+ method: "POST",
12
+ body: {},
13
+ });
14
+ }
@@ -0,0 +1,23 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type DenyArgs = {
4
+ runId: string;
5
+ nodeId: string;
6
+ iteration?: number;
7
+ note?: string;
8
+ baseUrl?: string;
9
+ apiKey?: string;
10
+ };
11
+
12
+ export async function deny(args: DenyArgs) {
13
+ return new SmithersPiHttpClient(args).json(
14
+ `/v1/runs/${args.runId}/nodes/${args.nodeId}/deny`,
15
+ {
16
+ method: "POST",
17
+ body: {
18
+ iteration: args.iteration ?? 0,
19
+ note: args.note,
20
+ },
21
+ },
22
+ );
23
+ }
@@ -0,0 +1,14 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type GetFramesArgs = {
4
+ runId: string;
5
+ tail?: number;
6
+ baseUrl?: string;
7
+ apiKey?: string;
8
+ };
9
+
10
+ export async function getFrames(args: GetFramesArgs) {
11
+ return new SmithersPiHttpClient(args).json(
12
+ `/v1/runs/${args.runId}/frames?limit=${args.tail ?? 20}`,
13
+ );
14
+ }
@@ -0,0 +1,11 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type GetStatusArgs = {
4
+ runId: string;
5
+ baseUrl?: string;
6
+ apiKey?: string;
7
+ };
8
+
9
+ export async function getStatus(args: GetStatusArgs) {
10
+ return new SmithersPiHttpClient(args).json(`/v1/runs/${args.runId}`);
11
+ }
@@ -0,0 +1,20 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type ListRunsArgs = {
4
+ limit?: number;
5
+ status?: string;
6
+ baseUrl?: string;
7
+ apiKey?: string;
8
+ };
9
+
10
+ export async function listRuns(args: ListRunsArgs = {}) {
11
+ const params = new URLSearchParams();
12
+ if (args.limit !== undefined) {
13
+ params.set("limit", String(args.limit));
14
+ }
15
+ if (args.status) {
16
+ params.set("status", args.status);
17
+ }
18
+ const query = params.toString();
19
+ return new SmithersPiHttpClient(args).json(`/v1/runs${query ? `?${query}` : ""}`);
20
+ }
@@ -0,0 +1,19 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type ResumeArgs = {
4
+ workflowPath: string;
5
+ runId: string;
6
+ baseUrl?: string;
7
+ apiKey?: string;
8
+ };
9
+
10
+ export async function resume(args: ResumeArgs) {
11
+ return new SmithersPiHttpClient(args).json("/v1/runs", {
12
+ method: "POST",
13
+ body: {
14
+ workflowPath: args.workflowPath,
15
+ runId: args.runId,
16
+ resume: true,
17
+ },
18
+ });
19
+ }
@@ -0,0 +1,20 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type RunWorkflowArgs = {
4
+ workflowPath: string;
5
+ input: unknown;
6
+ runId?: string;
7
+ baseUrl?: string;
8
+ apiKey?: string;
9
+ };
10
+
11
+ export async function runWorkflow(args: RunWorkflowArgs) {
12
+ return new SmithersPiHttpClient(args).json("/v1/runs", {
13
+ method: "POST",
14
+ body: {
15
+ workflowPath: args.workflowPath,
16
+ input: args.input,
17
+ runId: args.runId,
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,11 @@
1
+ import { SmithersPiHttpClient } from "./SmithersPiHttpClient.js";
2
+
3
+ type StreamEventsArgs = {
4
+ runId: string;
5
+ baseUrl?: string;
6
+ apiKey?: string;
7
+ };
8
+
9
+ export async function* streamEvents(args: StreamEventsArgs) {
10
+ yield* new SmithersPiHttpClient(args).events(`/v1/runs/${args.runId}/events`);
11
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ renderSmithersAgentPromptGuidance,
3
+ type SmithersAgentContract,
4
+ } from "@smithers-orchestrator/agents/agent-contract";
5
+ import type { SmithersPiRunContext } from "./SmithersPiRunContext.js";
6
+
7
+ function toolRef(contract: SmithersAgentContract, name: string, prefix = "smithers_") {
8
+ return contract.tools.some((tool) => tool.name === name) ? `\`${prefix}${name}\`` : undefined;
9
+ }
10
+
11
+ function buildTypicalWorkflowGuidance(contract: SmithersAgentContract) {
12
+ const discover = toolRef(contract, "list_workflows");
13
+ const run = toolRef(contract, "run_workflow");
14
+ const listRuns = toolRef(contract, "list_runs");
15
+ const getRun = toolRef(contract, "get_run");
16
+ const watchRun = toolRef(contract, "watch_run");
17
+ const explainRun = toolRef(contract, "explain_run");
18
+ const listApprovals = toolRef(contract, "list_pending_approvals");
19
+ const resolveApproval = toolRef(contract, "resolve_approval");
20
+ const getNodeDetail = toolRef(contract, "get_node_detail");
21
+ const getRunEvents = toolRef(contract, "get_run_events");
22
+ const listArtifacts = toolRef(contract, "list_artifacts");
23
+ const getTranscript = toolRef(contract, "get_chat_transcript");
24
+ const revertAttempt = toolRef(contract, "revert_attempt");
25
+ const steps = [
26
+ "**Write a workflow** -> Use your Smithers knowledge to help the user write workflow files.",
27
+ ];
28
+
29
+ if (discover && run) {
30
+ steps.push(`**Run it** -> Use ${discover} to find workflow IDs, then ${run} to launch the workflow.`);
31
+ } else if (run) {
32
+ steps.push(`**Run it** -> Use ${run} to launch the workflow.`);
33
+ }
34
+
35
+ const monitorTools = [listRuns, getRun, watchRun, explainRun].filter(Boolean);
36
+ if (monitorTools.length > 0) {
37
+ steps.push(
38
+ `**Monitor** -> Use ${monitorTools.join(", ")} to inspect progress, or tell the user about \`/smithers\`.`,
39
+ );
40
+ }
41
+
42
+ const approvalTools = [listApprovals, resolveApproval].filter(Boolean);
43
+ if (approvalTools.length > 0) {
44
+ steps.push(`**Approve** -> Use ${approvalTools.join(", ")} when runs are waiting for approval.`);
45
+ }
46
+
47
+ const debugTools = [getNodeDetail, getRunEvents, listArtifacts, getTranscript].filter(Boolean);
48
+ if (debugTools.length > 0) {
49
+ steps.push(`**Debug** -> Use ${debugTools.join(", ")} to gather evidence before changing anything.`);
50
+ }
51
+
52
+ if (revertAttempt) {
53
+ steps.push(
54
+ `**Revert** -> Use ${revertAttempt} only when the user explicitly asks to roll back or time travel.`,
55
+ );
56
+ }
57
+
58
+ return steps.map((step, index) => `${index + 1}. ${step}`);
59
+ }
60
+
61
+ export function buildSmithersPiSystemPrompt(
62
+ baseSystemPrompt: string,
63
+ docs: string,
64
+ contract: SmithersAgentContract,
65
+ activeRun?: SmithersPiRunContext,
66
+ ) {
67
+ const sections = [
68
+ "\n\n# Smithers Documentation\n",
69
+ "You are a Smithers workflow expert. Prefer the live Smithers tools over shelling out when they can answer the request.\n",
70
+ "## Smithers PI Extension - User Guide\n",
71
+ "The user is running PI with the Smithers extension. When they ask about capabilities, slash commands, or how to use this environment, refer to this section.\n",
72
+ "### Tools (available to you, the agent)",
73
+ renderSmithersAgentPromptGuidance(contract, { toolNamePrefix: "smithers_" }),
74
+ "",
75
+ "### Slash Commands (available to the user)",
76
+ "Tell the user about these when they ask what they can do:",
77
+ "- `/smithers` - Opens the live run inspector with a virtualized tree, frame scrubber, Output/Diff/Logs inspector tabs, heartbeat indicators, and ghost node badges.",
78
+ "- `/smithers-watch <runId>` - Attaches the inspector and event stream to a run by ID.",
79
+ "- `/smithers-runs` - Lists tracked runs and makes the selected run active.",
80
+ "- `/smithers-approve` - Interactive approval flow for nodes waiting on approval.",
81
+ "- `/smithers-cancel [runId]` - Cancels a running workflow with confirmation.",
82
+ "",
83
+ "### UI Features (always active)",
84
+ "- **Header**: Shows run state, the active run ID, engine and sandbox/viewer heartbeat indicators, and reconnect status.",
85
+ "- **Tree**: Left pane virtualized run tree. Failed descendants bubble an error marker to collapsed ancestors, and removed selected nodes stay inspectable as ghosts.",
86
+ "- **Inspector**: Right pane Output, Diff, and Logs tabs for the selected node.",
87
+ "- **Frame Scrubber**: Browse historical frames and return to live mode.",
88
+ "- **Stale Banner**: Appears while the gateway stream is disconnected long enough that the displayed tree may be stale.",
89
+ "",
90
+ "### Flags (passed via CLI)",
91
+ "- `--smithers-url` / `-u` - Smithers server or gateway URL (default: http://127.0.0.1:7331)",
92
+ "- `--smithers-key` / `-k` - Smithers API key (also reads SMITHERS_API_KEY env var)",
93
+ "",
94
+ "### Typical Workflows",
95
+ ...buildTypicalWorkflowGuidance(contract),
96
+ "",
97
+ "---\n",
98
+ docs,
99
+ ];
100
+
101
+ if (activeRun) {
102
+ sections.push("\n## Active Run Context");
103
+ sections.push(`Run: ${activeRun.runId} (${activeRun.workflowName})`);
104
+ sections.push(`Status: ${activeRun.status}`);
105
+
106
+ const waitingNodes = activeRun.nodeStates.filter(
107
+ (node) => node.state === "waiting-approval" || node.state === "waiting-timer",
108
+ );
109
+ if (waitingNodes.length > 0) {
110
+ sections.push(`Nodes waiting approval: ${waitingNodes.map((node) => node.nodeId).join(", ")}`);
111
+ }
112
+
113
+ const recentErrors = activeRun.errors.slice(-3);
114
+ if (recentErrors.length > 0) {
115
+ sections.push(`Recent errors: ${recentErrors.join("; ")}`);
116
+ }
117
+ }
118
+
119
+ return baseSystemPrompt + sections.join("\n");
120
+ }