@opsee/mcp-server 0.9.31 → 0.9.33
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/package.json +1 -1
- package/src/__tests__/blocknote-fixtures.test.ts +58 -0
- package/src/tools/descriptions.ts +25 -0
- package/src/tools/initiatives.ts +11 -6
- package/src/tools/tasks-bulk.ts +171 -0
- package/src/tools/tasks-delete.ts +52 -0
- package/src/tools/tasks-read-write.ts +372 -0
- package/src/tools/tasks-relations.ts +159 -0
- package/src/tools/tasks.ts +10 -704
- package/src/utils/format/blocknote.ts +115 -0
- package/src/utils/format/comment.ts +19 -0
- package/src/utils/format/common.ts +32 -0
- package/src/utils/format/cycle.ts +19 -0
- package/src/utils/format/docs.ts +49 -0
- package/src/utils/format/index.ts +30 -0
- package/src/utils/format/initiative.ts +210 -0
- package/src/utils/format/label.ts +26 -0
- package/src/utils/format/milestone.ts +34 -0
- package/src/utils/format/project.ts +21 -0
- package/src/utils/format/task-dependency.ts +25 -0
- package/src/utils/format/task.ts +81 -0
- package/src/utils/format/user.ts +14 -0
- package/src/utils/format.ts +4 -634
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// BlockNote JSON → Markdown
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
// `strike` is the key the BlockNote default schema uses, and it is what both Go
|
|
6
|
+
// writers persist (backend/service/task.go parseInlineMarkdown and
|
|
7
|
+
// orchestrator/internal/util/blocknote.go). `strikethrough` is accepted too so a
|
|
8
|
+
// payload using the long spelling is not silently dropped.
|
|
9
|
+
type BNInlineStyle = {
|
|
10
|
+
bold?: boolean;
|
|
11
|
+
italic?: boolean;
|
|
12
|
+
underline?: boolean;
|
|
13
|
+
strike?: boolean;
|
|
14
|
+
strikethrough?: boolean;
|
|
15
|
+
code?: boolean;
|
|
16
|
+
};
|
|
17
|
+
type BNTextContent = { type: "text"; text: string; styles?: BNInlineStyle };
|
|
18
|
+
type BNLinkContent = { type: "link"; href: string; content: BNTextContent[] };
|
|
19
|
+
type BNInlineContent = BNTextContent | BNLinkContent;
|
|
20
|
+
|
|
21
|
+
interface BNBlock {
|
|
22
|
+
type: string;
|
|
23
|
+
props?: Record<string, unknown>;
|
|
24
|
+
content?: BNInlineContent[];
|
|
25
|
+
children?: BNBlock[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderInline(content: BNInlineContent[]): string {
|
|
29
|
+
return content.map((node) => {
|
|
30
|
+
if (node.type === "link") {
|
|
31
|
+
const inner = renderInline(node.content ?? []);
|
|
32
|
+
return `[${inner}](${node.href})`;
|
|
33
|
+
}
|
|
34
|
+
// text node
|
|
35
|
+
const t = node as BNTextContent;
|
|
36
|
+
let s = t.text ?? "";
|
|
37
|
+
if (t.styles?.code) s = `\`${s}\``;
|
|
38
|
+
if (t.styles?.bold) s = `**${s}**`;
|
|
39
|
+
if (t.styles?.italic) s = `*${s}*`;
|
|
40
|
+
// Struck-through text usually means "decided against this". Dropping the
|
|
41
|
+
// markers would present an abandoned plan item as a live one.
|
|
42
|
+
if (t.styles?.strike || t.styles?.strikethrough) s = `~~${s}~~`;
|
|
43
|
+
if (t.styles?.underline) s = `<u>${s}</u>`;
|
|
44
|
+
return s;
|
|
45
|
+
}).join("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderBlock(block: BNBlock, indent: string): string[] {
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
const inline = renderInline(block.content ?? []);
|
|
51
|
+
|
|
52
|
+
switch (block.type) {
|
|
53
|
+
case "heading": {
|
|
54
|
+
const level = (block.props?.level as number) || 1;
|
|
55
|
+
const prefix = "#".repeat(Math.min(Math.max(level, 1), 3));
|
|
56
|
+
lines.push(`${indent}${prefix} ${inline}`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "bulletListItem":
|
|
60
|
+
lines.push(`${indent}- ${inline}`);
|
|
61
|
+
break;
|
|
62
|
+
case "numberedListItem":
|
|
63
|
+
lines.push(`${indent}1. ${inline}`);
|
|
64
|
+
break;
|
|
65
|
+
case "checkListItem": {
|
|
66
|
+
const checked = block.props?.checked ? "x" : " ";
|
|
67
|
+
lines.push(`${indent}- [${checked}] ${inline}`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "codeBlock": {
|
|
71
|
+
const lang = (block.props?.language as string) || "";
|
|
72
|
+
lines.push(`${indent}\`\`\`${lang}`);
|
|
73
|
+
// A code block's text is one node containing newlines. Indent every line,
|
|
74
|
+
// not just the first, or the rest escapes the list item it is nested in.
|
|
75
|
+
for (const body of inline.split("\n")) lines.push(`${indent}${body}`);
|
|
76
|
+
lines.push(`${indent}\`\`\``);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "quote":
|
|
80
|
+
lines.push(`${indent}> ${inline}`);
|
|
81
|
+
break;
|
|
82
|
+
case "paragraph":
|
|
83
|
+
default:
|
|
84
|
+
lines.push(`${indent}${inline}`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (block.children?.length) {
|
|
89
|
+
for (const child of block.children) {
|
|
90
|
+
lines.push(...renderBlock(child, indent + " "));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function blockNoteToMarkdown(content: string): string {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(content);
|
|
100
|
+
if (!Array.isArray(parsed)) return content;
|
|
101
|
+
|
|
102
|
+
const mdLines: string[] = [];
|
|
103
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
104
|
+
const block = parsed[i] as BNBlock;
|
|
105
|
+
if (typeof block !== "object" || block === null || !block.type) return content;
|
|
106
|
+
const blockLines = renderBlock(block, "");
|
|
107
|
+
mdLines.push(...blockLines);
|
|
108
|
+
// Separate top-level blocks with a blank line (except last)
|
|
109
|
+
if (i < parsed.length - 1) mdLines.push("");
|
|
110
|
+
}
|
|
111
|
+
return mdLines.join("\n");
|
|
112
|
+
} catch {
|
|
113
|
+
return content;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Comment } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
|
|
3
|
+
export function formatComment(c: Comment): string {
|
|
4
|
+
const lines = [];
|
|
5
|
+
const author = c.user?.fullName || `User #${c.userId}`;
|
|
6
|
+
const created = c.createdAt
|
|
7
|
+
? new Date((typeof c.createdAt.seconds === "bigint" ? Number(c.createdAt.seconds) : c.createdAt.seconds) * 1000).toISOString()
|
|
8
|
+
: "—";
|
|
9
|
+
lines.push(`${author} @ ${created}${c.isInternal ? " [internal]" : ""}`);
|
|
10
|
+
lines.push(` ${c.content}`);
|
|
11
|
+
lines.push(` ID: ${c.id}`);
|
|
12
|
+
return lines.join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatCommentList(comments: Comment[]): string {
|
|
16
|
+
if (comments.length === 0) return "No comments found.";
|
|
17
|
+
const header = `Found ${comments.length} comment(s):\n`;
|
|
18
|
+
return header + comments.map((c, i) => `${i + 1}. ${formatComment(c)}`).join("\n\n");
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
2
|
+
|
|
3
|
+
export function formatTimestamp(ts: { seconds: bigint | number } | undefined): string {
|
|
4
|
+
if (!ts) return "—";
|
|
5
|
+
const ms = typeof ts.seconds === "bigint" ? Number(ts.seconds) * 1000 : ts.seconds * 1000;
|
|
6
|
+
return new Date(ms).toISOString().split("T")[0];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatError(error: unknown): string {
|
|
10
|
+
// Prefer the structured gRPC/Connect code over substring-matching the
|
|
11
|
+
// message: a backend error whose text embeds a task title containing "401"
|
|
12
|
+
// or "ECONNREFUSED" must not be misclassified as auth/network.
|
|
13
|
+
if (error instanceof ConnectError) {
|
|
14
|
+
if (error.code === Code.Unauthenticated) {
|
|
15
|
+
return "Not authenticated. Run `npx opsee-mcp login` to connect your Opsee account.";
|
|
16
|
+
}
|
|
17
|
+
if (error.code === Code.Unavailable) {
|
|
18
|
+
return `Could not reach Opsee API. Check your connection and OPSEE_API_URL. (${error.rawMessage})`;
|
|
19
|
+
}
|
|
20
|
+
return `[${Code[error.code]}] ${error.rawMessage}`;
|
|
21
|
+
}
|
|
22
|
+
if (error instanceof Error) {
|
|
23
|
+
// Raw transport errors before a Connect response carry a machine-readable
|
|
24
|
+
// code property (Node sets err.code); match on that, not the message text.
|
|
25
|
+
const code = (error as { code?: unknown }).code;
|
|
26
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND") {
|
|
27
|
+
return `Could not reach Opsee API. Check your connection and OPSEE_API_URL. (${error.message})`;
|
|
28
|
+
}
|
|
29
|
+
return error.message;
|
|
30
|
+
}
|
|
31
|
+
return String(error);
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Cycle } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
|
|
3
|
+
export function formatCycle(c: Cycle): string {
|
|
4
|
+
const lines = [`${c.name}`];
|
|
5
|
+
if (c.description) lines.push(` Description: ${c.description}`);
|
|
6
|
+
if (c.goal) lines.push(` Goal: ${c.goal}`);
|
|
7
|
+
lines.push(` Start: ${c.startDate} | End: ${c.endDate}`);
|
|
8
|
+
lines.push(` Active: ${c.isActive ? "Yes" : "No"}`);
|
|
9
|
+
lines.push(` ID: ${c.id}`);
|
|
10
|
+
return lines.join("\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatCycleList(cycles: Cycle[]): string {
|
|
14
|
+
if (cycles.length === 0) return "No cycles found.";
|
|
15
|
+
const header = `Found ${cycles.length} cycle(s):\n`;
|
|
16
|
+
return (
|
|
17
|
+
header + cycles.map((c, i) => `${i + 1}. ${formatCycle(c)}`).join("\n\n")
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DocPage, DocSpace } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
|
|
3
|
+
export function formatDocSpace(s: DocSpace): string {
|
|
4
|
+
const lines = [`${s.title}`];
|
|
5
|
+
if (s.description) lines.push(` Description: ${s.description}`);
|
|
6
|
+
lines.push(` Slug: ${s.slug}`);
|
|
7
|
+
lines.push(` Default: ${s.isDefault ? "Yes" : "No"}`);
|
|
8
|
+
lines.push(` ID: ${s.id}`);
|
|
9
|
+
return lines.join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatDocSpaceList(spaces: DocSpace[]): string {
|
|
13
|
+
if (spaces.length === 0) return "No doc spaces found.";
|
|
14
|
+
const header = `Found ${spaces.length} doc space(s):\n`;
|
|
15
|
+
return (
|
|
16
|
+
header +
|
|
17
|
+
spaces.map((s, i) => `${i + 1}. ${formatDocSpace(s)}`).join("\n\n")
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatDocPage(p: DocPage): string {
|
|
22
|
+
const lines = [`${p.title}`];
|
|
23
|
+
lines.push(` Slug: ${p.slug}`);
|
|
24
|
+
if (p.currentContent) {
|
|
25
|
+
const content =
|
|
26
|
+
p.currentContent.length > 500
|
|
27
|
+
? p.currentContent.substring(0, 500) + "..."
|
|
28
|
+
: p.currentContent;
|
|
29
|
+
lines.push(` Content: ${content}`);
|
|
30
|
+
}
|
|
31
|
+
lines.push(` Version: ${p.currentVersionNumber}`);
|
|
32
|
+
if (p.createdByUser) lines.push(` Author: ${p.createdByUser.fullName}`);
|
|
33
|
+
lines.push(` ID: ${p.id}`);
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatDocPageList(pages: DocPage[]): string {
|
|
38
|
+
if (pages.length === 0) return "No doc pages found.";
|
|
39
|
+
const header = `Found ${pages.length} doc page(s):\n`;
|
|
40
|
+
return (
|
|
41
|
+
header +
|
|
42
|
+
pages
|
|
43
|
+
.map(
|
|
44
|
+
(p, i) =>
|
|
45
|
+
`${i + 1}. ${p.title} (slug: ${p.slug}, ID: ${p.id})`
|
|
46
|
+
)
|
|
47
|
+
.join("\n")
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Barrel for the per-entity formatters. Named re-exports rather than
|
|
2
|
+
// `export *` so that helpers shared between the modules (formatTimestamp)
|
|
3
|
+
// stay internal and the public surface only grows deliberately.
|
|
4
|
+
|
|
5
|
+
export { formatError } from "./common.js";
|
|
6
|
+
export { formatProject, formatProjectList } from "./project.js";
|
|
7
|
+
export { formatTask, formatTaskList } from "./task.js";
|
|
8
|
+
export { formatTaskDependency, formatTaskDependencyList } from "./task-dependency.js";
|
|
9
|
+
export { formatCycle, formatCycleList } from "./cycle.js";
|
|
10
|
+
export { formatDocSpace, formatDocSpaceList, formatDocPage, formatDocPageList } from "./docs.js";
|
|
11
|
+
export {
|
|
12
|
+
formatMilestone,
|
|
13
|
+
formatMilestoneList,
|
|
14
|
+
formatMilestoneTask,
|
|
15
|
+
formatMilestoneTaskList,
|
|
16
|
+
} from "./milestone.js";
|
|
17
|
+
export { formatUser, formatUserList } from "./user.js";
|
|
18
|
+
export { formatLabel, formatLabelList, formatTaskLabelList } from "./label.js";
|
|
19
|
+
export { formatComment, formatCommentList } from "./comment.js";
|
|
20
|
+
export { blockNoteToMarkdown } from "./blocknote.js";
|
|
21
|
+
export {
|
|
22
|
+
WORKING_AGREEMENT,
|
|
23
|
+
formatInitiative,
|
|
24
|
+
formatInitiativeList,
|
|
25
|
+
formatInitiativeMemoryEntry,
|
|
26
|
+
formatInitiativeMemoryList,
|
|
27
|
+
formatInitiativeGraph,
|
|
28
|
+
formatInitiativeContext,
|
|
29
|
+
formatReconcileChangeset,
|
|
30
|
+
} from "./initiative.js";
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { Comment, Initiative, InitiativeMemoryEntry, Task, TaskPullRequest } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
import type { InitiativeGraph, InitiativeSlice, ReconcileChangeset } from "../../../gen/api/v1/initiative_pb.js";
|
|
3
|
+
import { blockNoteToMarkdown } from "./blocknote.js";
|
|
4
|
+
import { formatComment } from "./comment.js";
|
|
5
|
+
|
|
6
|
+
export function formatInitiative(ini: Initiative): string {
|
|
7
|
+
const lines = [`${ini.title}`];
|
|
8
|
+
if (ini.summary) lines.push(` Summary: ${ini.summary}`);
|
|
9
|
+
lines.push(` Status: ${ini.status}`);
|
|
10
|
+
lines.push(` Anchor: ${ini.anchorType}${ini.anchorId ? ` #${ini.anchorId}` : ""}`);
|
|
11
|
+
if (ini.coreIdea) {
|
|
12
|
+
const md = blockNoteToMarkdown(ini.coreIdea);
|
|
13
|
+
const preview = md.length > 120 ? md.substring(0, 120) + "..." : md;
|
|
14
|
+
lines.push(` Core Idea: ${preview}`);
|
|
15
|
+
}
|
|
16
|
+
const creator = ini.createdByUser?.fullName || `User #${ini.createdByUserId}`;
|
|
17
|
+
lines.push(` Created by: ${creator}`);
|
|
18
|
+
lines.push(` Project ID: ${ini.projectId}`);
|
|
19
|
+
lines.push(` ID: ${ini.id} | UUID: ${ini.uuid}`);
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatInitiativeList(initiatives: Initiative[]): string {
|
|
24
|
+
if (initiatives.length === 0) return "No initiatives found.";
|
|
25
|
+
const header = `Found ${initiatives.length} initiative(s):\n`;
|
|
26
|
+
return header + initiatives.map((ini, i) => `${i + 1}. ${formatInitiative(ini)}`).join("\n\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatInitiativeMemoryEntry(e: InitiativeMemoryEntry): string {
|
|
30
|
+
const lines = [];
|
|
31
|
+
const who = e.isSystem ? "system" : e.isAgent ? "agent" : (e.authorUser?.fullName || `User #${e.authorUserId}`);
|
|
32
|
+
const created = e.createdAt
|
|
33
|
+
? new Date((typeof e.createdAt.seconds === "bigint" ? Number(e.createdAt.seconds) : e.createdAt.seconds) * 1000).toISOString()
|
|
34
|
+
: "—";
|
|
35
|
+
lines.push(`[${e.kind.toUpperCase()}] ${who} @ ${created}`);
|
|
36
|
+
lines.push(` ${e.body}`);
|
|
37
|
+
if (e.sourceType && e.sourceType !== "none") {
|
|
38
|
+
const src = e.sourceId ? `${e.sourceType} #${e.sourceId}` : e.sourceType;
|
|
39
|
+
lines.push(` Source: ${src}${e.sourceUrl ? ` (${e.sourceUrl})` : ""}`);
|
|
40
|
+
}
|
|
41
|
+
lines.push(` ID: ${e.id}`);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatInitiativeMemoryList(entries: InitiativeMemoryEntry[]): string {
|
|
46
|
+
if (entries.length === 0) return "No memory entries found.";
|
|
47
|
+
const header = `Found ${entries.length} memory entry(ies) (newest first):\n`;
|
|
48
|
+
return header + entries.map((e, i) => `${i + 1}. ${formatInitiativeMemoryEntry(e)}`).join("\n\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatInitiativeGraph(graph: InitiativeGraph, taskMap?: Map<number, Task>): string {
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
|
|
54
|
+
if (graph.tasks.length === 0) {
|
|
55
|
+
lines.push("No tasks in this initiative yet.");
|
|
56
|
+
} else {
|
|
57
|
+
lines.push(`Tasks (${graph.tasks.length}):`);
|
|
58
|
+
graph.tasks.forEach((t, i) => {
|
|
59
|
+
const status = t.boardColumn?.name || "Unknown";
|
|
60
|
+
const priority = t.taskPriority?.name || "None";
|
|
61
|
+
lines.push(` ${i + 1}. [${t.identifier || `#${t.id}`}] ${t.title}`);
|
|
62
|
+
lines.push(` Status: ${status} | Priority: ${priority} | ID: ${t.id}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (graph.edges.length > 0) {
|
|
67
|
+
lines.push(`\nDependency Edges (${graph.edges.length}):`);
|
|
68
|
+
graph.edges.forEach((e) => {
|
|
69
|
+
const localMap = taskMap || new Map(graph.tasks.map(t => [t.id, t]));
|
|
70
|
+
const fromTask = localMap.get(e.fromTaskId);
|
|
71
|
+
const toTask = localMap.get(e.toTaskId);
|
|
72
|
+
const from = fromTask ? (fromTask.identifier || `#${e.fromTaskId}`) : `#${e.fromTaskId}`;
|
|
73
|
+
const to = toTask ? (toTask.identifier || `#${e.toTaskId}`) : `#${e.toTaskId}`;
|
|
74
|
+
lines.push(` ${from} ${e.type.toUpperCase()} ${to}`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (graph.slices.length > 0) {
|
|
79
|
+
lines.push(`\nParallel Execution Batches:`);
|
|
80
|
+
const localMap = taskMap || new Map(graph.tasks.map(t => [t.id, t]));
|
|
81
|
+
const sorted = [...graph.slices].sort((a, b) => a.layer - b.layer);
|
|
82
|
+
sorted.forEach((slice: InitiativeSlice) => {
|
|
83
|
+
const taskLabels = slice.taskIds.map((id) => {
|
|
84
|
+
const t = localMap.get(id);
|
|
85
|
+
return t ? `[${t.identifier || `#${id}`}] ${t.title}` : `#${id}`;
|
|
86
|
+
});
|
|
87
|
+
lines.push(` Parallel batch ${slice.layer}: ${taskLabels.join(", ")}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const WORKING_AGREEMENT = [
|
|
95
|
+
"— Working agreement —",
|
|
96
|
+
"This Initiative is a shared brain that outlives this session. To keep it useful:",
|
|
97
|
+
"1. This context (core idea · task tree · memory · PRs) is the source of truth — read it before acting.",
|
|
98
|
+
"2. Log each decision / blocker / learning as it happens with opsee_add_initiative_memory (cite the task or PR).",
|
|
99
|
+
"3. When you finish a unit of work, call opsee_checkpoint to record what changed and what's next.",
|
|
100
|
+
"4. Link PRs to their tasks so they roll up here.",
|
|
101
|
+
].join("\n");
|
|
102
|
+
|
|
103
|
+
export function formatInitiativeContext(
|
|
104
|
+
initiative: Initiative,
|
|
105
|
+
graph: InitiativeGraph | undefined,
|
|
106
|
+
memoryEntries: InitiativeMemoryEntry[],
|
|
107
|
+
pullRequests: TaskPullRequest[],
|
|
108
|
+
comments: Comment[],
|
|
109
|
+
): string {
|
|
110
|
+
const sections: string[] = [];
|
|
111
|
+
|
|
112
|
+
sections.push("=== INITIATIVE ===");
|
|
113
|
+
sections.push(formatInitiative(initiative));
|
|
114
|
+
|
|
115
|
+
if (initiative.coreIdea) {
|
|
116
|
+
sections.push("\n=== CORE IDEA ===");
|
|
117
|
+
const md = blockNoteToMarkdown(initiative.coreIdea);
|
|
118
|
+
const body = md.length > 2000
|
|
119
|
+
? md.substring(0, 2000) + "\n... [truncated]"
|
|
120
|
+
: md;
|
|
121
|
+
sections.push(body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (graph) {
|
|
125
|
+
sections.push("\n=== EXECUTION GRAPH ===");
|
|
126
|
+
sections.push(formatInitiativeGraph(graph));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (memoryEntries.length > 0) {
|
|
130
|
+
sections.push("\n=== MEMORY LOG (newest first) ===");
|
|
131
|
+
memoryEntries.forEach((e, i) => {
|
|
132
|
+
sections.push(`${i + 1}. ${formatInitiativeMemoryEntry(e)}`);
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
sections.push("\n=== MEMORY LOG ===");
|
|
136
|
+
sections.push("No memory entries yet.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (pullRequests.length > 0) {
|
|
140
|
+
sections.push("\n=== PULL REQUESTS ===");
|
|
141
|
+
pullRequests.forEach((pr, i) => {
|
|
142
|
+
const repo = pr.projectRepository?.repositoryFullName || `repo #${pr.projectRepositoryId}`;
|
|
143
|
+
sections.push(` ${i + 1}. [${pr.state.toUpperCase()}] #${pr.prNumber} ${pr.title} (${repo})`);
|
|
144
|
+
sections.push(` ${pr.url}`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (comments.length > 0) {
|
|
149
|
+
sections.push("\n=== DISCUSSION ===");
|
|
150
|
+
comments.forEach((c, i) => {
|
|
151
|
+
sections.push(`${i + 1}. ${formatComment(c)}`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
sections.push("\n" + WORKING_AGREEMENT);
|
|
156
|
+
return sections.join("\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatReconcileChangeset(changeset: ReconcileChangeset): string {
|
|
160
|
+
const lines: string[] = [];
|
|
161
|
+
|
|
162
|
+
if (changeset.added.length > 0) {
|
|
163
|
+
lines.push(`Added (${changeset.added.length}):`);
|
|
164
|
+
changeset.added.forEach((t) => {
|
|
165
|
+
const id = t.identifier || (t.taskId ? `#${t.taskId}` : "(new)");
|
|
166
|
+
lines.push(` + [${id}] ${t.title}`);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (changeset.removed.length > 0) {
|
|
171
|
+
lines.push(`Removed — pristine, no work detected (${changeset.removed.length}):`);
|
|
172
|
+
changeset.removed.forEach((t) => {
|
|
173
|
+
const id = t.identifier || `#${t.taskId}`;
|
|
174
|
+
lines.push(` − [${id}] ${t.title}`);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (changeset.flagged.length > 0) {
|
|
179
|
+
lines.push(`Flagged — NOT removed, carries execution state (${changeset.flagged.length}):`);
|
|
180
|
+
lines.push(" These tasks have real work in progress and were left untouched.");
|
|
181
|
+
changeset.flagged.forEach((t) => {
|
|
182
|
+
const id = t.identifier || `#${t.taskId}`;
|
|
183
|
+
lines.push(` ! [${id}] ${t.title} — flagged: ${t.reason}`);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (changeset.edgesAdded.length > 0) {
|
|
188
|
+
lines.push(`Edges + (${changeset.edgesAdded.length}):`);
|
|
189
|
+
changeset.edgesAdded.forEach((e) => {
|
|
190
|
+
lines.push(` + #${e.fromTaskId} ${e.type.toUpperCase()} #${e.toTaskId}`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (changeset.edgesRemoved.length > 0) {
|
|
195
|
+
lines.push(`Edges − (${changeset.edgesRemoved.length}):`);
|
|
196
|
+
changeset.edgesRemoved.forEach((e) => {
|
|
197
|
+
lines.push(` − #${e.fromTaskId} ${e.type.toUpperCase()} #${e.toTaskId}`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (lines.length === 0) {
|
|
202
|
+
lines.push("No changes — the proposed plan matches the existing task tree.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (changeset.graph) {
|
|
206
|
+
lines.push("\n" + formatInitiativeGraph(changeset.graph));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Label, TaskLabel } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
|
|
3
|
+
export function formatLabel(l: Label): string {
|
|
4
|
+
const lines = [`${l.name}`];
|
|
5
|
+
lines.push(` Color: ${l.color}`);
|
|
6
|
+
if (l.description) lines.push(` Description: ${l.description}`);
|
|
7
|
+
lines.push(` Active: ${l.isActive ? "Yes" : "No"}`);
|
|
8
|
+
lines.push(` Project ID: ${l.projectId}`);
|
|
9
|
+
lines.push(` ID: ${l.id}`);
|
|
10
|
+
return lines.join("\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatLabelList(labels: Label[]): string {
|
|
14
|
+
if (labels.length === 0) return "No labels found.";
|
|
15
|
+
const header = `Found ${labels.length} label(s):\n`;
|
|
16
|
+
return header + labels.map((l, i) => `${i + 1}. ${formatLabel(l)}`).join("\n\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatTaskLabelList(taskLabels: TaskLabel[]): string {
|
|
20
|
+
if (taskLabels.length === 0) return "No labels attached to this task.";
|
|
21
|
+
const header = `Found ${taskLabels.length} label attachment(s):\n`;
|
|
22
|
+
return header + taskLabels.map((tl, i) => {
|
|
23
|
+
const labelName = tl.label?.name || `Label #${tl.labelId}`;
|
|
24
|
+
return `${i + 1}. ${labelName} (Label ID: ${tl.labelId} | TaskLabel ID: ${tl.id})`;
|
|
25
|
+
}).join("\n");
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Milestone, MilestoneTask } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
import { formatTimestamp } from "./common.js";
|
|
3
|
+
|
|
4
|
+
export function formatMilestone(m: Milestone): string {
|
|
5
|
+
const lines = [`${m.name}`];
|
|
6
|
+
if (m.description) lines.push(` Description: ${m.description}`);
|
|
7
|
+
lines.push(` Status: ${m.status}`);
|
|
8
|
+
lines.push(` Start: ${formatTimestamp(m.startDate)} | End: ${formatTimestamp(m.endDate)}`);
|
|
9
|
+
lines.push(` Project ID: ${m.projectId}`);
|
|
10
|
+
lines.push(` ID: ${m.id} | UUID: ${m.uuid}`);
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatMilestoneList(milestones: Milestone[]): string {
|
|
15
|
+
if (milestones.length === 0) return "No milestones found.";
|
|
16
|
+
const header = `Found ${milestones.length} milestone(s):\n`;
|
|
17
|
+
return header + milestones.map((m, i) => `${i + 1}. ${formatMilestone(m)}`).join("\n\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatMilestoneTask(mt: MilestoneTask): string {
|
|
21
|
+
const t = mt.task;
|
|
22
|
+
if (!t) return ` Task ID: ${mt.taskId} (MilestoneTask ID: ${mt.id})`;
|
|
23
|
+
const parts = [`${t.identifier ? `${t.identifier} ` : ""}${t.title || `Task #${mt.taskId}`}`];
|
|
24
|
+
if (t.boardColumn?.name) parts.push(` Status: ${t.boardColumn.name}`);
|
|
25
|
+
if (t.taskPriority?.name) parts.push(` Priority: ${t.taskPriority.name}`);
|
|
26
|
+
parts.push(` Task ID: ${mt.taskId} | MilestoneTask ID: ${mt.id}`);
|
|
27
|
+
return parts.join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatMilestoneTaskList(milestoneTasks: MilestoneTask[]): string {
|
|
31
|
+
if (milestoneTasks.length === 0) return "No tasks attached to this milestone.";
|
|
32
|
+
const header = `Found ${milestoneTasks.length} task(s):\n`;
|
|
33
|
+
return header + milestoneTasks.map((mt, i) => `${i + 1}. ${formatMilestoneTask(mt)}`).join("\n\n");
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Project } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
|
|
3
|
+
export function formatProject(p: Project): string {
|
|
4
|
+
const lines = [
|
|
5
|
+
`${p.name} (${p.shortName})`,
|
|
6
|
+
` Slug: ${p.slug}`,
|
|
7
|
+
];
|
|
8
|
+
if (p.description) lines.push(` Description: ${p.description}`);
|
|
9
|
+
lines.push(` Active: ${p.isActive ? "Yes" : "No"}`);
|
|
10
|
+
lines.push(` ID: ${p.id}`);
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatProjectList(projects: Project[]): string {
|
|
15
|
+
if (projects.length === 0) return "No projects found.";
|
|
16
|
+
const header = `Found ${projects.length} project(s):\n`;
|
|
17
|
+
return (
|
|
18
|
+
header +
|
|
19
|
+
projects.map((p, i) => `${i + 1}. ${formatProject(p)}`).join("\n\n")
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TaskDependency } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
import { TaskDependencyType } from "../../../gen/api/v1/models_pb.js";
|
|
3
|
+
|
|
4
|
+
function taskDependencyTypeToString(t: TaskDependencyType): string {
|
|
5
|
+
switch (t) {
|
|
6
|
+
case TaskDependencyType.BLOCKS: return "BLOCKS";
|
|
7
|
+
case TaskDependencyType.BLOCKED_BY: return "BLOCKED_BY";
|
|
8
|
+
case TaskDependencyType.DUPLICATES: return "DUPLICATES";
|
|
9
|
+
case TaskDependencyType.RELATES_TO: return "RELATES_TO";
|
|
10
|
+
default: return "UNSPECIFIED";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatTaskDependency(d: TaskDependency): string {
|
|
15
|
+
const typeStr = taskDependencyTypeToString(d.type);
|
|
16
|
+
const from = d.fromTask?.identifier || `Task #${d.fromTaskId}`;
|
|
17
|
+
const to = d.toTask?.identifier || `Task #${d.toTaskId}`;
|
|
18
|
+
return `${from} ${typeStr} ${to}\n ID: ${d.id}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatTaskDependencyList(deps: TaskDependency[]): string {
|
|
22
|
+
if (deps.length === 0) return "No task dependencies found.";
|
|
23
|
+
return `Found ${deps.length} dependency(ies):\n` +
|
|
24
|
+
deps.map((d, i) => `${i + 1}. ${formatTaskDependency(d)}`).join("\n\n");
|
|
25
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Task } from "../../../gen/api/v1/models_pb.js";
|
|
2
|
+
|
|
3
|
+
export function formatTask(t: Task, opts: { full?: boolean } = {}): string {
|
|
4
|
+
const lines = [`[${t.identifier}] ${t.title}`];
|
|
5
|
+
|
|
6
|
+
const status = t.boardColumn?.name || "Unknown";
|
|
7
|
+
const priority = t.taskPriority?.name || "None";
|
|
8
|
+
const type = t.taskType?.name || "Unknown";
|
|
9
|
+
lines.push(` Status: ${status} | Priority: ${priority} | Type: ${type}`);
|
|
10
|
+
|
|
11
|
+
const assignee = t.assignedUser?.fullName || "Unassigned";
|
|
12
|
+
const cycle = t.cycle?.name || "None";
|
|
13
|
+
const milestone = t.milestone?.name || "None";
|
|
14
|
+
lines.push(` Assignee: ${assignee} | Cycle: ${cycle} | Milestone: ${milestone}`);
|
|
15
|
+
|
|
16
|
+
const parent = t.parentTaskId ? `#${t.parentTaskId}` : "—";
|
|
17
|
+
lines.push(` Parent: ${parent}`);
|
|
18
|
+
|
|
19
|
+
if (t.storyPoints) lines.push(` Story Points: ${t.storyPoints}`);
|
|
20
|
+
if (t.estimatedHours) lines.push(` Estimated Hours: ${t.estimatedHours}`);
|
|
21
|
+
|
|
22
|
+
if (t.description) {
|
|
23
|
+
const desc =
|
|
24
|
+
!opts.full && t.description.length > 200
|
|
25
|
+
? t.description.substring(0, 200) + "..."
|
|
26
|
+
: t.description;
|
|
27
|
+
lines.push(` Description: ${desc}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (t.dueDate) {
|
|
31
|
+
lines.push(` Due: ${new Date(Number(t.dueDate.seconds) * 1000).toISOString().split("T")[0]}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lines.push(` ID: ${t.id}`);
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TaskListMeta {
|
|
39
|
+
totalCount?: number;
|
|
40
|
+
page?: number;
|
|
41
|
+
hasMore?: boolean;
|
|
42
|
+
nextPage?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatTaskList(tasks: Task[], projectNameOrMeta?: string | TaskListMeta): string {
|
|
46
|
+
if (tasks.length === 0) return "No tasks found.";
|
|
47
|
+
|
|
48
|
+
// Support both old signature (projectName: string) and new signature (meta: TaskListMeta)
|
|
49
|
+
const isMetaObject = projectNameOrMeta && typeof projectNameOrMeta === 'object';
|
|
50
|
+
const meta = isMetaObject ? projectNameOrMeta as TaskListMeta : undefined;
|
|
51
|
+
const projectName = !isMetaObject ? projectNameOrMeta as string | undefined : undefined;
|
|
52
|
+
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Pagination metadata header
|
|
56
|
+
if (meta?.totalCount !== undefined) {
|
|
57
|
+
const pageInfo = meta.page ? `page ${meta.page}` : '';
|
|
58
|
+
const totalInfo = `${meta.totalCount} total`;
|
|
59
|
+
const header = [pageInfo, totalInfo].filter(Boolean).join(', ');
|
|
60
|
+
lines.push(`Tasks (${header}):`);
|
|
61
|
+
if (meta.hasMore && meta.nextPage) {
|
|
62
|
+
lines.push(`(Use page: ${meta.nextPage} to fetch the next 50 tasks)`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
} else {
|
|
66
|
+
// Original header format
|
|
67
|
+
const header = projectName
|
|
68
|
+
? `Found ${tasks.length} task(s) in project "${projectName}":\n`
|
|
69
|
+
: `Found ${tasks.length} task(s):\n`;
|
|
70
|
+
lines.push(header.trimEnd());
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Task list
|
|
75
|
+
tasks.forEach((t, i) => {
|
|
76
|
+
lines.push(`${i + 1}. ${formatTask(t)}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return lines.join('\n').trimEnd();
|
|
81
|
+
}
|