popilot 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +40 -0
- package/scaffold/spec-site/package.json +4 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
// Unified MCP PM Server
|
|
2
|
+
// Combines project management tools + notification tools into a single MCP server.
|
|
3
|
+
// All operations go through the PM API via HTTP (no direct DB access).
|
|
4
|
+
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { apiGet, apiPost, apiPatch, apiPut } from './api-client.js'
|
|
9
|
+
|
|
10
|
+
let activeSprint = ''
|
|
11
|
+
const PM_API_URL = process.env.PM_API_URL ?? ''
|
|
12
|
+
|
|
13
|
+
async function fetchActiveSprint(): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const resp = await fetch(`${PM_API_URL}/api/sprint/active`, { signal: AbortSignal.timeout(5000) })
|
|
16
|
+
const data = await resp.json() as { sprint?: string }
|
|
17
|
+
if (data.sprint) activeSprint = data.sprint
|
|
18
|
+
} catch { /* will fail gracefully on tool calls */ }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: 'mcp-pm',
|
|
23
|
+
version: '2.0.0',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// ── Helpers ──
|
|
27
|
+
|
|
28
|
+
function text(t: string) {
|
|
29
|
+
return { content: [{ type: 'text' as const, text: t }] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function err(msg: string) {
|
|
33
|
+
return text(`Error: ${msg}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveSprint(arg?: string): string | null {
|
|
37
|
+
return arg || activeSprint || null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function progressBar(done: number, total: number, width = 10): string {
|
|
41
|
+
if (total === 0) return '\u2591'.repeat(width)
|
|
42
|
+
const filled = Math.round((done / total) * width)
|
|
43
|
+
return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Types (API responses) ──
|
|
47
|
+
|
|
48
|
+
interface DashboardData {
|
|
49
|
+
user: string
|
|
50
|
+
sprint: string
|
|
51
|
+
tasks: Record<string, number>
|
|
52
|
+
unread_memos: number
|
|
53
|
+
has_standup: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TaskRow {
|
|
57
|
+
task_id: number; task_title: string; task_status: string
|
|
58
|
+
story_id: number; story_title: string
|
|
59
|
+
epic_id: number | null; epic_title: string | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface TaskDetail {
|
|
63
|
+
task: {
|
|
64
|
+
id: number; story_id: number; title: string; assignee: string | null
|
|
65
|
+
status: string; description: string | null; sort_order: number
|
|
66
|
+
created_at: string; updated_at: string
|
|
67
|
+
}
|
|
68
|
+
story: {
|
|
69
|
+
id: number; title: string; description: string | null
|
|
70
|
+
acceptance_criteria: string | null; assignee: string | null
|
|
71
|
+
status: string; sprint: string
|
|
72
|
+
} | null
|
|
73
|
+
siblings: Array<{ id: number; title: string; status: string; assignee: string | null }>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface StandupData {
|
|
77
|
+
user: string; sprint: string; date: string
|
|
78
|
+
entry: {
|
|
79
|
+
id: number; done_text: string | null; plan_text: string | null
|
|
80
|
+
plan_story_ids: string | null; blockers: string | null
|
|
81
|
+
created_at: string; updated_at: string
|
|
82
|
+
} | null
|
|
83
|
+
stories: Array<{ id: number; title: string }>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface MemoRow {
|
|
87
|
+
id: number; from_user: string; to_user: string; content: string
|
|
88
|
+
story_id: number | null; is_read: number; reply: string | null
|
|
89
|
+
replied_by: string | null; replied_at: string | null; created_at: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface SprintData {
|
|
93
|
+
sprint: string
|
|
94
|
+
epics: Array<{ epic_title: string; total: number; done: number }>
|
|
95
|
+
assignees: Array<{ assignee: string; total: number; done: number; in_progress: number; todo: number }>
|
|
96
|
+
blockers: Array<{ user_name: string; blockers: string; entry_date: string }>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface NotificationRow {
|
|
100
|
+
id: number; type: string; title: string; body: string | null
|
|
101
|
+
source_type: string; source_id: string; page_id: string
|
|
102
|
+
actor: string; is_read: number; created_at: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface MemoV2Row {
|
|
106
|
+
id: number; page_id: string; content: string; memo_type: string
|
|
107
|
+
created_by: string; assigned_to: string | null; created_at: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ══════════════════════════════════════════════════
|
|
111
|
+
// PM TOOLS
|
|
112
|
+
// ══════════════════════════════════════════════════
|
|
113
|
+
|
|
114
|
+
// ── Tool 1: my_dashboard ──
|
|
115
|
+
|
|
116
|
+
server.tool(
|
|
117
|
+
'my_dashboard',
|
|
118
|
+
'My dashboard — task status, unread memos, today\'s standup status',
|
|
119
|
+
{},
|
|
120
|
+
async () => {
|
|
121
|
+
const sprint = resolveSprint()
|
|
122
|
+
if (!sprint) return err('No active sprint found. Set PM_SPRINT env var.')
|
|
123
|
+
|
|
124
|
+
const { data, error } = await apiGet<DashboardData>('/api/dashboard', { sprint })
|
|
125
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
126
|
+
|
|
127
|
+
const s = data.tasks
|
|
128
|
+
const lines = [
|
|
129
|
+
`Dashboard for ${data.user} (${sprint.toUpperCase()})`,
|
|
130
|
+
'─────────────',
|
|
131
|
+
`Tasks: todo ${s.todo ?? 0} | in-progress ${s['in-progress'] ?? 0} | done ${s.done ?? 0}`,
|
|
132
|
+
`Unread memos: ${data.unread_memos}`,
|
|
133
|
+
`Today's standup: ${data.has_standup ? 'Submitted' : 'Not submitted'}`,
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
return text(lines.join('\n'))
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// ── Tool 2: list_my_tasks ──
|
|
141
|
+
|
|
142
|
+
server.tool(
|
|
143
|
+
'list_my_tasks',
|
|
144
|
+
'My task list (Epic > Story > Task tree)',
|
|
145
|
+
{
|
|
146
|
+
status: z.enum(['todo', 'in-progress', 'done']).optional().describe('Filter by status'),
|
|
147
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
148
|
+
},
|
|
149
|
+
async ({ status, sprint: sprintArg }) => {
|
|
150
|
+
const sprint = resolveSprint(sprintArg)
|
|
151
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
152
|
+
|
|
153
|
+
const params: Record<string, string> = { sprint }
|
|
154
|
+
if (status) params.status = status
|
|
155
|
+
|
|
156
|
+
const { data, error } = await apiGet<{ user: string; sprint: string; tasks: TaskRow[] }>('/api/tasks', params)
|
|
157
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
158
|
+
if (data.tasks.length === 0) return text(`No tasks found for ${data.user}.`)
|
|
159
|
+
|
|
160
|
+
const statusIcon: Record<string, string> = { todo: '[ ]', 'in-progress': '[~]', done: '[x]' }
|
|
161
|
+
|
|
162
|
+
const tree = new Map<string, Map<string, Array<{ id: number; title: string; status: string }>>>()
|
|
163
|
+
for (const r of data.tasks) {
|
|
164
|
+
const epicKey = r.epic_title ?? '(No epic)'
|
|
165
|
+
if (!tree.has(epicKey)) tree.set(epicKey, new Map())
|
|
166
|
+
const epicMap = tree.get(epicKey)!
|
|
167
|
+
const storyKey = `[S${r.story_id}] ${r.story_title}`
|
|
168
|
+
if (!epicMap.has(storyKey)) epicMap.set(storyKey, [])
|
|
169
|
+
epicMap.get(storyKey)!.push({ id: r.task_id, title: r.task_title, status: r.task_status })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lines: string[] = [`Tasks for ${data.user} (${sprint.toUpperCase()})`, '─────────────']
|
|
173
|
+
for (const [epicTitle, stories] of tree) {
|
|
174
|
+
lines.push(`\n# ${epicTitle}`)
|
|
175
|
+
for (const [storyTitle, tasks] of stories) {
|
|
176
|
+
lines.push(` ${storyTitle}`)
|
|
177
|
+
for (const t of tasks) {
|
|
178
|
+
lines.push(` ${statusIcon[t.status] ?? '[ ]'} [T${t.id}] ${t.title}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return text(lines.join('\n'))
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// ── Tool 3: get_task ──
|
|
188
|
+
|
|
189
|
+
server.tool(
|
|
190
|
+
'get_task',
|
|
191
|
+
'Task detail + parent story context + sibling tasks',
|
|
192
|
+
{
|
|
193
|
+
task_id: z.number().describe('Task ID'),
|
|
194
|
+
},
|
|
195
|
+
async ({ task_id }) => {
|
|
196
|
+
const { data, error } = await apiGet<TaskDetail>(`/api/tasks/${task_id}`)
|
|
197
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
198
|
+
|
|
199
|
+
const t = data.task
|
|
200
|
+
const s = data.story
|
|
201
|
+
const statusIcon: Record<string, string> = { todo: '[ ]', 'in-progress': '[~]', done: '[x]' }
|
|
202
|
+
|
|
203
|
+
const lines = [
|
|
204
|
+
`Task #${t.id}: ${t.title}`,
|
|
205
|
+
'─────────────',
|
|
206
|
+
`Status: ${statusIcon[t.status] ?? '[ ]'} ${t.status}`,
|
|
207
|
+
`Assignee: ${t.assignee ?? 'Unassigned'}`,
|
|
208
|
+
t.description ? `Description: ${t.description}` : '',
|
|
209
|
+
`Created: ${t.created_at} | Updated: ${t.updated_at}`,
|
|
210
|
+
].filter(Boolean)
|
|
211
|
+
|
|
212
|
+
if (s) {
|
|
213
|
+
lines.push(
|
|
214
|
+
'',
|
|
215
|
+
`Parent Story [S${s.id}]: ${s.title}`,
|
|
216
|
+
` Sprint: ${s.sprint} | Status: ${s.status} | Assignee: ${s.assignee ?? 'Unassigned'}`,
|
|
217
|
+
)
|
|
218
|
+
if (s.description) lines.push(` Description: ${s.description}`)
|
|
219
|
+
if (s.acceptance_criteria) lines.push(` AC: ${s.acceptance_criteria}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (data.siblings.length > 0) {
|
|
223
|
+
lines.push('', 'Sibling Tasks:')
|
|
224
|
+
for (const sb of data.siblings) {
|
|
225
|
+
const marker = sb.id === task_id ? ' <-- current' : ''
|
|
226
|
+
lines.push(` ${statusIcon[sb.status] ?? '[ ]'} [T${sb.id}] ${sb.title} (${sb.assignee ?? 'Unassigned'})${marker}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return text(lines.join('\n'))
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// ── Tool 4: update_task_status ──
|
|
235
|
+
|
|
236
|
+
server.tool(
|
|
237
|
+
'update_task_status',
|
|
238
|
+
'Update task status',
|
|
239
|
+
{
|
|
240
|
+
task_id: z.number().describe('Task ID'),
|
|
241
|
+
status: z.enum(['todo', 'in-progress', 'done']).describe('New status'),
|
|
242
|
+
},
|
|
243
|
+
async ({ task_id, status }) => {
|
|
244
|
+
const { error } = await apiPatch(`/api/tasks/${task_id}/status`, { status })
|
|
245
|
+
if (error) return err(error)
|
|
246
|
+
return text(`Task #${task_id} updated to ${status}`)
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// ── Tool 5: add_task ──
|
|
251
|
+
|
|
252
|
+
server.tool(
|
|
253
|
+
'add_task',
|
|
254
|
+
'Add a new task to a story',
|
|
255
|
+
{
|
|
256
|
+
story_id: z.number().describe('Story ID'),
|
|
257
|
+
title: z.string().describe('Task title'),
|
|
258
|
+
assignee: z.string().optional().describe('Assignee (default: token user)'),
|
|
259
|
+
description: z.string().optional().describe('Task description'),
|
|
260
|
+
},
|
|
261
|
+
async ({ story_id, title, assignee, description }) => {
|
|
262
|
+
const { data, error } = await apiPost<{ ok: boolean; story_id: number; title: string }>('/api/tasks', {
|
|
263
|
+
story_id, title, assignee, description,
|
|
264
|
+
})
|
|
265
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
266
|
+
return text(`Task added to story #${story_id}: ${title}`)
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
// ── Tool 6: list_my_stories ──
|
|
271
|
+
|
|
272
|
+
server.tool(
|
|
273
|
+
'list_my_stories',
|
|
274
|
+
'My story list — find story IDs to reference in standups',
|
|
275
|
+
{
|
|
276
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
277
|
+
},
|
|
278
|
+
async ({ sprint: sprintArg }) => {
|
|
279
|
+
const sprint = resolveSprint(sprintArg)
|
|
280
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
281
|
+
|
|
282
|
+
const { data, error } = await apiGet<{
|
|
283
|
+
user: string; sprint: string
|
|
284
|
+
stories: Array<{ id: number; title: string; status: string; epic_title: string | null }>
|
|
285
|
+
}>('/api/stories', { sprint })
|
|
286
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
287
|
+
if (data.stories.length === 0) return text(`No stories found for ${data.user}.`)
|
|
288
|
+
|
|
289
|
+
const statusIcon: Record<string, string> = { todo: '[ ]', 'in-progress': '[~]', done: '[x]' }
|
|
290
|
+
|
|
291
|
+
const lines = [`Stories for ${data.user} (${sprint.toUpperCase()})`, '─────────────']
|
|
292
|
+
let lastEpic = ''
|
|
293
|
+
for (const s of data.stories) {
|
|
294
|
+
const epic = s.epic_title ?? '(No epic)'
|
|
295
|
+
if (epic !== lastEpic) {
|
|
296
|
+
lines.push(`\n# ${epic}`)
|
|
297
|
+
lastEpic = epic
|
|
298
|
+
}
|
|
299
|
+
lines.push(` ${statusIcon[s.status] ?? '[ ]'} [S${s.id}] ${s.title}`)
|
|
300
|
+
}
|
|
301
|
+
lines.push('', 'Tip: Pass story IDs as an array to save_standup\'s plan_story_ids parameter.')
|
|
302
|
+
|
|
303
|
+
return text(lines.join('\n'))
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
// ── Tool 7: get_standup ──
|
|
308
|
+
|
|
309
|
+
server.tool(
|
|
310
|
+
'get_standup',
|
|
311
|
+
'View standup entry',
|
|
312
|
+
{
|
|
313
|
+
date: z.string().optional().describe('Date (YYYY-MM-DD, default: today)'),
|
|
314
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
315
|
+
},
|
|
316
|
+
async ({ date, sprint: sprintArg }) => {
|
|
317
|
+
const sprint = resolveSprint(sprintArg)
|
|
318
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
319
|
+
|
|
320
|
+
const params: Record<string, string> = { sprint }
|
|
321
|
+
if (date) params.date = date
|
|
322
|
+
|
|
323
|
+
const { data, error } = await apiGet<StandupData>('/api/standup', params)
|
|
324
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
325
|
+
if (!data.entry) return text(`No standup entry for ${data.date}.`)
|
|
326
|
+
|
|
327
|
+
const e = data.entry
|
|
328
|
+
const lines = [
|
|
329
|
+
`Standup for ${data.user} (${data.date})`,
|
|
330
|
+
'─────────────',
|
|
331
|
+
`Done: ${e.done_text ?? '(none)'}`,
|
|
332
|
+
`Plan: ${e.plan_text ?? '(none)'}`,
|
|
333
|
+
]
|
|
334
|
+
if (data.stories.length > 0) {
|
|
335
|
+
const storyLines = data.stories.map(s => ` - [S${s.id}] ${s.title}`).join('\n')
|
|
336
|
+
lines.push(`Planned stories:\n${storyLines}`)
|
|
337
|
+
}
|
|
338
|
+
if (e.blockers) lines.push(`Blockers: ${e.blockers}`)
|
|
339
|
+
|
|
340
|
+
return text(lines.join('\n'))
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// ── Tool 8: save_standup ──
|
|
345
|
+
|
|
346
|
+
server.tool(
|
|
347
|
+
'save_standup',
|
|
348
|
+
'Save standup entry (upsert)',
|
|
349
|
+
{
|
|
350
|
+
done_text: z.string().optional().describe('Work completed'),
|
|
351
|
+
plan_text: z.string().optional().describe('Today\'s plan'),
|
|
352
|
+
plan_story_ids: z.array(z.number()).optional().describe('Planned story IDs array'),
|
|
353
|
+
blockers: z.string().optional().describe('Blockers'),
|
|
354
|
+
date: z.string().optional().describe('Date (YYYY-MM-DD, default: today)'),
|
|
355
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
356
|
+
},
|
|
357
|
+
async ({ done_text, plan_text, plan_story_ids, blockers, date, sprint: sprintArg }) => {
|
|
358
|
+
const sprint = resolveSprint(sprintArg)
|
|
359
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
360
|
+
|
|
361
|
+
const { data, error } = await apiPut<{ ok: boolean; date: string }>('/api/standup', {
|
|
362
|
+
sprint, done_text, plan_text, plan_story_ids, blockers, date,
|
|
363
|
+
})
|
|
364
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
365
|
+
return text(`Standup saved for ${data.date}`)
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
// ── Tool 9: send_memo ──
|
|
370
|
+
|
|
371
|
+
server.tool(
|
|
372
|
+
'send_memo',
|
|
373
|
+
'Send a memo to a team member',
|
|
374
|
+
{
|
|
375
|
+
to_user: z.string().describe('Recipient name'),
|
|
376
|
+
content: z.string().describe('Memo content'),
|
|
377
|
+
story_id: z.number().optional().describe('Related story ID'),
|
|
378
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
379
|
+
},
|
|
380
|
+
async ({ to_user, content, story_id, sprint: sprintArg }) => {
|
|
381
|
+
const sprint = resolveSprint(sprintArg)
|
|
382
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
383
|
+
|
|
384
|
+
const { data, error } = await apiPost<{ ok: boolean; from_user: string; to_user: string }>('/api/memos', {
|
|
385
|
+
to_user, content, story_id, sprint,
|
|
386
|
+
})
|
|
387
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
388
|
+
return text(`Memo sent: ${data.from_user} -> ${data.to_user}`)
|
|
389
|
+
},
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
// ── Tool 10: list_my_memos ──
|
|
393
|
+
|
|
394
|
+
server.tool(
|
|
395
|
+
'list_my_memos',
|
|
396
|
+
'List memos sent to me',
|
|
397
|
+
{
|
|
398
|
+
unread_only: z.boolean().optional().describe('Unread only (default: false)'),
|
|
399
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
400
|
+
},
|
|
401
|
+
async ({ unread_only, sprint: sprintArg }) => {
|
|
402
|
+
const sprint = resolveSprint(sprintArg)
|
|
403
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
404
|
+
|
|
405
|
+
const params: Record<string, string> = { sprint }
|
|
406
|
+
if (unread_only) params.unread_only = 'true'
|
|
407
|
+
|
|
408
|
+
const { data, error } = await apiGet<{ user: string; sprint: string; memos: MemoRow[] }>('/api/memos', params)
|
|
409
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
410
|
+
if (data.memos.length === 0) return text('No memos found.')
|
|
411
|
+
|
|
412
|
+
const lines = [`Memos for ${data.user} (${sprint.toUpperCase()})`, '─────────────']
|
|
413
|
+
for (const m of data.memos) {
|
|
414
|
+
const readIcon = m.is_read ? '(read)' : '(new)'
|
|
415
|
+
const storyTag = m.story_id ? ` [S${m.story_id}]` : ''
|
|
416
|
+
const preview = m.content.length > 60 ? m.content.slice(0, 60) + '...' : m.content
|
|
417
|
+
lines.push(`${readIcon} [M${m.id}] ${m.from_user}${storyTag}: ${preview}`)
|
|
418
|
+
if (m.reply) {
|
|
419
|
+
const replyPreview = m.reply.length > 50 ? m.reply.slice(0, 50) + '...' : m.reply
|
|
420
|
+
lines.push(` -> ${m.replied_by}: ${replyPreview}`)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return text(lines.join('\n'))
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
// ── Tool 11: read_memo ──
|
|
429
|
+
|
|
430
|
+
server.tool(
|
|
431
|
+
'read_memo',
|
|
432
|
+
'Read memo detail and mark as read',
|
|
433
|
+
{
|
|
434
|
+
memo_id: z.number().describe('Memo ID'),
|
|
435
|
+
},
|
|
436
|
+
async ({ memo_id }) => {
|
|
437
|
+
const { data, error } = await apiPatch<{ memo: MemoRow }>(`/api/memos/${memo_id}/read`, {})
|
|
438
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
439
|
+
|
|
440
|
+
const m = data.memo
|
|
441
|
+
const lines = [
|
|
442
|
+
`Memo #${m.id}`,
|
|
443
|
+
'─────────────',
|
|
444
|
+
`From: ${m.from_user} -> ${m.to_user}`,
|
|
445
|
+
m.story_id ? `Story: [S${m.story_id}]` : '',
|
|
446
|
+
`Date: ${m.created_at}`,
|
|
447
|
+
'',
|
|
448
|
+
m.content,
|
|
449
|
+
].filter(Boolean)
|
|
450
|
+
|
|
451
|
+
if (m.reply) {
|
|
452
|
+
lines.push('', `Reply (${m.replied_by}, ${m.replied_at}):`, m.reply)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return text(lines.join('\n'))
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
// ── Tool 12: reply_memo ──
|
|
460
|
+
|
|
461
|
+
server.tool(
|
|
462
|
+
'reply_memo',
|
|
463
|
+
'Reply to a memo',
|
|
464
|
+
{
|
|
465
|
+
memo_id: z.number().describe('Memo ID'),
|
|
466
|
+
content: z.string().describe('Reply content'),
|
|
467
|
+
},
|
|
468
|
+
async ({ memo_id, content }) => {
|
|
469
|
+
const { error } = await apiPost(`/api/memos/${memo_id}/reply`, { content })
|
|
470
|
+
if (error) return err(error)
|
|
471
|
+
return text(`Reply sent to memo #${memo_id}`)
|
|
472
|
+
},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
// ── Tool 13: sprint_summary ──
|
|
476
|
+
|
|
477
|
+
server.tool(
|
|
478
|
+
'sprint_summary',
|
|
479
|
+
'Sprint overview — progress by epic, workload by assignee, blockers',
|
|
480
|
+
{
|
|
481
|
+
sprint: z.string().optional().describe('Sprint ID (default: active sprint)'),
|
|
482
|
+
},
|
|
483
|
+
async ({ sprint: sprintArg }) => {
|
|
484
|
+
const sprint = resolveSprint(sprintArg)
|
|
485
|
+
if (!sprint) return err('Please specify a sprint.')
|
|
486
|
+
|
|
487
|
+
const { data, error } = await apiGet<SprintData>('/api/sprint/summary', { sprint })
|
|
488
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
489
|
+
|
|
490
|
+
const lines = [`Sprint Summary: ${sprint.toUpperCase()}`, '─────────────']
|
|
491
|
+
|
|
492
|
+
for (const e of data.epics) {
|
|
493
|
+
const pct = e.total > 0 ? Math.round((e.done / e.total) * 100) : 0
|
|
494
|
+
lines.push(`# ${e.epic_title}: ${progressBar(e.done, e.total)} ${e.done}/${e.total} (${pct}%)`)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (data.assignees.length > 0) {
|
|
498
|
+
lines.push('')
|
|
499
|
+
for (const a of data.assignees) {
|
|
500
|
+
lines.push(`${a.assignee}: ${a.total} tasks (done ${a.done}, in-progress ${a.in_progress}, todo ${a.todo})`)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (data.blockers.length > 0) {
|
|
505
|
+
lines.push('', `Blockers: ${data.blockers.length}`)
|
|
506
|
+
for (const b of data.blockers) {
|
|
507
|
+
lines.push(` - [${b.entry_date}] ${b.user_name}: ${b.blockers}`)
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
lines.push('', 'Blockers: 0')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return text(lines.join('\n'))
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
// ══════════════════════════════════════════════════
|
|
518
|
+
// NOTIFICATION TOOLS
|
|
519
|
+
// ══════════════════════════════════════════════════
|
|
520
|
+
|
|
521
|
+
// ── Tool 14: check_notifications ──
|
|
522
|
+
|
|
523
|
+
server.tool(
|
|
524
|
+
'check_notifications',
|
|
525
|
+
'Check unread notifications for a user (max 20)',
|
|
526
|
+
{
|
|
527
|
+
user_name: z.string().describe('User name to check'),
|
|
528
|
+
},
|
|
529
|
+
async ({ user_name }) => {
|
|
530
|
+
const { data, error } = await apiGet<{ notifications: NotificationRow[] }>(
|
|
531
|
+
'/api/notifications', { user_name, unread_only: 'true', limit: '20' },
|
|
532
|
+
)
|
|
533
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
534
|
+
|
|
535
|
+
if (data.notifications.length === 0) {
|
|
536
|
+
return text(`No unread notifications for ${user_name}.`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const lines = data.notifications.map((n, i) => {
|
|
540
|
+
const icon = n.type === 'memo_assigned' ? '[memo]'
|
|
541
|
+
: n.type === 'reply_received' ? '[reply]'
|
|
542
|
+
: '[notice]'
|
|
543
|
+
return `${i + 1}. ${icon} [ID:${n.id}] ${n.title}\n ${n.body ?? ''}\n Page: ${n.page_id} | ${n.created_at}`
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
return text(`Unread notifications for ${user_name} (${data.notifications.length}):\n\n${lines.join('\n\n')}`)
|
|
547
|
+
},
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
// ── Tool 15: mark_notification_read ──
|
|
551
|
+
|
|
552
|
+
server.tool(
|
|
553
|
+
'mark_notification_read',
|
|
554
|
+
'Mark a notification as read',
|
|
555
|
+
{
|
|
556
|
+
notification_id: z.number().describe('Notification ID to mark as read'),
|
|
557
|
+
},
|
|
558
|
+
async ({ notification_id }) => {
|
|
559
|
+
const { data, error } = await apiPatch<{ ok: boolean; rows_affected: number }>(
|
|
560
|
+
`/api/notifications/${notification_id}/read`, {},
|
|
561
|
+
)
|
|
562
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
563
|
+
|
|
564
|
+
return text(
|
|
565
|
+
data.rows_affected > 0
|
|
566
|
+
? `Notification #${notification_id} marked as read.`
|
|
567
|
+
: `Notification #${notification_id} not found.`,
|
|
568
|
+
)
|
|
569
|
+
},
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
// ── Tool 16: check_open_memos ──
|
|
573
|
+
|
|
574
|
+
server.tool(
|
|
575
|
+
'check_open_memos',
|
|
576
|
+
'Check all open memos assigned to a user',
|
|
577
|
+
{
|
|
578
|
+
user_name: z.string().describe('User name to check'),
|
|
579
|
+
},
|
|
580
|
+
async ({ user_name }) => {
|
|
581
|
+
const { data, error } = await apiGet<{ memos: MemoV2Row[] }>(
|
|
582
|
+
'/api/memos/v2', { assigned_to: user_name, status: 'open' },
|
|
583
|
+
)
|
|
584
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
585
|
+
|
|
586
|
+
if (data.memos.length === 0) {
|
|
587
|
+
return text(`No open memos assigned to ${user_name}.`)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const typeIcons: Record<string, string> = {
|
|
591
|
+
memo: '[memo]', decision: '[decision]', request: '[request]', backlog: '[idea]',
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const lines = data.memos.map((m, i) => {
|
|
595
|
+
const icon = typeIcons[m.memo_type] ?? '[memo]'
|
|
596
|
+
const preview = m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content
|
|
597
|
+
return `${i + 1}. ${icon} [ID:${m.id}] ${m.created_by} -> ${m.assigned_to}\n ${preview}\n Page: ${m.page_id} | ${m.created_at}`
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
return text(`Open memos for ${user_name} (${data.memos.length}):\n\n${lines.join('\n\n')}`)
|
|
601
|
+
},
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
// ── Tool 17: create_notification ──
|
|
605
|
+
|
|
606
|
+
server.tool(
|
|
607
|
+
'create_notification',
|
|
608
|
+
'Create a notification for a user (triggered by memo events like replies, resolve, reopen)',
|
|
609
|
+
{
|
|
610
|
+
user_name: z.string().describe('Notification recipient'),
|
|
611
|
+
type: z.enum(['memo_assigned', 'memo_mention_all', 'reply_received', 'memo_resolved', 'memo_reopened'])
|
|
612
|
+
.describe('Notification type'),
|
|
613
|
+
title: z.string().describe('Notification title'),
|
|
614
|
+
body: z.string().optional().describe('Notification body preview (under 60 chars)'),
|
|
615
|
+
source_id: z.number().describe('Related memo ID'),
|
|
616
|
+
page_id: z.string().describe('Page ID where the memo belongs (e.g., home, diagnosis)'),
|
|
617
|
+
actor: z.string().describe('Name of the user who performed the action'),
|
|
618
|
+
},
|
|
619
|
+
async ({ user_name, type, title, body, source_id, page_id, actor }) => {
|
|
620
|
+
const { data, error } = await apiPost<{ ok: boolean }>('/api/notifications', {
|
|
621
|
+
user_name, type, title, body, source_type: 'memo', source_id, page_id, actor,
|
|
622
|
+
})
|
|
623
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
624
|
+
|
|
625
|
+
return text(`Notification created: [${type}] "${title}" -> ${user_name}`)
|
|
626
|
+
},
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
// ── Tool 18: resolve_memo ──
|
|
630
|
+
|
|
631
|
+
server.tool(
|
|
632
|
+
'resolve_memo',
|
|
633
|
+
'Resolve a memo (mark as resolved) and notify the author',
|
|
634
|
+
{
|
|
635
|
+
memo_id: z.number().describe('Memo ID to resolve'),
|
|
636
|
+
resolved_by: z.string().describe('Name of the user resolving the memo'),
|
|
637
|
+
},
|
|
638
|
+
async ({ memo_id, resolved_by }) => {
|
|
639
|
+
const { data, error } = await apiPatch<{ ok: boolean; already_resolved?: boolean }>(
|
|
640
|
+
`/api/memos/v2/${memo_id}/resolve`, { resolved_by },
|
|
641
|
+
)
|
|
642
|
+
if (error || !data) return err(error ?? 'Unknown error')
|
|
643
|
+
|
|
644
|
+
if (data.already_resolved) {
|
|
645
|
+
return text(`Memo #${memo_id} is already resolved.`)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return text(`Memo #${memo_id} resolved by ${resolved_by}.`)
|
|
649
|
+
},
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
// ── Start ──
|
|
653
|
+
|
|
654
|
+
async function main() {
|
|
655
|
+
await fetchActiveSprint()
|
|
656
|
+
const transport = new StdioServerTransport()
|
|
657
|
+
await server.connect(transport)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pm-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"build": "tsc --noEmit",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"hono": "^4.7.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@cloudflare/workers-types": "^4.20250309.0",
|
|
17
|
+
"typescript": "^5.7.0",
|
|
18
|
+
"vitest": "^4.1.0",
|
|
19
|
+
"wrangler": "^4.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|