grov 0.2.3 → 0.5.3

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.
Files changed (64) hide show
  1. package/README.md +44 -5
  2. package/dist/cli.js +40 -2
  3. package/dist/commands/login.d.ts +1 -0
  4. package/dist/commands/login.js +115 -0
  5. package/dist/commands/logout.d.ts +1 -0
  6. package/dist/commands/logout.js +13 -0
  7. package/dist/commands/sync.d.ts +8 -0
  8. package/dist/commands/sync.js +127 -0
  9. package/dist/lib/api-client.d.ts +57 -0
  10. package/dist/lib/api-client.js +174 -0
  11. package/dist/lib/cloud-sync.d.ts +33 -0
  12. package/dist/lib/cloud-sync.js +176 -0
  13. package/dist/lib/credentials.d.ts +53 -0
  14. package/dist/lib/credentials.js +201 -0
  15. package/dist/lib/llm-extractor.d.ts +15 -39
  16. package/dist/lib/llm-extractor.js +400 -418
  17. package/dist/lib/store/convenience.d.ts +40 -0
  18. package/dist/lib/store/convenience.js +104 -0
  19. package/dist/lib/store/database.d.ts +22 -0
  20. package/dist/lib/store/database.js +375 -0
  21. package/dist/lib/store/drift.d.ts +9 -0
  22. package/dist/lib/store/drift.js +89 -0
  23. package/dist/lib/store/index.d.ts +7 -0
  24. package/dist/lib/store/index.js +13 -0
  25. package/dist/lib/store/sessions.d.ts +32 -0
  26. package/dist/lib/store/sessions.js +240 -0
  27. package/dist/lib/store/steps.d.ts +40 -0
  28. package/dist/lib/store/steps.js +161 -0
  29. package/dist/lib/store/tasks.d.ts +33 -0
  30. package/dist/lib/store/tasks.js +133 -0
  31. package/dist/lib/store/types.d.ts +167 -0
  32. package/dist/lib/store/types.js +2 -0
  33. package/dist/lib/store.d.ts +1 -406
  34. package/dist/lib/store.js +2 -1356
  35. package/dist/lib/utils.d.ts +5 -0
  36. package/dist/lib/utils.js +45 -0
  37. package/dist/proxy/action-parser.d.ts +10 -2
  38. package/dist/proxy/action-parser.js +4 -2
  39. package/dist/proxy/cache.d.ts +36 -0
  40. package/dist/proxy/cache.js +51 -0
  41. package/dist/proxy/config.d.ts +1 -0
  42. package/dist/proxy/config.js +2 -0
  43. package/dist/proxy/extended-cache.d.ts +10 -0
  44. package/dist/proxy/extended-cache.js +155 -0
  45. package/dist/proxy/forwarder.d.ts +7 -1
  46. package/dist/proxy/forwarder.js +157 -7
  47. package/dist/proxy/handlers/preprocess.d.ts +20 -0
  48. package/dist/proxy/handlers/preprocess.js +169 -0
  49. package/dist/proxy/injection/delta-tracking.d.ts +11 -0
  50. package/dist/proxy/injection/delta-tracking.js +93 -0
  51. package/dist/proxy/injection/injectors.d.ts +7 -0
  52. package/dist/proxy/injection/injectors.js +139 -0
  53. package/dist/proxy/request-processor.d.ts +18 -3
  54. package/dist/proxy/request-processor.js +151 -28
  55. package/dist/proxy/response-processor.js +116 -47
  56. package/dist/proxy/server.d.ts +4 -1
  57. package/dist/proxy/server.js +592 -253
  58. package/dist/proxy/types.d.ts +13 -0
  59. package/dist/proxy/types.js +2 -0
  60. package/dist/proxy/utils/extractors.d.ts +18 -0
  61. package/dist/proxy/utils/extractors.js +109 -0
  62. package/dist/proxy/utils/logging.d.ts +18 -0
  63. package/dist/proxy/utils/logging.js +42 -0
  64. package/package.json +22 -4
@@ -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
+ }