grov 0.5.2 → 0.5.4
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 +34 -4
- package/dist/cli.js +8 -0
- package/dist/lib/api-client.d.ts +18 -1
- package/dist/lib/api-client.js +57 -0
- package/dist/lib/llm-extractor.d.ts +14 -39
- package/dist/lib/llm-extractor.js +379 -407
- package/dist/lib/store/convenience.d.ts +40 -0
- package/dist/lib/store/convenience.js +104 -0
- package/dist/lib/store/database.d.ts +22 -0
- package/dist/lib/store/database.js +375 -0
- package/dist/lib/store/drift.d.ts +9 -0
- package/dist/lib/store/drift.js +89 -0
- package/dist/lib/store/index.d.ts +7 -0
- package/dist/lib/store/index.js +13 -0
- package/dist/lib/store/sessions.d.ts +32 -0
- package/dist/lib/store/sessions.js +240 -0
- package/dist/lib/store/steps.d.ts +40 -0
- package/dist/lib/store/steps.js +161 -0
- package/dist/lib/store/tasks.d.ts +33 -0
- package/dist/lib/store/tasks.js +133 -0
- package/dist/lib/store/types.d.ts +167 -0
- package/dist/lib/store/types.js +2 -0
- package/dist/lib/store.d.ts +1 -436
- package/dist/lib/store.js +2 -1478
- package/dist/proxy/cache.d.ts +36 -0
- package/dist/proxy/cache.js +51 -0
- package/dist/proxy/config.d.ts +1 -0
- package/dist/proxy/config.js +2 -0
- package/dist/proxy/extended-cache.d.ts +10 -0
- package/dist/proxy/extended-cache.js +155 -0
- package/dist/proxy/handlers/preprocess.d.ts +20 -0
- package/dist/proxy/handlers/preprocess.js +169 -0
- package/dist/proxy/injection/delta-tracking.d.ts +11 -0
- package/dist/proxy/injection/delta-tracking.js +93 -0
- package/dist/proxy/injection/injectors.d.ts +7 -0
- package/dist/proxy/injection/injectors.js +139 -0
- package/dist/proxy/request-processor.d.ts +18 -4
- package/dist/proxy/request-processor.js +151 -30
- package/dist/proxy/response-processor.js +93 -45
- package/dist/proxy/server.d.ts +0 -1
- package/dist/proxy/server.js +366 -582
- package/dist/proxy/types.d.ts +13 -0
- package/dist/proxy/types.js +2 -0
- package/dist/proxy/utils/extractors.d.ts +18 -0
- package/dist/proxy/utils/extractors.js +109 -0
- package/dist/proxy/utils/logging.d.ts +18 -0
- package/dist/proxy/utils/logging.js +42 -0
- package/package.json +7 -2
- package/postinstall.js +19 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionState, CreateSessionStateInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a new session state.
|
|
4
|
+
* Uses INSERT OR IGNORE to handle race conditions safely.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createSessionState(input: CreateSessionStateInput): SessionState;
|
|
7
|
+
/**
|
|
8
|
+
* Get a session state by ID
|
|
9
|
+
*/
|
|
10
|
+
export declare function getSessionState(sessionId: string): SessionState | null;
|
|
11
|
+
/**
|
|
12
|
+
* Update a session state.
|
|
13
|
+
* Uses transaction for atomic updates to prevent race conditions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function updateSessionState(sessionId: string, updates: Partial<Omit<SessionState, 'session_id' | 'start_time'>>): void;
|
|
16
|
+
/**
|
|
17
|
+
* Delete a session state
|
|
18
|
+
*/
|
|
19
|
+
export declare function deleteSessionState(sessionId: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Get active session for a specific user in a project
|
|
22
|
+
*/
|
|
23
|
+
export declare function getActiveSessionForUser(projectPath: string, userId?: string): SessionState | null;
|
|
24
|
+
/**
|
|
25
|
+
* Get all active sessions (for proxy-status command)
|
|
26
|
+
*/
|
|
27
|
+
export declare function getActiveSessionsForStatus(): SessionState[];
|
|
28
|
+
/**
|
|
29
|
+
* Get completed session for project (for new_task detection)
|
|
30
|
+
* Returns most recent completed session if exists
|
|
31
|
+
*/
|
|
32
|
+
export declare function getCompletedSessionForProject(projectPath: string): SessionState | null;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Session state CRUD operations
|
|
2
|
+
import { getDb, safeJsonParse } from './database.js';
|
|
3
|
+
/**
|
|
4
|
+
* Convert database row to SessionState object
|
|
5
|
+
*/
|
|
6
|
+
function rowToSessionState(row) {
|
|
7
|
+
return {
|
|
8
|
+
// Base fields
|
|
9
|
+
session_id: row.session_id,
|
|
10
|
+
user_id: row.user_id,
|
|
11
|
+
project_path: row.project_path,
|
|
12
|
+
original_goal: row.original_goal,
|
|
13
|
+
expected_scope: safeJsonParse(row.expected_scope, []),
|
|
14
|
+
constraints: safeJsonParse(row.constraints, []),
|
|
15
|
+
keywords: safeJsonParse(row.keywords, []),
|
|
16
|
+
escalation_count: row.escalation_count || 0,
|
|
17
|
+
last_checked_at: row.last_checked_at || 0,
|
|
18
|
+
start_time: row.start_time,
|
|
19
|
+
last_update: row.last_update,
|
|
20
|
+
status: row.status,
|
|
21
|
+
// Hook-specific fields
|
|
22
|
+
success_criteria: safeJsonParse(row.success_criteria, []),
|
|
23
|
+
last_drift_score: row.last_drift_score,
|
|
24
|
+
pending_recovery_plan: safeJsonParse(row.pending_recovery_plan, undefined),
|
|
25
|
+
drift_history: safeJsonParse(row.drift_history, []),
|
|
26
|
+
actions_taken: safeJsonParse(row.actions_taken, []),
|
|
27
|
+
files_explored: safeJsonParse(row.files_explored, []),
|
|
28
|
+
current_intent: row.current_intent,
|
|
29
|
+
drift_warnings: safeJsonParse(row.drift_warnings, []),
|
|
30
|
+
// Proxy-specific fields
|
|
31
|
+
token_count: row.token_count || 0,
|
|
32
|
+
session_mode: row.session_mode || 'normal',
|
|
33
|
+
waiting_for_recovery: Boolean(row.waiting_for_recovery),
|
|
34
|
+
last_clear_at: row.last_clear_at,
|
|
35
|
+
completed_at: row.completed_at,
|
|
36
|
+
parent_session_id: row.parent_session_id,
|
|
37
|
+
task_type: row.task_type || 'main',
|
|
38
|
+
pending_correction: row.pending_correction,
|
|
39
|
+
pending_forced_recovery: row.pending_forced_recovery,
|
|
40
|
+
pending_clear_summary: row.pending_clear_summary,
|
|
41
|
+
final_response: row.final_response,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a new session state.
|
|
46
|
+
* Uses INSERT OR IGNORE to handle race conditions safely.
|
|
47
|
+
*/
|
|
48
|
+
export function createSessionState(input) {
|
|
49
|
+
const database = getDb();
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
const sessionState = {
|
|
52
|
+
// Base fields
|
|
53
|
+
session_id: input.session_id,
|
|
54
|
+
user_id: input.user_id,
|
|
55
|
+
project_path: input.project_path,
|
|
56
|
+
original_goal: input.original_goal,
|
|
57
|
+
expected_scope: input.expected_scope || [],
|
|
58
|
+
constraints: input.constraints || [],
|
|
59
|
+
keywords: input.keywords || [],
|
|
60
|
+
escalation_count: 0,
|
|
61
|
+
last_checked_at: 0,
|
|
62
|
+
start_time: now,
|
|
63
|
+
last_update: now,
|
|
64
|
+
status: 'active',
|
|
65
|
+
// Hook-specific fields
|
|
66
|
+
success_criteria: input.success_criteria || [],
|
|
67
|
+
last_drift_score: undefined,
|
|
68
|
+
pending_recovery_plan: undefined,
|
|
69
|
+
drift_history: [],
|
|
70
|
+
actions_taken: [],
|
|
71
|
+
files_explored: [],
|
|
72
|
+
current_intent: undefined,
|
|
73
|
+
drift_warnings: [],
|
|
74
|
+
// Proxy-specific fields
|
|
75
|
+
token_count: 0,
|
|
76
|
+
session_mode: 'normal',
|
|
77
|
+
waiting_for_recovery: false,
|
|
78
|
+
last_clear_at: undefined,
|
|
79
|
+
completed_at: undefined,
|
|
80
|
+
parent_session_id: input.parent_session_id,
|
|
81
|
+
task_type: input.task_type || 'main',
|
|
82
|
+
};
|
|
83
|
+
const stmt = database.prepare(`
|
|
84
|
+
INSERT OR IGNORE INTO session_states (
|
|
85
|
+
session_id, user_id, project_path, original_goal,
|
|
86
|
+
expected_scope, constraints, keywords,
|
|
87
|
+
token_count, escalation_count, session_mode,
|
|
88
|
+
waiting_for_recovery, last_checked_at, last_clear_at,
|
|
89
|
+
start_time, last_update, status,
|
|
90
|
+
parent_session_id, task_type,
|
|
91
|
+
success_criteria, last_drift_score, pending_recovery_plan, drift_history,
|
|
92
|
+
completed_at
|
|
93
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
94
|
+
`);
|
|
95
|
+
stmt.run(sessionState.session_id, sessionState.user_id || null, sessionState.project_path, sessionState.original_goal || null, JSON.stringify(sessionState.expected_scope), JSON.stringify(sessionState.constraints), JSON.stringify(sessionState.keywords), sessionState.token_count, sessionState.escalation_count, sessionState.session_mode, sessionState.waiting_for_recovery ? 1 : 0, sessionState.last_checked_at, sessionState.last_clear_at || null, sessionState.start_time, sessionState.last_update, sessionState.status, sessionState.parent_session_id || null, sessionState.task_type, JSON.stringify(sessionState.success_criteria || []), sessionState.last_drift_score || null, sessionState.pending_recovery_plan ? JSON.stringify(sessionState.pending_recovery_plan) : null, JSON.stringify(sessionState.drift_history || []), sessionState.completed_at || null);
|
|
96
|
+
return sessionState;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get a session state by ID
|
|
100
|
+
*/
|
|
101
|
+
export function getSessionState(sessionId) {
|
|
102
|
+
const database = getDb();
|
|
103
|
+
const stmt = database.prepare('SELECT * FROM session_states WHERE session_id = ?');
|
|
104
|
+
const row = stmt.get(sessionId);
|
|
105
|
+
return row ? rowToSessionState(row) : null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Update a session state.
|
|
109
|
+
* Uses transaction for atomic updates to prevent race conditions.
|
|
110
|
+
*/
|
|
111
|
+
export function updateSessionState(sessionId, updates) {
|
|
112
|
+
const database = getDb();
|
|
113
|
+
const setClauses = [];
|
|
114
|
+
const params = [];
|
|
115
|
+
if (updates.user_id !== undefined) {
|
|
116
|
+
setClauses.push('user_id = ?');
|
|
117
|
+
params.push(updates.user_id || null);
|
|
118
|
+
}
|
|
119
|
+
if (updates.project_path !== undefined) {
|
|
120
|
+
setClauses.push('project_path = ?');
|
|
121
|
+
params.push(updates.project_path);
|
|
122
|
+
}
|
|
123
|
+
if (updates.original_goal !== undefined) {
|
|
124
|
+
setClauses.push('original_goal = ?');
|
|
125
|
+
params.push(updates.original_goal || null);
|
|
126
|
+
}
|
|
127
|
+
if (updates.expected_scope !== undefined) {
|
|
128
|
+
setClauses.push('expected_scope = ?');
|
|
129
|
+
params.push(JSON.stringify(updates.expected_scope));
|
|
130
|
+
}
|
|
131
|
+
if (updates.constraints !== undefined) {
|
|
132
|
+
setClauses.push('constraints = ?');
|
|
133
|
+
params.push(JSON.stringify(updates.constraints));
|
|
134
|
+
}
|
|
135
|
+
if (updates.keywords !== undefined) {
|
|
136
|
+
setClauses.push('keywords = ?');
|
|
137
|
+
params.push(JSON.stringify(updates.keywords));
|
|
138
|
+
}
|
|
139
|
+
if (updates.token_count !== undefined) {
|
|
140
|
+
setClauses.push('token_count = ?');
|
|
141
|
+
params.push(updates.token_count);
|
|
142
|
+
}
|
|
143
|
+
if (updates.escalation_count !== undefined) {
|
|
144
|
+
setClauses.push('escalation_count = ?');
|
|
145
|
+
params.push(updates.escalation_count);
|
|
146
|
+
}
|
|
147
|
+
if (updates.session_mode !== undefined) {
|
|
148
|
+
setClauses.push('session_mode = ?');
|
|
149
|
+
params.push(updates.session_mode);
|
|
150
|
+
}
|
|
151
|
+
if (updates.waiting_for_recovery !== undefined) {
|
|
152
|
+
setClauses.push('waiting_for_recovery = ?');
|
|
153
|
+
params.push(updates.waiting_for_recovery ? 1 : 0);
|
|
154
|
+
}
|
|
155
|
+
if (updates.last_checked_at !== undefined) {
|
|
156
|
+
setClauses.push('last_checked_at = ?');
|
|
157
|
+
params.push(updates.last_checked_at);
|
|
158
|
+
}
|
|
159
|
+
if (updates.last_clear_at !== undefined) {
|
|
160
|
+
setClauses.push('last_clear_at = ?');
|
|
161
|
+
params.push(updates.last_clear_at);
|
|
162
|
+
}
|
|
163
|
+
if (updates.status !== undefined) {
|
|
164
|
+
setClauses.push('status = ?');
|
|
165
|
+
params.push(updates.status);
|
|
166
|
+
}
|
|
167
|
+
if (updates.pending_correction !== undefined) {
|
|
168
|
+
setClauses.push('pending_correction = ?');
|
|
169
|
+
params.push(updates.pending_correction || null);
|
|
170
|
+
}
|
|
171
|
+
if (updates.pending_forced_recovery !== undefined) {
|
|
172
|
+
setClauses.push('pending_forced_recovery = ?');
|
|
173
|
+
params.push(updates.pending_forced_recovery || null);
|
|
174
|
+
}
|
|
175
|
+
if (updates.pending_clear_summary !== undefined) {
|
|
176
|
+
setClauses.push('pending_clear_summary = ?');
|
|
177
|
+
params.push(updates.pending_clear_summary || null);
|
|
178
|
+
}
|
|
179
|
+
if (updates.final_response !== undefined) {
|
|
180
|
+
setClauses.push('final_response = ?');
|
|
181
|
+
params.push(updates.final_response || null);
|
|
182
|
+
}
|
|
183
|
+
// Always update last_update
|
|
184
|
+
setClauses.push('last_update = ?');
|
|
185
|
+
params.push(new Date().toISOString());
|
|
186
|
+
if (setClauses.length === 0)
|
|
187
|
+
return;
|
|
188
|
+
params.push(sessionId);
|
|
189
|
+
const sql = `UPDATE session_states SET ${setClauses.join(', ')} WHERE session_id = ?`;
|
|
190
|
+
const transaction = database.transaction(() => {
|
|
191
|
+
database.prepare(sql).run(...params);
|
|
192
|
+
});
|
|
193
|
+
transaction();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Delete a session state
|
|
197
|
+
*/
|
|
198
|
+
export function deleteSessionState(sessionId) {
|
|
199
|
+
const database = getDb();
|
|
200
|
+
database.prepare('DELETE FROM session_states WHERE session_id = ?').run(sessionId);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get active session for a specific user in a project
|
|
204
|
+
*/
|
|
205
|
+
export function getActiveSessionForUser(projectPath, userId) {
|
|
206
|
+
const database = getDb();
|
|
207
|
+
if (userId) {
|
|
208
|
+
const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND user_id = ? AND status = 'active' ORDER BY last_update DESC LIMIT 1");
|
|
209
|
+
const row = stmt.get(projectPath, userId);
|
|
210
|
+
return row ? rowToSessionState(row) : null;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const stmt = database.prepare("SELECT * FROM session_states WHERE project_path = ? AND status = 'active' ORDER BY last_update DESC LIMIT 1");
|
|
214
|
+
const row = stmt.get(projectPath);
|
|
215
|
+
return row ? rowToSessionState(row) : null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get all active sessions (for proxy-status command)
|
|
220
|
+
*/
|
|
221
|
+
export function getActiveSessionsForStatus() {
|
|
222
|
+
const database = getDb();
|
|
223
|
+
const stmt = database.prepare("SELECT * FROM session_states WHERE status = 'active' ORDER BY last_update DESC LIMIT 20");
|
|
224
|
+
const rows = stmt.all();
|
|
225
|
+
return rows.map(rowToSessionState);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get completed session for project (for new_task detection)
|
|
229
|
+
* Returns most recent completed session if exists
|
|
230
|
+
*/
|
|
231
|
+
export function getCompletedSessionForProject(projectPath) {
|
|
232
|
+
const database = getDb();
|
|
233
|
+
const row = database.prepare(`
|
|
234
|
+
SELECT * FROM session_states
|
|
235
|
+
WHERE project_path = ? AND status = 'completed'
|
|
236
|
+
ORDER BY completed_at DESC
|
|
237
|
+
LIMIT 1
|
|
238
|
+
`).get(projectPath);
|
|
239
|
+
return row ? rowToSessionState(row) : null;
|
|
240
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StepRecord, CreateStepInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a new step record
|
|
4
|
+
*/
|
|
5
|
+
export declare function createStep(input: CreateStepInput): StepRecord;
|
|
6
|
+
/**
|
|
7
|
+
* Get steps for a session
|
|
8
|
+
*/
|
|
9
|
+
export declare function getStepsForSession(sessionId: string, limit?: number): StepRecord[];
|
|
10
|
+
/**
|
|
11
|
+
* Get recent steps for a session (most recent N)
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRecentSteps(sessionId: string, count?: number): StepRecord[];
|
|
14
|
+
/**
|
|
15
|
+
* Get validated steps only (for summary generation)
|
|
16
|
+
*/
|
|
17
|
+
export declare function getValidatedSteps(sessionId: string): StepRecord[];
|
|
18
|
+
/**
|
|
19
|
+
* Get key decision steps for a session (is_key_decision = 1)
|
|
20
|
+
* Used for user message injection - important decisions with reasoning
|
|
21
|
+
*/
|
|
22
|
+
export declare function getKeyDecisions(sessionId: string, limit?: number): StepRecord[];
|
|
23
|
+
/**
|
|
24
|
+
* Get edited files for a session (action_type IN ('edit', 'write'))
|
|
25
|
+
* Used for user message injection - prevent re-work
|
|
26
|
+
*/
|
|
27
|
+
export declare function getEditedFiles(sessionId: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Delete steps for a session
|
|
30
|
+
*/
|
|
31
|
+
export declare function deleteStepsForSession(sessionId: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Update reasoning for recent steps that don't have reasoning yet
|
|
34
|
+
* Called at end_turn to backfill reasoning from Claude's text response
|
|
35
|
+
*/
|
|
36
|
+
export declare function updateRecentStepsReasoning(sessionId: string, reasoning: string, limit?: number): number;
|
|
37
|
+
/**
|
|
38
|
+
* Update last_checked_at timestamp for a session
|
|
39
|
+
*/
|
|
40
|
+
export declare function updateLastChecked(sessionId: string, timestamp: number): void;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Step record CRUD operations
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { getDb, safeJsonParse } from './database.js';
|
|
4
|
+
/**
|
|
5
|
+
* Convert database row to StepRecord object
|
|
6
|
+
*/
|
|
7
|
+
function rowToStep(row) {
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
session_id: row.session_id,
|
|
11
|
+
action_type: row.action_type,
|
|
12
|
+
files: safeJsonParse(row.files, []),
|
|
13
|
+
folders: safeJsonParse(row.folders, []),
|
|
14
|
+
command: row.command,
|
|
15
|
+
reasoning: row.reasoning,
|
|
16
|
+
drift_score: row.drift_score,
|
|
17
|
+
drift_type: row.drift_type,
|
|
18
|
+
is_key_decision: Boolean(row.is_key_decision),
|
|
19
|
+
is_validated: Boolean(row.is_validated),
|
|
20
|
+
correction_given: row.correction_given,
|
|
21
|
+
correction_level: row.correction_level,
|
|
22
|
+
keywords: safeJsonParse(row.keywords, []),
|
|
23
|
+
timestamp: row.timestamp
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a new step record
|
|
28
|
+
*/
|
|
29
|
+
export function createStep(input) {
|
|
30
|
+
const database = getDb();
|
|
31
|
+
const step = {
|
|
32
|
+
id: randomUUID(),
|
|
33
|
+
session_id: input.session_id,
|
|
34
|
+
action_type: input.action_type,
|
|
35
|
+
files: input.files || [],
|
|
36
|
+
folders: input.folders || [],
|
|
37
|
+
command: input.command,
|
|
38
|
+
reasoning: input.reasoning,
|
|
39
|
+
drift_score: input.drift_score,
|
|
40
|
+
drift_type: input.drift_type,
|
|
41
|
+
is_key_decision: input.is_key_decision || false,
|
|
42
|
+
is_validated: input.is_validated !== false,
|
|
43
|
+
correction_given: input.correction_given,
|
|
44
|
+
correction_level: input.correction_level,
|
|
45
|
+
keywords: input.keywords || [],
|
|
46
|
+
timestamp: Date.now()
|
|
47
|
+
};
|
|
48
|
+
const stmt = database.prepare(`
|
|
49
|
+
INSERT INTO steps (
|
|
50
|
+
id, session_id, action_type, files, folders, command, reasoning,
|
|
51
|
+
drift_score, drift_type, is_key_decision, is_validated,
|
|
52
|
+
correction_given, correction_level, keywords, timestamp
|
|
53
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
`);
|
|
55
|
+
stmt.run(step.id, step.session_id, step.action_type, JSON.stringify(step.files), JSON.stringify(step.folders), step.command || null, step.reasoning || null, step.drift_score || null, step.drift_type || null, step.is_key_decision ? 1 : 0, step.is_validated ? 1 : 0, step.correction_given || null, step.correction_level || null, JSON.stringify(step.keywords), step.timestamp);
|
|
56
|
+
return step;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get steps for a session
|
|
60
|
+
*/
|
|
61
|
+
export function getStepsForSession(sessionId, limit) {
|
|
62
|
+
const database = getDb();
|
|
63
|
+
let sql = 'SELECT * FROM steps WHERE session_id = ? ORDER BY timestamp DESC';
|
|
64
|
+
const params = [sessionId];
|
|
65
|
+
if (limit) {
|
|
66
|
+
sql += ' LIMIT ?';
|
|
67
|
+
params.push(limit);
|
|
68
|
+
}
|
|
69
|
+
const stmt = database.prepare(sql);
|
|
70
|
+
const rows = stmt.all(...params);
|
|
71
|
+
return rows.map(rowToStep);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get recent steps for a session (most recent N)
|
|
75
|
+
*/
|
|
76
|
+
export function getRecentSteps(sessionId, count = 10) {
|
|
77
|
+
return getStepsForSession(sessionId, count);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get validated steps only (for summary generation)
|
|
81
|
+
*/
|
|
82
|
+
export function getValidatedSteps(sessionId) {
|
|
83
|
+
const database = getDb();
|
|
84
|
+
const stmt = database.prepare('SELECT * FROM steps WHERE session_id = ? AND is_validated = 1 ORDER BY timestamp ASC');
|
|
85
|
+
const rows = stmt.all(sessionId);
|
|
86
|
+
return rows.map(rowToStep);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get key decision steps for a session (is_key_decision = 1)
|
|
90
|
+
* Used for user message injection - important decisions with reasoning
|
|
91
|
+
*/
|
|
92
|
+
export function getKeyDecisions(sessionId, limit = 5) {
|
|
93
|
+
const database = getDb();
|
|
94
|
+
const stmt = database.prepare(`SELECT * FROM steps
|
|
95
|
+
WHERE session_id = ? AND is_key_decision = 1 AND reasoning IS NOT NULL
|
|
96
|
+
ORDER BY timestamp DESC
|
|
97
|
+
LIMIT ?`);
|
|
98
|
+
const rows = stmt.all(sessionId, limit);
|
|
99
|
+
return rows.map(rowToStep);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get edited files for a session (action_type IN ('edit', 'write'))
|
|
103
|
+
* Used for user message injection - prevent re-work
|
|
104
|
+
*/
|
|
105
|
+
export function getEditedFiles(sessionId) {
|
|
106
|
+
const database = getDb();
|
|
107
|
+
const stmt = database.prepare(`SELECT DISTINCT files FROM steps
|
|
108
|
+
WHERE session_id = ? AND action_type IN ('edit', 'write')
|
|
109
|
+
ORDER BY timestamp DESC`);
|
|
110
|
+
const rows = stmt.all(sessionId);
|
|
111
|
+
const allFiles = [];
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
try {
|
|
114
|
+
const files = JSON.parse(row.files || '[]');
|
|
115
|
+
if (Array.isArray(files)) {
|
|
116
|
+
allFiles.push(...files);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Skip invalid JSON
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...new Set(allFiles)];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Delete steps for a session
|
|
127
|
+
*/
|
|
128
|
+
export function deleteStepsForSession(sessionId) {
|
|
129
|
+
const database = getDb();
|
|
130
|
+
database.prepare('DELETE FROM steps WHERE session_id = ?').run(sessionId);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Update reasoning for recent steps that don't have reasoning yet
|
|
134
|
+
* Called at end_turn to backfill reasoning from Claude's text response
|
|
135
|
+
*/
|
|
136
|
+
export function updateRecentStepsReasoning(sessionId, reasoning, limit = 10) {
|
|
137
|
+
const database = getDb();
|
|
138
|
+
const stmt = database.prepare(`
|
|
139
|
+
UPDATE steps
|
|
140
|
+
SET reasoning = ?
|
|
141
|
+
WHERE session_id = ?
|
|
142
|
+
AND (reasoning IS NULL OR reasoning = '')
|
|
143
|
+
AND id IN (
|
|
144
|
+
SELECT id FROM steps
|
|
145
|
+
WHERE session_id = ?
|
|
146
|
+
ORDER BY timestamp DESC
|
|
147
|
+
LIMIT ?
|
|
148
|
+
)
|
|
149
|
+
`);
|
|
150
|
+
const result = stmt.run(reasoning, sessionId, sessionId, limit);
|
|
151
|
+
return result.changes;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Update last_checked_at timestamp for a session
|
|
155
|
+
*/
|
|
156
|
+
export function updateLastChecked(sessionId, timestamp) {
|
|
157
|
+
const database = getDb();
|
|
158
|
+
database.prepare(`
|
|
159
|
+
UPDATE session_states SET last_checked_at = ? WHERE session_id = ?
|
|
160
|
+
`).run(timestamp, sessionId);
|
|
161
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Task, CreateTaskInput, TaskStatus } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a new task
|
|
4
|
+
*/
|
|
5
|
+
export declare function createTask(input: CreateTaskInput): Task;
|
|
6
|
+
/**
|
|
7
|
+
* Get tasks for a project from LOCAL SQLite database
|
|
8
|
+
*
|
|
9
|
+
* Used by:
|
|
10
|
+
* - `grov status` command (CLI)
|
|
11
|
+
*
|
|
12
|
+
* For CLOUD injection, use fetchTeamMemories() from api-client.ts instead.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getTasksForProject(projectPath: string, options?: {
|
|
15
|
+
status?: TaskStatus;
|
|
16
|
+
limit?: number;
|
|
17
|
+
}): Task[];
|
|
18
|
+
/**
|
|
19
|
+
* Get task count for a project
|
|
20
|
+
*/
|
|
21
|
+
export declare function getTaskCount(projectPath: string): number;
|
|
22
|
+
/**
|
|
23
|
+
* Get unsynced tasks for a project (synced_at is NULL)
|
|
24
|
+
*/
|
|
25
|
+
export declare function getUnsyncedTasks(projectPath: string, limit?: number): Task[];
|
|
26
|
+
/**
|
|
27
|
+
* Mark a task as synced and clear any previous sync error
|
|
28
|
+
*/
|
|
29
|
+
export declare function markTaskSynced(id: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Record a sync error for a task
|
|
32
|
+
*/
|
|
33
|
+
export declare function setTaskSyncError(id: string, error: string): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Task CRUD operations
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { getDb, safeJsonParse } from './database.js';
|
|
4
|
+
/**
|
|
5
|
+
* Convert database row to Task object
|
|
6
|
+
*/
|
|
7
|
+
function rowToTask(row) {
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
project_path: row.project_path,
|
|
11
|
+
user: row.user,
|
|
12
|
+
original_query: row.original_query,
|
|
13
|
+
goal: row.goal,
|
|
14
|
+
reasoning_trace: safeJsonParse(row.reasoning_trace, []),
|
|
15
|
+
files_touched: safeJsonParse(row.files_touched, []),
|
|
16
|
+
decisions: safeJsonParse(row.decisions, []),
|
|
17
|
+
constraints: safeJsonParse(row.constraints, []),
|
|
18
|
+
status: row.status,
|
|
19
|
+
trigger_reason: row.trigger_reason,
|
|
20
|
+
linked_commit: row.linked_commit,
|
|
21
|
+
parent_task_id: row.parent_task_id,
|
|
22
|
+
turn_number: row.turn_number,
|
|
23
|
+
tags: safeJsonParse(row.tags, []),
|
|
24
|
+
created_at: row.created_at,
|
|
25
|
+
synced_at: row.synced_at,
|
|
26
|
+
sync_error: row.sync_error
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a new task
|
|
31
|
+
*/
|
|
32
|
+
export function createTask(input) {
|
|
33
|
+
const database = getDb();
|
|
34
|
+
const task = {
|
|
35
|
+
id: randomUUID(),
|
|
36
|
+
project_path: input.project_path,
|
|
37
|
+
user: input.user,
|
|
38
|
+
original_query: input.original_query,
|
|
39
|
+
goal: input.goal,
|
|
40
|
+
reasoning_trace: input.reasoning_trace || [],
|
|
41
|
+
files_touched: input.files_touched || [],
|
|
42
|
+
decisions: input.decisions || [],
|
|
43
|
+
constraints: input.constraints || [],
|
|
44
|
+
status: input.status,
|
|
45
|
+
trigger_reason: input.trigger_reason,
|
|
46
|
+
linked_commit: input.linked_commit,
|
|
47
|
+
parent_task_id: input.parent_task_id,
|
|
48
|
+
turn_number: input.turn_number,
|
|
49
|
+
tags: input.tags || [],
|
|
50
|
+
created_at: new Date().toISOString(),
|
|
51
|
+
synced_at: null,
|
|
52
|
+
sync_error: null
|
|
53
|
+
};
|
|
54
|
+
const stmt = database.prepare(`
|
|
55
|
+
INSERT INTO tasks (
|
|
56
|
+
id, project_path, user, original_query, goal,
|
|
57
|
+
reasoning_trace, files_touched, decisions, constraints,
|
|
58
|
+
status, trigger_reason, linked_commit,
|
|
59
|
+
parent_task_id, turn_number, tags, created_at, synced_at, sync_error
|
|
60
|
+
) VALUES (
|
|
61
|
+
?, ?, ?, ?, ?,
|
|
62
|
+
?, ?, ?, ?,
|
|
63
|
+
?, ?, ?,
|
|
64
|
+
?, ?, ?, ?, ?, ?
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
67
|
+
stmt.run(task.id, task.project_path, task.user || null, task.original_query, task.goal || null, JSON.stringify(task.reasoning_trace), JSON.stringify(task.files_touched), JSON.stringify(task.decisions), JSON.stringify(task.constraints), task.status, task.trigger_reason || null, task.linked_commit || null, task.parent_task_id || null, task.turn_number || null, JSON.stringify(task.tags), task.created_at, task.synced_at, task.sync_error);
|
|
68
|
+
return task;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get tasks for a project from LOCAL SQLite database
|
|
72
|
+
*
|
|
73
|
+
* Used by:
|
|
74
|
+
* - `grov status` command (CLI)
|
|
75
|
+
*
|
|
76
|
+
* For CLOUD injection, use fetchTeamMemories() from api-client.ts instead.
|
|
77
|
+
*/
|
|
78
|
+
export function getTasksForProject(projectPath, options = {}) {
|
|
79
|
+
const database = getDb();
|
|
80
|
+
let sql = 'SELECT * FROM tasks WHERE project_path = ?';
|
|
81
|
+
const params = [projectPath];
|
|
82
|
+
if (options.status) {
|
|
83
|
+
sql += ' AND status = ?';
|
|
84
|
+
params.push(options.status);
|
|
85
|
+
}
|
|
86
|
+
sql += ' ORDER BY created_at DESC';
|
|
87
|
+
if (options.limit) {
|
|
88
|
+
sql += ' LIMIT ?';
|
|
89
|
+
params.push(options.limit);
|
|
90
|
+
}
|
|
91
|
+
const stmt = database.prepare(sql);
|
|
92
|
+
const rows = stmt.all(...params);
|
|
93
|
+
return rows.map(rowToTask);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get task count for a project
|
|
97
|
+
*/
|
|
98
|
+
export function getTaskCount(projectPath) {
|
|
99
|
+
const database = getDb();
|
|
100
|
+
const stmt = database.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_path = ?');
|
|
101
|
+
const row = stmt.get(projectPath);
|
|
102
|
+
return row?.count ?? 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get unsynced tasks for a project (synced_at is NULL)
|
|
106
|
+
*/
|
|
107
|
+
export function getUnsyncedTasks(projectPath, limit) {
|
|
108
|
+
const database = getDb();
|
|
109
|
+
let sql = 'SELECT * FROM tasks WHERE project_path = ? AND synced_at IS NULL ORDER BY created_at DESC';
|
|
110
|
+
const params = [projectPath];
|
|
111
|
+
if (limit) {
|
|
112
|
+
sql += ' LIMIT ?';
|
|
113
|
+
params.push(limit);
|
|
114
|
+
}
|
|
115
|
+
const stmt = database.prepare(sql);
|
|
116
|
+
const rows = stmt.all(...params);
|
|
117
|
+
return rows.map(rowToTask);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Mark a task as synced and clear any previous sync error
|
|
121
|
+
*/
|
|
122
|
+
export function markTaskSynced(id) {
|
|
123
|
+
const database = getDb();
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
database.prepare('UPDATE tasks SET synced_at = ?, sync_error = NULL WHERE id = ?').run(now, id);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Record a sync error for a task
|
|
129
|
+
*/
|
|
130
|
+
export function setTaskSyncError(id, error) {
|
|
131
|
+
const database = getDb();
|
|
132
|
+
database.prepare('UPDATE tasks SET sync_error = ? WHERE id = ?').run(error, id);
|
|
133
|
+
}
|