@h-rig/pi-rig 0.0.6-alpha.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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/pi-rig
@@ -0,0 +1,194 @@
1
+ // @bun
2
+ // packages/pi-rig/src/client.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, resolve } from "path";
6
+ function cleanString(value) {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
8
+ }
9
+ function readJson(path) {
10
+ if (!existsSync(path))
11
+ return null;
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
14
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function findRigProjectRoot(cwd) {
20
+ let current = resolve(cwd);
21
+ while (true) {
22
+ if (existsSync(resolve(current, ".rig", "state", "connection.json")) || existsSync(resolve(current, "rig.config.ts")) || existsSync(resolve(current, "rig.config.json"))) {
23
+ return current;
24
+ }
25
+ const parent = dirname(current);
26
+ if (parent === current)
27
+ return null;
28
+ current = parent;
29
+ }
30
+ }
31
+ function resolveGlobalConnectionsPath(env) {
32
+ const explicit = cleanString(env.RIG_CONNECTIONS_FILE);
33
+ if (explicit)
34
+ return resolve(explicit);
35
+ const stateDir = cleanString(env.RIG_GLOBAL_STATE_DIR);
36
+ if (stateDir)
37
+ return resolve(stateDir, "connections.json");
38
+ return resolve(homedir(), ".rig", "connections.json");
39
+ }
40
+ function discoverRigContext(env) {
41
+ const cwd = cleanString(env.PWD) ?? process.cwd();
42
+ const projectRoot = findRigProjectRoot(cwd);
43
+ if (!projectRoot)
44
+ return {};
45
+ const repoConnection = readJson(resolve(projectRoot, ".rig", "state", "connection.json"));
46
+ const selected = cleanString(repoConnection?.selected);
47
+ if (!selected)
48
+ return { projectRoot };
49
+ if (selected === "local") {
50
+ const server = readJson(resolve(projectRoot, ".rig", "state", "rig-server.json"));
51
+ const host = cleanString(server?.host);
52
+ const port = typeof server?.port === "number" ? server.port : null;
53
+ const authToken = cleanString(server?.authToken);
54
+ return {
55
+ projectRoot,
56
+ ...host && port ? { serverUrl: `http://${host}:${port}` } : {},
57
+ ...authToken ? { authToken } : {}
58
+ };
59
+ }
60
+ const global = readJson(resolveGlobalConnectionsPath(env));
61
+ const connections = global?.connections && typeof global.connections === "object" && !Array.isArray(global.connections) ? global.connections : {};
62
+ const selectedConnection = connections[selected];
63
+ const record = selectedConnection && typeof selectedConnection === "object" && !Array.isArray(selectedConnection) ? selectedConnection : null;
64
+ const baseUrl = record?.kind === "remote" ? cleanString(record.baseUrl) : null;
65
+ return { projectRoot, ...baseUrl ? { serverUrl: baseUrl } : {} };
66
+ }
67
+ function createRigContextFromEnv(env = process.env) {
68
+ const runId = env.RIG_RUN_ID ?? env.RIG_SERVER_RUN_ID;
69
+ const taskId = env.RIG_TASK_ID;
70
+ const discovered = discoverRigContext(env);
71
+ const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
72
+ const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
73
+ const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_GITHUB_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN ?? discovered.authToken;
74
+ const active = Boolean(runId || taskId || serverUrl || projectRoot);
75
+ if (!active)
76
+ return { active: false };
77
+ return {
78
+ active: true,
79
+ ...runId ? { runId } : {},
80
+ ...taskId ? { taskId } : {},
81
+ ...serverUrl ? { serverUrl } : {},
82
+ ...projectRoot ? { projectRoot } : {},
83
+ ...authToken ? { authToken } : {}
84
+ };
85
+ }
86
+ function joinUrl(baseUrl, pathname) {
87
+ return `${baseUrl.replace(/\/+$/, "")}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
88
+ }
89
+ async function readJsonResponse(response) {
90
+ const text = await response.text();
91
+ if (!response.ok) {
92
+ throw new Error(`Rig server request failed (${response.status}): ${text || response.statusText}`);
93
+ }
94
+ if (!text.trim())
95
+ return null;
96
+ try {
97
+ return JSON.parse(text);
98
+ } catch {
99
+ return text;
100
+ }
101
+ }
102
+ function requireServerUrl(context) {
103
+ if (!context.serverUrl) {
104
+ throw new Error("Rig server URL is not available in this Pi session. Set RIG_SERVER_URL or start Pi through Rig.");
105
+ }
106
+ return context.serverUrl;
107
+ }
108
+
109
+ class RigBridgeClient {
110
+ context;
111
+ fetchImpl;
112
+ constructor(input) {
113
+ this.context = input.context;
114
+ this.fetchImpl = input.fetchImpl ?? fetch;
115
+ }
116
+ async request(pathname, init) {
117
+ const headers = new Headers(init?.headers);
118
+ if (this.context.authToken && !headers.has("authorization")) {
119
+ headers.set("authorization", `Bearer ${this.context.authToken}`);
120
+ }
121
+ const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers });
122
+ return readJsonResponse(response);
123
+ }
124
+ async status() {
125
+ const payload = await this.request("/api/server/status");
126
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
127
+ }
128
+ async listTasks() {
129
+ const payload = await this.request("/api/workspace/tasks");
130
+ return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
131
+ }
132
+ async runTask(taskId) {
133
+ const endpoint = taskId ? "/api/runs/task" : "/api/runs/adhoc";
134
+ const body = taskId ? { taskId, runtimeAdapter: "pi", runtimeMode: "full-access", interactionMode: "default" } : { title: "Pi Rig ad-hoc run", runtimeAdapter: "pi", runtimeMode: "full-access", interactionMode: "default", initialPrompt: "Continue with the selected Rig task." };
135
+ const payload = await this.request(endpoint, {
136
+ method: "POST",
137
+ headers: { "content-type": "application/json" },
138
+ body: JSON.stringify(body)
139
+ });
140
+ const runId = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.runId : null;
141
+ return { runId: typeof runId === "string" ? runId : "" };
142
+ }
143
+ async attach(runId = this.context.runId) {
144
+ if (!runId)
145
+ throw new Error("runId is required");
146
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
147
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
148
+ }
149
+ async steer(message, runId = this.context.runId) {
150
+ if (!runId)
151
+ throw new Error("runId is required");
152
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/steer`, {
153
+ method: "POST",
154
+ headers: { "content-type": "application/json" },
155
+ body: JSON.stringify({ message, actor: "pi" })
156
+ });
157
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
158
+ }
159
+ async pollSteering(runId = this.context.runId) {
160
+ if (!runId)
161
+ return [];
162
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/steering`);
163
+ const messages = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.messages : null;
164
+ return Array.isArray(messages) ? messages.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
165
+ }
166
+ async consumeSteering(runId = this.context.runId) {
167
+ if (!runId)
168
+ return [];
169
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/steering?ack=1`);
170
+ const messages = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.messages : null;
171
+ return Array.isArray(messages) ? messages.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
172
+ }
173
+ async emitEvent(event) {
174
+ const runId = typeof event.runId === "string" ? event.runId : this.context.runId;
175
+ if (!runId)
176
+ throw new Error("runId is required");
177
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/events`, {
178
+ method: "POST",
179
+ headers: { "content-type": "application/json" },
180
+ body: JSON.stringify(event)
181
+ });
182
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
183
+ }
184
+ async readTaskMetadata(taskId = this.context.taskId) {
185
+ if (!taskId)
186
+ return {};
187
+ const payload = await this.request(`/api/workspace/tasks/${encodeURIComponent(taskId)}`);
188
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
189
+ }
190
+ }
191
+ export {
192
+ createRigContextFromEnv,
193
+ RigBridgeClient
194
+ };
@@ -0,0 +1,49 @@
1
+ // @bun
2
+ // packages/pi-rig/src/commands.ts
3
+ function createRigSlashCommands(input) {
4
+ const notify = input.notify ?? (() => {});
5
+ async function handleRig(args) {
6
+ const parts = args.trim().split(/\s+/).filter(Boolean);
7
+ const [first, second, third] = parts;
8
+ try {
9
+ if (!first || first === "status") {
10
+ const status = await input.client.status();
11
+ notify(`Rig server: ${status.ok === false ? "failed" : "ok"}`, "info");
12
+ return;
13
+ }
14
+ if (first === "task" && second === "list") {
15
+ const tasks = await input.client.listTasks();
16
+ if (tasks.length === 0) {
17
+ notify("No matching Rig tasks.", "info");
18
+ return;
19
+ }
20
+ notify(tasks.map((task) => `${String(task.id ?? "<unknown>")} \xB7 ${String(task.status ?? "unknown")} \xB7 ${String(task.title ?? "Untitled task")}`).join(`
21
+ `), "info");
22
+ return;
23
+ }
24
+ if (first === "task" && second === "run") {
25
+ const result = await input.client.runTask(third);
26
+ notify(`Run submitted: ${result.runId}`, "info");
27
+ return;
28
+ }
29
+ if (first === "attach") {
30
+ const run = await input.client.attach(second);
31
+ const runRecord = run.run && typeof run.run === "object" ? run.run : run;
32
+ notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
33
+ return;
34
+ }
35
+ notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id]", "error");
36
+ } catch (error) {
37
+ notify(error instanceof Error ? error.message : String(error), "error");
38
+ }
39
+ }
40
+ return {
41
+ rig: {
42
+ description: "Rig control-plane commands: status, task list, task run, attach",
43
+ handler: handleRig
44
+ }
45
+ };
46
+ }
47
+ export {
48
+ createRigSlashCommands
49
+ };
@@ -0,0 +1,378 @@
1
+ // @bun
2
+ // packages/pi-rig/src/client.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { dirname, resolve } from "path";
6
+ function cleanString(value) {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
8
+ }
9
+ function readJson(path) {
10
+ if (!existsSync(path))
11
+ return null;
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
14
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function findRigProjectRoot(cwd) {
20
+ let current = resolve(cwd);
21
+ while (true) {
22
+ if (existsSync(resolve(current, ".rig", "state", "connection.json")) || existsSync(resolve(current, "rig.config.ts")) || existsSync(resolve(current, "rig.config.json"))) {
23
+ return current;
24
+ }
25
+ const parent = dirname(current);
26
+ if (parent === current)
27
+ return null;
28
+ current = parent;
29
+ }
30
+ }
31
+ function resolveGlobalConnectionsPath(env) {
32
+ const explicit = cleanString(env.RIG_CONNECTIONS_FILE);
33
+ if (explicit)
34
+ return resolve(explicit);
35
+ const stateDir = cleanString(env.RIG_GLOBAL_STATE_DIR);
36
+ if (stateDir)
37
+ return resolve(stateDir, "connections.json");
38
+ return resolve(homedir(), ".rig", "connections.json");
39
+ }
40
+ function discoverRigContext(env) {
41
+ const cwd = cleanString(env.PWD) ?? process.cwd();
42
+ const projectRoot = findRigProjectRoot(cwd);
43
+ if (!projectRoot)
44
+ return {};
45
+ const repoConnection = readJson(resolve(projectRoot, ".rig", "state", "connection.json"));
46
+ const selected = cleanString(repoConnection?.selected);
47
+ if (!selected)
48
+ return { projectRoot };
49
+ if (selected === "local") {
50
+ const server = readJson(resolve(projectRoot, ".rig", "state", "rig-server.json"));
51
+ const host = cleanString(server?.host);
52
+ const port = typeof server?.port === "number" ? server.port : null;
53
+ const authToken = cleanString(server?.authToken);
54
+ return {
55
+ projectRoot,
56
+ ...host && port ? { serverUrl: `http://${host}:${port}` } : {},
57
+ ...authToken ? { authToken } : {}
58
+ };
59
+ }
60
+ const global = readJson(resolveGlobalConnectionsPath(env));
61
+ const connections = global?.connections && typeof global.connections === "object" && !Array.isArray(global.connections) ? global.connections : {};
62
+ const selectedConnection = connections[selected];
63
+ const record = selectedConnection && typeof selectedConnection === "object" && !Array.isArray(selectedConnection) ? selectedConnection : null;
64
+ const baseUrl = record?.kind === "remote" ? cleanString(record.baseUrl) : null;
65
+ return { projectRoot, ...baseUrl ? { serverUrl: baseUrl } : {} };
66
+ }
67
+ function createRigContextFromEnv(env = process.env) {
68
+ const runId = env.RIG_RUN_ID ?? env.RIG_SERVER_RUN_ID;
69
+ const taskId = env.RIG_TASK_ID;
70
+ const discovered = discoverRigContext(env);
71
+ const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
72
+ const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
73
+ const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_GITHUB_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN ?? discovered.authToken;
74
+ const active = Boolean(runId || taskId || serverUrl || projectRoot);
75
+ if (!active)
76
+ return { active: false };
77
+ return {
78
+ active: true,
79
+ ...runId ? { runId } : {},
80
+ ...taskId ? { taskId } : {},
81
+ ...serverUrl ? { serverUrl } : {},
82
+ ...projectRoot ? { projectRoot } : {},
83
+ ...authToken ? { authToken } : {}
84
+ };
85
+ }
86
+ function joinUrl(baseUrl, pathname) {
87
+ return `${baseUrl.replace(/\/+$/, "")}${pathname.startsWith("/") ? pathname : `/${pathname}`}`;
88
+ }
89
+ async function readJsonResponse(response) {
90
+ const text = await response.text();
91
+ if (!response.ok) {
92
+ throw new Error(`Rig server request failed (${response.status}): ${text || response.statusText}`);
93
+ }
94
+ if (!text.trim())
95
+ return null;
96
+ try {
97
+ return JSON.parse(text);
98
+ } catch {
99
+ return text;
100
+ }
101
+ }
102
+ function requireServerUrl(context) {
103
+ if (!context.serverUrl) {
104
+ throw new Error("Rig server URL is not available in this Pi session. Set RIG_SERVER_URL or start Pi through Rig.");
105
+ }
106
+ return context.serverUrl;
107
+ }
108
+
109
+ class RigBridgeClient {
110
+ context;
111
+ fetchImpl;
112
+ constructor(input) {
113
+ this.context = input.context;
114
+ this.fetchImpl = input.fetchImpl ?? fetch;
115
+ }
116
+ async request(pathname, init) {
117
+ const headers = new Headers(init?.headers);
118
+ if (this.context.authToken && !headers.has("authorization")) {
119
+ headers.set("authorization", `Bearer ${this.context.authToken}`);
120
+ }
121
+ const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers });
122
+ return readJsonResponse(response);
123
+ }
124
+ async status() {
125
+ const payload = await this.request("/api/server/status");
126
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
127
+ }
128
+ async listTasks() {
129
+ const payload = await this.request("/api/workspace/tasks");
130
+ return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
131
+ }
132
+ async runTask(taskId) {
133
+ const endpoint = taskId ? "/api/runs/task" : "/api/runs/adhoc";
134
+ const body = taskId ? { taskId, runtimeAdapter: "pi", runtimeMode: "full-access", interactionMode: "default" } : { title: "Pi Rig ad-hoc run", runtimeAdapter: "pi", runtimeMode: "full-access", interactionMode: "default", initialPrompt: "Continue with the selected Rig task." };
135
+ const payload = await this.request(endpoint, {
136
+ method: "POST",
137
+ headers: { "content-type": "application/json" },
138
+ body: JSON.stringify(body)
139
+ });
140
+ const runId = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.runId : null;
141
+ return { runId: typeof runId === "string" ? runId : "" };
142
+ }
143
+ async attach(runId = this.context.runId) {
144
+ if (!runId)
145
+ throw new Error("runId is required");
146
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
147
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
148
+ }
149
+ async steer(message, runId = this.context.runId) {
150
+ if (!runId)
151
+ throw new Error("runId is required");
152
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/steer`, {
153
+ method: "POST",
154
+ headers: { "content-type": "application/json" },
155
+ body: JSON.stringify({ message, actor: "pi" })
156
+ });
157
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
158
+ }
159
+ async pollSteering(runId = this.context.runId) {
160
+ if (!runId)
161
+ return [];
162
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/steering`);
163
+ const messages = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.messages : null;
164
+ return Array.isArray(messages) ? messages.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
165
+ }
166
+ async consumeSteering(runId = this.context.runId) {
167
+ if (!runId)
168
+ return [];
169
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/steering?ack=1`);
170
+ const messages = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.messages : null;
171
+ return Array.isArray(messages) ? messages.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
172
+ }
173
+ async emitEvent(event) {
174
+ const runId = typeof event.runId === "string" ? event.runId : this.context.runId;
175
+ if (!runId)
176
+ throw new Error("runId is required");
177
+ const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/events`, {
178
+ method: "POST",
179
+ headers: { "content-type": "application/json" },
180
+ body: JSON.stringify(event)
181
+ });
182
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
183
+ }
184
+ async readTaskMetadata(taskId = this.context.taskId) {
185
+ if (!taskId)
186
+ return {};
187
+ const payload = await this.request(`/api/workspace/tasks/${encodeURIComponent(taskId)}`);
188
+ return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
189
+ }
190
+ }
191
+
192
+ // packages/pi-rig/src/commands.ts
193
+ function createRigSlashCommands(input) {
194
+ const notify = input.notify ?? (() => {});
195
+ async function handleRig(args) {
196
+ const parts = args.trim().split(/\s+/).filter(Boolean);
197
+ const [first, second, third] = parts;
198
+ try {
199
+ if (!first || first === "status") {
200
+ const status = await input.client.status();
201
+ notify(`Rig server: ${status.ok === false ? "failed" : "ok"}`, "info");
202
+ return;
203
+ }
204
+ if (first === "task" && second === "list") {
205
+ const tasks = await input.client.listTasks();
206
+ if (tasks.length === 0) {
207
+ notify("No matching Rig tasks.", "info");
208
+ return;
209
+ }
210
+ notify(tasks.map((task) => `${String(task.id ?? "<unknown>")} \xB7 ${String(task.status ?? "unknown")} \xB7 ${String(task.title ?? "Untitled task")}`).join(`
211
+ `), "info");
212
+ return;
213
+ }
214
+ if (first === "task" && second === "run") {
215
+ const result = await input.client.runTask(third);
216
+ notify(`Run submitted: ${result.runId}`, "info");
217
+ return;
218
+ }
219
+ if (first === "attach") {
220
+ const run = await input.client.attach(second);
221
+ const runRecord = run.run && typeof run.run === "object" ? run.run : run;
222
+ notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
223
+ return;
224
+ }
225
+ notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id]", "error");
226
+ } catch (error) {
227
+ notify(error instanceof Error ? error.message : String(error), "error");
228
+ }
229
+ }
230
+ return {
231
+ rig: {
232
+ description: "Rig control-plane commands: status, task list, task run, attach",
233
+ handler: handleRig
234
+ }
235
+ };
236
+ }
237
+
238
+ // packages/pi-rig/src/tools.ts
239
+ function textResult(text, details) {
240
+ return { content: [{ type: "text", text }], ...details ? { details } : {} };
241
+ }
242
+ function createRigTools(input) {
243
+ const runId = input.context.runId;
244
+ const taskId = input.context.taskId;
245
+ return [
246
+ {
247
+ name: "rig_status_update",
248
+ label: "Rig status update",
249
+ description: "Report a concise status update for the active Rig run.",
250
+ parameters: { type: "object", properties: { status: { type: "string" }, message: { type: "string" } }, required: ["message"] },
251
+ async execute(_toolCallId, params) {
252
+ const message = typeof params.message === "string" ? params.message : "Status updated";
253
+ const status = typeof params.status === "string" ? params.status : "running";
254
+ await input.client.emitEvent({ kind: "status", runId, taskId, status, message, createdAt: new Date().toISOString() });
255
+ return textResult(`Rig status updated: ${message}`);
256
+ }
257
+ },
258
+ {
259
+ name: "rig_artifact_write",
260
+ label: "Rig artifact write",
261
+ description: "Write an artifact for the active Rig run through the Rig bridge.",
262
+ parameters: { type: "object", properties: { filename: { type: "string" }, content: { type: "string" } }, required: ["filename", "content"] },
263
+ async execute(_toolCallId, params) {
264
+ const filename = typeof params.filename === "string" ? params.filename : "artifact.txt";
265
+ const content = typeof params.content === "string" ? params.content : "";
266
+ await input.client.emitEvent({ kind: "artifact", runId, taskId, filename, content, createdAt: new Date().toISOString() });
267
+ return textResult(`Rig artifact queued: ${filename}`);
268
+ }
269
+ },
270
+ {
271
+ name: "rig_task_metadata_read",
272
+ label: "Rig task metadata read",
273
+ description: "Read normalized Rig task metadata for the active task.",
274
+ parameters: { type: "object", properties: { taskId: { type: "string" } } },
275
+ async execute(_toolCallId, params) {
276
+ const metadata = await input.client.readTaskMetadata(typeof params.taskId === "string" ? params.taskId : taskId);
277
+ return textResult(JSON.stringify(metadata, null, 2), metadata);
278
+ }
279
+ },
280
+ {
281
+ name: "rig_task_metadata_update",
282
+ label: "Rig task metadata update",
283
+ description: "Queue a Rig-owned task metadata update event.",
284
+ parameters: { type: "object", properties: { taskId: { type: "string" }, metadata: { type: "object" } }, required: ["metadata"] },
285
+ async execute(_toolCallId, params) {
286
+ const targetTaskId = typeof params.taskId === "string" ? params.taskId : taskId;
287
+ const metadata = params.metadata && typeof params.metadata === "object" && !Array.isArray(params.metadata) ? params.metadata : {};
288
+ await input.client.emitEvent({ kind: "task-metadata", runId, taskId: targetTaskId, metadata, createdAt: new Date().toISOString() });
289
+ return textResult(`Rig task metadata update queued for ${targetTaskId ?? "active task"}.`);
290
+ }
291
+ }
292
+ ];
293
+ }
294
+
295
+ // packages/pi-rig/src/index.ts
296
+ function createPiRigExtensionState(input = {}) {
297
+ const context = createRigContextFromEnv(input.env ?? process.env);
298
+ return {
299
+ ...context,
300
+ client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl })
301
+ };
302
+ }
303
+ function notify(ctx, message, level = "info") {
304
+ const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
305
+ const notifyFn = ui && typeof ui === "object" ? ui.notify : null;
306
+ if (typeof notifyFn === "function") {
307
+ notifyFn.call(ui, message, level);
308
+ }
309
+ }
310
+ function steeringText(message) {
311
+ const text = typeof message.message === "string" ? message.message.trim() : "";
312
+ if (!text)
313
+ return null;
314
+ const actor = typeof message.actor === "string" && message.actor.trim() ? message.actor.trim() : "operator";
315
+ return `[Rig steering from ${actor}]
316
+ ${text}`;
317
+ }
318
+ async function consumeQueuedSteering(pi, state, ctx) {
319
+ if (!state.active || !state.runId || typeof pi.sendUserMessage !== "function")
320
+ return;
321
+ try {
322
+ const messages = await state.client.consumeSteering(state.runId);
323
+ for (const message of messages) {
324
+ const text = steeringText(message);
325
+ if (!text)
326
+ continue;
327
+ await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
328
+ }
329
+ if (messages.length > 0) {
330
+ notify(ctx, `Delivered ${messages.length} Rig steering message${messages.length === 1 ? "" : "s"}.`);
331
+ }
332
+ } catch (error) {
333
+ notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
334
+ }
335
+ }
336
+ function createPiRigExtension(pi, options = {}) {
337
+ const state = options.state ?? createPiRigExtensionState();
338
+ const commands = createRigSlashCommands({
339
+ context: state,
340
+ client: state.client,
341
+ notify: (message, level) => notify(globalThis, message, level)
342
+ });
343
+ for (const [name, command] of Object.entries(commands)) {
344
+ pi.registerCommand?.(name, {
345
+ description: command.description,
346
+ handler: async (args, ctx) => {
347
+ const nextCommands = createRigSlashCommands({
348
+ context: state,
349
+ client: state.client,
350
+ notify: (message, level) => notify(ctx, message, level)
351
+ });
352
+ await nextCommands.rig.handler(args, ctx);
353
+ }
354
+ });
355
+ }
356
+ if (state.active && state.runId) {
357
+ for (const tool of createRigTools({ context: state, client: state.client })) {
358
+ pi.registerTool?.(tool);
359
+ }
360
+ }
361
+ pi.on?.("session_start", async (_event, ctx) => {
362
+ if (!state.active || !state.runId)
363
+ return;
364
+ const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
365
+ const setStatus = ui && typeof ui === "object" ? ui.setStatus : null;
366
+ if (typeof setStatus === "function") {
367
+ setStatus.call(ui, "rig", `Rig ${state.runId}`);
368
+ }
369
+ await consumeQueuedSteering(pi, state, ctx);
370
+ });
371
+ pi.on?.("turn_end", async (_event, ctx) => {
372
+ await consumeQueuedSteering(pi, state, ctx);
373
+ });
374
+ }
375
+ export {
376
+ createPiRigExtension as default,
377
+ createPiRigExtensionState
378
+ };
@@ -0,0 +1,60 @@
1
+ // @bun
2
+ // packages/pi-rig/src/tools.ts
3
+ function textResult(text, details) {
4
+ return { content: [{ type: "text", text }], ...details ? { details } : {} };
5
+ }
6
+ function createRigTools(input) {
7
+ const runId = input.context.runId;
8
+ const taskId = input.context.taskId;
9
+ return [
10
+ {
11
+ name: "rig_status_update",
12
+ label: "Rig status update",
13
+ description: "Report a concise status update for the active Rig run.",
14
+ parameters: { type: "object", properties: { status: { type: "string" }, message: { type: "string" } }, required: ["message"] },
15
+ async execute(_toolCallId, params) {
16
+ const message = typeof params.message === "string" ? params.message : "Status updated";
17
+ const status = typeof params.status === "string" ? params.status : "running";
18
+ await input.client.emitEvent({ kind: "status", runId, taskId, status, message, createdAt: new Date().toISOString() });
19
+ return textResult(`Rig status updated: ${message}`);
20
+ }
21
+ },
22
+ {
23
+ name: "rig_artifact_write",
24
+ label: "Rig artifact write",
25
+ description: "Write an artifact for the active Rig run through the Rig bridge.",
26
+ parameters: { type: "object", properties: { filename: { type: "string" }, content: { type: "string" } }, required: ["filename", "content"] },
27
+ async execute(_toolCallId, params) {
28
+ const filename = typeof params.filename === "string" ? params.filename : "artifact.txt";
29
+ const content = typeof params.content === "string" ? params.content : "";
30
+ await input.client.emitEvent({ kind: "artifact", runId, taskId, filename, content, createdAt: new Date().toISOString() });
31
+ return textResult(`Rig artifact queued: ${filename}`);
32
+ }
33
+ },
34
+ {
35
+ name: "rig_task_metadata_read",
36
+ label: "Rig task metadata read",
37
+ description: "Read normalized Rig task metadata for the active task.",
38
+ parameters: { type: "object", properties: { taskId: { type: "string" } } },
39
+ async execute(_toolCallId, params) {
40
+ const metadata = await input.client.readTaskMetadata(typeof params.taskId === "string" ? params.taskId : taskId);
41
+ return textResult(JSON.stringify(metadata, null, 2), metadata);
42
+ }
43
+ },
44
+ {
45
+ name: "rig_task_metadata_update",
46
+ label: "Rig task metadata update",
47
+ description: "Queue a Rig-owned task metadata update event.",
48
+ parameters: { type: "object", properties: { taskId: { type: "string" }, metadata: { type: "object" } }, required: ["metadata"] },
49
+ async execute(_toolCallId, params) {
50
+ const targetTaskId = typeof params.taskId === "string" ? params.taskId : taskId;
51
+ const metadata = params.metadata && typeof params.metadata === "object" && !Array.isArray(params.metadata) ? params.metadata : {};
52
+ await input.client.emitEvent({ kind: "task-metadata", runId, taskId: targetTaskId, metadata, createdAt: new Date().toISOString() });
53
+ return textResult(`Rig task metadata update queued for ${targetTaskId ?? "active task"}.`);
54
+ }
55
+ }
56
+ ];
57
+ }
58
+ export {
59
+ createRigTools
60
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@h-rig/pi-rig",
3
+ "version": "0.0.6-alpha.0",
4
+ "type": "module",
5
+ "description": "Rig package",
6
+ "license": "UNLICENSED",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/src/index.js"
14
+ },
15
+ "./client": {
16
+ "import": "./dist/src/client.js"
17
+ },
18
+ "./commands": {
19
+ "import": "./dist/src/commands.js"
20
+ },
21
+ "./tools": {
22
+ "import": "./dist/src/tools.js"
23
+ }
24
+ },
25
+ "engines": {
26
+ "bun": ">=1.3.11"
27
+ },
28
+ "main": "./dist/src/index.js",
29
+ "module": "./dist/src/index.js",
30
+ "pi": {
31
+ "extensions": [
32
+ "./dist/src/index.js"
33
+ ]
34
+ },
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "typebox": "*"
38
+ }
39
+ }