popilot 0.5.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/adapters/codex/.codex/commands/_domain.md.hbs +33 -0
- package/adapters/codex/.codex/commands/analytics.md.hbs +55 -0
- package/adapters/codex/.codex/commands/daily.md.hbs +301 -0
- package/adapters/codex/.codex/commands/dev.md.hbs +62 -0
- package/adapters/codex/.codex/commands/gtm.md +82 -0
- package/adapters/codex/.codex/commands/handoff.md +259 -0
- package/adapters/codex/.codex/commands/market.md +120 -0
- package/adapters/codex/.codex/commands/metrics.md +123 -0
- package/adapters/codex/.codex/commands/oscar-loop.md +436 -0
- package/adapters/codex/.codex/commands/party.md +85 -0
- package/adapters/codex/.codex/commands/plan.md +43 -0
- package/adapters/codex/.codex/commands/research.md +203 -0
- package/adapters/codex/.codex/commands/retro.md +68 -0
- package/adapters/codex/.codex/commands/save.md +440 -0
- package/adapters/codex/.codex/commands/sessions.md +139 -0
- package/adapters/codex/.codex/commands/sprint.md +106 -0
- package/adapters/codex/.codex/commands/start.md +396 -0
- package/adapters/codex/.codex/commands/strategy.md +41 -0
- package/adapters/codex/.codex/commands/task.md +220 -0
- package/adapters/codex/.codex/commands/tracking.md +116 -0
- package/adapters/codex/.codex/commands/validate.md +58 -0
- package/adapters/codex/AGENTS.md.hbs +210 -0
- package/adapters/codex/manifest.yaml +36 -0
- package/adapters/gemini/.gemini/commands/_domain.md.hbs +33 -0
- package/adapters/gemini/.gemini/commands/analytics.md.hbs +55 -0
- package/adapters/gemini/.gemini/commands/daily.md.hbs +301 -0
- package/adapters/gemini/.gemini/commands/dev.md.hbs +62 -0
- package/adapters/gemini/.gemini/commands/gtm.md +82 -0
- package/adapters/gemini/.gemini/commands/handoff.md +259 -0
- package/adapters/gemini/.gemini/commands/market.md +120 -0
- package/adapters/gemini/.gemini/commands/metrics.md +123 -0
- package/adapters/gemini/.gemini/commands/oscar-loop.md +436 -0
- package/adapters/gemini/.gemini/commands/party.md +85 -0
- package/adapters/gemini/.gemini/commands/plan.md +43 -0
- package/adapters/gemini/.gemini/commands/research.md +203 -0
- package/adapters/gemini/.gemini/commands/retro.md +68 -0
- package/adapters/gemini/.gemini/commands/save.md +440 -0
- package/adapters/gemini/.gemini/commands/sessions.md +139 -0
- package/adapters/gemini/.gemini/commands/sprint.md +106 -0
- package/adapters/gemini/.gemini/commands/start.md +396 -0
- package/adapters/gemini/.gemini/commands/strategy.md +41 -0
- package/adapters/gemini/.gemini/commands/task.md +220 -0
- package/adapters/gemini/.gemini/commands/tracking.md +116 -0
- package/adapters/gemini/.gemini/commands/validate.md +58 -0
- package/adapters/gemini/GEMINI.md.hbs +210 -0
- package/adapters/gemini/manifest.yaml +36 -0
- package/bin/cli.mjs +215 -4
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/industry-presets.mjs +135 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +71 -2
- package/package.json +1 -1
- package/scaffold/.context/agents/TEMPLATE.md +14 -0
- package/scaffold/.context/agents/analyst.md.hbs +3 -3
- package/scaffold/.context/agents/developer.md.hbs +5 -5
- package/scaffold/.context/agents/gtm-strategist.md.hbs +3 -3
- package/scaffold/.context/agents/handoff-specialist.md.hbs +18 -18
- package/scaffold/.context/agents/market-researcher.md.hbs +6 -6
- package/scaffold/.context/agents/orchestrator.md.hbs +8 -8
- package/scaffold/.context/agents/planner.md.hbs +6 -6
- package/scaffold/.context/agents/qa.md.hbs +5 -5
- package/scaffold/.context/agents/researcher.md.hbs +33 -6
- package/scaffold/.context/agents/strategist.md.hbs +8 -8
- package/scaffold/.context/agents/tracking-governor.md.hbs +2 -2
- package/scaffold/.context/project.yaml.example +25 -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,114 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { cors } from 'hono/cors'
|
|
3
|
+
import type { AppEnv } from './types.js'
|
|
4
|
+
import { setAdapter } from './db/adapter.js'
|
|
5
|
+
import { TursoAdapter } from './db/turso.js'
|
|
6
|
+
import { authMiddleware } from './auth.js'
|
|
7
|
+
import mcpRoutes from './mcp.js'
|
|
8
|
+
import authRoutes from './routes/auth.js'
|
|
9
|
+
|
|
10
|
+
import v2NavRoutes from './routes/v2-nav.js'
|
|
11
|
+
import v2PageContentRoutes from './routes/v2-page-content.js'
|
|
12
|
+
import v2ScenariosRoutes from './routes/v2-scenarios.js'
|
|
13
|
+
import v2PmRoutes from './routes/v2-pm.js'
|
|
14
|
+
import v2PolicyRoutes from './routes/v2-policy.js'
|
|
15
|
+
import v2StandupRoutes from './routes/v2-standup.js'
|
|
16
|
+
import v2RetroRoutes from './routes/v2-retro.js'
|
|
17
|
+
import v2NotificationsRoutes from './routes/v2-notifications.js'
|
|
18
|
+
import v2MemosRoutes from './routes/v2-memos.js'
|
|
19
|
+
import v2UserRoutes from './routes/v2-user.js'
|
|
20
|
+
import v2AdminRoutes from './routes/v2-admin.js'
|
|
21
|
+
import v2DashboardRoutes from './routes/v2-dashboard.js'
|
|
22
|
+
import v2KickoffRoutes from './routes/v2-kickoff.js'
|
|
23
|
+
import v2InitiativeRoutes from './routes/v2-initiatives.js'
|
|
24
|
+
import v2DocsRoutes from './routes/v2-docs.js'
|
|
25
|
+
import v2MeetingsRoutes from './routes/v2-meetings.js'
|
|
26
|
+
import v2RewardsRoutes from './routes/v2-rewards.js'
|
|
27
|
+
import v2ActivityRoutes from './routes/v2-activity.js'
|
|
28
|
+
import v2SearchRoutes from './routes/v2-search.js'
|
|
29
|
+
|
|
30
|
+
const app = new Hono<AppEnv>()
|
|
31
|
+
|
|
32
|
+
// CORS — origins from env var (comma-separated) or default to localhost
|
|
33
|
+
app.use('*', cors({
|
|
34
|
+
origin: (origin, c) => {
|
|
35
|
+
const allowed = c.env.ALLOWED_ORIGINS
|
|
36
|
+
? c.env.ALLOWED_ORIGINS.split(',').map(s => s.trim())
|
|
37
|
+
: ['http://localhost:5173', 'http://localhost:5174']
|
|
38
|
+
return allowed.includes(origin) ? origin : null
|
|
39
|
+
},
|
|
40
|
+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
41
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
// DB error handling
|
|
45
|
+
app.onError((err, c) => {
|
|
46
|
+
if (err.name === 'DbError') {
|
|
47
|
+
return c.json({ error: err.message }, 500)
|
|
48
|
+
}
|
|
49
|
+
throw err
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Inject DB adapter per request
|
|
53
|
+
app.use('*', async (c, next) => {
|
|
54
|
+
setAdapter(new TursoAdapter(c.env.TURSO_URL, c.env.TURSO_AUTH_TOKEN))
|
|
55
|
+
await next()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Public routes (no auth)
|
|
59
|
+
app.get('/health', (c) => c.json({ ok: true }))
|
|
60
|
+
app.route('/api/auth', authRoutes)
|
|
61
|
+
|
|
62
|
+
// Auth middleware for /api/* and /mcp
|
|
63
|
+
app.use('/api/*', authMiddleware)
|
|
64
|
+
app.use('/mcp/*', authMiddleware)
|
|
65
|
+
|
|
66
|
+
// MCP endpoint
|
|
67
|
+
app.route('/mcp', mcpRoutes)
|
|
68
|
+
|
|
69
|
+
// v2 API routes
|
|
70
|
+
app.route('/api/v2/nav', v2NavRoutes)
|
|
71
|
+
app.route('/api/v2/page-content', v2PageContentRoutes)
|
|
72
|
+
app.route('/api/v2/scenarios', v2ScenariosRoutes)
|
|
73
|
+
app.route('/api/v2/pm', v2PmRoutes)
|
|
74
|
+
app.route('/api/v2/policy', v2PolicyRoutes)
|
|
75
|
+
app.route('/api/v2/standup', v2StandupRoutes)
|
|
76
|
+
app.route('/api/v2/retro', v2RetroRoutes)
|
|
77
|
+
app.route('/api/v2/notifications', v2NotificationsRoutes)
|
|
78
|
+
app.route('/api/v2/memos', v2MemosRoutes)
|
|
79
|
+
app.route('/api/v2/user', v2UserRoutes)
|
|
80
|
+
app.route('/api/v2/admin', v2AdminRoutes)
|
|
81
|
+
app.route('/api/v2/dashboard', v2DashboardRoutes)
|
|
82
|
+
app.route('/api/v2/kickoff', v2KickoffRoutes)
|
|
83
|
+
app.route('/api/v2/initiatives', v2InitiativeRoutes)
|
|
84
|
+
app.route('/api/v2/docs', v2DocsRoutes)
|
|
85
|
+
app.route('/api/v2/meetings', v2MeetingsRoutes)
|
|
86
|
+
app.route('/api/v2/rewards', v2RewardsRoutes)
|
|
87
|
+
app.route('/api/v2/activity', v2ActivityRoutes)
|
|
88
|
+
app.route('/api/v2/search', v2SearchRoutes)
|
|
89
|
+
|
|
90
|
+
// Proactive Nudge (Cron Trigger)
|
|
91
|
+
import { handleScheduled } from './nudge.js'
|
|
92
|
+
import { isAdmin } from './utils/admin.js'
|
|
93
|
+
|
|
94
|
+
// Manual nudge trigger — admin only
|
|
95
|
+
app.post('/api/v2/dashboard/nudge-trigger', async (c) => {
|
|
96
|
+
const userName = c.get('userName')
|
|
97
|
+
if (!await isAdmin(userName)) {
|
|
98
|
+
return c.json({ error: 'Admin permission required' }, 403)
|
|
99
|
+
}
|
|
100
|
+
const env = {
|
|
101
|
+
TURSO_URL: c.env.TURSO_URL,
|
|
102
|
+
TURSO_AUTH_TOKEN: c.env.TURSO_AUTH_TOKEN,
|
|
103
|
+
NUDGE_WEBHOOK_URL: c.env.NUDGE_WEBHOOK_URL,
|
|
104
|
+
}
|
|
105
|
+
await handleScheduled(env)
|
|
106
|
+
return c.json({ ok: true, message: 'Nudge triggered manually' })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
fetch: app.fetch,
|
|
111
|
+
scheduled: async (event: ScheduledEvent, env: Record<string, string>) => {
|
|
112
|
+
await handleScheduled(env as unknown as { TURSO_URL: string; TURSO_AUTH_TOKEN: string; NUDGE_WEBHOOK_URL?: string })
|
|
113
|
+
},
|
|
114
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolDashboard(user: string): Promise<ToolResult> {
|
|
5
|
+
const sprint = await resolveSprint()
|
|
6
|
+
if (!sprint) return err('No active sprint found.')
|
|
7
|
+
|
|
8
|
+
const [taskResult, memoResult, standupResult, notifResult] = await Promise.all([
|
|
9
|
+
query<{ status: string; cnt: number }>(
|
|
10
|
+
`SELECT t.status, COUNT(*) as cnt FROM pm_tasks t JOIN pm_stories s ON t.story_id = s.id WHERE t.assignee = ? AND s.sprint = ? GROUP BY t.status`,
|
|
11
|
+
[user, sprint],
|
|
12
|
+
),
|
|
13
|
+
query<{ cnt: number }>("SELECT COUNT(*) as cnt FROM memos_v2 WHERE (assigned_to = ? OR assigned_to LIKE ? OR assigned_to LIKE ? OR assigned_to LIKE ?) AND status = 'open'", [user, `${user},%`, `%,${user},%`, `%,${user}`]),
|
|
14
|
+
query<{ id: number }>('SELECT id FROM pm_standup_entries WHERE user_name = ? AND sprint = ? AND entry_date = ? LIMIT 1', [user, sprint, today()]),
|
|
15
|
+
query<{ cnt: number }>("SELECT COUNT(*) as cnt FROM notifications WHERE user_name = ? AND is_read = 0", [user]),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
const s: Record<string, number> = { todo: 0, 'in-progress': 0, done: 0 }
|
|
19
|
+
if (!taskResult.error) for (const r of taskResult.rows) s[r.status] = r.cnt
|
|
20
|
+
|
|
21
|
+
return text([
|
|
22
|
+
`📊 ${user}'s Dashboard (${sprint.toUpperCase()})`,
|
|
23
|
+
'─────────────',
|
|
24
|
+
`📋 Tasks: todo ${s.todo} | in-progress ${s['in-progress']} | done ${s.done}`,
|
|
25
|
+
`📩 Unread memos: ${memoResult.rows[0]?.cnt ?? 0}`,
|
|
26
|
+
`🔔 Unread notifications: ${notifResult.rows[0]?.cnt ?? 0}`,
|
|
27
|
+
`📝 Today's standup: ${standupResult.rows.length > 0 ? 'submitted' : 'not submitted'}`,
|
|
28
|
+
].join('\n'))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function toolListTeamMembers(): Promise<ToolResult> {
|
|
32
|
+
const result = await query<{ id: number; display_name: string; role: string }>('SELECT id, display_name, role FROM members WHERE is_active = 1 ORDER BY display_name')
|
|
33
|
+
if (result.error) return err(result.error)
|
|
34
|
+
const lines = ['👥 Team Members', '─────────────']
|
|
35
|
+
for (const r of result.rows) {
|
|
36
|
+
const badge = r.role === 'admin' ? ' 👑' : ''
|
|
37
|
+
lines.push(` • [M${r.id}] ${r.display_name}${badge}`)
|
|
38
|
+
}
|
|
39
|
+
return text(lines.join('\n'))
|
|
40
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolListEpics(): Promise<ToolResult> {
|
|
5
|
+
const result = await query<{ id: number; title: string; status: string; owner: string | null; story_count: number }>(
|
|
6
|
+
`SELECT e.id, e.title, e.status, e.owner, COUNT(s.id) as story_count
|
|
7
|
+
FROM pm_epics e LEFT JOIN pm_stories s ON s.epic_id = e.id
|
|
8
|
+
GROUP BY e.id ORDER BY e.title`,
|
|
9
|
+
)
|
|
10
|
+
if (result.error) return err(result.error)
|
|
11
|
+
if (result.rows.length === 0) return text('No epics found.')
|
|
12
|
+
|
|
13
|
+
const lines = ['🏷 Epic List', '─────────────']
|
|
14
|
+
for (const e of result.rows) {
|
|
15
|
+
const statusIcon: Record<string, string> = { active: '🟢', planned: '📋', completed: '✅', archived: '📦' }
|
|
16
|
+
lines.push(`${statusIcon[e.status] ?? '⚪'} [E${e.id}] ${e.title} (${e.status}, ${e.story_count} stories${e.owner ? `, owner: ${e.owner}` : ''})`)
|
|
17
|
+
}
|
|
18
|
+
return text(lines.join('\n'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function toolAddEpic(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
22
|
+
const title = args.title as string
|
|
23
|
+
const description = (args.description as string) ?? null
|
|
24
|
+
const owner = (args.owner as string) || user
|
|
25
|
+
const status = (args.status as string) || 'active'
|
|
26
|
+
|
|
27
|
+
const result = await execute(
|
|
28
|
+
'INSERT INTO pm_epics (title, description, owner, status) VALUES (?, ?, ?, ?)',
|
|
29
|
+
[title, description, owner, status],
|
|
30
|
+
)
|
|
31
|
+
if (result.error) return err(result.error)
|
|
32
|
+
const idResult = await query<{ id: number }>('SELECT last_insert_rowid() as id')
|
|
33
|
+
const newId = idResult.rows[0]?.id ?? '?'
|
|
34
|
+
return text(`✅ Epic created: ${title} (ID: ${newId})`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function toolUpdateEpic(args: Record<string, unknown>): Promise<ToolResult> {
|
|
38
|
+
const epicId = args.epic_id as number
|
|
39
|
+
const fieldMap: Record<string, string> = { title: 'title', description: 'description', status: 'status', owner: 'owner' }
|
|
40
|
+
const sets: string[] = []
|
|
41
|
+
const sqlArgs: (string | number | null)[] = []
|
|
42
|
+
|
|
43
|
+
for (const [key, col] of Object.entries(fieldMap)) {
|
|
44
|
+
if (args[key] !== undefined) {
|
|
45
|
+
sets.push(`${col} = ?`)
|
|
46
|
+
sqlArgs.push(args[key] as string | number | null)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (sets.length === 0) return text('No fields to update.')
|
|
50
|
+
|
|
51
|
+
sets.push('updated_at = CURRENT_TIMESTAMP')
|
|
52
|
+
sqlArgs.push(epicId)
|
|
53
|
+
const result = await execute(`UPDATE pm_epics SET ${sets.join(', ')} WHERE id = ?`, sqlArgs)
|
|
54
|
+
if (result.error) return err(result.error)
|
|
55
|
+
if (result.rowsAffected === 0) return err(`Epic #${epicId} not found.`)
|
|
56
|
+
return text(`✅ Epic #${epicId} updated`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function toolDeleteEpic(args: Record<string, unknown>): Promise<ToolResult> {
|
|
60
|
+
const epicId = args.epic_id as number
|
|
61
|
+
const r1 = await execute('UPDATE pm_stories SET epic_id = NULL WHERE epic_id = ?', [epicId])
|
|
62
|
+
if (r1.error) return err(r1.error)
|
|
63
|
+
const r2 = await execute('DELETE FROM pm_epics WHERE id = ?', [epicId])
|
|
64
|
+
if (r2.error) return err(r2.error)
|
|
65
|
+
if (r2.rowsAffected === 0) return err(`Epic #${epicId} not found.`)
|
|
66
|
+
return text(`✅ Epic #${epicId} deleted`)
|
|
67
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolEmitEvent(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
if (!checkRateLimit(user)) {
|
|
6
|
+
return err('Rate limit exceeded: Maximum 10 events per minute.')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const eventType = args.event_type as string
|
|
10
|
+
|
|
11
|
+
const HOOK_ONLY_TYPES = ['memo_assigned', 'memo_replied', 'memo_resolved']
|
|
12
|
+
if (HOOK_ONLY_TYPES.includes(eventType)) {
|
|
13
|
+
return err('Memo-related events are emitted automatically. Cannot emit directly.')
|
|
14
|
+
}
|
|
15
|
+
const targetAgent = args.target_agent as string
|
|
16
|
+
const targetUser = args.target_user as string
|
|
17
|
+
const payload = args.payload as string
|
|
18
|
+
const ttlHours = (args.ttl_hours as number) ?? 24
|
|
19
|
+
const sourceAgent = (args.source_agent as string) ?? user
|
|
20
|
+
|
|
21
|
+
// Validate payload is valid JSON
|
|
22
|
+
try {
|
|
23
|
+
JSON.parse(payload)
|
|
24
|
+
} catch {
|
|
25
|
+
return err('Payload is not valid JSON.')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const expiresAt = new Date(Date.now() + ttlHours * 3600_000).toISOString()
|
|
29
|
+
|
|
30
|
+
const result = await execute(
|
|
31
|
+
`INSERT INTO agent_events (event_type, source_agent, target_agent, target_user, payload, expires_at)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
33
|
+
[eventType, sourceAgent, targetAgent, targetUser, payload, expiresAt],
|
|
34
|
+
)
|
|
35
|
+
if (result.error) return err(result.error)
|
|
36
|
+
|
|
37
|
+
const idResult = await query<{ id: number }>('SELECT last_insert_rowid() as id')
|
|
38
|
+
const newId = idResult.rows[0]?.id ?? '?'
|
|
39
|
+
|
|
40
|
+
return text(`✅ Event emitted: [${eventType}] → ${targetUser} (ID: ${newId}, TTL: ${ttlHours}h)`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function toolPollEvents(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
44
|
+
const eventType = args.event_type as string | undefined
|
|
45
|
+
const limit = (args.limit as number) ?? 20
|
|
46
|
+
|
|
47
|
+
let sql = `SELECT id, event_type, source_agent, target_agent, payload, status, created_at
|
|
48
|
+
FROM agent_events
|
|
49
|
+
WHERE target_user = ? AND status = 'pending'
|
|
50
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))`
|
|
51
|
+
const sqlArgs: (string | number)[] = [user]
|
|
52
|
+
|
|
53
|
+
if (eventType) {
|
|
54
|
+
sql += ' AND event_type = ?'
|
|
55
|
+
sqlArgs.push(eventType)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sql += ' ORDER BY created_at DESC LIMIT ?'
|
|
59
|
+
sqlArgs.push(limit)
|
|
60
|
+
|
|
61
|
+
const result = await query<{
|
|
62
|
+
id: number; event_type: string; source_agent: string; target_agent: string
|
|
63
|
+
payload: string; status: string; created_at: string
|
|
64
|
+
}>(sql, sqlArgs)
|
|
65
|
+
|
|
66
|
+
if (result.error) return err(result.error)
|
|
67
|
+
if (result.rows.length === 0) return text('📭 No pending events.')
|
|
68
|
+
|
|
69
|
+
const lines = [`📬 Pending events (${result.rows.length})`, '─────────────']
|
|
70
|
+
for (const e of result.rows) {
|
|
71
|
+
const payloadPreview = e.payload.length > 60 ? e.payload.slice(0, 60) + '...' : e.payload
|
|
72
|
+
lines.push(`[E${e.id}] ${e.event_type} from ${e.source_agent} (${e.created_at.slice(5, 16)})`)
|
|
73
|
+
lines.push(` payload: ${payloadPreview}`)
|
|
74
|
+
}
|
|
75
|
+
return text(lines.join('\n'))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function toolAckEvent(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
79
|
+
const eventId = args.event_id as number
|
|
80
|
+
|
|
81
|
+
const result = await execute(
|
|
82
|
+
`UPDATE agent_events SET status = 'acked', acked_at = datetime('now')
|
|
83
|
+
WHERE id = ? AND status IN ('pending', 'delivered') AND target_user = ?`,
|
|
84
|
+
[eventId, user],
|
|
85
|
+
)
|
|
86
|
+
if (result.error) return err(result.error)
|
|
87
|
+
if (result.rowsAffected === 0) return err(`Event #${eventId} not found or already acknowledged.`)
|
|
88
|
+
return text(`✅ Event #${eventId} acknowledged`)
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './sprint.js'
|
|
2
|
+
export * from './epic.js'
|
|
3
|
+
export * from './story.js'
|
|
4
|
+
export * from './task.js'
|
|
5
|
+
export * from './memo.js'
|
|
6
|
+
export * from './initiative.js'
|
|
7
|
+
export * from './notification.js'
|
|
8
|
+
export * from './standup.js'
|
|
9
|
+
export * from './retro.js'
|
|
10
|
+
export * from './dashboard.js'
|
|
11
|
+
export * from './event.js'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolCreateInitiative(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
const title = args.title as string
|
|
6
|
+
const content = args.content as string
|
|
7
|
+
if (!title?.trim() || !content?.trim()) return err('title, content required')
|
|
8
|
+
const decider = (args.decider as string) ?? null
|
|
9
|
+
const sourceContext = (args.source_context as string) ?? null
|
|
10
|
+
const r = await execute(
|
|
11
|
+
'INSERT INTO initiatives (title, content, author, decider, source_context) VALUES (?, ?, ?, ?, ?)',
|
|
12
|
+
[title, content, user, decider, sourceContext],
|
|
13
|
+
)
|
|
14
|
+
if (r.error) return err(r.error)
|
|
15
|
+
return text(`✅ Initiative created: "${title}"`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function toolListInitiatives(args: Record<string, unknown>): Promise<ToolResult> {
|
|
19
|
+
const status = args.status as string | undefined
|
|
20
|
+
let sql = 'SELECT id, title, author, decider, status, created_at FROM initiatives'
|
|
21
|
+
const sqlArgs: string[] = []
|
|
22
|
+
if (status) { sql += ' WHERE status = ?'; sqlArgs.push(status) }
|
|
23
|
+
sql += ' ORDER BY created_at DESC LIMIT 20'
|
|
24
|
+
const r = await query<{ id: number; title: string; author: string; decider: string | null; status: string; created_at: string }>(sql, sqlArgs)
|
|
25
|
+
if (r.error) return err(r.error)
|
|
26
|
+
if (!r.rows.length) return text('No initiatives found.')
|
|
27
|
+
const lines = ['📋 Initiative List', '─────────────']
|
|
28
|
+
for (const i of r.rows) {
|
|
29
|
+
const dec = i.decider ? ` → ${i.decider}` : ''
|
|
30
|
+
lines.push(` [${i.status}] #${i.id}: ${i.title} (${i.author}${dec})`)
|
|
31
|
+
}
|
|
32
|
+
return text(lines.join('\n'))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function toolUpdateInitiativeStatus(args: Record<string, unknown>): Promise<ToolResult> {
|
|
36
|
+
const id = args.initiative_id as number
|
|
37
|
+
const newStatus = args.status as string
|
|
38
|
+
const note = (args.decision_note as string) ?? null
|
|
39
|
+
if (!id || !newStatus) return err('initiative_id, status required')
|
|
40
|
+
const { canTransition } = await import('../utils/initiative.js')
|
|
41
|
+
const current = await query<{ status: string }>('SELECT status FROM initiatives WHERE id = ?', [id])
|
|
42
|
+
if (!current.rows.length) return err('Initiative not found.')
|
|
43
|
+
if (!canTransition(current.rows[0].status as 'pending' | 'approved' | 'rejected' | 'deferred', newStatus as 'pending' | 'approved' | 'rejected' | 'deferred')) {
|
|
44
|
+
return err(`${current.rows[0].status} → ${newStatus} transition not allowed`)
|
|
45
|
+
}
|
|
46
|
+
await execute(
|
|
47
|
+
`UPDATE initiatives SET status = ?, decision_note = ?, decided_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
|
48
|
+
[newStatus, note, id],
|
|
49
|
+
)
|
|
50
|
+
return text(`✅ Initiative #${id}: ${current.rows[0].status} → ${newStatus}`)
|
|
51
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolSendMemo(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
const toUserRaw = args.to_user as string
|
|
6
|
+
const recipients = toUserRaw.split(',').map(s => s.trim()).filter(Boolean)
|
|
7
|
+
const assignedTo = recipients.join(',')
|
|
8
|
+
const content = args.content as string
|
|
9
|
+
const pageId = (args.page_id as string) || 'home'
|
|
10
|
+
const memoType = (args.memo_type as string) || 'memo'
|
|
11
|
+
const preview = content.length > 50 ? content.slice(0, 50) + '…' : content
|
|
12
|
+
|
|
13
|
+
// Parse [D-XX] tags → related_decisions
|
|
14
|
+
const decisionMatches = content.match(/\[D-\d+\]/g)
|
|
15
|
+
const relatedDecisions = decisionMatches ? JSON.stringify([...new Set(decisionMatches.map(m => m.slice(1, -1)))]) : null
|
|
16
|
+
const reviewRequired = (args.review_required as boolean) ? 1 : 0
|
|
17
|
+
const title = (args.title as string) ?? null
|
|
18
|
+
const supersedesId = (args.supersedes_id as number) ?? null
|
|
19
|
+
|
|
20
|
+
// Decision type requires title
|
|
21
|
+
if (memoType === 'decision' && !title) {
|
|
22
|
+
return err('Decision type memos require a title.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Supersede: update previous memo status
|
|
26
|
+
if (supersedesId) {
|
|
27
|
+
await execute('UPDATE memos_v2 SET status = ? WHERE id = ?', ['superseded', supersedesId])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resolve member IDs
|
|
31
|
+
const createdById = await resolveMemberId(user)
|
|
32
|
+
const assignedToId = recipients.length === 1 ? await resolveMemberId(recipients[0]) : null
|
|
33
|
+
|
|
34
|
+
const result = await execute(
|
|
35
|
+
'INSERT INTO memos_v2 (page_id, content, memo_type, created_by, created_by_id, assigned_to, assigned_to_id, related_decisions, review_required, title, supersedes_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
36
|
+
[pageId, content, memoType, user, createdById, assignedTo, assignedToId, relatedDecisions, reviewRequired, title, supersedesId],
|
|
37
|
+
)
|
|
38
|
+
if (result.error) return err(result.error)
|
|
39
|
+
|
|
40
|
+
const idResult = await query<{ id: number }>('SELECT last_insert_rowid() as id')
|
|
41
|
+
const memoId = idResult.rows[0]?.id ?? 0
|
|
42
|
+
|
|
43
|
+
// Notify each recipient
|
|
44
|
+
for (const r of recipients) {
|
|
45
|
+
await notify(r, 'memo_received', `New memo: ${user}`, preview, 'memo', memoId, pageId, user)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Auto-emit agent events
|
|
49
|
+
for (const r of recipients) {
|
|
50
|
+
await emitAgentEvent('memo_assigned', user, r, r, {
|
|
51
|
+
memo_id: memoId, from: user, preview, page_id: pageId,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Agent webhook notification
|
|
56
|
+
const { notifyByName } = await import('../utils/agent-notify.js')
|
|
57
|
+
for (const r of recipients) {
|
|
58
|
+
await notifyByName(r, `📋 New memo: ${user}`, `${preview}\n\nMemo #${memoId} | ${memoType}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return text(`✅ Memo sent: ${user} → ${assignedTo}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function toolListMemos(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
65
|
+
let sql = "SELECT * FROM memos_v2 WHERE (assigned_to = ? OR assigned_to LIKE ? OR assigned_to LIKE ? OR assigned_to LIKE ?)"
|
|
66
|
+
const sqlArgs: (string | number)[] = [user, `${user},%`, `%,${user},%`, `%,${user}`]
|
|
67
|
+
if (args.unread_only) { sql += " AND status = 'open'" }
|
|
68
|
+
sql += ' ORDER BY created_at DESC LIMIT 20'
|
|
69
|
+
|
|
70
|
+
const result = await query<{ id: number; page_id: string; content: string; memo_type: string; status: string; created_by: string; assigned_to: string | null; created_at: string }>(sql, sqlArgs)
|
|
71
|
+
if (result.error) return err(result.error)
|
|
72
|
+
if (result.rows.length === 0) return text('📩 No memos found.')
|
|
73
|
+
|
|
74
|
+
const lines = [`📩 ${user}'s Memos`, '─────────────']
|
|
75
|
+
for (const m of result.rows) {
|
|
76
|
+
const icon = m.status === 'open' ? '📩' : m.status === 'resolved' ? '✅' : '📖'
|
|
77
|
+
const preview = m.content.length > 60 ? m.content.slice(0, 60) + '…' : m.content
|
|
78
|
+
lines.push(`${icon} [M${m.id}] ${m.created_by} (${m.page_id}): ${preview}`)
|
|
79
|
+
}
|
|
80
|
+
return text(lines.join('\n'))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function toolReadMemo(args: Record<string, unknown>): Promise<ToolResult> {
|
|
84
|
+
const memoId = args.memo_id as number
|
|
85
|
+
const result = await query<{ id: number; page_id: string; content: string; memo_type: string; status: string; created_by: string; assigned_to: string | null; resolved_by: string | null; resolved_at: string | null; created_at: string }>('SELECT * FROM memos_v2 WHERE id = ?', [memoId])
|
|
86
|
+
if (result.error) return err(result.error)
|
|
87
|
+
if (result.rows.length === 0) return err(`Memo #${memoId} not found.`)
|
|
88
|
+
|
|
89
|
+
const m = result.rows[0]
|
|
90
|
+
|
|
91
|
+
const repliesResult = await query<{ id: number; content: string; created_by: string; created_at: string }>('SELECT * FROM memo_replies WHERE memo_id = ? ORDER BY created_at ASC', [memoId])
|
|
92
|
+
|
|
93
|
+
const lines = [`📩 Memo #${m.id}`, '─────────────', `From: ${m.created_by} → ${m.assigned_to ?? 'all'}`, `Page: ${m.page_id}`, `Status: ${m.status}`, `Date: ${m.created_at}`, '', m.content]
|
|
94
|
+
if (m.resolved_by) lines.push('', `✅ Resolved: ${m.resolved_by} (${m.resolved_at})`)
|
|
95
|
+
if (!repliesResult.error && repliesResult.rows.length > 0) {
|
|
96
|
+
lines.push('', '💬 Replies:')
|
|
97
|
+
for (const r of repliesResult.rows) lines.push(` ${r.created_by} (${r.created_at}): ${r.content}`)
|
|
98
|
+
}
|
|
99
|
+
return text(lines.join('\n'))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function toolReplyMemo(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
103
|
+
const memoId = args.memo_id as number
|
|
104
|
+
const content = args.content as string
|
|
105
|
+
const reviewType = (args.review_type as string) || 'comment'
|
|
106
|
+
const result = await execute(
|
|
107
|
+
'INSERT INTO memo_replies (memo_id, content, created_by, review_type) VALUES (?, ?, ?, ?)',
|
|
108
|
+
[memoId, content, user, reviewType],
|
|
109
|
+
)
|
|
110
|
+
if (result.error) return err(result.error)
|
|
111
|
+
|
|
112
|
+
// Notify memo author
|
|
113
|
+
const memoResult = await query<{ created_by: string; assigned_to: string | null }>(
|
|
114
|
+
'SELECT created_by, assigned_to FROM memos_v2 WHERE id = ?', [memoId],
|
|
115
|
+
)
|
|
116
|
+
if (!memoResult.error && memoResult.rows.length > 0) {
|
|
117
|
+
const memo = memoResult.rows[0]
|
|
118
|
+
const preview = content.length > 50 ? content.slice(0, 50) + '…' : content
|
|
119
|
+
// Notify author
|
|
120
|
+
await notify(memo.created_by, 'memo_reply', `Memo reply: ${user}`, preview, 'memo', memoId, 'home', user)
|
|
121
|
+
// If assigned_to is different from author and from replier, notify them too
|
|
122
|
+
if (memo.assigned_to && memo.assigned_to !== memo.created_by && memo.assigned_to !== user) {
|
|
123
|
+
await notify(memo.assigned_to, 'memo_reply', `Memo reply: ${user}`, preview, 'memo', memoId, 'home', user)
|
|
124
|
+
}
|
|
125
|
+
// Auto-emit agent event
|
|
126
|
+
await emitAgentEvent('memo_replied', user, memo.created_by, memo.created_by, {
|
|
127
|
+
memo_id: memoId, reply_by: user, preview,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return text(`✅ Reply sent to Memo #${memoId}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function toolResolveMemo(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
135
|
+
const memoId = args.memo_id as number
|
|
136
|
+
const result = await execute(
|
|
137
|
+
`UPDATE memos_v2 SET status = 'resolved', resolved_by = ?, resolved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
|
138
|
+
[user, memoId],
|
|
139
|
+
)
|
|
140
|
+
if (result.error) return err(result.error)
|
|
141
|
+
if (result.rowsAffected === 0) return err(`Memo #${memoId} not found.`)
|
|
142
|
+
|
|
143
|
+
// Notify memo author
|
|
144
|
+
const memoResult = await query<{ created_by: string }>(
|
|
145
|
+
'SELECT created_by FROM memos_v2 WHERE id = ?', [memoId],
|
|
146
|
+
)
|
|
147
|
+
if (!memoResult.error && memoResult.rows.length > 0) {
|
|
148
|
+
await notify(memoResult.rows[0].created_by, 'memo_resolved', `Memo resolved: ${user}`, `${user} resolved Memo #${memoId}.`, 'memo', memoId, 'home', user)
|
|
149
|
+
// Auto-emit agent event
|
|
150
|
+
await emitAgentEvent('memo_resolved', user, memoResult.rows[0].created_by, memoResult.rows[0].created_by, {
|
|
151
|
+
memo_id: memoId, resolved_by: user,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return text(`✅ Memo #${memoId} resolved`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function toolRejectMemo(args: Record<string, unknown>): Promise<ToolResult> {
|
|
159
|
+
const memoId = args.memo_id as number
|
|
160
|
+
if (!memoId) return err('memo_id required')
|
|
161
|
+
const r = await execute("UPDATE memos_v2 SET status = 'open', resolved_by = NULL, resolved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?", [memoId])
|
|
162
|
+
if (r.error) return err(r.error)
|
|
163
|
+
return text(`✅ Memo #${memoId} rejected (reopened)`)
|
|
164
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { query, execute } from '../db/adapter.js'
|
|
2
|
+
import { text, err, today, resolveSprint, notify, checkRateLimit, emitAgentEvent, validateAssignee, resolveMemberId, type ToolResult } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export async function toolCheckNotifications(user: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
5
|
+
const unreadOnly = args.unread_only as boolean | undefined
|
|
6
|
+
let sql = 'SELECT * FROM notifications WHERE user_name = ?'
|
|
7
|
+
const sqlArgs: (string | number)[] = [user]
|
|
8
|
+
if (unreadOnly) { sql += ' AND is_read = 0' }
|
|
9
|
+
sql += ' ORDER BY created_at DESC LIMIT 30'
|
|
10
|
+
|
|
11
|
+
const result = await query<{ id: number; type: string; title: string; body: string | null; source_type: string; source_id: string; actor: string; is_read: number; created_at: string }>(sql, sqlArgs)
|
|
12
|
+
if (result.error) return err(result.error)
|
|
13
|
+
if (result.rows.length === 0) return text(unreadOnly ? '🔔 No unread notifications.' : '🔔 No notifications.')
|
|
14
|
+
|
|
15
|
+
const unread = result.rows.filter(n => !n.is_read).length
|
|
16
|
+
const lines = [`🔔 Notifications (${unread} unread)`, '─────────────']
|
|
17
|
+
for (const n of result.rows) {
|
|
18
|
+
const icon = n.is_read ? ' ' : '🔴'
|
|
19
|
+
const body = n.body ? ` — ${n.body.length > 40 ? n.body.slice(0, 40) + '…' : n.body}` : ''
|
|
20
|
+
lines.push(`${icon} [N${n.id}] ${n.title}${body} (${n.actor}, ${n.created_at.slice(5, 16)})`)
|
|
21
|
+
}
|
|
22
|
+
return text(lines.join('\n'))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function toolMarkNotificationRead(args: Record<string, unknown>): Promise<ToolResult> {
|
|
26
|
+
const notifId = args.notification_id as number
|
|
27
|
+
const result = await execute('UPDATE notifications SET is_read = 1 WHERE id = ?', [notifId])
|
|
28
|
+
if (result.error) return err(result.error)
|
|
29
|
+
if (result.rowsAffected === 0) return err(`Notification #${notifId} not found.`)
|
|
30
|
+
return text(`✅ Notification #${notifId} marked as read`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function toolMarkAllNotificationsRead(user: string): Promise<ToolResult> {
|
|
34
|
+
const result = await execute('UPDATE notifications SET is_read = 1 WHERE user_name = ? AND is_read = 0', [user])
|
|
35
|
+
if (result.error) return err(result.error)
|
|
36
|
+
return text(`✅ ${result.rowsAffected} notifications marked as read.`)
|
|
37
|
+
}
|