@hachej/boring-tasks 0.1.79

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,161 @@
1
+ import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
2
+ import { BoringTaskAdapterSummary, BoringTaskBoardConfig, BoringTaskCard, BoringTaskMoveInput, BoringTaskDeleteInput } from '../shared/index.js';
3
+
4
+ interface BoringTaskSourceContext {
5
+ workspaceId?: string;
6
+ workspaceRoot?: string;
7
+ }
8
+ type BoringTaskSourceSummary = BoringTaskAdapterSummary;
9
+ interface BoringTaskSourceRuntime {
10
+ summary(): BoringTaskSourceSummary;
11
+ getBoardConfig(ctx: BoringTaskSourceContext): Promise<BoringTaskBoardConfig> | BoringTaskBoardConfig;
12
+ listTasks(ctx: BoringTaskSourceContext): Promise<BoringTaskCard[]> | BoringTaskCard[];
13
+ moveTask?(ctx: BoringTaskSourceContext, input: BoringTaskMoveInput): Promise<BoringTaskCard> | BoringTaskCard;
14
+ deleteTask?(ctx: BoringTaskSourceContext, input: BoringTaskDeleteInput): Promise<void> | void;
15
+ }
16
+ interface BoringTaskSourceRegistry {
17
+ listSources(): BoringTaskSourceRuntime[];
18
+ getSource(sourceId: string): BoringTaskSourceRuntime | undefined;
19
+ }
20
+ declare function createTaskSourceRegistry(sources: readonly BoringTaskSourceRuntime[]): BoringTaskSourceRegistry;
21
+
22
+ interface GitHubLabel {
23
+ name?: string;
24
+ }
25
+ interface GitHubMilestone {
26
+ id?: number;
27
+ number?: number;
28
+ title?: string;
29
+ url?: string;
30
+ }
31
+ interface GitHubIssue {
32
+ number: number;
33
+ title: string;
34
+ body?: string | null;
35
+ url?: string;
36
+ state: "OPEN" | "CLOSED" | "open" | "closed";
37
+ labels?: GitHubLabel[];
38
+ milestone?: GitHubMilestone | null;
39
+ }
40
+ interface GitHubPullRequest {
41
+ number: number;
42
+ title: string;
43
+ body?: string | null;
44
+ url?: string;
45
+ state?: string;
46
+ }
47
+ interface GitHubIssueExecutor {
48
+ listIssues(input: {
49
+ owner: string;
50
+ repo: string;
51
+ limit: number;
52
+ state: "open" | "closed" | "all";
53
+ }): Promise<GitHubIssue[]>;
54
+ listPullRequests?(input: {
55
+ owner: string;
56
+ repo: string;
57
+ limit: number;
58
+ state: "open" | "closed" | "all";
59
+ }): Promise<GitHubPullRequest[]>;
60
+ viewIssue(input: {
61
+ owner: string;
62
+ repo: string;
63
+ issueNumber: number;
64
+ }): Promise<GitHubIssue>;
65
+ addLabels(input: {
66
+ owner: string;
67
+ repo: string;
68
+ issueNumber: number;
69
+ labels: string[];
70
+ }): Promise<void>;
71
+ removeLabels(input: {
72
+ owner: string;
73
+ repo: string;
74
+ issueNumber: number;
75
+ labels: string[];
76
+ }): Promise<void>;
77
+ closeIssue(input: {
78
+ owner: string;
79
+ repo: string;
80
+ issueNumber: number;
81
+ }): Promise<void>;
82
+ reopenIssue(input: {
83
+ owner: string;
84
+ repo: string;
85
+ issueNumber: number;
86
+ }): Promise<void>;
87
+ }
88
+ interface GitHubRepositoryDetector {
89
+ detectRepository(input: {
90
+ workspaceRoot: string;
91
+ }): Promise<{
92
+ owner: string;
93
+ repo: string;
94
+ }>;
95
+ }
96
+ interface GitHubTaskSourceOptions {
97
+ owner: string;
98
+ repo: string;
99
+ limit?: number;
100
+ state?: "open" | "closed" | "all";
101
+ executor?: GitHubIssueExecutor;
102
+ }
103
+ interface WorkspaceGitHubTaskSourceOptions {
104
+ workspaceRoot?: string;
105
+ sourceId?: string;
106
+ limit?: number;
107
+ state?: "open" | "closed" | "all";
108
+ detector?: GitHubRepositoryDetector;
109
+ executorFactory?: (input: {
110
+ workspaceRoot: string;
111
+ owner: string;
112
+ repo: string;
113
+ }) => GitHubIssueExecutor;
114
+ }
115
+ declare function createGhCliGitHubIssueExecutor(options?: {
116
+ workspaceRoot?: string;
117
+ }): GitHubIssueExecutor;
118
+ declare function createGhCliGitHubRepositoryDetector(): GitHubRepositoryDetector;
119
+ declare function createGitHubTaskSource({ owner, repo, limit, state, executor }: GitHubTaskSourceOptions): BoringTaskSourceRuntime;
120
+ declare function createWorkspaceGitHubTaskSource({ workspaceRoot, sourceId, limit, state, detector, executorFactory, }?: WorkspaceGitHubTaskSourceOptions): BoringTaskSourceRuntime;
121
+
122
+ declare class TaskSourceServiceError extends Error {
123
+ readonly status: number;
124
+ readonly code: string;
125
+ constructor(status: number, code: string, message: string);
126
+ }
127
+ interface TaskListInput {
128
+ sourceIds?: string[];
129
+ }
130
+ interface TaskListOutput {
131
+ configs: Record<string, BoringTaskBoardConfig>;
132
+ tasks: BoringTaskCard[];
133
+ }
134
+ interface TaskMoveInput extends BoringTaskMoveInput {
135
+ sourceId: string;
136
+ }
137
+ interface TaskDeleteInput {
138
+ sourceId: string;
139
+ taskId: string;
140
+ }
141
+ declare function createTaskSourceService(registry: BoringTaskSourceRegistry): {
142
+ listSources(): BoringTaskSourceSummary[];
143
+ listTasks(ctx: BoringTaskSourceContext, input?: TaskListInput): Promise<TaskListOutput>;
144
+ moveTask(ctx: BoringTaskSourceContext, input: TaskMoveInput): Promise<BoringTaskCard>;
145
+ deleteTask(ctx: BoringTaskSourceContext, input: TaskDeleteInput): Promise<void>;
146
+ };
147
+
148
+ interface TasksServerPluginOptions {
149
+ config?: unknown;
150
+ sources?: BoringTaskSourceRuntime[];
151
+ workspaceRoot?: string;
152
+ }
153
+ declare function createTaskSourceRegistryFromConfig(config: unknown, options?: {
154
+ workspaceRoot?: string;
155
+ }): BoringTaskSourceRegistry;
156
+ declare function createTasksServerPlugin(options?: TasksServerPluginOptions): WorkspaceServerPlugin;
157
+ declare function defaultTasksServerPlugin(options?: TasksServerPluginOptions, ctx?: {
158
+ workspaceRoot?: string;
159
+ }): WorkspaceServerPlugin;
160
+
161
+ export { TaskSourceServiceError, type TasksServerPluginOptions, createGhCliGitHubIssueExecutor, createGhCliGitHubRepositoryDetector, createGitHubTaskSource, createTaskSourceRegistry, createTaskSourceRegistryFromConfig, createTaskSourceService, createTasksServerPlugin, createWorkspaceGitHubTaskSource, defaultTasksServerPlugin as default };
@@ -0,0 +1,469 @@
1
+ // src/server/index.ts
2
+ import { defineServerPlugin } from "@hachej/boring-workspace/server";
3
+
4
+ // src/shared/index.ts
5
+ var TASKS_PLUGIN_ID = "tasks";
6
+ var TASKS_PLUGIN_LABEL = "Tasks";
7
+
8
+ // src/server/githubSource.ts
9
+ import { execFile } from "child_process";
10
+ import { promisify } from "util";
11
+
12
+ // src/server/taskSourceService.ts
13
+ var TaskSourceServiceError = class extends Error {
14
+ constructor(status, code, message) {
15
+ super(message);
16
+ this.status = status;
17
+ this.code = code;
18
+ this.name = "TaskSourceServiceError";
19
+ }
20
+ status;
21
+ code;
22
+ };
23
+ function createTaskSourceService(registry) {
24
+ return {
25
+ listSources() {
26
+ return registry.listSources().map((source) => source.summary());
27
+ },
28
+ async listTasks(ctx, input = {}) {
29
+ const selected = input.sourceIds?.length ? input.sourceIds.map((sourceId) => {
30
+ const source = registry.getSource(sourceId);
31
+ if (!source) throw new TaskSourceServiceError(404, "TASK_SOURCE_NOT_FOUND", `Task source not found: ${sourceId}`);
32
+ return source;
33
+ }) : registry.listSources();
34
+ const entries = await Promise.all(selected.map(async (source) => {
35
+ const summary = source.summary();
36
+ const [config, tasks] = await Promise.all([source.getBoardConfig(ctx), source.listTasks(ctx)]);
37
+ return { sourceId: summary.id, config, tasks };
38
+ }));
39
+ return {
40
+ configs: Object.fromEntries(entries.map((entry) => [entry.sourceId, entry.config])),
41
+ tasks: entries.flatMap((entry) => entry.tasks)
42
+ };
43
+ },
44
+ async moveTask(ctx, input) {
45
+ const source = registry.getSource(input.sourceId);
46
+ if (!source) throw new TaskSourceServiceError(404, "TASK_SOURCE_NOT_FOUND", `Task source not found: ${input.sourceId}`);
47
+ if (!source.summary().capabilities.move || !source.moveTask) {
48
+ throw new TaskSourceServiceError(409, "TASK_SOURCE_MOVE_UNSUPPORTED", `Task source does not support moves: ${input.sourceId}`);
49
+ }
50
+ return await source.moveTask(ctx, { taskId: input.taskId, statusId: input.statusId });
51
+ },
52
+ async deleteTask(ctx, input) {
53
+ const source = registry.getSource(input.sourceId);
54
+ if (!source) throw new TaskSourceServiceError(404, "TASK_SOURCE_NOT_FOUND", `Task source not found: ${input.sourceId}`);
55
+ if (!source.summary().capabilities.delete || !source.deleteTask) {
56
+ throw new TaskSourceServiceError(409, "TASK_SOURCE_DELETE_UNSUPPORTED", `Task source does not support issue deletion: ${input.sourceId}`);
57
+ }
58
+ await source.deleteTask(ctx, { taskId: input.taskId });
59
+ }
60
+ };
61
+ }
62
+
63
+ // src/server/githubSource.ts
64
+ var execFileAsync = promisify(execFile);
65
+ var GITHUB_COLUMNS = [
66
+ { id: "needs-triage", title: "Needs triage", description: "Fresh issues that need a first pass", color: "#8b5cf6" },
67
+ { id: "needs-info", title: "Needs info", description: "Blocked on clarification or missing context", color: "#ef4444" },
68
+ { id: "ready-for-agent", title: "Ready for agent", description: "Clear agent-pickable work", color: "#0ea5e9" },
69
+ { id: "ready-for-human", title: "Ready for human", description: "Waiting for owner review or human decision", color: "#f59e0b" },
70
+ { id: "done", title: "Done", description: "Closed GitHub issues", color: "#64748b" }
71
+ ];
72
+ var WORKFLOW_LABELS = ["needs-triage", "needs-info", "ready-for-agent", "ready-for-human", "done"];
73
+ var STATUS_MAPPINGS = {
74
+ "needs-triage": { removeStateLabels: true, addLabels: ["needs-triage"], reopen: true },
75
+ "needs-info": { removeStateLabels: true, addLabels: ["needs-info"], reopen: true },
76
+ "ready-for-agent": { removeStateLabels: true, addLabels: ["ready-for-agent"], reopen: true },
77
+ "ready-for-human": { removeStateLabels: true, addLabels: ["ready-for-human"], reopen: true },
78
+ done: { removeStateLabels: true, addLabels: ["done"], close: true }
79
+ };
80
+ function defaultWorkspaceRoot() {
81
+ return process.env.BORING_AGENT_WORKSPACE_ROOT || process.cwd();
82
+ }
83
+ async function runGhJson(args, cwd = defaultWorkspaceRoot()) {
84
+ try {
85
+ const { stdout } = await execFileAsync("gh", args, {
86
+ cwd,
87
+ env: { ...process.env, GH_PROMPT_DISABLED: "1" },
88
+ maxBuffer: 1024 * 1024 * 8,
89
+ timeout: 3e4
90
+ });
91
+ return JSON.parse(stdout);
92
+ } catch {
93
+ throw new TaskSourceServiceError(500, "TASK_GITHUB_COMMAND_FAILED", "GitHub command failed; check server authentication and repository access.");
94
+ }
95
+ }
96
+ async function runGh(args, cwd = defaultWorkspaceRoot()) {
97
+ try {
98
+ await execFileAsync("gh", args, {
99
+ cwd,
100
+ env: { ...process.env, GH_PROMPT_DISABLED: "1" },
101
+ maxBuffer: 1024 * 1024,
102
+ timeout: 3e4
103
+ });
104
+ } catch {
105
+ throw new TaskSourceServiceError(500, "TASK_GITHUB_COMMAND_FAILED", "GitHub command failed; check server authentication and repository access.");
106
+ }
107
+ }
108
+ function createGhCliGitHubIssueExecutor(options = {}) {
109
+ const repoArg = (owner, repo) => `${owner}/${repo}`;
110
+ const jsonFields = "number,title,body,url,state,labels,milestone";
111
+ const cwd = options.workspaceRoot;
112
+ return {
113
+ listIssues: ({ owner, repo, limit, state }) => runGhJson([
114
+ "issue",
115
+ "list",
116
+ "--repo",
117
+ repoArg(owner, repo),
118
+ "--state",
119
+ state,
120
+ "--limit",
121
+ String(limit),
122
+ "--json",
123
+ jsonFields
124
+ ], cwd),
125
+ listPullRequests: ({ owner, repo, limit, state }) => runGhJson([
126
+ "pr",
127
+ "list",
128
+ "--repo",
129
+ repoArg(owner, repo),
130
+ "--state",
131
+ state,
132
+ "--limit",
133
+ String(limit),
134
+ "--json",
135
+ "number,title,body,url,state"
136
+ ], cwd),
137
+ viewIssue: ({ owner, repo, issueNumber }) => runGhJson([
138
+ "issue",
139
+ "view",
140
+ String(issueNumber),
141
+ "--repo",
142
+ repoArg(owner, repo),
143
+ "--json",
144
+ jsonFields
145
+ ], cwd),
146
+ addLabels: async ({ owner, repo, issueNumber, labels }) => {
147
+ if (labels.length === 0) return;
148
+ await runGh(["issue", "edit", String(issueNumber), "--repo", repoArg(owner, repo), "--add-label", labels.join(",")], cwd);
149
+ },
150
+ removeLabels: async ({ owner, repo, issueNumber, labels }) => {
151
+ if (labels.length === 0) return;
152
+ await runGh(["issue", "edit", String(issueNumber), "--repo", repoArg(owner, repo), "--remove-label", labels.join(",")], cwd);
153
+ },
154
+ closeIssue: ({ owner, repo, issueNumber }) => runGh(["issue", "close", String(issueNumber), "--repo", repoArg(owner, repo)], cwd),
155
+ reopenIssue: ({ owner, repo, issueNumber }) => runGh(["issue", "reopen", String(issueNumber), "--repo", repoArg(owner, repo)], cwd)
156
+ };
157
+ }
158
+ function createGhCliGitHubRepositoryDetector() {
159
+ return {
160
+ async detectRepository({ workspaceRoot }) {
161
+ const payload = await runGhJson(["repo", "view", "--json", "nameWithOwner"], workspaceRoot);
162
+ const [owner, repo] = payload.nameWithOwner?.split("/") ?? [];
163
+ if (!owner || !repo) {
164
+ throw new TaskSourceServiceError(404, "TASK_GITHUB_REPO_NOT_FOUND", "No GitHub repository is associated with this workspace.");
165
+ }
166
+ return { owner, repo };
167
+ }
168
+ };
169
+ }
170
+ function issueLabels(issue) {
171
+ return (issue.labels ?? []).map((label) => label.name?.trim()).filter((label) => Boolean(label));
172
+ }
173
+ function issueStatus(issue) {
174
+ if (issue.state.toLowerCase() === "closed") return "done";
175
+ const labels = issueLabels(issue).map((label) => label.toLowerCase());
176
+ return WORKFLOW_LABELS.find((label) => labels.includes(label)) ?? "needs-triage";
177
+ }
178
+ function descriptionFromBody(body) {
179
+ if (!body) return void 0;
180
+ const compact = body.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]+)`/g, "$1").replace(/[#>*_\-[\]()]/g, " ").replace(/\s+/g, " ").trim();
181
+ return compact.length > 180 ? `${compact.slice(0, 177)}\u2026` : compact || void 0;
182
+ }
183
+ function associatedPullRequests(issue, pullRequests) {
184
+ const issueRef = `#${issue.number}`;
185
+ const issueUrl = issue.url?.toLowerCase();
186
+ return pullRequests.filter((pr) => {
187
+ const haystack = `${pr.title}
188
+ ${pr.body ?? ""}
189
+ ${pr.url ?? ""}`.toLowerCase();
190
+ return haystack.includes(issueRef.toLowerCase()) || Boolean(issueUrl && haystack.includes(issueUrl));
191
+ });
192
+ }
193
+ function taskFromIssue(issue, adapterId, pullRequests = []) {
194
+ const prs = associatedPullRequests(issue, pullRequests);
195
+ return {
196
+ id: String(issue.number),
197
+ number: `#${issue.number}`,
198
+ title: issue.title,
199
+ description: descriptionFromBody(issue.body),
200
+ statusId: issueStatus(issue),
201
+ tags: issueLabels(issue).filter((label) => !WORKFLOW_LABELS.includes(label.toLowerCase())),
202
+ epic: issue.milestone?.title ? {
203
+ id: String(issue.milestone.id ?? issue.milestone.number ?? issue.milestone.title),
204
+ title: issue.milestone.title,
205
+ url: issue.milestone.url
206
+ } : void 0,
207
+ adapterId,
208
+ pullRequests: prs.map((pr) => ({
209
+ id: String(pr.number),
210
+ number: `#${pr.number}`,
211
+ title: pr.title,
212
+ url: pr.url,
213
+ state: pr.state
214
+ })),
215
+ url: issue.url
216
+ };
217
+ }
218
+ function createGitHubTaskSource({ owner, repo, limit = 200, state = "open", executor = createGhCliGitHubIssueExecutor() }) {
219
+ const sourceId = `github:${owner}/${repo}`;
220
+ const board = {
221
+ adapterId: sourceId,
222
+ defaultColumnId: "needs-triage",
223
+ columns: GITHUB_COLUMNS
224
+ };
225
+ return {
226
+ summary: () => ({
227
+ id: sourceId,
228
+ label: `GitHub ${owner}/${repo}`,
229
+ description: "GitHub Issues via backend task source",
230
+ capabilities: { move: true, delete: true }
231
+ }),
232
+ getBoardConfig: () => board,
233
+ async listTasks(_ctx) {
234
+ const [issues, pullRequests] = await Promise.all([
235
+ executor.listIssues({ owner, repo, limit, state }),
236
+ executor.listPullRequests?.({ owner, repo, limit: 100, state: "open" }) ?? Promise.resolve([])
237
+ ]);
238
+ return issues.map((issue) => taskFromIssue(issue, sourceId, pullRequests));
239
+ },
240
+ async moveTask(_ctx, { taskId, statusId }) {
241
+ const issueNumber = Number(taskId);
242
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
243
+ throw new TaskSourceServiceError(400, "TASK_INVALID_ID", `Invalid GitHub issue task id: ${taskId}`);
244
+ }
245
+ const mapping = STATUS_MAPPINGS[statusId];
246
+ if (!mapping) throw new TaskSourceServiceError(400, "TASK_STATUS_NOT_FOUND", `Unknown GitHub task status: ${statusId}`);
247
+ const before = await executor.viewIssue({ owner, repo, issueNumber });
248
+ if (mapping.close) await executor.closeIssue({ owner, repo, issueNumber });
249
+ if (mapping.reopen && before.state.toLowerCase() === "closed") await executor.reopenIssue({ owner, repo, issueNumber });
250
+ if (mapping.removeStateLabels) {
251
+ const stateLabels = issueLabels(before).filter((label) => WORKFLOW_LABELS.includes(label.toLowerCase()));
252
+ await executor.removeLabels({ owner, repo, issueNumber, labels: stateLabels });
253
+ }
254
+ await executor.addLabels({ owner, repo, issueNumber, labels: mapping.addLabels ?? [] });
255
+ const after = await executor.viewIssue({ owner, repo, issueNumber });
256
+ return taskFromIssue(after, sourceId);
257
+ },
258
+ async deleteTask(_ctx, { taskId }) {
259
+ const issueNumber = Number(taskId);
260
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
261
+ throw new TaskSourceServiceError(400, "TASK_INVALID_ID", `Invalid GitHub issue task id: ${taskId}`);
262
+ }
263
+ await executor.closeIssue({ owner, repo, issueNumber });
264
+ }
265
+ };
266
+ }
267
+ function createWorkspaceGitHubTaskSource({
268
+ workspaceRoot,
269
+ sourceId = "github:workspace",
270
+ limit = 200,
271
+ state = "open",
272
+ detector = createGhCliGitHubRepositoryDetector(),
273
+ executorFactory = ({ workspaceRoot: workspaceRoot2 }) => createGhCliGitHubIssueExecutor({ workspaceRoot: workspaceRoot2 })
274
+ } = {}) {
275
+ const board = {
276
+ adapterId: sourceId,
277
+ defaultColumnId: "needs-triage",
278
+ columns: GITHUB_COLUMNS
279
+ };
280
+ const resolveWorkspaceRoot = (ctx) => ctx.workspaceRoot ?? workspaceRoot ?? defaultWorkspaceRoot();
281
+ const resolveRepo = async (ctx) => {
282
+ const root = resolveWorkspaceRoot(ctx);
283
+ const repoInfo = await detector.detectRepository({ workspaceRoot: root });
284
+ return { ...repoInfo, workspaceRoot: root };
285
+ };
286
+ return {
287
+ summary: () => ({
288
+ id: sourceId,
289
+ label: "GitHub repository",
290
+ description: "GitHub Issues from the current workspace repository via gh CLI",
291
+ capabilities: { move: true, delete: true }
292
+ }),
293
+ getBoardConfig: () => board,
294
+ async listTasks(ctx) {
295
+ const repoInfo = await resolveRepo(ctx);
296
+ const executor = executorFactory(repoInfo);
297
+ const [issues, pullRequests] = await Promise.all([
298
+ executor.listIssues({ owner: repoInfo.owner, repo: repoInfo.repo, limit, state }),
299
+ executor.listPullRequests?.({ owner: repoInfo.owner, repo: repoInfo.repo, limit: 100, state: "open" }) ?? Promise.resolve([])
300
+ ]);
301
+ return issues.map((issue) => taskFromIssue(issue, sourceId, pullRequests));
302
+ },
303
+ async moveTask(ctx, { taskId, statusId }) {
304
+ const issueNumber = Number(taskId);
305
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
306
+ throw new TaskSourceServiceError(400, "TASK_INVALID_ID", `Invalid GitHub issue task id: ${taskId}`);
307
+ }
308
+ const mapping = STATUS_MAPPINGS[statusId];
309
+ if (!mapping) throw new TaskSourceServiceError(400, "TASK_STATUS_NOT_FOUND", `Unknown GitHub task status: ${statusId}`);
310
+ const repoInfo = await resolveRepo(ctx);
311
+ const executor = executorFactory(repoInfo);
312
+ const input = { owner: repoInfo.owner, repo: repoInfo.repo, issueNumber };
313
+ const before = await executor.viewIssue(input);
314
+ if (mapping.close) await executor.closeIssue(input);
315
+ if (mapping.reopen && before.state.toLowerCase() === "closed") await executor.reopenIssue(input);
316
+ if (mapping.removeStateLabels) {
317
+ const stateLabels = issueLabels(before).filter((label) => WORKFLOW_LABELS.includes(label.toLowerCase()));
318
+ await executor.removeLabels({ ...input, labels: stateLabels });
319
+ }
320
+ await executor.addLabels({ ...input, labels: mapping.addLabels ?? [] });
321
+ const after = await executor.viewIssue(input);
322
+ return taskFromIssue(after, sourceId);
323
+ },
324
+ async deleteTask(ctx, { taskId }) {
325
+ const issueNumber = Number(taskId);
326
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
327
+ throw new TaskSourceServiceError(400, "TASK_INVALID_ID", `Invalid GitHub issue task id: ${taskId}`);
328
+ }
329
+ const repoInfo = await resolveRepo(ctx);
330
+ const executor = executorFactory(repoInfo);
331
+ await executor.closeIssue({ owner: repoInfo.owner, repo: repoInfo.repo, issueNumber });
332
+ }
333
+ };
334
+ }
335
+
336
+ // src/server/sourceRuntime.ts
337
+ function createTaskSourceRegistry(sources) {
338
+ const byId = /* @__PURE__ */ new Map();
339
+ for (const source of sources) {
340
+ const id = source.summary().id;
341
+ if (byId.has(id)) throw new Error(`Duplicate task source id: ${id}`);
342
+ byId.set(id, source);
343
+ }
344
+ return {
345
+ listSources: () => [...sources],
346
+ getSource: (sourceId) => byId.get(sourceId)
347
+ };
348
+ }
349
+
350
+ // src/server/index.ts
351
+ function workspaceIdFromRequest(request) {
352
+ const header = request.headers["x-boring-workspace-id"];
353
+ if (typeof header === "string" && header.length > 0) return header;
354
+ const query = request.query;
355
+ return typeof query?.workspaceId === "string" && query.workspaceId.length > 0 ? query.workspaceId : void 0;
356
+ }
357
+ function responseError(cause) {
358
+ if (cause instanceof TaskSourceServiceError) {
359
+ return { ok: false, code: cause.code, error: cause.message };
360
+ }
361
+ return { ok: false, code: "TASK_SOURCE_ERROR", error: "Task source request failed." };
362
+ }
363
+ function statusFor(cause) {
364
+ return cause instanceof TaskSourceServiceError ? cause.status : 500;
365
+ }
366
+ function stringArray(value) {
367
+ if (value === void 0) return void 0;
368
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
369
+ throw new TaskSourceServiceError(400, "TASK_INVALID_BODY", "sourceIds must be an array of non-empty strings");
370
+ }
371
+ return value;
372
+ }
373
+ function requiredString(body, key) {
374
+ const value = body[key];
375
+ if (typeof value !== "string" || value.length === 0) {
376
+ throw new TaskSourceServiceError(400, "TASK_INVALID_BODY", `${key} must be a non-empty string`);
377
+ }
378
+ return value;
379
+ }
380
+ function bodyObject(body) {
381
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
382
+ throw new TaskSourceServiceError(400, "TASK_INVALID_BODY", "request body must be an object");
383
+ }
384
+ return body;
385
+ }
386
+ function taskProvidersFromConfig(config) {
387
+ if (!config || typeof config !== "object" || Array.isArray(config)) return [];
388
+ const providers = config.providers;
389
+ return Array.isArray(providers) ? providers.filter((provider) => Boolean(provider) && typeof provider === "object" && !Array.isArray(provider)) : [];
390
+ }
391
+ function createTaskSourceRegistryFromConfig(config, options = {}) {
392
+ const sources = taskProvidersFromConfig(config).flatMap((provider, index) => {
393
+ if (provider.provider !== "github") return [];
394
+ const repo = typeof provider.repo === "string" ? provider.repo.trim() : "";
395
+ if (repo && repo !== "auto") {
396
+ const [owner, name] = repo.split("/");
397
+ if (!owner || !name) return [];
398
+ return [createGitHubTaskSource({
399
+ owner,
400
+ repo: name,
401
+ executor: createGhCliGitHubIssueExecutor({ workspaceRoot: options.workspaceRoot })
402
+ })];
403
+ }
404
+ return [createWorkspaceGitHubTaskSource({
405
+ workspaceRoot: options.workspaceRoot,
406
+ sourceId: index === 0 ? "github:workspace" : `github:workspace:${index + 1}`
407
+ })];
408
+ });
409
+ return createTaskSourceRegistry(sources);
410
+ }
411
+ function createTasksServerPlugin(options = {}) {
412
+ const registry = options.sources ? createTaskSourceRegistry(options.sources) : createTaskSourceRegistryFromConfig(options.config, { workspaceRoot: options.workspaceRoot });
413
+ const service = createTaskSourceService(registry);
414
+ return defineServerPlugin({
415
+ id: TASKS_PLUGIN_ID,
416
+ label: TASKS_PLUGIN_LABEL,
417
+ routes: async (app) => {
418
+ app.get("/api/boring-tasks/sources", async () => ({ ok: true, sources: service.listSources() }));
419
+ app.post("/api/boring-tasks/sources/tasks/list", async (request, reply) => {
420
+ try {
421
+ const body = request.body === void 0 ? {} : bodyObject(request.body);
422
+ return { ok: true, ...await service.listTasks({ workspaceId: workspaceIdFromRequest(request), workspaceRoot: options.workspaceRoot }, { sourceIds: stringArray(body.sourceIds) }) };
423
+ } catch (cause) {
424
+ return reply.status(statusFor(cause)).send(responseError(cause));
425
+ }
426
+ });
427
+ app.post("/api/boring-tasks/sources/tasks/move", async (request, reply) => {
428
+ try {
429
+ const body = bodyObject(request.body);
430
+ const task = await service.moveTask({ workspaceId: workspaceIdFromRequest(request), workspaceRoot: options.workspaceRoot }, {
431
+ sourceId: requiredString(body, "sourceId"),
432
+ taskId: requiredString(body, "taskId"),
433
+ statusId: requiredString(body, "statusId")
434
+ });
435
+ return { ok: true, task };
436
+ } catch (cause) {
437
+ return reply.status(statusFor(cause)).send(responseError(cause));
438
+ }
439
+ });
440
+ app.post("/api/boring-tasks/sources/tasks/delete", async (request, reply) => {
441
+ try {
442
+ const body = bodyObject(request.body);
443
+ await service.deleteTask({ workspaceId: workspaceIdFromRequest(request), workspaceRoot: options.workspaceRoot }, {
444
+ sourceId: requiredString(body, "sourceId"),
445
+ taskId: requiredString(body, "taskId")
446
+ });
447
+ return { ok: true };
448
+ } catch (cause) {
449
+ return reply.status(statusFor(cause)).send(responseError(cause));
450
+ }
451
+ });
452
+ }
453
+ });
454
+ }
455
+ function defaultTasksServerPlugin(options, ctx) {
456
+ return createTasksServerPlugin({ ...options, workspaceRoot: options?.workspaceRoot ?? ctx?.workspaceRoot });
457
+ }
458
+ export {
459
+ TaskSourceServiceError,
460
+ createGhCliGitHubIssueExecutor,
461
+ createGhCliGitHubRepositoryDetector,
462
+ createGitHubTaskSource,
463
+ createTaskSourceRegistry,
464
+ createTaskSourceRegistryFromConfig,
465
+ createTaskSourceService,
466
+ createTasksServerPlugin,
467
+ createWorkspaceGitHubTaskSource,
468
+ defaultTasksServerPlugin as default
469
+ };