@opsee/mcp-server 0.9.32 → 0.9.34

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,14 @@
1
+ import type { User } from "../../../gen/api/v1/models_pb.js";
2
+
3
+ export function formatUser(u: User): string {
4
+ const lines = [`${u.fullName} (${u.email})`];
5
+ lines.push(` ID: ${u.id}`);
6
+ if (u.flattenedRoles?.length) lines.push(` Roles: ${u.flattenedRoles.join(", ")}`);
7
+ return lines.join("\n");
8
+ }
9
+
10
+ export function formatUserList(users: User[]): string {
11
+ if (users.length === 0) return "No users found.";
12
+ const header = `Found ${users.length} user(s):\n`;
13
+ return header + users.map((u, i) => `${i + 1}. ${formatUser(u)}`).join("\n\n");
14
+ }
@@ -1,634 +1,4 @@
1
- import type { Task, Project, Cycle, DocPage, DocSpace, Milestone, MilestoneTask, User, Label, TaskLabel, Comment, TaskDependency, Initiative, InitiativeMemoryEntry, TaskPullRequest } from "../../gen/api/v1/models_pb.js";
2
- import { TaskDependencyType } from "../../gen/api/v1/models_pb.js";
3
- import type { InitiativeGraph, InitiativeSlice, ReconcileChangeset } from "../../gen/api/v1/initiative_pb.js";
4
- import { Code, ConnectError } from "@connectrpc/connect";
5
-
6
- export function formatProject(p: Project): string {
7
- const lines = [
8
- `${p.name} (${p.shortName})`,
9
- ` Slug: ${p.slug}`,
10
- ];
11
- if (p.description) lines.push(` Description: ${p.description}`);
12
- lines.push(` Active: ${p.isActive ? "Yes" : "No"}`);
13
- lines.push(` ID: ${p.id}`);
14
- return lines.join("\n");
15
- }
16
-
17
- export function formatProjectList(projects: Project[]): string {
18
- if (projects.length === 0) return "No projects found.";
19
- const header = `Found ${projects.length} project(s):\n`;
20
- return (
21
- header +
22
- projects.map((p, i) => `${i + 1}. ${formatProject(p)}`).join("\n\n")
23
- );
24
- }
25
-
26
- export function formatTask(t: Task, opts: { full?: boolean } = {}): string {
27
- const lines = [`[${t.identifier}] ${t.title}`];
28
-
29
- const status = t.boardColumn?.name || "Unknown";
30
- const priority = t.taskPriority?.name || "None";
31
- const type = t.taskType?.name || "Unknown";
32
- lines.push(` Status: ${status} | Priority: ${priority} | Type: ${type}`);
33
-
34
- const assignee = t.assignedUser?.fullName || "Unassigned";
35
- const cycle = t.cycle?.name || "None";
36
- const milestone = t.milestone?.name || "None";
37
- lines.push(` Assignee: ${assignee} | Cycle: ${cycle} | Milestone: ${milestone}`);
38
-
39
- const parent = t.parentTaskId ? `#${t.parentTaskId}` : "—";
40
- lines.push(` Parent: ${parent}`);
41
-
42
- if (t.storyPoints) lines.push(` Story Points: ${t.storyPoints}`);
43
- if (t.estimatedHours) lines.push(` Estimated Hours: ${t.estimatedHours}`);
44
-
45
- if (t.description) {
46
- const desc =
47
- !opts.full && t.description.length > 200
48
- ? t.description.substring(0, 200) + "..."
49
- : t.description;
50
- lines.push(` Description: ${desc}`);
51
- }
52
-
53
- if (t.dueDate) {
54
- lines.push(` Due: ${new Date(Number(t.dueDate.seconds) * 1000).toISOString().split("T")[0]}`);
55
- }
56
-
57
- lines.push(` ID: ${t.id}`);
58
- return lines.join("\n");
59
- }
60
-
61
- interface TaskListMeta {
62
- totalCount?: number;
63
- page?: number;
64
- hasMore?: boolean;
65
- nextPage?: number;
66
- }
67
-
68
- export function formatTaskList(tasks: Task[], projectNameOrMeta?: string | TaskListMeta): string {
69
- if (tasks.length === 0) return "No tasks found.";
70
-
71
- // Support both old signature (projectName: string) and new signature (meta: TaskListMeta)
72
- const isMetaObject = projectNameOrMeta && typeof projectNameOrMeta === 'object';
73
- const meta = isMetaObject ? projectNameOrMeta as TaskListMeta : undefined;
74
- const projectName = !isMetaObject ? projectNameOrMeta as string | undefined : undefined;
75
-
76
- const lines: string[] = [];
77
-
78
- // Pagination metadata header
79
- if (meta?.totalCount !== undefined) {
80
- const pageInfo = meta.page ? `page ${meta.page}` : '';
81
- const totalInfo = `${meta.totalCount} total`;
82
- const header = [pageInfo, totalInfo].filter(Boolean).join(', ');
83
- lines.push(`Tasks (${header}):`);
84
- if (meta.hasMore && meta.nextPage) {
85
- lines.push(`(Use page: ${meta.nextPage} to fetch the next 50 tasks)`);
86
- }
87
- lines.push('');
88
- } else {
89
- // Original header format
90
- const header = projectName
91
- ? `Found ${tasks.length} task(s) in project "${projectName}":\n`
92
- : `Found ${tasks.length} task(s):\n`;
93
- lines.push(header.trimEnd());
94
- lines.push('');
95
- }
96
-
97
- // Task list
98
- tasks.forEach((t, i) => {
99
- lines.push(`${i + 1}. ${formatTask(t)}`);
100
- lines.push('');
101
- });
102
-
103
- return lines.join('\n').trimEnd();
104
- }
105
-
106
- export function formatCycle(c: Cycle): string {
107
- const lines = [`${c.name}`];
108
- if (c.description) lines.push(` Description: ${c.description}`);
109
- if (c.goal) lines.push(` Goal: ${c.goal}`);
110
- lines.push(` Start: ${c.startDate} | End: ${c.endDate}`);
111
- lines.push(` Active: ${c.isActive ? "Yes" : "No"}`);
112
- lines.push(` ID: ${c.id}`);
113
- return lines.join("\n");
114
- }
115
-
116
- export function formatCycleList(cycles: Cycle[]): string {
117
- if (cycles.length === 0) return "No cycles found.";
118
- const header = `Found ${cycles.length} cycle(s):\n`;
119
- return (
120
- header + cycles.map((c, i) => `${i + 1}. ${formatCycle(c)}`).join("\n\n")
121
- );
122
- }
123
-
124
- export function formatDocSpace(s: DocSpace): string {
125
- const lines = [`${s.title}`];
126
- if (s.description) lines.push(` Description: ${s.description}`);
127
- lines.push(` Slug: ${s.slug}`);
128
- lines.push(` Default: ${s.isDefault ? "Yes" : "No"}`);
129
- lines.push(` ID: ${s.id}`);
130
- return lines.join("\n");
131
- }
132
-
133
- export function formatDocSpaceList(spaces: DocSpace[]): string {
134
- if (spaces.length === 0) return "No doc spaces found.";
135
- const header = `Found ${spaces.length} doc space(s):\n`;
136
- return (
137
- header +
138
- spaces.map((s, i) => `${i + 1}. ${formatDocSpace(s)}`).join("\n\n")
139
- );
140
- }
141
-
142
- export function formatDocPage(p: DocPage): string {
143
- const lines = [`${p.title}`];
144
- lines.push(` Slug: ${p.slug}`);
145
- if (p.currentContent) {
146
- const content =
147
- p.currentContent.length > 500
148
- ? p.currentContent.substring(0, 500) + "..."
149
- : p.currentContent;
150
- lines.push(` Content: ${content}`);
151
- }
152
- lines.push(` Version: ${p.currentVersionNumber}`);
153
- if (p.createdByUser) lines.push(` Author: ${p.createdByUser.fullName}`);
154
- lines.push(` ID: ${p.id}`);
155
- return lines.join("\n");
156
- }
157
-
158
- export function formatDocPageList(pages: DocPage[]): string {
159
- if (pages.length === 0) return "No doc pages found.";
160
- const header = `Found ${pages.length} doc page(s):\n`;
161
- return (
162
- header +
163
- pages
164
- .map(
165
- (p, i) =>
166
- `${i + 1}. ${p.title} (slug: ${p.slug}, ID: ${p.id})`
167
- )
168
- .join("\n")
169
- );
170
- }
171
-
172
- function formatTimestamp(ts: { seconds: bigint | number } | undefined): string {
173
- if (!ts) return "—";
174
- const ms = typeof ts.seconds === "bigint" ? Number(ts.seconds) * 1000 : ts.seconds * 1000;
175
- return new Date(ms).toISOString().split("T")[0];
176
- }
177
-
178
- export function formatMilestone(m: Milestone): string {
179
- const lines = [`${m.name}`];
180
- if (m.description) lines.push(` Description: ${m.description}`);
181
- lines.push(` Status: ${m.status}`);
182
- lines.push(` Start: ${formatTimestamp(m.startDate)} | End: ${formatTimestamp(m.endDate)}`);
183
- lines.push(` Project ID: ${m.projectId}`);
184
- lines.push(` ID: ${m.id} | UUID: ${m.uuid}`);
185
- return lines.join("\n");
186
- }
187
-
188
- export function formatMilestoneList(milestones: Milestone[]): string {
189
- if (milestones.length === 0) return "No milestones found.";
190
- const header = `Found ${milestones.length} milestone(s):\n`;
191
- return header + milestones.map((m, i) => `${i + 1}. ${formatMilestone(m)}`).join("\n\n");
192
- }
193
-
194
- export function formatMilestoneTask(mt: MilestoneTask): string {
195
- const t = mt.task;
196
- if (!t) return ` Task ID: ${mt.taskId} (MilestoneTask ID: ${mt.id})`;
197
- const parts = [`${t.identifier ? `${t.identifier} ` : ""}${t.title || `Task #${mt.taskId}`}`];
198
- if (t.boardColumn?.name) parts.push(` Status: ${t.boardColumn.name}`);
199
- if (t.taskPriority?.name) parts.push(` Priority: ${t.taskPriority.name}`);
200
- parts.push(` Task ID: ${mt.taskId} | MilestoneTask ID: ${mt.id}`);
201
- return parts.join("\n");
202
- }
203
-
204
- export function formatMilestoneTaskList(milestoneTasks: MilestoneTask[]): string {
205
- if (milestoneTasks.length === 0) return "No tasks attached to this milestone.";
206
- const header = `Found ${milestoneTasks.length} task(s):\n`;
207
- return header + milestoneTasks.map((mt, i) => `${i + 1}. ${formatMilestoneTask(mt)}`).join("\n\n");
208
- }
209
-
210
- export function formatUser(u: User): string {
211
- const lines = [`${u.fullName} (${u.email})`];
212
- lines.push(` ID: ${u.id}`);
213
- if (u.flattenedRoles?.length) lines.push(` Roles: ${u.flattenedRoles.join(", ")}`);
214
- return lines.join("\n");
215
- }
216
-
217
- export function formatUserList(users: User[]): string {
218
- if (users.length === 0) return "No users found.";
219
- const header = `Found ${users.length} user(s):\n`;
220
- return header + users.map((u, i) => `${i + 1}. ${formatUser(u)}`).join("\n\n");
221
- }
222
-
223
- export function formatLabel(l: Label): string {
224
- const lines = [`${l.name}`];
225
- lines.push(` Color: ${l.color}`);
226
- if (l.description) lines.push(` Description: ${l.description}`);
227
- lines.push(` Active: ${l.isActive ? "Yes" : "No"}`);
228
- lines.push(` Project ID: ${l.projectId}`);
229
- lines.push(` ID: ${l.id}`);
230
- return lines.join("\n");
231
- }
232
-
233
- export function formatLabelList(labels: Label[]): string {
234
- if (labels.length === 0) return "No labels found.";
235
- const header = `Found ${labels.length} label(s):\n`;
236
- return header + labels.map((l, i) => `${i + 1}. ${formatLabel(l)}`).join("\n\n");
237
- }
238
-
239
- export function formatTaskLabelList(taskLabels: TaskLabel[]): string {
240
- if (taskLabels.length === 0) return "No labels attached to this task.";
241
- const header = `Found ${taskLabels.length} label attachment(s):\n`;
242
- return header + taskLabels.map((tl, i) => {
243
- const labelName = tl.label?.name || `Label #${tl.labelId}`;
244
- return `${i + 1}. ${labelName} (Label ID: ${tl.labelId} | TaskLabel ID: ${tl.id})`;
245
- }).join("\n");
246
- }
247
-
248
- export function formatComment(c: Comment): string {
249
- const lines = [];
250
- const author = c.user?.fullName || `User #${c.userId}`;
251
- const created = c.createdAt
252
- ? new Date((typeof c.createdAt.seconds === "bigint" ? Number(c.createdAt.seconds) : c.createdAt.seconds) * 1000).toISOString()
253
- : "—";
254
- lines.push(`${author} @ ${created}${c.isInternal ? " [internal]" : ""}`);
255
- lines.push(` ${c.content}`);
256
- lines.push(` ID: ${c.id}`);
257
- return lines.join("\n");
258
- }
259
-
260
- export function formatCommentList(comments: Comment[]): string {
261
- if (comments.length === 0) return "No comments found.";
262
- const header = `Found ${comments.length} comment(s):\n`;
263
- return header + comments.map((c, i) => `${i + 1}. ${formatComment(c)}`).join("\n\n");
264
- }
265
-
266
- function taskDependencyTypeToString(t: TaskDependencyType): string {
267
- switch (t) {
268
- case TaskDependencyType.BLOCKS: return "BLOCKS";
269
- case TaskDependencyType.BLOCKED_BY: return "BLOCKED_BY";
270
- case TaskDependencyType.DUPLICATES: return "DUPLICATES";
271
- case TaskDependencyType.RELATES_TO: return "RELATES_TO";
272
- default: return "UNSPECIFIED";
273
- }
274
- }
275
-
276
- export function formatTaskDependency(d: TaskDependency): string {
277
- const typeStr = taskDependencyTypeToString(d.type);
278
- const from = d.fromTask?.identifier || `Task #${d.fromTaskId}`;
279
- const to = d.toTask?.identifier || `Task #${d.toTaskId}`;
280
- return `${from} ${typeStr} ${to}\n ID: ${d.id}`;
281
- }
282
-
283
- export function formatTaskDependencyList(deps: TaskDependency[]): string {
284
- if (deps.length === 0) return "No task dependencies found.";
285
- return `Found ${deps.length} dependency(ies):\n` +
286
- deps.map((d, i) => `${i + 1}. ${formatTaskDependency(d)}`).join("\n\n");
287
- }
288
-
289
- // ---------------------------------------------------------------------------
290
- // BlockNote JSON → Markdown
291
- // ---------------------------------------------------------------------------
292
-
293
- // `strike` is the key the BlockNote default schema uses, and it is what both Go
294
- // writers persist (backend/service/task.go parseInlineMarkdown and
295
- // orchestrator/internal/util/blocknote.go). `strikethrough` is accepted too so a
296
- // payload using the long spelling is not silently dropped.
297
- type BNInlineStyle = {
298
- bold?: boolean;
299
- italic?: boolean;
300
- underline?: boolean;
301
- strike?: boolean;
302
- strikethrough?: boolean;
303
- code?: boolean;
304
- };
305
- type BNTextContent = { type: "text"; text: string; styles?: BNInlineStyle };
306
- type BNLinkContent = { type: "link"; href: string; content: BNTextContent[] };
307
- type BNInlineContent = BNTextContent | BNLinkContent;
308
-
309
- interface BNBlock {
310
- type: string;
311
- props?: Record<string, unknown>;
312
- content?: BNInlineContent[];
313
- children?: BNBlock[];
314
- }
315
-
316
- function renderInline(content: BNInlineContent[]): string {
317
- return content.map((node) => {
318
- if (node.type === "link") {
319
- const inner = renderInline(node.content ?? []);
320
- return `[${inner}](${node.href})`;
321
- }
322
- // text node
323
- const t = node as BNTextContent;
324
- let s = t.text ?? "";
325
- if (t.styles?.code) s = `\`${s}\``;
326
- if (t.styles?.bold) s = `**${s}**`;
327
- if (t.styles?.italic) s = `*${s}*`;
328
- // Struck-through text usually means "decided against this". Dropping the
329
- // markers would present an abandoned plan item as a live one.
330
- if (t.styles?.strike || t.styles?.strikethrough) s = `~~${s}~~`;
331
- if (t.styles?.underline) s = `<u>${s}</u>`;
332
- return s;
333
- }).join("");
334
- }
335
-
336
- function renderBlock(block: BNBlock, indent: string): string[] {
337
- const lines: string[] = [];
338
- const inline = renderInline(block.content ?? []);
339
-
340
- switch (block.type) {
341
- case "heading": {
342
- const level = (block.props?.level as number) || 1;
343
- const prefix = "#".repeat(Math.min(Math.max(level, 1), 3));
344
- lines.push(`${indent}${prefix} ${inline}`);
345
- break;
346
- }
347
- case "bulletListItem":
348
- lines.push(`${indent}- ${inline}`);
349
- break;
350
- case "numberedListItem":
351
- lines.push(`${indent}1. ${inline}`);
352
- break;
353
- case "checkListItem": {
354
- const checked = block.props?.checked ? "x" : " ";
355
- lines.push(`${indent}- [${checked}] ${inline}`);
356
- break;
357
- }
358
- case "codeBlock": {
359
- const lang = (block.props?.language as string) || "";
360
- lines.push(`${indent}\`\`\`${lang}`);
361
- // A code block's text is one node containing newlines. Indent every line,
362
- // not just the first, or the rest escapes the list item it is nested in.
363
- for (const body of inline.split("\n")) lines.push(`${indent}${body}`);
364
- lines.push(`${indent}\`\`\``);
365
- break;
366
- }
367
- case "quote":
368
- lines.push(`${indent}> ${inline}`);
369
- break;
370
- case "paragraph":
371
- default:
372
- lines.push(`${indent}${inline}`);
373
- break;
374
- }
375
-
376
- if (block.children?.length) {
377
- for (const child of block.children) {
378
- lines.push(...renderBlock(child, indent + " "));
379
- }
380
- }
381
-
382
- return lines;
383
- }
384
-
385
- export function blockNoteToMarkdown(content: string): string {
386
- try {
387
- const parsed = JSON.parse(content);
388
- if (!Array.isArray(parsed)) return content;
389
-
390
- const mdLines: string[] = [];
391
- for (let i = 0; i < parsed.length; i++) {
392
- const block = parsed[i] as BNBlock;
393
- if (typeof block !== "object" || block === null || !block.type) return content;
394
- const blockLines = renderBlock(block, "");
395
- mdLines.push(...blockLines);
396
- // Separate top-level blocks with a blank line (except last)
397
- if (i < parsed.length - 1) mdLines.push("");
398
- }
399
- return mdLines.join("\n");
400
- } catch {
401
- return content;
402
- }
403
- }
404
-
405
- export function formatInitiative(ini: Initiative): string {
406
- const lines = [`${ini.title}`];
407
- if (ini.summary) lines.push(` Summary: ${ini.summary}`);
408
- lines.push(` Status: ${ini.status}`);
409
- lines.push(` Anchor: ${ini.anchorType}${ini.anchorId ? ` #${ini.anchorId}` : ""}`);
410
- if (ini.coreIdea) {
411
- const md = blockNoteToMarkdown(ini.coreIdea);
412
- const preview = md.length > 120 ? md.substring(0, 120) + "..." : md;
413
- lines.push(` Core Idea: ${preview}`);
414
- }
415
- const creator = ini.createdByUser?.fullName || `User #${ini.createdByUserId}`;
416
- lines.push(` Created by: ${creator}`);
417
- lines.push(` Project ID: ${ini.projectId}`);
418
- lines.push(` ID: ${ini.id} | UUID: ${ini.uuid}`);
419
- return lines.join("\n");
420
- }
421
-
422
- export function formatInitiativeList(initiatives: Initiative[]): string {
423
- if (initiatives.length === 0) return "No initiatives found.";
424
- const header = `Found ${initiatives.length} initiative(s):\n`;
425
- return header + initiatives.map((ini, i) => `${i + 1}. ${formatInitiative(ini)}`).join("\n\n");
426
- }
427
-
428
- export function formatInitiativeMemoryEntry(e: InitiativeMemoryEntry): string {
429
- const lines = [];
430
- const who = e.isSystem ? "system" : e.isAgent ? "agent" : (e.authorUser?.fullName || `User #${e.authorUserId}`);
431
- const created = e.createdAt
432
- ? new Date((typeof e.createdAt.seconds === "bigint" ? Number(e.createdAt.seconds) : e.createdAt.seconds) * 1000).toISOString()
433
- : "—";
434
- lines.push(`[${e.kind.toUpperCase()}] ${who} @ ${created}`);
435
- lines.push(` ${e.body}`);
436
- if (e.sourceType && e.sourceType !== "none") {
437
- const src = e.sourceId ? `${e.sourceType} #${e.sourceId}` : e.sourceType;
438
- lines.push(` Source: ${src}${e.sourceUrl ? ` (${e.sourceUrl})` : ""}`);
439
- }
440
- lines.push(` ID: ${e.id}`);
441
- return lines.join("\n");
442
- }
443
-
444
- export function formatInitiativeMemoryList(entries: InitiativeMemoryEntry[]): string {
445
- if (entries.length === 0) return "No memory entries found.";
446
- const header = `Found ${entries.length} memory entry(ies) (newest first):\n`;
447
- return header + entries.map((e, i) => `${i + 1}. ${formatInitiativeMemoryEntry(e)}`).join("\n\n");
448
- }
449
-
450
- export function formatInitiativeGraph(graph: InitiativeGraph, taskMap?: Map<number, Task>): string {
451
- const lines: string[] = [];
452
-
453
- if (graph.tasks.length === 0) {
454
- lines.push("No tasks in this initiative yet.");
455
- } else {
456
- lines.push(`Tasks (${graph.tasks.length}):`);
457
- graph.tasks.forEach((t, i) => {
458
- const status = t.boardColumn?.name || "Unknown";
459
- const priority = t.taskPriority?.name || "None";
460
- lines.push(` ${i + 1}. [${t.identifier || `#${t.id}`}] ${t.title}`);
461
- lines.push(` Status: ${status} | Priority: ${priority} | ID: ${t.id}`);
462
- });
463
- }
464
-
465
- if (graph.edges.length > 0) {
466
- lines.push(`\nDependency Edges (${graph.edges.length}):`);
467
- graph.edges.forEach((e) => {
468
- const localMap = taskMap || new Map(graph.tasks.map(t => [t.id, t]));
469
- const fromTask = localMap.get(e.fromTaskId);
470
- const toTask = localMap.get(e.toTaskId);
471
- const from = fromTask ? (fromTask.identifier || `#${e.fromTaskId}`) : `#${e.fromTaskId}`;
472
- const to = toTask ? (toTask.identifier || `#${e.toTaskId}`) : `#${e.toTaskId}`;
473
- lines.push(` ${from} ${e.type.toUpperCase()} ${to}`);
474
- });
475
- }
476
-
477
- if (graph.slices.length > 0) {
478
- lines.push(`\nParallel Execution Batches:`);
479
- const localMap = taskMap || new Map(graph.tasks.map(t => [t.id, t]));
480
- const sorted = [...graph.slices].sort((a, b) => a.layer - b.layer);
481
- sorted.forEach((slice: InitiativeSlice) => {
482
- const taskLabels = slice.taskIds.map((id) => {
483
- const t = localMap.get(id);
484
- return t ? `[${t.identifier || `#${id}`}] ${t.title}` : `#${id}`;
485
- });
486
- lines.push(` Parallel batch ${slice.layer}: ${taskLabels.join(", ")}`);
487
- });
488
- }
489
-
490
- return lines.join("\n");
491
- }
492
-
493
- export const WORKING_AGREEMENT = [
494
- "— Working agreement —",
495
- "This Initiative is a shared brain that outlives this session. To keep it useful:",
496
- "1. This context (core idea · task tree · memory · PRs) is the source of truth — read it before acting.",
497
- "2. Log each decision / blocker / learning as it happens with opsee_add_initiative_memory (cite the task or PR).",
498
- "3. When you finish a unit of work, call opsee_checkpoint to record what changed and what's next.",
499
- "4. Link PRs to their tasks so they roll up here.",
500
- ].join("\n");
501
-
502
- export function formatInitiativeContext(
503
- initiative: Initiative,
504
- graph: InitiativeGraph | undefined,
505
- memoryEntries: InitiativeMemoryEntry[],
506
- pullRequests: TaskPullRequest[],
507
- comments: Comment[],
508
- ): string {
509
- const sections: string[] = [];
510
-
511
- sections.push("=== INITIATIVE ===");
512
- sections.push(formatInitiative(initiative));
513
-
514
- if (initiative.coreIdea) {
515
- sections.push("\n=== CORE IDEA ===");
516
- const md = blockNoteToMarkdown(initiative.coreIdea);
517
- const body = md.length > 2000
518
- ? md.substring(0, 2000) + "\n... [truncated]"
519
- : md;
520
- sections.push(body);
521
- }
522
-
523
- if (graph) {
524
- sections.push("\n=== EXECUTION GRAPH ===");
525
- sections.push(formatInitiativeGraph(graph));
526
- }
527
-
528
- if (memoryEntries.length > 0) {
529
- sections.push("\n=== MEMORY LOG (newest first) ===");
530
- memoryEntries.forEach((e, i) => {
531
- sections.push(`${i + 1}. ${formatInitiativeMemoryEntry(e)}`);
532
- });
533
- } else {
534
- sections.push("\n=== MEMORY LOG ===");
535
- sections.push("No memory entries yet.");
536
- }
537
-
538
- if (pullRequests.length > 0) {
539
- sections.push("\n=== PULL REQUESTS ===");
540
- pullRequests.forEach((pr, i) => {
541
- const repo = pr.projectRepository?.repositoryFullName || `repo #${pr.projectRepositoryId}`;
542
- sections.push(` ${i + 1}. [${pr.state.toUpperCase()}] #${pr.prNumber} ${pr.title} (${repo})`);
543
- sections.push(` ${pr.url}`);
544
- });
545
- }
546
-
547
- if (comments.length > 0) {
548
- sections.push("\n=== DISCUSSION ===");
549
- comments.forEach((c, i) => {
550
- sections.push(`${i + 1}. ${formatComment(c)}`);
551
- });
552
- }
553
-
554
- sections.push("\n" + WORKING_AGREEMENT);
555
- return sections.join("\n");
556
- }
557
-
558
- export function formatReconcileChangeset(changeset: ReconcileChangeset): string {
559
- const lines: string[] = [];
560
-
561
- if (changeset.added.length > 0) {
562
- lines.push(`Added (${changeset.added.length}):`);
563
- changeset.added.forEach((t) => {
564
- const id = t.identifier || (t.taskId ? `#${t.taskId}` : "(new)");
565
- lines.push(` + [${id}] ${t.title}`);
566
- });
567
- }
568
-
569
- if (changeset.removed.length > 0) {
570
- lines.push(`Removed — pristine, no work detected (${changeset.removed.length}):`);
571
- changeset.removed.forEach((t) => {
572
- const id = t.identifier || `#${t.taskId}`;
573
- lines.push(` − [${id}] ${t.title}`);
574
- });
575
- }
576
-
577
- if (changeset.flagged.length > 0) {
578
- lines.push(`Flagged — NOT removed, carries execution state (${changeset.flagged.length}):`);
579
- lines.push(" These tasks have real work in progress and were left untouched.");
580
- changeset.flagged.forEach((t) => {
581
- const id = t.identifier || `#${t.taskId}`;
582
- lines.push(` ! [${id}] ${t.title} — flagged: ${t.reason}`);
583
- });
584
- }
585
-
586
- if (changeset.edgesAdded.length > 0) {
587
- lines.push(`Edges + (${changeset.edgesAdded.length}):`);
588
- changeset.edgesAdded.forEach((e) => {
589
- lines.push(` + #${e.fromTaskId} ${e.type.toUpperCase()} #${e.toTaskId}`);
590
- });
591
- }
592
-
593
- if (changeset.edgesRemoved.length > 0) {
594
- lines.push(`Edges − (${changeset.edgesRemoved.length}):`);
595
- changeset.edgesRemoved.forEach((e) => {
596
- lines.push(` − #${e.fromTaskId} ${e.type.toUpperCase()} #${e.toTaskId}`);
597
- });
598
- }
599
-
600
- if (lines.length === 0) {
601
- lines.push("No changes — the proposed plan matches the existing task tree.");
602
- }
603
-
604
- if (changeset.graph) {
605
- lines.push("\n" + formatInitiativeGraph(changeset.graph));
606
- }
607
-
608
- return lines.join("\n");
609
- }
610
-
611
- export function formatError(error: unknown): string {
612
- // Prefer the structured gRPC/Connect code over substring-matching the
613
- // message: a backend error whose text embeds a task title containing "401"
614
- // or "ECONNREFUSED" must not be misclassified as auth/network.
615
- if (error instanceof ConnectError) {
616
- if (error.code === Code.Unauthenticated) {
617
- return "Not authenticated. Run `npx opsee-mcp login` to connect your Opsee account.";
618
- }
619
- if (error.code === Code.Unavailable) {
620
- return `Could not reach Opsee API. Check your connection and OPSEE_API_URL. (${error.rawMessage})`;
621
- }
622
- return `[${Code[error.code]}] ${error.rawMessage}`;
623
- }
624
- if (error instanceof Error) {
625
- // Raw transport errors before a Connect response carry a machine-readable
626
- // code property (Node sets err.code); match on that, not the message text.
627
- const code = (error as { code?: unknown }).code;
628
- if (code === "ECONNREFUSED" || code === "ENOTFOUND") {
629
- return `Could not reach Opsee API. Check your connection and OPSEE_API_URL. (${error.message})`;
630
- }
631
- return error.message;
632
- }
633
- return String(error);
634
- }
1
+ // The formatters live in ./format/ one module per entity. Node16 resolution
2
+ // will not fold a directory into "../utils/format.js", so this file keeps that
3
+ // specifier working for the ~15 existing import sites.
4
+ export * from "./format/index.js";