pi-onedev-toolkit 0.1.1 → 0.2.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.
@@ -2,8 +2,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { StringEnum } from "@earendil-works/pi-ai";
3
3
  import { Type, type Static } from "typebox";
4
4
  import {
5
- confirmGate,
6
5
  FIELDS_DESCRIPTION,
6
+ confirmMutation,
7
7
  pushRepeated,
8
8
  QUERY_DESCRIPTION,
9
9
  REF_DESCRIPTION,
@@ -13,6 +13,7 @@ import {
13
13
  withFooter,
14
14
  type ToolDeps,
15
15
  } from "./common.js";
16
+ import type { MutationOperation } from "../mutation-approvals.js";
16
17
 
17
18
  const ISSUE_ACTIONS = [
18
19
  "list",
@@ -33,6 +34,17 @@ const ISSUE_ACTIONS = [
33
34
  ] as const;
34
35
  export type IssueAction = (typeof ISSUE_ACTIONS)[number];
35
36
 
37
+ const ISSUE_MUTATIONS: Partial<Record<IssueAction, MutationOperation>> = {
38
+ create: "issue.create",
39
+ edit: "issue.edit",
40
+ change_state: "issue.change_state",
41
+ add_comment: "issue.add_comment",
42
+ link: "issue.link",
43
+ log_work: "issue.log_work",
44
+ create_branch: "issue.create_branch",
45
+ checkout: "issue.checkout",
46
+ };
47
+
36
48
  export interface IssueParams {
37
49
  action?: IssueAction;
38
50
  ref?: string;
@@ -41,14 +53,14 @@ export interface IssueParams {
41
53
  state?: string;
42
54
  comment?: string;
43
55
  content?: string;
44
- linkName?: string;
45
- targetRef?: string;
56
+ link_name?: string;
57
+ target_ref?: string;
46
58
  hours?: number;
47
59
  fields?: string[];
48
60
  iterations?: string[];
49
- ownEstimatedTime?: number;
61
+ own_estimated_time?: number;
50
62
  confidential?: boolean;
51
- forWrite?: boolean;
63
+ for_write?: boolean;
52
64
  project?: string;
53
65
  query?: string;
54
66
  count?: number;
@@ -79,9 +91,10 @@ export function buildIssueArgs(params: IssueParams): string[] {
79
91
  if (params.project !== undefined) args.push("--project", params.project);
80
92
  pushRepeated(args, "--field", params.fields);
81
93
  pushRepeated(args, "--iteration", params.iterations);
82
- if (params.ownEstimatedTime !== undefined)
83
- args.push("--own-estimated-time", String(params.ownEstimatedTime));
84
- if (params.confidential === true) args.push("--confidential");
94
+ if (params.own_estimated_time !== undefined)
95
+ args.push("--own-estimated-time", String(params.own_estimated_time));
96
+ if (params.confidential !== undefined)
97
+ args.push(`--confidential=${params.confidential}`);
85
98
  return args;
86
99
  case "edit":
87
100
  args.push("edit", ref());
@@ -90,10 +103,10 @@ export function buildIssueArgs(params: IssueParams): string[] {
90
103
  args.push("--description", params.description);
91
104
  pushRepeated(args, "--field", params.fields);
92
105
  pushRepeated(args, "--iteration", params.iterations);
93
- if (params.ownEstimatedTime !== undefined)
94
- args.push("--own-estimated-time", String(params.ownEstimatedTime));
106
+ if (params.own_estimated_time !== undefined)
107
+ args.push("--own-estimated-time", String(params.own_estimated_time));
95
108
  if (params.confidential !== undefined)
96
- args.push("--confidential", String(params.confidential));
109
+ args.push(`--confidential=${params.confidential}`);
97
110
  return args;
98
111
  case "change_state":
99
112
  args.push(
@@ -116,8 +129,8 @@ export function buildIssueArgs(params: IssueParams): string[] {
116
129
  ...args,
117
130
  "link",
118
131
  ref(),
119
- requireParam(params.linkName, action, "link_name"),
120
- requireParam(params.targetRef, action, "target_ref"),
132
+ requireParam(params.link_name, action, "link_name"),
133
+ requireParam(params.target_ref, action, "target_ref"),
121
134
  ];
122
135
  case "log_work":
123
136
  args.push(
@@ -131,7 +144,7 @@ export function buildIssueArgs(params: IssueParams): string[] {
131
144
  return [...args, "create-branch", ref()];
132
145
  case "checkout":
133
146
  args.push("checkout", ref());
134
- if (params.forWrite === true) args.push("--for-write");
147
+ if (params.for_write === true) args.push("--for-write");
135
148
  return args;
136
149
  case "current_reference":
137
150
  return [...args, "current-reference"];
@@ -141,6 +154,8 @@ export function buildIssueArgs(params: IssueParams): string[] {
141
154
  return [...args, "get-valid-links"];
142
155
  case "query_description":
143
156
  return [...args, "get-query-description"];
157
+ default:
158
+ throw new Error(`unsupported issue action: ${String(action)}`);
144
159
  }
145
160
  }
146
161
 
@@ -163,10 +178,10 @@ export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
163
178
  Type.String({ description: "Markdown note attached to the action" }),
164
179
  ),
165
180
  content: Type.Optional(Type.String({ description: "Markdown comment text" })),
166
- linkName: Type.Optional(
181
+ link_name: Type.Optional(
167
182
  Type.String({ description: "Issue link name; discover via valid_links" }),
168
183
  ),
169
- targetRef: Type.Optional(
184
+ target_ref: Type.Optional(
170
185
  Type.String({ description: "Target issue reference for the link action" }),
171
186
  ),
172
187
  hours: Type.Optional(
@@ -176,11 +191,11 @@ export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
176
191
  Type.Array(Type.String(), { description: FIELDS_DESCRIPTION }),
177
192
  ),
178
193
  iterations: Type.Optional(Type.Array(Type.String())),
179
- ownEstimatedTime: Type.Optional(
180
- Type.Integer({ minimum: 0, description: "Estimated hours" }),
194
+ own_estimated_time: Type.Optional(
195
+ Type.Number({ minimum: 0, description: "Estimated hours" }),
181
196
  ),
182
197
  confidential: Type.Optional(Type.Boolean()),
183
- forWrite: Type.Optional(
198
+ for_write: Type.Optional(
184
199
  Type.Boolean({ description: "checkout: set up the branch for pushing" }),
185
200
  ),
186
201
  project: Type.Optional(
@@ -198,7 +213,7 @@ export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
198
213
  }),
199
214
  ),
200
215
  offset: Type.Optional(Type.Integer({ minimum: 0 })),
201
- });
216
+ }, { additionalProperties: false });
202
217
  type Params = Static<typeof parameters>;
203
218
 
204
219
  pi.registerTool({
@@ -206,13 +221,22 @@ export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
206
221
  label: "OneDev Issues",
207
222
  description:
208
223
  "Read and edit OneDev issues: query, inspect, comment, change fields/labels/state, log work, manage issue branches. Runs in the current repository's project; plain numbers resolve against it.",
209
- promptGuidelines: [
210
- "Use onedev_issue valid_fields before setting fields or state you have not seen this session.",
211
- "Post comments and field changes only after presenting the intent to the user.",
212
- ],
213
224
  parameters,
214
- async execute(_toolCallId, params: Params, signal) {
215
- const output = await runInContext(deps, buildIssueArgs(params), { signal });
225
+ async execute(_toolCallId, params: Params, signal, _onUpdate, ctx) {
226
+ const args = buildIssueArgs(params);
227
+ const operation = ISSUE_MUTATIONS[params.action];
228
+ if (operation) {
229
+ await confirmMutation(deps, ctx, {
230
+ operation,
231
+ title: `Approve OneDev issue action: ${params.action}`,
232
+ details: [
233
+ params.ref ? `Issue: ${params.ref}` : undefined,
234
+ params.title ? `Title: ${params.title}` : undefined,
235
+ ].filter(Boolean).join("\n") || "This action changes issue or local branch state.",
236
+ signal,
237
+ });
238
+ }
239
+ const output = await runInContext(deps, args, { signal });
216
240
  return toolResult(
217
241
  withFooter(output.text, deps.context(), output.truncated),
218
242
  {
package/src/tools/pull.ts CHANGED
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { StringEnum } from "@earendil-works/pi-ai";
3
3
  import { Type, type Static } from "typebox";
4
4
  import {
5
- confirmGate,
5
+ confirmMutation,
6
6
  pushRepeated,
7
7
  QUERY_DESCRIPTION,
8
8
  REF_DESCRIPTION,
@@ -12,6 +12,7 @@ import {
12
12
  withFooter,
13
13
  type ToolDeps,
14
14
  } from "./common.js";
15
+ import type { MutationOperation } from "../mutation-approvals.js";
15
16
 
16
17
  const PULL_ACTIONS = [
17
18
  "list",
@@ -24,6 +25,9 @@ const PULL_ACTIONS = [
24
25
  "edit",
25
26
  "add_comment",
26
27
  "add_code_comment",
28
+ "reply_code_comment",
29
+ "resolve_code_comment",
30
+ "unresolve_code_comment",
27
31
  "approve",
28
32
  "request_changes",
29
33
  "merge",
@@ -37,6 +41,21 @@ const PULL_ACTIONS = [
37
41
  ] as const;
38
42
  export type PullAction = (typeof PULL_ACTIONS)[number];
39
43
 
44
+ const PULL_MUTATIONS: Partial<Record<PullAction, MutationOperation>> = {
45
+ create: "pull.create",
46
+ edit: "pull.edit",
47
+ add_comment: "pull.add_comment",
48
+ add_code_comment: "pull.add_code_comment",
49
+ reply_code_comment: "pull.reply_code_comment",
50
+ resolve_code_comment: "pull.resolve_code_comment",
51
+ unresolve_code_comment: "pull.unresolve_code_comment",
52
+ approve: "pull.approve",
53
+ request_changes: "pull.request_changes",
54
+ merge: "pull.merge",
55
+ discard: "pull.discard",
56
+ checkout: "pull.checkout",
57
+ };
58
+
40
59
  export interface PullParams {
41
60
  action?: PullAction;
42
61
  ref?: string;
@@ -44,29 +63,29 @@ export interface PullParams {
44
63
  description?: string;
45
64
  content?: string;
46
65
  summary?: string;
47
- commitMessage?: string;
66
+ commit_message?: string;
48
67
  comment?: string;
49
68
  file?: string;
50
- fromLine?: number;
51
- toLine?: number;
52
- sourceBranch?: string;
53
- targetBranch?: string;
54
- sourceProject?: string;
55
- targetProject?: string;
69
+ from_line?: number;
70
+ to_line?: number;
71
+ comment_id?: number;
72
+ source_branch?: string;
73
+ target_branch?: string;
74
+ source_project?: string;
75
+ target_project?: string;
56
76
  assignees?: string[];
57
77
  reviewers?: string[];
58
- removeReviewers?: string[];
78
+ remove_reviewers?: string[];
59
79
  labels?: string[];
60
- mergeStrategy?: string;
61
- autoMerge?: boolean;
62
- autoMergeCommitMessage?: string;
63
- forCodeReview?: boolean;
64
- forWrite?: boolean;
80
+ merge_strategy?: string;
81
+ auto_merge?: boolean;
82
+ auto_merge_commit_message?: string;
83
+ for_code_review?: boolean;
84
+ for_write?: boolean;
65
85
  project?: string;
66
86
  query?: string;
67
87
  count?: number;
68
88
  offset?: number;
69
- confirm?: boolean;
70
89
  }
71
90
 
72
91
  export function buildPullArgs(params: PullParams): string[] {
@@ -74,14 +93,14 @@ export function buildPullArgs(params: PullParams): string[] {
74
93
  const action = requireParam(params.action, "pr", "action");
75
94
  const ref = (): string => requireParam(params.ref, action, "ref");
76
95
  const branchScope = (): void => {
77
- if (params.sourceBranch !== undefined)
78
- args.push("--source-branch", params.sourceBranch);
79
- if (params.targetBranch !== undefined)
80
- args.push("--target-branch", params.targetBranch);
81
- if (params.sourceProject !== undefined)
82
- args.push("--source-project", params.sourceProject);
83
- if (params.targetProject !== undefined)
84
- args.push("--target-project", params.targetProject);
96
+ if (params.source_branch !== undefined)
97
+ args.push("--source-branch", params.source_branch);
98
+ if (params.target_branch !== undefined)
99
+ args.push("--target-branch", params.target_branch);
100
+ if (params.source_project !== undefined)
101
+ args.push("--source-project", params.source_project);
102
+ if (params.target_project !== undefined)
103
+ args.push("--target-project", params.target_project);
85
104
  };
86
105
  switch (action) {
87
106
  case "list":
@@ -102,7 +121,7 @@ export function buildPullArgs(params: PullParams): string[] {
102
121
  return [...args, "get-builds", ref()];
103
122
  case "get_patch":
104
123
  args.push("get-patch", ref());
105
- if (params.forCodeReview === true) args.push("--for-code-review");
124
+ if (params.for_code_review === true) args.push("--for-code-review");
106
125
  return args;
107
126
  case "create":
108
127
  args.push("create", requireParam(params.title, action, "title"));
@@ -112,8 +131,8 @@ export function buildPullArgs(params: PullParams): string[] {
112
131
  pushRepeated(args, "--assignee", params.assignees);
113
132
  pushRepeated(args, "--reviewer", params.reviewers);
114
133
  pushRepeated(args, "--label", params.labels);
115
- if (params.mergeStrategy !== undefined)
116
- args.push("--merge-strategy", params.mergeStrategy);
134
+ if (params.merge_strategy !== undefined)
135
+ args.push("--merge-strategy", params.merge_strategy);
117
136
  return args;
118
137
  case "edit":
119
138
  args.push("edit", ref());
@@ -122,15 +141,15 @@ export function buildPullArgs(params: PullParams): string[] {
122
141
  args.push("--description", params.description);
123
142
  pushRepeated(args, "--assignee", params.assignees);
124
143
  pushRepeated(args, "--add-reviewer", params.reviewers);
125
- pushRepeated(args, "--remove-reviewer", params.removeReviewers);
144
+ pushRepeated(args, "--remove-reviewer", params.remove_reviewers);
126
145
  pushRepeated(args, "--label", params.labels);
127
- if (params.mergeStrategy !== undefined)
128
- args.push("--merge-strategy", params.mergeStrategy);
129
- if (params.autoMerge === true) {
130
- args.push("--auto-merge");
131
- if (params.autoMergeCommitMessage !== undefined)
132
- args.push("--auto-merge-commit-message", params.autoMergeCommitMessage);
146
+ if (params.merge_strategy !== undefined)
147
+ args.push("--merge-strategy", params.merge_strategy);
148
+ if (params.auto_merge !== undefined) {
149
+ args.push(`--auto-merge=${params.auto_merge}`);
133
150
  }
151
+ if (params.auto_merge_commit_message !== undefined)
152
+ args.push("--auto-merge-commit-message", params.auto_merge_commit_message);
134
153
  return args;
135
154
  case "add_comment":
136
155
  return [
@@ -148,10 +167,29 @@ export function buildPullArgs(params: PullParams): string[] {
148
167
  args.push("--file", requireParam(params.file, action, "file"));
149
168
  args.push(
150
169
  "--from-line",
151
- String(requireParam(params.fromLine, action, "from_line")),
170
+ String(requireParam(params.from_line, action, "from_line")),
152
171
  );
153
- args.push("--to-line", String(params.toLine ?? params.fromLine));
172
+ args.push("--to-line", String(params.to_line ?? params.from_line));
154
173
  return args;
174
+ case "reply_code_comment":
175
+ return [
176
+ "code-comment",
177
+ "add-reply",
178
+ String(requireParam(params.comment_id, action, "comment_id")),
179
+ requireParam(params.content, action, "content"),
180
+ ];
181
+ case "resolve_code_comment":
182
+ return [
183
+ "code-comment",
184
+ "resolve",
185
+ String(requireParam(params.comment_id, action, "comment_id")),
186
+ ];
187
+ case "unresolve_code_comment":
188
+ return [
189
+ "code-comment",
190
+ "unresolve",
191
+ String(requireParam(params.comment_id, action, "comment_id")),
192
+ ];
155
193
  case "approve":
156
194
  args.push("approve", ref());
157
195
  if (params.summary !== undefined) args.push("--summary", params.summary);
@@ -161,19 +199,17 @@ export function buildPullArgs(params: PullParams): string[] {
161
199
  if (params.summary !== undefined) args.push("--summary", params.summary);
162
200
  return args;
163
201
  case "merge":
164
- confirmGate(action, params.confirm);
165
202
  args.push("merge", ref());
166
- if (params.commitMessage !== undefined)
167
- args.push("--commit-message", params.commitMessage);
203
+ if (params.commit_message !== undefined)
204
+ args.push("--commit-message", params.commit_message);
168
205
  return args;
169
206
  case "discard":
170
- confirmGate(action, params.confirm);
171
207
  args.push("discard", ref());
172
208
  if (params.comment !== undefined) args.push("--comment", params.comment);
173
209
  return args;
174
210
  case "checkout":
175
211
  args.push("checkout", ref());
176
- if (params.forWrite === true) args.push("--for-write");
212
+ if (params.for_write === true) args.push("--for-write");
177
213
  return args;
178
214
  case "current_reference":
179
215
  return [...args, "current-reference"];
@@ -189,6 +225,8 @@ export function buildPullArgs(params: PullParams): string[] {
189
225
  args.push("get-commit-message-requirement");
190
226
  branchScope();
191
227
  return args;
228
+ default:
229
+ throw new Error(`unsupported pull action: ${String(action)}`);
192
230
  }
193
231
  }
194
232
 
@@ -196,7 +234,7 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
196
234
  const parameters = Type.Object({
197
235
  action: StringEnum(PULL_ACTIONS, {
198
236
  description:
199
- "list, get, get_comments, get_code_comments, get_builds, get_patch (diff; for_code_review scopes it), create, edit, add_comment, add_code_comment (line-anchored), approve, request_changes, merge (confirm), discard (confirm), checkout, current_reference, valid_labels, query_description, title_description_requirement, commit_message_requirement",
237
+ "list, get, get_comments, get_code_comments, get_builds, get_patch, create, edit, add_comment, add_code_comment, reply_code_comment, resolve_code_comment, unresolve_code_comment, approve, request_changes, merge, discard, checkout, current_reference, valid_labels, query_description, title_description_requirement, commit_message_requirement",
200
238
  }),
201
239
  ref: Type.Optional(Type.String({ description: REF_DESCRIPTION })),
202
240
  title: Type.Optional(Type.String()),
@@ -210,7 +248,7 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
210
248
  comment: Type.Optional(
211
249
  Type.String({ description: "Markdown note for discard" }),
212
250
  ),
213
- commitMessage: Type.Optional(
251
+ commit_message: Type.Optional(
214
252
  Type.String({
215
253
  description: "Commit message for merge (required by squash merges)",
216
254
  }),
@@ -218,29 +256,36 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
218
256
  file: Type.Optional(
219
257
  Type.String({ description: "add_code_comment: file path in the PR patch" }),
220
258
  ),
221
- fromLine: Type.Optional(
259
+ from_line: Type.Optional(
222
260
  Type.Integer({
223
261
  minimum: 1,
224
262
  description: "add_code_comment: 1-based start line (right side of patch)",
225
263
  }),
226
264
  ),
227
- toLine: Type.Optional(
265
+ to_line: Type.Optional(
228
266
  Type.Integer({
229
267
  minimum: 1,
230
268
  description: "add_code_comment: 1-based end line; defaults to from_line",
231
269
  }),
232
270
  ),
233
- sourceBranch: Type.Optional(Type.String()),
234
- targetBranch: Type.Optional(Type.String()),
235
- sourceProject: Type.Optional(Type.String()),
236
- targetProject: Type.Optional(Type.String()),
271
+ comment_id: Type.Optional(
272
+ Type.Integer({
273
+ minimum: 1,
274
+ description:
275
+ "Code comment id for reply_code_comment, resolve_code_comment, or unresolve_code_comment",
276
+ }),
277
+ ),
278
+ source_branch: Type.Optional(Type.String()),
279
+ target_branch: Type.Optional(Type.String()),
280
+ source_project: Type.Optional(Type.String()),
281
+ target_project: Type.Optional(Type.String()),
237
282
  assignees: Type.Optional(
238
283
  Type.Array(Type.String(), { description: "User logins, repeatable" }),
239
284
  ),
240
285
  reviewers: Type.Optional(
241
286
  Type.Array(Type.String(), { description: "User logins, repeatable" }),
242
287
  ),
243
- removeReviewers: Type.Optional(
288
+ remove_reviewers: Type.Optional(
244
289
  Type.Array(Type.String(), { description: "edit: user logins to remove" }),
245
290
  ),
246
291
  labels: Type.Optional(
@@ -248,13 +293,13 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
248
293
  description: "Label names; discover via valid_labels",
249
294
  }),
250
295
  ),
251
- mergeStrategy: Type.Optional(Type.String()),
252
- autoMerge: Type.Optional(Type.Boolean()),
253
- autoMergeCommitMessage: Type.Optional(Type.String()),
254
- forCodeReview: Type.Optional(
296
+ merge_strategy: Type.Optional(Type.String()),
297
+ auto_merge: Type.Optional(Type.Boolean()),
298
+ auto_merge_commit_message: Type.Optional(Type.String()),
299
+ for_code_review: Type.Optional(
255
300
  Type.Boolean({ description: "get_patch: review-scoped patch" }),
256
301
  ),
257
- forWrite: Type.Optional(
302
+ for_write: Type.Optional(
258
303
  Type.Boolean({ description: "checkout: set up the branch for pushing" }),
259
304
  ),
260
305
  project: Type.Optional(
@@ -265,13 +310,7 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
265
310
  query: Type.Optional(Type.String({ description: QUERY_DESCRIPTION })),
266
311
  count: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
267
312
  offset: Type.Optional(Type.Integer({ minimum: 0 })),
268
- confirm: Type.Optional(
269
- Type.Boolean({
270
- description:
271
- "Required true for merge/discard: re-run with confirm after the user approves the destructive action",
272
- }),
273
- ),
274
- });
313
+ }, { additionalProperties: false });
275
314
  type Params = Static<typeof parameters>;
276
315
 
277
316
  pi.registerTool({
@@ -279,13 +318,23 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
279
318
  label: "OneDev Pull Requests",
280
319
  description:
281
320
  "Read and edit OneDev pull requests: metadata, labels, comments, line-anchored code comments, builds, patch, review outcomes, and guarded merge/discard. Runs in the current repository's project.",
282
- promptGuidelines: [
283
- "Use onedev_pull get_patch with for_code_review for review-scoped diffs.",
284
- "merge and discard require confirm: true after explicit user approval.",
285
- ],
286
321
  parameters,
287
- async execute(_toolCallId, params: Params, signal) {
288
- const output = await runInContext(deps, buildPullArgs(params), { signal });
322
+ async execute(_toolCallId, params: Params, signal, _onUpdate, ctx) {
323
+ const args = buildPullArgs(params);
324
+ const operation = PULL_MUTATIONS[params.action];
325
+ if (operation) {
326
+ await confirmMutation(deps, ctx, {
327
+ operation,
328
+ title: `Approve OneDev pull request action: ${params.action}`,
329
+ details: [
330
+ params.ref ? `Pull request: ${params.ref}` : undefined,
331
+ params.comment_id ? `Code comment: ${params.comment_id}` : undefined,
332
+ params.title ? `Title: ${params.title}` : undefined,
333
+ ].filter(Boolean).join("\n") || "This action changes pull request or local branch state.",
334
+ signal,
335
+ });
336
+ }
337
+ const output = await runInContext(deps, args, { signal });
289
338
  return toolResult(
290
339
  withFooter(output.text, deps.context(), output.truncated),
291
340
  {
@@ -0,0 +1,70 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { StringEnum } from "@earendil-works/pi-ai";
3
+ import { Type } from "typebox";
4
+ import type { OneDevSourceWatchManager } from "../source-watch.js";
5
+ import { toolResult } from "./common.js";
6
+
7
+ export function registerWatchTool(
8
+ pi: ExtensionAPI,
9
+ watches: OneDevSourceWatchManager,
10
+ ): void {
11
+ pi.registerTool({
12
+ name: "onedev_watch",
13
+ label: "OneDev Watch",
14
+ description:
15
+ "Create session-scoped one-shot watches for a build result or pull-request attention, or list/stop active watches. Notifications contain bounded metadata; use read tools for details.",
16
+ parameters: Type.Object(
17
+ {
18
+ action: StringEnum([
19
+ "watch_build",
20
+ "watch_pull_attention",
21
+ "list",
22
+ "stop",
23
+ ]),
24
+ ref: Type.Optional(
25
+ Type.String({ description: "Build or pull request reference to watch" }),
26
+ ),
27
+ watch_id: Type.Optional(
28
+ Type.String({ description: "Watch id required by stop" }),
29
+ ),
30
+ interval_seconds: Type.Optional(
31
+ Type.Integer({ minimum: 15, maximum: 300, default: 30 }),
32
+ ),
33
+ ttl_minutes: Type.Optional(
34
+ Type.Integer({ minimum: 1, maximum: 1440, default: 60 }),
35
+ ),
36
+ },
37
+ { additionalProperties: false },
38
+ ),
39
+ async execute(_toolCallId, params, signal) {
40
+ if (params.action === "list") {
41
+ const active = watches.list();
42
+ return toolResult(
43
+ active.length > 0
44
+ ? JSON.stringify(active, null, 2)
45
+ : "No active OneDev watches.",
46
+ { watches: active },
47
+ );
48
+ }
49
+ if (params.action === "stop") {
50
+ if (!params.watch_id) throw new Error("stop requires watch_id");
51
+ const stopped = watches.stop(params.watch_id);
52
+ return toolResult(
53
+ stopped
54
+ ? `Stopped OneDev watch ${params.watch_id}.`
55
+ : `OneDev watch ${params.watch_id} was not active.`,
56
+ { watch_id: params.watch_id, stopped },
57
+ );
58
+ }
59
+ if (!params.ref) throw new Error(`${params.action} requires ref`);
60
+ const result = await watches.add({
61
+ kind: params.action === "watch_build" ? "build" : "pull_attention",
62
+ ref: params.ref,
63
+ intervalSeconds: params.interval_seconds,
64
+ ttlMinutes: params.ttl_minutes,
65
+ signal,
66
+ });
67
+ return toolResult(result.message, result.watch ?? { active: false });
68
+ },
69
+ });
70
+ }