popilot 0.7.0 → 0.8.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/package.json +1 -1
- package/scaffold/mcp-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -0
- package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
- package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
- package/scaffold/spec-site/package-lock.json +852 -0
- package/scaffold/spec-site/package.json +12 -1
- package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
- package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
- package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
- package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- package/scaffold/spec-site/src/utils/timezone.ts +18 -0
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-notification-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.7.0",
|
|
16
|
+
"@types/node": "^22.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { query, execute } from './turso-client.js'
|
|
5
|
+
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: 'spec-notifications',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// Tool 1: Check unread notifications
|
|
12
|
+
server.tool(
|
|
13
|
+
'check_notifications',
|
|
14
|
+
'Check unread notifications for a user (max 20)',
|
|
15
|
+
{ user_name: z.string().describe('User name to check notifications for') },
|
|
16
|
+
async ({ user_name }) => {
|
|
17
|
+
const result = await query<{
|
|
18
|
+
id: number; type: string; title: string; body: string | null
|
|
19
|
+
source_type: string; source_id: number; page_id: string
|
|
20
|
+
actor: string; is_read: number; created_at: string
|
|
21
|
+
}>(
|
|
22
|
+
`SELECT id, type, title, body, source_type, source_id, page_id, actor, is_read, created_at
|
|
23
|
+
FROM notifications
|
|
24
|
+
WHERE user_name = ? AND is_read = 0
|
|
25
|
+
ORDER BY created_at DESC
|
|
26
|
+
LIMIT 20`,
|
|
27
|
+
[user_name],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.rows.length === 0) {
|
|
35
|
+
return { content: [{ type: 'text' as const, text: `No unread notifications for ${user_name}.` }] }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lines = result.rows.map((n, i) => {
|
|
39
|
+
const icon = n.type === 'memo_assigned' ? '📩' : n.type === 'reply_received' ? '💬' : '📢'
|
|
40
|
+
return `${i + 1}. ${icon} [ID:${n.id}] ${n.title}\n ${n.body ?? ''}\n Page: ${n.page_id} | ${n.created_at}`
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: [{
|
|
45
|
+
type: 'text' as const,
|
|
46
|
+
text: `Unread notifications for ${user_name} (${result.rows.length}):\n\n${lines.join('\n\n')}`,
|
|
47
|
+
}],
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Tool 2: Mark notification as read
|
|
53
|
+
server.tool(
|
|
54
|
+
'mark_notification_read',
|
|
55
|
+
'Mark a notification as read by ID',
|
|
56
|
+
{ notification_id: z.number().describe('Notification ID to mark as read') },
|
|
57
|
+
async ({ notification_id }) => {
|
|
58
|
+
const result = await execute(
|
|
59
|
+
'UPDATE notifications SET is_read = 1 WHERE id = ?',
|
|
60
|
+
[notification_id],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (result.error) {
|
|
64
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: 'text' as const,
|
|
70
|
+
text: result.rowsAffected > 0
|
|
71
|
+
? `Notification #${notification_id} marked as read.`
|
|
72
|
+
: `Notification #${notification_id} not found.`,
|
|
73
|
+
}],
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Tool 3: Check open memos assigned to user
|
|
79
|
+
server.tool(
|
|
80
|
+
'check_open_memos',
|
|
81
|
+
'Check all open memos assigned to a user',
|
|
82
|
+
{ user_name: z.string().describe('User name to check memos for') },
|
|
83
|
+
async ({ user_name }) => {
|
|
84
|
+
const result = await query<{
|
|
85
|
+
id: number; page_id: string; content: string; memo_type: string
|
|
86
|
+
created_by: string; assigned_to: string | null; created_at: string
|
|
87
|
+
}>(
|
|
88
|
+
`SELECT id, page_id, content, memo_type, created_by, assigned_to, created_at
|
|
89
|
+
FROM memos_v2
|
|
90
|
+
WHERE assigned_to = ? AND status = 'open'
|
|
91
|
+
ORDER BY created_at DESC`,
|
|
92
|
+
[user_name],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (result.error) {
|
|
96
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.rows.length === 0) {
|
|
100
|
+
return { content: [{ type: 'text' as const, text: `No open memos assigned to ${user_name}.` }] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const typeIcons: Record<string, string> = {
|
|
104
|
+
memo: '📝', decision: '⚡', request: '📋', backlog: '💡',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = result.rows.map((m, i) => {
|
|
108
|
+
const icon = typeIcons[m.memo_type] ?? '📝'
|
|
109
|
+
const preview = m.content.length > 80 ? m.content.slice(0, 80) + '…' : m.content
|
|
110
|
+
return `${i + 1}. ${icon} [ID:${m.id}] ${m.created_by} → ${m.assigned_to}\n ${preview}\n Page: ${m.page_id} | ${m.created_at}`
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text' as const,
|
|
116
|
+
text: `Open memos for ${user_name} (${result.rows.length}):\n\n${lines.join('\n\n')}`,
|
|
117
|
+
}],
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Tool 4: Create notification
|
|
123
|
+
server.tool(
|
|
124
|
+
'create_notification',
|
|
125
|
+
'Create a notification for a user (memo events: assignment, reply, resolve, reopen)',
|
|
126
|
+
{
|
|
127
|
+
user_name: z.string().describe('Notification recipient'),
|
|
128
|
+
type: z.enum(['memo_assigned', 'memo_mention_all', 'reply_received', 'memo_resolved', 'memo_reopened'])
|
|
129
|
+
.describe('Notification type'),
|
|
130
|
+
title: z.string().describe('Notification title'),
|
|
131
|
+
body: z.string().optional().describe('Preview body (60 chars max)'),
|
|
132
|
+
source_id: z.number().describe('Related memo ID'),
|
|
133
|
+
page_id: z.string().describe('Page ID the memo belongs to'),
|
|
134
|
+
actor: z.string().describe('Name of the person who performed the action'),
|
|
135
|
+
},
|
|
136
|
+
async ({ user_name, type, title, body, source_id, page_id, actor }) => {
|
|
137
|
+
const result = await execute(
|
|
138
|
+
`INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
|
|
139
|
+
VALUES (?, ?, ?, ?, 'memo', ?, ?, ?)`,
|
|
140
|
+
[user_name, type, title, body ?? null, source_id, page_id, actor],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if (result.error) {
|
|
144
|
+
return { content: [{ type: 'text' as const, text: `Error: ${result.error}` }] }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
content: [{
|
|
149
|
+
type: 'text' as const,
|
|
150
|
+
text: `Notification created: [${type}] "${title}" for ${user_name}`,
|
|
151
|
+
}],
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Tool 5: Reply to memo
|
|
157
|
+
server.tool(
|
|
158
|
+
'reply_memo',
|
|
159
|
+
'Reply to a memo and notify the memo author',
|
|
160
|
+
{
|
|
161
|
+
memo_id: z.number().describe('Memo ID to reply to'),
|
|
162
|
+
content: z.string().describe('Reply content'),
|
|
163
|
+
created_by: z.string().describe('Reply author name'),
|
|
164
|
+
},
|
|
165
|
+
async ({ memo_id, content, created_by }) => {
|
|
166
|
+
const memo = await query<{
|
|
167
|
+
id: number; page_id: string; created_by: string; assigned_to: string | null; status: string
|
|
168
|
+
}>(
|
|
169
|
+
'SELECT id, page_id, created_by, assigned_to, status FROM memos_v2 WHERE id = ?',
|
|
170
|
+
[memo_id],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if (memo.error) {
|
|
174
|
+
return { content: [{ type: 'text' as const, text: `Error: ${memo.error}` }] }
|
|
175
|
+
}
|
|
176
|
+
if (memo.rows.length === 0) {
|
|
177
|
+
return { content: [{ type: 'text' as const, text: `Memo #${memo_id} not found.` }] }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const m = memo.rows[0]
|
|
181
|
+
|
|
182
|
+
const insertResult = await execute(
|
|
183
|
+
'INSERT INTO memo_replies (memo_id, content, created_by) VALUES (?, ?, ?)',
|
|
184
|
+
[memo_id, content, created_by],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (insertResult.error) {
|
|
188
|
+
return { content: [{ type: 'text' as const, text: `Error: ${insertResult.error}` }] }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Notify memo author (skip self-notification)
|
|
192
|
+
if (m.created_by !== created_by) {
|
|
193
|
+
await execute(
|
|
194
|
+
`INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
|
|
195
|
+
VALUES (?, 'reply_received', ?, ?, 'memo', ?, ?, ?)`,
|
|
196
|
+
[
|
|
197
|
+
m.created_by,
|
|
198
|
+
`${created_by} replied to your memo`,
|
|
199
|
+
content.length > 60 ? content.slice(0, 57) + '…' : content,
|
|
200
|
+
memo_id,
|
|
201
|
+
m.page_id,
|
|
202
|
+
created_by,
|
|
203
|
+
],
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: 'text' as const,
|
|
210
|
+
text: `Reply posted on memo #${memo_id} by ${created_by}.\nContent: ${content}`,
|
|
211
|
+
}],
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// Tool 6: Resolve memo
|
|
217
|
+
server.tool(
|
|
218
|
+
'resolve_memo',
|
|
219
|
+
'Mark a memo as resolved and notify related users',
|
|
220
|
+
{
|
|
221
|
+
memo_id: z.number().describe('Memo ID to resolve'),
|
|
222
|
+
resolved_by: z.string().describe('Name of resolver'),
|
|
223
|
+
},
|
|
224
|
+
async ({ memo_id, resolved_by }) => {
|
|
225
|
+
const memo = await query<{
|
|
226
|
+
id: number; page_id: string; created_by: string; assigned_to: string | null; status: string
|
|
227
|
+
}>(
|
|
228
|
+
'SELECT id, page_id, created_by, assigned_to, status FROM memos_v2 WHERE id = ?',
|
|
229
|
+
[memo_id],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (memo.error) {
|
|
233
|
+
return { content: [{ type: 'text' as const, text: `Error: ${memo.error}` }] }
|
|
234
|
+
}
|
|
235
|
+
if (memo.rows.length === 0) {
|
|
236
|
+
return { content: [{ type: 'text' as const, text: `Memo #${memo_id} not found.` }] }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const m = memo.rows[0]
|
|
240
|
+
if (m.status === 'resolved') {
|
|
241
|
+
return { content: [{ type: 'text' as const, text: `Memo #${memo_id} is already resolved.` }] }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const updateResult = await execute(
|
|
245
|
+
"UPDATE memos_v2 SET status = 'resolved' WHERE id = ?",
|
|
246
|
+
[memo_id],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if (updateResult.error) {
|
|
250
|
+
return { content: [{ type: 'text' as const, text: `Error: ${updateResult.error}` }] }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (m.created_by !== resolved_by) {
|
|
254
|
+
await execute(
|
|
255
|
+
`INSERT INTO notifications (user_name, type, title, body, source_type, source_id, page_id, actor)
|
|
256
|
+
VALUES (?, 'memo_resolved', ?, NULL, 'memo', ?, ?, ?)`,
|
|
257
|
+
[m.created_by, `${resolved_by} resolved the memo`, memo_id, m.page_id, resolved_by],
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
content: [{
|
|
263
|
+
type: 'text' as const,
|
|
264
|
+
text: `Memo #${memo_id} resolved by ${resolved_by}.`,
|
|
265
|
+
}],
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async function main() {
|
|
271
|
+
const transport = new StdioServerTransport()
|
|
272
|
+
await server.connect(transport)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database HTTP API client (Turso Hrana v2 protocol)
|
|
3
|
+
* Reads DB_URL and DB_AUTH_TOKEN from environment.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DB_URL = process.env.DB_URL ?? ''
|
|
7
|
+
const DB_AUTH_TOKEN = process.env.DB_AUTH_TOKEN ?? ''
|
|
8
|
+
|
|
9
|
+
type ArgValue = string | number | null
|
|
10
|
+
|
|
11
|
+
interface HranaValue {
|
|
12
|
+
type: 'text' | 'integer' | 'float' | 'null'
|
|
13
|
+
value?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toHranaArg(v: ArgValue): HranaValue {
|
|
17
|
+
if (v === null) return { type: 'null' }
|
|
18
|
+
if (typeof v === 'number') {
|
|
19
|
+
return Number.isInteger(v)
|
|
20
|
+
? { type: 'integer', value: String(v) }
|
|
21
|
+
: { type: 'float', value: String(v) }
|
|
22
|
+
}
|
|
23
|
+
return { type: 'text', value: String(v) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PipelineResult {
|
|
27
|
+
results: Array<{
|
|
28
|
+
type: 'ok' | 'error'
|
|
29
|
+
response?: {
|
|
30
|
+
type: string
|
|
31
|
+
result?: {
|
|
32
|
+
cols: Array<{ name: string }>
|
|
33
|
+
rows: Array<Array<HranaValue>>
|
|
34
|
+
affected_row_count: number
|
|
35
|
+
last_insert_rowid: string | null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
error?: { message: string }
|
|
39
|
+
}>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function query<T = Record<string, unknown>>(
|
|
43
|
+
sql: string,
|
|
44
|
+
args: ArgValue[] = [],
|
|
45
|
+
): Promise<{ rows: T[]; error?: string }> {
|
|
46
|
+
if (!DB_URL || !DB_AUTH_TOKEN) {
|
|
47
|
+
return { rows: [], error: 'Missing DB_URL or DB_AUTH_TOKEN' }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const resp = await fetch(DB_URL + '/v2/pipeline', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: 'Bearer ' + DB_AUTH_TOKEN,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
requests: [
|
|
59
|
+
{ type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
|
|
60
|
+
{ type: 'close' },
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
signal: AbortSignal.timeout(10000),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
const text = await resp.text().catch(() => '')
|
|
68
|
+
return { rows: [], error: `HTTP ${resp.status}: ${text}` }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data: PipelineResult = await resp.json()
|
|
72
|
+
const first = data.results[0]
|
|
73
|
+
|
|
74
|
+
if (first.type === 'error') {
|
|
75
|
+
return { rows: [], error: first.error?.message ?? 'Unknown error' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = first.response?.result
|
|
79
|
+
if (!result) return { rows: [] }
|
|
80
|
+
|
|
81
|
+
const colNames = result.cols.map(c => c.name)
|
|
82
|
+
const rows = result.rows.map(row => {
|
|
83
|
+
const obj: Record<string, unknown> = {}
|
|
84
|
+
colNames.forEach((col, i) => {
|
|
85
|
+
const cell = row[i]
|
|
86
|
+
if (cell.type === 'null') obj[col] = null
|
|
87
|
+
else if (cell.type === 'integer') obj[col] = Number(cell.value)
|
|
88
|
+
else if (cell.type === 'float') obj[col] = Number(cell.value)
|
|
89
|
+
else obj[col] = cell.value
|
|
90
|
+
})
|
|
91
|
+
return obj as T
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return { rows }
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
97
|
+
return { rows: [], error: message }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function execute(
|
|
102
|
+
sql: string,
|
|
103
|
+
args: ArgValue[] = [],
|
|
104
|
+
): Promise<{ rowsAffected: number; error?: string }> {
|
|
105
|
+
if (!DB_URL || !DB_AUTH_TOKEN) {
|
|
106
|
+
return { rowsAffected: 0, error: 'Missing DB_URL or DB_AUTH_TOKEN' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const resp = await fetch(DB_URL + '/v2/pipeline', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: 'Bearer ' + DB_AUTH_TOKEN,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
requests: [
|
|
118
|
+
{ type: 'execute', stmt: { sql, args: args.map(toHranaArg) } },
|
|
119
|
+
{ type: 'close' },
|
|
120
|
+
],
|
|
121
|
+
}),
|
|
122
|
+
signal: AbortSignal.timeout(10000),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if (!resp.ok) {
|
|
126
|
+
const text = await resp.text().catch(() => '')
|
|
127
|
+
return { rowsAffected: 0, error: `HTTP ${resp.status}: ${text}` }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data: PipelineResult = await resp.json()
|
|
131
|
+
const first = data.results[0]
|
|
132
|
+
|
|
133
|
+
if (first.type === 'error') {
|
|
134
|
+
return { rowsAffected: 0, error: first.error?.message ?? 'Unknown error' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { rowsAffected: first.response?.result?.affected_row_count ?? 0 }
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
const message = err instanceof Error ? err.message : 'Unknown error'
|
|
140
|
+
return { rowsAffected: 0, error: message }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -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,49 @@
|
|
|
1
|
+
-- Memo v2 migration — authentication, user activity, memos, replies
|
|
2
|
+
|
|
3
|
+
-- 1. Auth tokens
|
|
4
|
+
CREATE TABLE IF NOT EXISTS auth_tokens (
|
|
5
|
+
token TEXT PRIMARY KEY,
|
|
6
|
+
user_name TEXT NOT NULL,
|
|
7
|
+
user_email TEXT,
|
|
8
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
+
expires_at TIMESTAMP,
|
|
10
|
+
is_active INTEGER DEFAULT 1
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
-- 2. User activity tracking
|
|
14
|
+
CREATE TABLE IF NOT EXISTS user_activity (
|
|
15
|
+
user_name TEXT PRIMARY KEY,
|
|
16
|
+
last_seen_at TIMESTAMP,
|
|
17
|
+
last_memo_seen TIMESTAMP,
|
|
18
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- 3. Memos v2
|
|
22
|
+
CREATE TABLE IF NOT EXISTS memos_v2 (
|
|
23
|
+
id INTEGER PRIMARY KEY,
|
|
24
|
+
page_id TEXT NOT NULL,
|
|
25
|
+
content TEXT NOT NULL,
|
|
26
|
+
memo_type TEXT DEFAULT 'memo',
|
|
27
|
+
status TEXT DEFAULT 'open',
|
|
28
|
+
created_by TEXT NOT NULL,
|
|
29
|
+
assigned_to TEXT,
|
|
30
|
+
resolved_by TEXT,
|
|
31
|
+
resolved_at TIMESTAMP,
|
|
32
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
33
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_memos_v2_page ON memos_v2(page_id);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_memos_v2_assigned ON memos_v2(assigned_to, status);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_memos_v2_type ON memos_v2(memo_type, status);
|
|
39
|
+
|
|
40
|
+
-- 4. Memo replies
|
|
41
|
+
CREATE TABLE IF NOT EXISTS memo_replies (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
memo_id INTEGER NOT NULL,
|
|
44
|
+
content TEXT NOT NULL,
|
|
45
|
+
created_by TEXT NOT NULL,
|
|
46
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_memo_replies_memo ON memo_replies(memo_id);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Notifications table for per-user alert management
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
user_name TEXT NOT NULL,
|
|
6
|
+
type TEXT NOT NULL DEFAULT 'memo_assigned',
|
|
7
|
+
title TEXT NOT NULL,
|
|
8
|
+
body TEXT,
|
|
9
|
+
source_type TEXT NOT NULL DEFAULT 'memo',
|
|
10
|
+
source_id INTEGER NOT NULL,
|
|
11
|
+
page_id TEXT NOT NULL,
|
|
12
|
+
actor TEXT NOT NULL,
|
|
13
|
+
is_read INTEGER DEFAULT 0,
|
|
14
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_name, is_read, created_at DESC);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_source ON notifications(source_type, source_id);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- Spec-site content schema — rules, scenarios, areas, versions, wireframe meta
|
|
2
|
+
|
|
3
|
+
-- 1. Rules (flat, individually queryable)
|
|
4
|
+
CREATE TABLE IF NOT EXISTS spec_rules (
|
|
5
|
+
id TEXT NOT NULL,
|
|
6
|
+
page_id TEXT NOT NULL,
|
|
7
|
+
rule_group TEXT NOT NULL,
|
|
8
|
+
category TEXT NOT NULL,
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
condition TEXT NOT NULL,
|
|
11
|
+
severity TEXT NOT NULL,
|
|
12
|
+
home_message TEXT NOT NULL DEFAULT '',
|
|
13
|
+
action TEXT NOT NULL DEFAULT '',
|
|
14
|
+
data_source TEXT NOT NULL DEFAULT '',
|
|
15
|
+
impl_status TEXT NOT NULL DEFAULT 'logic-needed',
|
|
16
|
+
impl_note TEXT,
|
|
17
|
+
action_route TEXT,
|
|
18
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
+
PRIMARY KEY (page_id, id)
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_spec_rules_page ON spec_rules(page_id);
|
|
23
|
+
|
|
24
|
+
-- 2. Scenarios
|
|
25
|
+
CREATE TABLE IF NOT EXISTS spec_scenarios (
|
|
26
|
+
page_id TEXT NOT NULL,
|
|
27
|
+
scenario_id TEXT NOT NULL,
|
|
28
|
+
label TEXT NOT NULL,
|
|
29
|
+
data_json TEXT NOT NULL,
|
|
30
|
+
is_default INTEGER DEFAULT 0,
|
|
31
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
33
|
+
PRIMARY KEY (page_id, scenario_id)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- 3. Spec areas
|
|
37
|
+
CREATE TABLE IF NOT EXISTS spec_areas (
|
|
38
|
+
page_id TEXT NOT NULL,
|
|
39
|
+
area_id TEXT NOT NULL,
|
|
40
|
+
label TEXT NOT NULL,
|
|
41
|
+
short_label TEXT NOT NULL,
|
|
42
|
+
rule_count INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
PRIMARY KEY (page_id, area_id)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- 4. Page versions
|
|
48
|
+
CREATE TABLE IF NOT EXISTS spec_versions (
|
|
49
|
+
page_id TEXT PRIMARY KEY,
|
|
50
|
+
version TEXT NOT NULL,
|
|
51
|
+
last_updated TEXT NOT NULL,
|
|
52
|
+
sprint TEXT NOT NULL,
|
|
53
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
54
|
+
changelog TEXT NOT NULL DEFAULT '[]',
|
|
55
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
-- 5. Wireframe metadata
|
|
59
|
+
CREATE TABLE IF NOT EXISTS spec_wireframe_meta (
|
|
60
|
+
page_id TEXT NOT NULL,
|
|
61
|
+
sprint TEXT NOT NULL,
|
|
62
|
+
default_scenario_id TEXT NOT NULL,
|
|
63
|
+
spec_title TEXT NOT NULL,
|
|
64
|
+
route_title TEXT NOT NULL,
|
|
65
|
+
PRIMARY KEY (page_id, sprint)
|
|
66
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Agent events table for push-based inter-agent communication
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS agent_events (
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
event_type TEXT NOT NULL,
|
|
6
|
+
source_agent TEXT NOT NULL,
|
|
7
|
+
target_agent TEXT NOT NULL,
|
|
8
|
+
target_user TEXT NOT NULL,
|
|
9
|
+
payload TEXT NOT NULL,
|
|
10
|
+
status TEXT DEFAULT 'pending',
|
|
11
|
+
delivered_at TIMESTAMP,
|
|
12
|
+
acked_at TIMESTAMP,
|
|
13
|
+
expires_at TIMESTAMP,
|
|
14
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_events_target
|
|
18
|
+
ON agent_events(target_user, status, created_at DESC);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_events_type
|
|
21
|
+
ON agent_events(event_type, status);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Epic-Sprint decoupling migration
|
|
2
|
+
-- Purpose: Make pm_epics the SSOT, remove nav_epics dependency
|
|
3
|
+
-- Epics are global; stories move between sprints.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE pm_epics ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
|
6
|
+
ALTER TABLE pm_epics ADD COLUMN origin_sprint TEXT;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retro → Kickoff Link — pure domain logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RetroAction {
|
|
6
|
+
id: number
|
|
7
|
+
content: string
|
|
8
|
+
assignee: string | null
|
|
9
|
+
status: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BacklogStory {
|
|
13
|
+
title: string
|
|
14
|
+
description: string
|
|
15
|
+
assignee: string | null
|
|
16
|
+
source: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Convert action item → backlog story */
|
|
20
|
+
export function actionToStory(action: RetroAction, sprintId: string): BacklogStory {
|
|
21
|
+
return {
|
|
22
|
+
title: `[Retro] ${action.content.slice(0, 60)}`,
|
|
23
|
+
description: `Created from retro (${sprintId}) action item.\n\nOriginal: ${action.content}`,
|
|
24
|
+
assignee: action.assignee,
|
|
25
|
+
source: `retro:${sprintId}`,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Filter incomplete action items */
|
|
30
|
+
export function getPendingActions(actions: RetroAction[]): RetroAction[] {
|
|
31
|
+
return actions.filter(a => a.status !== 'done')
|
|
32
|
+
}
|