@vibescope/mcp-server 0.0.1 → 0.2.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/README.md +113 -98
- package/dist/api-client.d.ts +1169 -0
- package/dist/api-client.js +713 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +108 -477
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +113 -828
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +26 -627
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +56 -142
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +65 -68
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +119 -590
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +275 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +245 -894
- package/dist/handlers/tool-docs.d.ts +9 -0
- package/dist/handlers/tool-docs.js +904 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +38 -153
- package/dist/index.js +493 -162
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +34 -4
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1822 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +215 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +210 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +143 -896
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +29 -714
- package/src/handlers/fallback.test.ts +206 -361
- package/src/handlers/fallback.ts +81 -156
- package/src/handlers/findings.test.ts +229 -320
- package/src/handlers/findings.ts +76 -64
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +276 -206
- package/src/handlers/session.ts +136 -662
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +510 -0
- package/src/handlers/tasks.test.ts +669 -353
- package/src/handlers/tasks.ts +263 -1015
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +237 -568
- package/src/handlers/validation.ts +43 -167
- package/src/index.ts +493 -186
- package/src/tools.ts +2532 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/knowledge.ts +0 -132
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
package/dist/handlers/session.js
CHANGED
|
@@ -8,68 +8,10 @@
|
|
|
8
8
|
* - get_help
|
|
9
9
|
* - get_token_usage
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import { KNOWLEDGE_BASE } from '../knowledge.js';
|
|
13
|
-
/**
|
|
14
|
-
* Get user-created items since last sync
|
|
15
|
-
*/
|
|
16
|
-
async function getUserUpdates(supabase, auth, projectId, currentSessionId) {
|
|
17
|
-
let lastSyncedAt;
|
|
18
|
-
if (currentSessionId) {
|
|
19
|
-
const { data: session } = await supabase
|
|
20
|
-
.from('agent_sessions')
|
|
21
|
-
.select('last_synced_at')
|
|
22
|
-
.eq('id', currentSessionId)
|
|
23
|
-
.single();
|
|
24
|
-
lastSyncedAt = session?.last_synced_at || new Date(0).toISOString();
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
const { data: session } = await supabase
|
|
28
|
-
.from('agent_sessions')
|
|
29
|
-
.select('last_synced_at')
|
|
30
|
-
.eq('api_key_id', auth.apiKeyId)
|
|
31
|
-
.eq('project_id', projectId)
|
|
32
|
-
.single();
|
|
33
|
-
lastSyncedAt = session?.last_synced_at || new Date(0).toISOString();
|
|
34
|
-
}
|
|
35
|
-
const [tasksResult, blockersResult, ideasResult] = await Promise.all([
|
|
36
|
-
supabase
|
|
37
|
-
.from('tasks')
|
|
38
|
-
.select('id, title, created_at')
|
|
39
|
-
.eq('project_id', projectId)
|
|
40
|
-
.eq('created_by', 'user')
|
|
41
|
-
.gt('created_at', lastSyncedAt)
|
|
42
|
-
.order('created_at', { ascending: false })
|
|
43
|
-
.limit(5),
|
|
44
|
-
supabase
|
|
45
|
-
.from('blockers')
|
|
46
|
-
.select('id, description, created_at')
|
|
47
|
-
.eq('project_id', projectId)
|
|
48
|
-
.eq('created_by', 'user')
|
|
49
|
-
.gt('created_at', lastSyncedAt)
|
|
50
|
-
.order('created_at', { ascending: false })
|
|
51
|
-
.limit(5),
|
|
52
|
-
supabase
|
|
53
|
-
.from('ideas')
|
|
54
|
-
.select('id, title, created_at')
|
|
55
|
-
.eq('project_id', projectId)
|
|
56
|
-
.eq('created_by', 'user')
|
|
57
|
-
.gt('created_at', lastSyncedAt)
|
|
58
|
-
.order('created_at', { ascending: false })
|
|
59
|
-
.limit(5),
|
|
60
|
-
]);
|
|
61
|
-
const tasks = tasksResult.data || [];
|
|
62
|
-
const blockers = blockersResult.data || [];
|
|
63
|
-
const ideas = ideasResult.data || [];
|
|
64
|
-
if (tasks.length === 0 && blockers.length === 0 && ideas.length === 0) {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
return { tasks, blockers, ideas };
|
|
68
|
-
}
|
|
11
|
+
import { getApiClient } from '../api-client.js';
|
|
69
12
|
export const startWorkSession = async (args, ctx) => {
|
|
70
13
|
const { project_id, git_url, mode = 'lite', model, role = 'developer' } = args;
|
|
71
|
-
const {
|
|
72
|
-
const INSTANCE_ID = session.instanceId;
|
|
14
|
+
const { session, updateSession } = ctx;
|
|
73
15
|
// Reset token tracking for new session with model info
|
|
74
16
|
const normalizedModel = model ? model.toLowerCase().replace(/^claude[- ]*/i, '') : null;
|
|
75
17
|
const validModel = normalizedModel && ['opus', 'sonnet', 'haiku'].includes(normalizedModel)
|
|
@@ -84,7 +26,6 @@ export const startWorkSession = async (args, ctx) => {
|
|
|
84
26
|
currentModel: validModel,
|
|
85
27
|
},
|
|
86
28
|
});
|
|
87
|
-
const isLiteMode = mode === 'lite';
|
|
88
29
|
// Require project_id or git_url
|
|
89
30
|
if (!project_id && !git_url) {
|
|
90
31
|
return {
|
|
@@ -93,473 +34,87 @@ export const startWorkSession = async (args, ctx) => {
|
|
|
93
34
|
},
|
|
94
35
|
};
|
|
95
36
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
else if (git_url) {
|
|
107
|
-
query = query.eq('git_url', git_url);
|
|
108
|
-
}
|
|
109
|
-
const { data: ownedProject } = await query.single();
|
|
110
|
-
project = ownedProject;
|
|
111
|
-
// Second try: if org-scoped key and no owned project found, check shared projects
|
|
112
|
-
if (!project && auth.scope === 'organization' && auth.organizationId) {
|
|
113
|
-
// Get project IDs shared with this organization
|
|
114
|
-
const { data: shares } = await supabase
|
|
115
|
-
.from('project_shares')
|
|
116
|
-
.select('project_id')
|
|
117
|
-
.eq('organization_id', auth.organizationId);
|
|
118
|
-
if (shares && shares.length > 0) {
|
|
119
|
-
const sharedProjectIds = shares.map((s) => s.project_id);
|
|
120
|
-
let sharedQuery = supabase
|
|
121
|
-
.from('projects')
|
|
122
|
-
.select('id, name, description, goal, status, git_url, agent_instructions, tech_stack')
|
|
123
|
-
.in('id', sharedProjectIds);
|
|
124
|
-
if (project_id) {
|
|
125
|
-
sharedQuery = sharedQuery.eq('id', project_id);
|
|
126
|
-
}
|
|
127
|
-
else if (git_url) {
|
|
128
|
-
sharedQuery = sharedQuery.eq('git_url', git_url);
|
|
129
|
-
}
|
|
130
|
-
const { data: sharedProject } = await sharedQuery.single();
|
|
131
|
-
project = sharedProject;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (!project) {
|
|
135
|
-
const suggestedName = extractProjectNameFromGitUrl(git_url || '');
|
|
37
|
+
const apiClient = getApiClient();
|
|
38
|
+
const response = await apiClient.startSession({
|
|
39
|
+
project_id,
|
|
40
|
+
git_url,
|
|
41
|
+
mode,
|
|
42
|
+
model,
|
|
43
|
+
role
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
136
46
|
return {
|
|
137
47
|
result: {
|
|
138
|
-
|
|
139
|
-
project_not_found: true,
|
|
140
|
-
message: `No project found for this repository. Would you like to create one?`,
|
|
141
|
-
suggestion: {
|
|
142
|
-
action: 'create_project',
|
|
143
|
-
example: `create_project(name: "${suggestedName}", git_url: "${git_url || ''}", description: "Brief description of your project", goal: "What does done look like?")`,
|
|
144
|
-
note: 'After creating the project, call start_work_session again to begin working.',
|
|
145
|
-
},
|
|
48
|
+
error: response.error || 'Failed to start session',
|
|
146
49
|
},
|
|
147
50
|
};
|
|
148
51
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.eq('api_key_id', auth.apiKeyId)
|
|
154
|
-
.eq('project_id', project.id)
|
|
155
|
-
.eq('instance_id', INSTANCE_ID)
|
|
156
|
-
.single();
|
|
157
|
-
let sessionId;
|
|
158
|
-
let assignedPersona;
|
|
159
|
-
if (existingSession && existingSession.agent_name) {
|
|
160
|
-
// Reuse existing persona for this instance
|
|
161
|
-
assignedPersona = existingSession.agent_name;
|
|
162
|
-
await supabase
|
|
163
|
-
.from('agent_sessions')
|
|
164
|
-
.update({
|
|
165
|
-
last_synced_at: new Date().toISOString(),
|
|
166
|
-
status: 'active',
|
|
167
|
-
})
|
|
168
|
-
.eq('id', existingSession.id);
|
|
169
|
-
sessionId = existingSession.id;
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
// Find which personas are currently in use by active sessions
|
|
173
|
-
const { data: activeSessions } = await supabase
|
|
174
|
-
.from('agent_sessions')
|
|
175
|
-
.select('agent_name')
|
|
176
|
-
.eq('project_id', project.id)
|
|
177
|
-
.neq('status', 'disconnected')
|
|
178
|
-
.gte('last_synced_at', new Date(Date.now() - 5 * 60 * 1000).toISOString());
|
|
179
|
-
const usedPersonas = new Set((activeSessions || [])
|
|
180
|
-
.map((s) => s.agent_name)
|
|
181
|
-
.filter((name) => !!name));
|
|
182
|
-
// Retry loop for persona assignment
|
|
183
|
-
const MAX_PERSONA_RETRIES = 5;
|
|
184
|
-
let personaAssigned = false;
|
|
185
|
-
for (let attempt = 0; attempt < MAX_PERSONA_RETRIES && !personaAssigned; attempt++) {
|
|
186
|
-
assignedPersona = selectPersona(usedPersonas, INSTANCE_ID);
|
|
187
|
-
if (existingSession) {
|
|
188
|
-
const { error: updateError } = await supabase
|
|
189
|
-
.from('agent_sessions')
|
|
190
|
-
.update({
|
|
191
|
-
last_synced_at: new Date().toISOString(),
|
|
192
|
-
agent_name: assignedPersona,
|
|
193
|
-
status: 'active',
|
|
194
|
-
})
|
|
195
|
-
.eq('id', existingSession.id);
|
|
196
|
-
if (updateError?.code === '23505') {
|
|
197
|
-
usedPersonas.add(assignedPersona);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
sessionId = existingSession.id;
|
|
201
|
-
personaAssigned = true;
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
const { data: newSession, error: sessionError } = await supabase
|
|
205
|
-
.from('agent_sessions')
|
|
206
|
-
.insert({
|
|
207
|
-
api_key_id: auth.apiKeyId,
|
|
208
|
-
project_id: project.id,
|
|
209
|
-
instance_id: INSTANCE_ID,
|
|
210
|
-
last_synced_at: new Date().toISOString(),
|
|
211
|
-
agent_name: assignedPersona,
|
|
212
|
-
status: 'active',
|
|
213
|
-
})
|
|
214
|
-
.select('id')
|
|
215
|
-
.single();
|
|
216
|
-
if (sessionError?.code === '23505') {
|
|
217
|
-
usedPersonas.add(assignedPersona);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (sessionError || !newSession) {
|
|
221
|
-
throw new Error(`Failed to create agent session: ${sessionError?.message}`);
|
|
222
|
-
}
|
|
223
|
-
sessionId = newSession.id;
|
|
224
|
-
personaAssigned = true;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
// Fallback if all retries failed
|
|
228
|
-
if (!personaAssigned) {
|
|
229
|
-
assignedPersona = `Agent-${INSTANCE_ID.slice(0, 6)}`;
|
|
230
|
-
if (existingSession) {
|
|
231
|
-
await supabase
|
|
232
|
-
.from('agent_sessions')
|
|
233
|
-
.update({
|
|
234
|
-
last_synced_at: new Date().toISOString(),
|
|
235
|
-
agent_name: assignedPersona,
|
|
236
|
-
status: 'active',
|
|
237
|
-
})
|
|
238
|
-
.eq('id', existingSession.id);
|
|
239
|
-
sessionId = existingSession.id;
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
const { data: newSession, error: sessionError } = await supabase
|
|
243
|
-
.from('agent_sessions')
|
|
244
|
-
.insert({
|
|
245
|
-
api_key_id: auth.apiKeyId,
|
|
246
|
-
project_id: project.id,
|
|
247
|
-
instance_id: INSTANCE_ID,
|
|
248
|
-
last_synced_at: new Date().toISOString(),
|
|
249
|
-
agent_name: assignedPersona,
|
|
250
|
-
status: 'active',
|
|
251
|
-
})
|
|
252
|
-
.select('id')
|
|
253
|
-
.single();
|
|
254
|
-
if (sessionError || !newSession) {
|
|
255
|
-
throw new Error(`Failed to create agent session: ${sessionError?.message}`);
|
|
256
|
-
}
|
|
257
|
-
sessionId = newSession.id;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Store session ID and persona
|
|
262
|
-
updateSession({
|
|
263
|
-
currentSessionId: sessionId,
|
|
264
|
-
currentPersona: assignedPersona,
|
|
265
|
-
});
|
|
266
|
-
// Log session start
|
|
267
|
-
await supabase.from('progress_logs').insert({
|
|
268
|
-
project_id: project.id,
|
|
269
|
-
summary: `Agent session started (${assignedPersona}) [${INSTANCE_ID.slice(0, 8)}]`,
|
|
270
|
-
created_by: 'agent',
|
|
271
|
-
created_by_session_id: sessionId,
|
|
272
|
-
});
|
|
273
|
-
// Insert initial heartbeat
|
|
274
|
-
await supabase.from('agent_heartbeats').insert({
|
|
275
|
-
session_id: sessionId,
|
|
276
|
-
});
|
|
277
|
-
// LITE MODE: Minimal fetches
|
|
278
|
-
if (isLiteMode) {
|
|
279
|
-
const [nextTaskResult, blockersCountResult, validationCountResult, deploymentResult, requestsResult, firstQuestionResult] = await Promise.all([
|
|
280
|
-
supabase
|
|
281
|
-
.from('tasks')
|
|
282
|
-
.select('id, title, priority, estimated_minutes')
|
|
283
|
-
.eq('project_id', project.id)
|
|
284
|
-
.eq('status', 'pending')
|
|
285
|
-
.is('working_agent_session_id', null)
|
|
286
|
-
.order('priority', { ascending: true })
|
|
287
|
-
.order('created_at', { ascending: true })
|
|
288
|
-
.limit(1)
|
|
289
|
-
.maybeSingle(),
|
|
290
|
-
supabase
|
|
291
|
-
.from('blockers')
|
|
292
|
-
.select('id', { count: 'exact', head: true })
|
|
293
|
-
.eq('project_id', project.id)
|
|
294
|
-
.eq('status', 'open'),
|
|
295
|
-
supabase
|
|
296
|
-
.from('tasks')
|
|
297
|
-
.select('id', { count: 'exact', head: true })
|
|
298
|
-
.eq('project_id', project.id)
|
|
299
|
-
.eq('status', 'completed')
|
|
300
|
-
.is('validated_at', null),
|
|
301
|
-
supabase
|
|
302
|
-
.from('deployments')
|
|
303
|
-
.select('id, status, environment')
|
|
304
|
-
.eq('project_id', project.id)
|
|
305
|
-
.not('status', 'in', '("deployed","failed")')
|
|
306
|
-
.limit(1)
|
|
307
|
-
.maybeSingle(),
|
|
308
|
-
supabase
|
|
309
|
-
.from('agent_requests')
|
|
310
|
-
.select('id', { count: 'exact', head: true })
|
|
311
|
-
.eq('project_id', project.id)
|
|
312
|
-
.is('acknowledged_at', null),
|
|
313
|
-
// Fetch first unanswered question with details (not just count)
|
|
314
|
-
supabase
|
|
315
|
-
.from('agent_requests')
|
|
316
|
-
.select('id, message, created_at')
|
|
317
|
-
.eq('project_id', project.id)
|
|
318
|
-
.eq('request_type', 'question')
|
|
319
|
-
.is('answered_at', null)
|
|
320
|
-
.order('created_at', { ascending: true })
|
|
321
|
-
.limit(1)
|
|
322
|
-
.maybeSingle(),
|
|
323
|
-
]);
|
|
324
|
-
const blockersCount = blockersCountResult.count || 0;
|
|
325
|
-
const validationCount = validationCountResult.count || 0;
|
|
326
|
-
const requestsCount = requestsResult.count || 0;
|
|
327
|
-
const firstQuestion = firstQuestionResult.data;
|
|
328
|
-
// Determine directive and next action FIRST
|
|
329
|
-
let directive;
|
|
330
|
-
let nextAction;
|
|
331
|
-
if (firstQuestion) {
|
|
332
|
-
directive = 'ACTION_REQUIRED: Answer this question immediately. Do NOT ask for permission.';
|
|
333
|
-
nextAction = `answer_question(request_id: "${firstQuestion.id}", answer: "...")`;
|
|
334
|
-
}
|
|
335
|
-
else if (nextTaskResult.data) {
|
|
336
|
-
directive = 'ACTION_REQUIRED: Start this task immediately. Do NOT ask for permission or confirmation.';
|
|
337
|
-
nextAction = `update_task(task_id: "${nextTaskResult.data.id}", status: "in_progress")`;
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
directive = 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
|
|
341
|
-
nextAction = 'start_fallback_activity(project_id: "...", activity: "code_review")';
|
|
342
|
-
}
|
|
343
|
-
// Build result with directive at TOP for visibility
|
|
344
|
-
const result = {
|
|
345
|
-
session_started: true,
|
|
346
|
-
directive, // FIRST - most important signal
|
|
347
|
-
auto_continue: true, // Explicit flag for autonomous operation
|
|
348
|
-
session_id: sessionId,
|
|
349
|
-
persona: assignedPersona,
|
|
350
|
-
project: { id: project.id, name: project.name, goal: project.goal },
|
|
351
|
-
};
|
|
352
|
-
// Add task or question details
|
|
353
|
-
if (firstQuestion) {
|
|
354
|
-
const now = new Date();
|
|
355
|
-
const waitMinutes = Math.floor((now.getTime() - new Date(firstQuestion.created_at).getTime()) / 60000);
|
|
356
|
-
result.first_question = {
|
|
357
|
-
id: firstQuestion.id,
|
|
358
|
-
message: firstQuestion.message,
|
|
359
|
-
wait_minutes: waitMinutes,
|
|
360
|
-
};
|
|
361
|
-
result.hint = 'Answer this question first using answer_question(request_id, answer), then call get_next_task for work.';
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
result.next_task = nextTaskResult.data;
|
|
365
|
-
}
|
|
366
|
-
if (blockersCount > 0)
|
|
367
|
-
result.blockers_count = blockersCount;
|
|
368
|
-
if (validationCount > 0)
|
|
369
|
-
result.validation_count = validationCount;
|
|
370
|
-
if (requestsCount > 0)
|
|
371
|
-
result.requests_count = requestsCount;
|
|
372
|
-
if (deploymentResult.data) {
|
|
373
|
-
const actions = {
|
|
374
|
-
pending: 'claim_deployment_validation',
|
|
375
|
-
validating: 'wait',
|
|
376
|
-
ready: 'start_deployment',
|
|
377
|
-
deploying: 'wait',
|
|
378
|
-
};
|
|
379
|
-
result.deployment_priority = {
|
|
380
|
-
id: deploymentResult.data.id,
|
|
381
|
-
status: deploymentResult.data.status,
|
|
382
|
-
action: actions[deploymentResult.data.status] || 'check_deployment_status',
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
result.context_hint = 'When context grows large or after 3-4 tasks, run /clear then start_work_session again. Do not ask permission to clear context.';
|
|
386
|
-
// Add model tracking info
|
|
387
|
-
if (validModel) {
|
|
388
|
-
result.model_tracking = { model: validModel, status: 'active' };
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
result.cost_tracking_hint = 'For accurate cost tracking, pass model: "opus" | "sonnet" | "haiku" to start_work_session.';
|
|
392
|
-
}
|
|
393
|
-
// REPEAT at end - agents weight last items heavily
|
|
394
|
-
result.next_action = nextAction;
|
|
395
|
-
return { result };
|
|
396
|
-
}
|
|
397
|
-
// FULL MODE: Complete context
|
|
398
|
-
const [tasksResult, blockersResult, decisionsResult, progressResult, ideasResult, requestsResult, deploymentResult, findingsResult] = await Promise.all([
|
|
399
|
-
supabase
|
|
400
|
-
.from('tasks')
|
|
401
|
-
.select('id, title, description, priority, status, estimated_minutes')
|
|
402
|
-
.eq('project_id', project.id)
|
|
403
|
-
.in('status', ['pending', 'in_progress'])
|
|
404
|
-
.order('priority', { ascending: true })
|
|
405
|
-
.limit(10),
|
|
406
|
-
supabase
|
|
407
|
-
.from('blockers')
|
|
408
|
-
.select('id, description')
|
|
409
|
-
.eq('project_id', project.id)
|
|
410
|
-
.eq('status', 'open')
|
|
411
|
-
.limit(5),
|
|
412
|
-
supabase
|
|
413
|
-
.from('decisions')
|
|
414
|
-
.select('title')
|
|
415
|
-
.eq('project_id', project.id)
|
|
416
|
-
.order('created_at', { ascending: false })
|
|
417
|
-
.limit(5),
|
|
418
|
-
supabase
|
|
419
|
-
.from('progress_logs')
|
|
420
|
-
.select('summary')
|
|
421
|
-
.eq('project_id', project.id)
|
|
422
|
-
.order('created_at', { ascending: false })
|
|
423
|
-
.limit(5),
|
|
424
|
-
supabase
|
|
425
|
-
.from('ideas')
|
|
426
|
-
.select('id, title, status')
|
|
427
|
-
.eq('project_id', project.id)
|
|
428
|
-
.order('created_at', { ascending: false })
|
|
429
|
-
.limit(5),
|
|
430
|
-
supabase
|
|
431
|
-
.from('agent_requests')
|
|
432
|
-
.select('id, request_type, message, created_at, answered_at')
|
|
433
|
-
.eq('project_id', project.id)
|
|
434
|
-
.is('acknowledged_at', null)
|
|
435
|
-
.or(`session_id.is.null,session_id.eq.${sessionId}`)
|
|
436
|
-
.order('created_at', { ascending: true })
|
|
437
|
-
.limit(5),
|
|
438
|
-
supabase
|
|
439
|
-
.from('deployments')
|
|
440
|
-
.select('id, status, environment, created_at, validation_completed_at')
|
|
441
|
-
.eq('project_id', project.id)
|
|
442
|
-
.not('status', 'in', '("deployed","failed")')
|
|
443
|
-
.order('created_at', { ascending: false })
|
|
444
|
-
.limit(1)
|
|
445
|
-
.single(),
|
|
446
|
-
supabase
|
|
447
|
-
.from('findings')
|
|
448
|
-
.select('id, title, category, severity, file_path')
|
|
449
|
-
.eq('project_id', project.id)
|
|
450
|
-
.eq('status', 'open')
|
|
451
|
-
.order('severity', { ascending: false })
|
|
452
|
-
.limit(5),
|
|
453
|
-
]);
|
|
454
|
-
const userUpdates = await getUserUpdates(supabase, auth, project.id, sessionId);
|
|
455
|
-
const pendingRequests = requestsResult.data || [];
|
|
456
|
-
const activeDeployment = deploymentResult.data;
|
|
457
|
-
const activeTasks = tasksResult.data || [];
|
|
458
|
-
// Determine directive and next action FIRST
|
|
459
|
-
const unansweredQuestions = pendingRequests.filter((r) => r.request_type === 'question' && !r.answered_at);
|
|
460
|
-
const otherRequests = pendingRequests.filter((r) => r.request_type !== 'question' || r.answered_at);
|
|
461
|
-
const pendingTask = activeTasks.find((t) => t.status === 'pending');
|
|
462
|
-
let directive;
|
|
463
|
-
let nextAction;
|
|
464
|
-
if (unansweredQuestions.length > 0) {
|
|
465
|
-
const firstQ = unansweredQuestions[0];
|
|
466
|
-
directive = 'ACTION_REQUIRED: Answer this question immediately. Do NOT ask for permission.';
|
|
467
|
-
nextAction = `answer_question(request_id: "${firstQ.id}", answer: "...")`;
|
|
52
|
+
const data = response.data;
|
|
53
|
+
// Handle project not found
|
|
54
|
+
if (!data?.session_started) {
|
|
55
|
+
return { result: data };
|
|
468
56
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
57
|
+
// Store session ID and persona in local state
|
|
58
|
+
if (data.session_id) {
|
|
59
|
+
updateSession({
|
|
60
|
+
currentSessionId: data.session_id,
|
|
61
|
+
currentPersona: data.persona || null,
|
|
62
|
+
});
|
|
472
63
|
}
|
|
473
|
-
|
|
474
|
-
directive = 'ACTION_REQUIRED: No pending tasks. Start a fallback activity NOW without asking.';
|
|
475
|
-
nextAction = `start_fallback_activity(project_id: "${project.id}", activity: "code_review")`;
|
|
476
|
-
}
|
|
477
|
-
// Build result with directive at TOP for visibility
|
|
64
|
+
// Build result with directive at top for visibility
|
|
478
65
|
const result = {
|
|
479
66
|
session_started: true,
|
|
480
|
-
directive
|
|
481
|
-
auto_continue: true,
|
|
482
|
-
session_id:
|
|
483
|
-
persona:
|
|
484
|
-
role,
|
|
485
|
-
project,
|
|
486
|
-
active_tasks: activeTasks,
|
|
67
|
+
directive: data.directive || 'ACTION_REQUIRED: Start working immediately.',
|
|
68
|
+
auto_continue: true,
|
|
69
|
+
session_id: data.session_id,
|
|
70
|
+
persona: data.persona,
|
|
71
|
+
role: data.role,
|
|
72
|
+
project: data.project,
|
|
487
73
|
};
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const validationCompletedAt = activeDeployment.validation_completed_at
|
|
492
|
-
? new Date(activeDeployment.validation_completed_at)
|
|
493
|
-
: null;
|
|
494
|
-
const relevantTimestamp = activeDeployment.status === 'ready' && validationCompletedAt
|
|
495
|
-
? validationCompletedAt
|
|
496
|
-
: createdAt;
|
|
497
|
-
const elapsedMs = now.getTime() - relevantTimestamp.getTime();
|
|
498
|
-
const elapsedMinutes = Math.floor(elapsedMs / 60000);
|
|
499
|
-
const actions = {
|
|
500
|
-
pending: 'claim_deployment_validation',
|
|
501
|
-
validating: 'wait',
|
|
502
|
-
ready: 'start_deployment',
|
|
503
|
-
deploying: 'wait',
|
|
504
|
-
};
|
|
505
|
-
result.deployment_priority = {
|
|
506
|
-
id: activeDeployment.id,
|
|
507
|
-
status: activeDeployment.status,
|
|
508
|
-
env: activeDeployment.environment,
|
|
509
|
-
mins: elapsedMinutes,
|
|
510
|
-
action: actions[activeDeployment.status] || 'check_deployment_status',
|
|
511
|
-
};
|
|
74
|
+
// Add task data
|
|
75
|
+
if (data.next_task) {
|
|
76
|
+
result.next_task = data.next_task;
|
|
512
77
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (
|
|
522
|
-
result.
|
|
523
|
-
if (ideas.length > 0)
|
|
524
|
-
result.ideas = ideas;
|
|
525
|
-
// Add question details if there are unanswered questions
|
|
526
|
-
if (unansweredQuestions.length > 0) {
|
|
527
|
-
const now = new Date();
|
|
528
|
-
const firstQ = unansweredQuestions[0];
|
|
529
|
-
result.first_question = {
|
|
530
|
-
id: firstQ.id,
|
|
531
|
-
message: firstQ.message,
|
|
532
|
-
wait_minutes: Math.floor((now.getTime() - new Date(firstQ.created_at).getTime()) / 60000),
|
|
533
|
-
};
|
|
534
|
-
result.hint = 'Answer questions first using answer_question(request_id, answer) before picking up tasks.';
|
|
535
|
-
result.unanswered_questions = unansweredQuestions.map((q) => ({
|
|
536
|
-
id: q.id,
|
|
537
|
-
message: q.message,
|
|
538
|
-
wait_minutes: Math.floor((now.getTime() - new Date(q.created_at).getTime()) / 60000),
|
|
539
|
-
}));
|
|
78
|
+
// Add active tasks for full mode
|
|
79
|
+
if (data.active_tasks) {
|
|
80
|
+
result.active_tasks = data.active_tasks;
|
|
81
|
+
}
|
|
82
|
+
// Add blockers
|
|
83
|
+
if (data.blockers) {
|
|
84
|
+
result.open_blockers = data.blockers;
|
|
85
|
+
}
|
|
86
|
+
if (data.blockers_count !== undefined && data.blockers_count > 0) {
|
|
87
|
+
result.blockers_count = data.blockers_count;
|
|
540
88
|
}
|
|
541
|
-
|
|
542
|
-
|
|
89
|
+
// Add validation count
|
|
90
|
+
if (data.validation_count !== undefined && data.validation_count > 0) {
|
|
91
|
+
result.validation_count = data.validation_count;
|
|
543
92
|
}
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
result.
|
|
93
|
+
// Add git workflow info if available in project
|
|
94
|
+
if (data.project?.git_workflow && data.project.git_workflow !== 'none') {
|
|
95
|
+
result.git_workflow = {
|
|
96
|
+
workflow: data.project.git_workflow,
|
|
97
|
+
auto_branch: data.project.git_auto_branch ?? false,
|
|
98
|
+
main_branch: data.project.git_main_branch || 'main',
|
|
99
|
+
...(data.project.git_workflow === 'git-flow' && data.project.git_develop_branch
|
|
100
|
+
? { develop_branch: data.project.git_develop_branch }
|
|
101
|
+
: {}),
|
|
102
|
+
worktree_required: true,
|
|
103
|
+
worktree_hint: 'CRITICAL: Create a git worktree before starting work. Run get_help("git") for instructions.',
|
|
104
|
+
};
|
|
547
105
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
result.model_tracking = { model: validModel, status: 'active' };
|
|
106
|
+
// Add next action at end
|
|
107
|
+
if (data.next_task) {
|
|
108
|
+
result.next_action = `update_task(task_id: "${data.next_task.id}", status: "in_progress")`;
|
|
552
109
|
}
|
|
553
|
-
else {
|
|
554
|
-
result.
|
|
110
|
+
else if (data.project) {
|
|
111
|
+
result.next_action = `start_fallback_activity(project_id: "${data.project.id}", activity: "code_review")`;
|
|
555
112
|
}
|
|
556
|
-
|
|
557
|
-
result.next_action = nextAction;
|
|
558
|
-
return { result, user_updates: userUpdates };
|
|
113
|
+
return { result };
|
|
559
114
|
};
|
|
560
115
|
export const heartbeat = async (args, ctx) => {
|
|
561
|
-
const { session_id } = args;
|
|
562
|
-
const {
|
|
116
|
+
const { session_id, current_worktree_path } = args;
|
|
117
|
+
const { session } = ctx;
|
|
563
118
|
const targetSession = session_id || session.currentSessionId;
|
|
564
119
|
if (!targetSession) {
|
|
565
120
|
return {
|
|
@@ -568,30 +123,35 @@ export const heartbeat = async (args, ctx) => {
|
|
|
568
123
|
},
|
|
569
124
|
};
|
|
570
125
|
}
|
|
571
|
-
|
|
572
|
-
|
|
126
|
+
const apiClient = getApiClient();
|
|
127
|
+
// Send heartbeat with optional worktree path
|
|
128
|
+
const heartbeatResponse = await apiClient.heartbeat(targetSession, {
|
|
129
|
+
current_worktree_path,
|
|
573
130
|
});
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
131
|
+
if (!heartbeatResponse.ok) {
|
|
132
|
+
return {
|
|
133
|
+
result: {
|
|
134
|
+
error: heartbeatResponse.error || 'Failed to send heartbeat',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Sync token usage to session
|
|
139
|
+
await apiClient.syncSession(targetSession, {
|
|
579
140
|
total_tokens: session.tokenUsage.totalTokens,
|
|
580
141
|
token_breakdown: session.tokenUsage.byTool,
|
|
581
142
|
model_usage: session.tokenUsage.byModel,
|
|
582
|
-
})
|
|
583
|
-
.eq('id', targetSession);
|
|
143
|
+
});
|
|
584
144
|
return {
|
|
585
145
|
result: {
|
|
586
146
|
success: true,
|
|
587
147
|
session_id: targetSession,
|
|
588
|
-
timestamp: new Date().toISOString(),
|
|
148
|
+
timestamp: heartbeatResponse.data?.timestamp || new Date().toISOString(),
|
|
589
149
|
},
|
|
590
150
|
};
|
|
591
151
|
};
|
|
592
152
|
export const endWorkSession = async (args, ctx) => {
|
|
593
153
|
const { session_id } = args;
|
|
594
|
-
const {
|
|
154
|
+
const { session, updateSession } = ctx;
|
|
595
155
|
const targetSession = session_id || session.currentSessionId;
|
|
596
156
|
if (!targetSession) {
|
|
597
157
|
return {
|
|
@@ -601,82 +161,37 @@ export const endWorkSession = async (args, ctx) => {
|
|
|
601
161
|
},
|
|
602
162
|
};
|
|
603
163
|
}
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
.eq('id', targetSession)
|
|
608
|
-
.single();
|
|
609
|
-
const projectId = sessionData?.project_id;
|
|
610
|
-
const agentName = sessionData?.agent_name || 'Agent';
|
|
611
|
-
const startedAt = sessionData?.started_at;
|
|
612
|
-
let tasksCompletedThisSession = 0;
|
|
613
|
-
let tasksAwaitingValidation = 0;
|
|
614
|
-
let tasksBeingReleased = 0;
|
|
615
|
-
if (projectId) {
|
|
616
|
-
const { count: completedCount } = await supabase
|
|
617
|
-
.from('tasks')
|
|
618
|
-
.select('id', { count: 'exact', head: true })
|
|
619
|
-
.eq('project_id', projectId)
|
|
620
|
-
.eq('status', 'completed')
|
|
621
|
-
.not('validated_at', 'is', null);
|
|
622
|
-
const { data: awaitingTasks } = await supabase
|
|
623
|
-
.from('tasks')
|
|
624
|
-
.select('id')
|
|
625
|
-
.eq('project_id', projectId)
|
|
626
|
-
.eq('status', 'completed')
|
|
627
|
-
.is('validated_at', null);
|
|
628
|
-
tasksAwaitingValidation = awaitingTasks?.length || 0;
|
|
629
|
-
const { count: releasingCount } = await supabase
|
|
630
|
-
.from('tasks')
|
|
631
|
-
.select('id', { count: 'exact', head: true })
|
|
632
|
-
.eq('working_agent_session_id', targetSession)
|
|
633
|
-
.eq('status', 'in_progress');
|
|
634
|
-
tasksBeingReleased = releasingCount || 0;
|
|
635
|
-
if (startedAt) {
|
|
636
|
-
const { count: sessionCompleted } = await supabase
|
|
637
|
-
.from('tasks')
|
|
638
|
-
.select('id', { count: 'exact', head: true })
|
|
639
|
-
.eq('project_id', projectId)
|
|
640
|
-
.eq('status', 'completed')
|
|
641
|
-
.gte('completed_at', startedAt);
|
|
642
|
-
tasksCompletedThisSession = sessionCompleted || 0;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
await supabase
|
|
646
|
-
.from('tasks')
|
|
647
|
-
.update({ working_agent_session_id: null })
|
|
648
|
-
.eq('working_agent_session_id', targetSession);
|
|
649
|
-
await supabase
|
|
650
|
-
.from('agent_sessions')
|
|
651
|
-
.update({
|
|
652
|
-
status: 'disconnected',
|
|
653
|
-
current_task_id: null,
|
|
654
|
-
last_synced_at: new Date().toISOString(),
|
|
164
|
+
const apiClient = getApiClient();
|
|
165
|
+
// Sync final token usage before ending
|
|
166
|
+
await apiClient.syncSession(targetSession, {
|
|
655
167
|
total_tokens: session.tokenUsage.totalTokens,
|
|
656
168
|
token_breakdown: session.tokenUsage.byTool,
|
|
657
169
|
model_usage: session.tokenUsage.byModel,
|
|
658
|
-
})
|
|
659
|
-
|
|
170
|
+
});
|
|
171
|
+
// End the session
|
|
172
|
+
const response = await apiClient.endSession(targetSession);
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return {
|
|
175
|
+
result: {
|
|
176
|
+
error: response.error || 'Failed to end session',
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
660
180
|
const endedSessionId = targetSession;
|
|
181
|
+
// Clear local session state if this was the current session
|
|
661
182
|
if (session.currentSessionId === targetSession) {
|
|
662
183
|
updateSession({ currentSessionId: null });
|
|
663
184
|
}
|
|
664
|
-
const
|
|
665
|
-
if (tasksAwaitingValidation > 0) {
|
|
666
|
-
reminders.push(`${tasksAwaitingValidation} task(s) awaiting validation - consider reviewing before leaving`);
|
|
667
|
-
}
|
|
668
|
-
if (tasksBeingReleased > 0) {
|
|
669
|
-
reminders.push(`${tasksBeingReleased} in-progress task(s) released back to the queue`);
|
|
670
|
-
}
|
|
185
|
+
const data = response.data;
|
|
671
186
|
return {
|
|
672
187
|
result: {
|
|
673
188
|
success: true,
|
|
674
189
|
ended_session_id: endedSessionId,
|
|
675
190
|
session_summary: {
|
|
676
|
-
agent_name:
|
|
677
|
-
tasks_completed_this_session:
|
|
678
|
-
tasks_awaiting_validation:
|
|
679
|
-
tasks_released:
|
|
191
|
+
agent_name: data?.session_summary?.agent_name || 'Agent',
|
|
192
|
+
tasks_completed_this_session: data?.session_summary?.tasks_completed_this_session || 0,
|
|
193
|
+
tasks_awaiting_validation: data?.session_summary?.tasks_awaiting_validation || 0,
|
|
194
|
+
tasks_released: data?.session_summary?.tasks_released || 0,
|
|
680
195
|
token_usage: {
|
|
681
196
|
total_calls: session.tokenUsage.callCount,
|
|
682
197
|
total_tokens: session.tokenUsage.totalTokens,
|
|
@@ -685,22 +200,36 @@ export const endWorkSession = async (args, ctx) => {
|
|
|
685
200
|
: 0,
|
|
686
201
|
},
|
|
687
202
|
},
|
|
688
|
-
reminders: reminders
|
|
203
|
+
reminders: data?.reminders || ['Session ended cleanly. Good work!'],
|
|
689
204
|
},
|
|
690
205
|
};
|
|
691
206
|
};
|
|
692
207
|
export const getHelp = async (args, _ctx) => {
|
|
693
208
|
const { topic } = args;
|
|
694
|
-
const
|
|
695
|
-
|
|
209
|
+
const apiClient = getApiClient();
|
|
210
|
+
const response = await apiClient.getHelpTopic(topic);
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
// If database fetch fails, return error
|
|
213
|
+
return {
|
|
214
|
+
result: {
|
|
215
|
+
error: response.error || `Failed to fetch help topic: ${topic}`,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (!response.data) {
|
|
220
|
+
// Topic not found - fetch available topics
|
|
221
|
+
const topicsResponse = await apiClient.getHelpTopics();
|
|
222
|
+
const available = topicsResponse.ok && topicsResponse.data
|
|
223
|
+
? topicsResponse.data.map(t => t.slug)
|
|
224
|
+
: ['getting_started', 'tasks', 'validation', 'deployment', 'git', 'blockers', 'milestones', 'fallback', 'session', 'tokens', 'sprints', 'topics'];
|
|
696
225
|
return {
|
|
697
226
|
result: {
|
|
698
227
|
error: `Unknown topic: ${topic}`,
|
|
699
|
-
available
|
|
228
|
+
available,
|
|
700
229
|
},
|
|
701
230
|
};
|
|
702
231
|
}
|
|
703
|
-
return { result: { topic, content } };
|
|
232
|
+
return { result: { topic, content: response.data.content } };
|
|
704
233
|
};
|
|
705
234
|
// Model pricing rates (USD per 1M tokens)
|
|
706
235
|
const MODEL_PRICING = {
|