grov 0.2.3 → 0.5.2
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 +25 -4
- package/dist/cli.js +32 -2
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +115 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +13 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +127 -0
- package/dist/lib/api-client.d.ts +40 -0
- package/dist/lib/api-client.js +117 -0
- package/dist/lib/cloud-sync.d.ts +33 -0
- package/dist/lib/cloud-sync.js +176 -0
- package/dist/lib/credentials.d.ts +53 -0
- package/dist/lib/credentials.js +201 -0
- package/dist/lib/llm-extractor.d.ts +1 -1
- package/dist/lib/llm-extractor.js +20 -12
- package/dist/lib/store.d.ts +32 -2
- package/dist/lib/store.js +133 -11
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +45 -0
- package/dist/proxy/action-parser.d.ts +10 -2
- package/dist/proxy/action-parser.js +4 -2
- package/dist/proxy/forwarder.d.ts +7 -1
- package/dist/proxy/forwarder.js +157 -7
- package/dist/proxy/request-processor.d.ts +4 -3
- package/dist/proxy/request-processor.js +7 -5
- package/dist/proxy/response-processor.js +26 -5
- package/dist/proxy/server.d.ts +5 -1
- package/dist/proxy/server.js +667 -104
- package/package.json +18 -3
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Cloud sync logic - Upload memories from local database to API
|
|
2
|
+
// Handles batching, retries, and conversion from Task to Memory format
|
|
3
|
+
import { getSyncStatus, getAccessToken } from './credentials.js';
|
|
4
|
+
import { syncMemories, sleep, getApiUrl } from './api-client.js';
|
|
5
|
+
// Sync configuration
|
|
6
|
+
const SYNC_CONFIG = {
|
|
7
|
+
batchSize: 10, // Number of memories per batch
|
|
8
|
+
retryAttempts: 3, // Number of retry attempts per batch
|
|
9
|
+
retryDelay: 1000, // Base delay between retries (ms)
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Convert local Task to CreateMemoryInput for API
|
|
13
|
+
*/
|
|
14
|
+
export function taskToMemory(task) {
|
|
15
|
+
return {
|
|
16
|
+
client_task_id: task.id,
|
|
17
|
+
project_path: task.project_path,
|
|
18
|
+
original_query: task.original_query,
|
|
19
|
+
goal: task.goal,
|
|
20
|
+
reasoning_trace: task.reasoning_trace,
|
|
21
|
+
files_touched: task.files_touched,
|
|
22
|
+
decisions: task.decisions,
|
|
23
|
+
constraints: task.constraints,
|
|
24
|
+
tags: task.tags,
|
|
25
|
+
status: task.status,
|
|
26
|
+
linked_commit: task.linked_commit,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if sync is enabled and configured
|
|
31
|
+
*/
|
|
32
|
+
export function isSyncEnabled() {
|
|
33
|
+
const status = getSyncStatus();
|
|
34
|
+
return status?.enabled === true && !!status.teamId;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the configured team ID for sync
|
|
38
|
+
*/
|
|
39
|
+
export function getSyncTeamId() {
|
|
40
|
+
const status = getSyncStatus();
|
|
41
|
+
return status?.teamId || null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sync a single task to the cloud
|
|
45
|
+
* Called when a task is completed
|
|
46
|
+
*/
|
|
47
|
+
export async function syncTask(task) {
|
|
48
|
+
if (!isSyncEnabled()) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const teamId = getSyncTeamId();
|
|
52
|
+
if (!teamId) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const token = await getAccessToken();
|
|
56
|
+
if (!token) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const memory = taskToMemory(task);
|
|
61
|
+
const result = await syncMemories(teamId, { memories: [memory] });
|
|
62
|
+
return result.synced === 1;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Sync multiple tasks with batching and retry
|
|
70
|
+
*/
|
|
71
|
+
export async function syncTasks(tasks) {
|
|
72
|
+
if (!isSyncEnabled()) {
|
|
73
|
+
return {
|
|
74
|
+
synced: 0,
|
|
75
|
+
failed: tasks.length,
|
|
76
|
+
errors: [`Sync is not enabled. Run "grov sync --enable --team <team-id>" first. (API: ${getApiUrl()})`],
|
|
77
|
+
syncedIds: [],
|
|
78
|
+
failedIds: tasks.map(t => t.id),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const teamId = getSyncTeamId();
|
|
82
|
+
if (!teamId) {
|
|
83
|
+
return {
|
|
84
|
+
synced: 0,
|
|
85
|
+
failed: tasks.length,
|
|
86
|
+
errors: [`No team configured. Run "grov sync --enable --team <team-id>" first. (API: ${getApiUrl()})`],
|
|
87
|
+
syncedIds: [],
|
|
88
|
+
failedIds: tasks.map(t => t.id),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const token = await getAccessToken();
|
|
92
|
+
if (!token) {
|
|
93
|
+
return {
|
|
94
|
+
synced: 0,
|
|
95
|
+
failed: tasks.length,
|
|
96
|
+
errors: [`Not authenticated. Run "grov login" first. (API: ${getApiUrl()})`],
|
|
97
|
+
syncedIds: [],
|
|
98
|
+
failedIds: tasks.map(t => t.id),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Convert tasks to memories
|
|
102
|
+
const memories = tasks.map(taskToMemory);
|
|
103
|
+
// Batch and sync
|
|
104
|
+
const batches = [];
|
|
105
|
+
for (let i = 0; i < memories.length; i += SYNC_CONFIG.batchSize) {
|
|
106
|
+
batches.push(memories.slice(i, i + SYNC_CONFIG.batchSize));
|
|
107
|
+
}
|
|
108
|
+
let totalSynced = 0;
|
|
109
|
+
let totalFailed = 0;
|
|
110
|
+
const allErrors = [];
|
|
111
|
+
const syncedIds = [];
|
|
112
|
+
const failedIds = [];
|
|
113
|
+
for (const batch of batches) {
|
|
114
|
+
const batchResult = await syncBatchWithRetry(teamId, batch);
|
|
115
|
+
totalSynced += batchResult.synced;
|
|
116
|
+
totalFailed += batchResult.failed;
|
|
117
|
+
if (batchResult.errors) {
|
|
118
|
+
allErrors.push(...batchResult.errors);
|
|
119
|
+
}
|
|
120
|
+
const batchIds = batch.map((m) => m.client_task_id || '');
|
|
121
|
+
if (batchResult.synced === batch.length) {
|
|
122
|
+
syncedIds.push(...batchIds);
|
|
123
|
+
}
|
|
124
|
+
else if (batchResult.failed === batch.length) {
|
|
125
|
+
failedIds.push(...batchIds);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
synced: totalSynced,
|
|
130
|
+
failed: totalFailed,
|
|
131
|
+
errors: allErrors,
|
|
132
|
+
syncedIds: syncedIds.filter(Boolean),
|
|
133
|
+
failedIds: failedIds.filter(Boolean),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Sync a batch with retry logic
|
|
138
|
+
*/
|
|
139
|
+
async function syncBatchWithRetry(teamId, memories) {
|
|
140
|
+
let lastError;
|
|
141
|
+
for (let attempt = 0; attempt < SYNC_CONFIG.retryAttempts; attempt++) {
|
|
142
|
+
try {
|
|
143
|
+
return await syncMemories(teamId, { memories });
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
lastError = err instanceof Error ? err.message : 'Unknown error';
|
|
147
|
+
// Exponential backoff
|
|
148
|
+
if (attempt < SYNC_CONFIG.retryAttempts - 1) {
|
|
149
|
+
const delay = SYNC_CONFIG.retryDelay * Math.pow(2, attempt);
|
|
150
|
+
await sleep(delay);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// All retries failed
|
|
155
|
+
return {
|
|
156
|
+
synced: 0,
|
|
157
|
+
failed: memories.length,
|
|
158
|
+
errors: [lastError || 'Sync failed after retries'],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get sync status summary
|
|
163
|
+
*/
|
|
164
|
+
export function getSyncStatusSummary() {
|
|
165
|
+
const status = getSyncStatus();
|
|
166
|
+
if (!status) {
|
|
167
|
+
return 'Not logged in';
|
|
168
|
+
}
|
|
169
|
+
if (!status.enabled) {
|
|
170
|
+
return 'Sync disabled';
|
|
171
|
+
}
|
|
172
|
+
if (!status.teamId) {
|
|
173
|
+
return 'No team configured';
|
|
174
|
+
}
|
|
175
|
+
return `Syncing to team: ${status.teamId}`;
|
|
176
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token: string;
|
|
4
|
+
expires_at: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
team_id?: string;
|
|
8
|
+
sync_enabled: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read credentials from disk
|
|
12
|
+
* @returns Credentials or null if not found/invalid
|
|
13
|
+
*/
|
|
14
|
+
export declare function readCredentials(): Credentials | null;
|
|
15
|
+
/**
|
|
16
|
+
* Write credentials to disk with secure permissions
|
|
17
|
+
*/
|
|
18
|
+
export declare function writeCredentials(creds: Credentials): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clear credentials (logout)
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearCredentials(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Check if user is authenticated (has valid credentials)
|
|
25
|
+
*/
|
|
26
|
+
export declare function isAuthenticated(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Get a valid access token, refreshing if necessary
|
|
29
|
+
* @returns Access token or null if not authenticated
|
|
30
|
+
*/
|
|
31
|
+
export declare function getAccessToken(): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Set the team ID for sync
|
|
34
|
+
*/
|
|
35
|
+
export declare function setTeamId(teamId: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Enable or disable sync
|
|
38
|
+
*/
|
|
39
|
+
export declare function setSyncEnabled(enabled: boolean): void;
|
|
40
|
+
/**
|
|
41
|
+
* Get current sync status
|
|
42
|
+
*/
|
|
43
|
+
export declare function getSyncStatus(): {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
teamId: string | undefined;
|
|
46
|
+
} | null;
|
|
47
|
+
/**
|
|
48
|
+
* Get current user info
|
|
49
|
+
*/
|
|
50
|
+
export declare function getCurrentUser(): {
|
|
51
|
+
id: string;
|
|
52
|
+
email: string;
|
|
53
|
+
} | null;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Secure credential storage for CLI authentication
|
|
2
|
+
// Stores tokens at ~/.grov/credentials.json with 0o600 permissions
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, chmodSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { request } from 'undici';
|
|
7
|
+
const GROV_DIR = join(homedir(), '.grov');
|
|
8
|
+
const CREDENTIALS_PATH = join(GROV_DIR, 'credentials.json');
|
|
9
|
+
/**
|
|
10
|
+
* Ensure .grov directory exists with proper permissions
|
|
11
|
+
*/
|
|
12
|
+
function ensureGrovDir() {
|
|
13
|
+
if (!existsSync(GROV_DIR)) {
|
|
14
|
+
mkdirSync(GROV_DIR, { recursive: true, mode: 0o700 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read credentials from disk
|
|
19
|
+
* @returns Credentials or null if not found/invalid
|
|
20
|
+
*/
|
|
21
|
+
export function readCredentials() {
|
|
22
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(CREDENTIALS_PATH, 'utf-8');
|
|
27
|
+
const creds = JSON.parse(content);
|
|
28
|
+
// Validate required fields
|
|
29
|
+
if (!creds.access_token || !creds.refresh_token || !creds.expires_at) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return creds;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Write credentials to disk with secure permissions
|
|
40
|
+
*/
|
|
41
|
+
export function writeCredentials(creds) {
|
|
42
|
+
ensureGrovDir();
|
|
43
|
+
// Write with restrictive permissions (owner read/write only)
|
|
44
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
45
|
+
// Ensure permissions are correct even if file existed
|
|
46
|
+
chmodSync(CREDENTIALS_PATH, 0o600);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Clear credentials (logout)
|
|
50
|
+
*/
|
|
51
|
+
export function clearCredentials() {
|
|
52
|
+
if (existsSync(CREDENTIALS_PATH)) {
|
|
53
|
+
unlinkSync(CREDENTIALS_PATH);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if user is authenticated (has valid credentials)
|
|
58
|
+
*/
|
|
59
|
+
export function isAuthenticated() {
|
|
60
|
+
const creds = readCredentials();
|
|
61
|
+
return creds !== null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if access token is expired or will expire soon (within 5 minutes)
|
|
65
|
+
*/
|
|
66
|
+
function isTokenExpiringSoon(expiresAt) {
|
|
67
|
+
const expiryTime = new Date(expiresAt).getTime();
|
|
68
|
+
const bufferTime = 5 * 60 * 1000; // 5 minutes
|
|
69
|
+
return Date.now() > expiryTime - bufferTime;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Refresh tokens using the API
|
|
73
|
+
* @returns New credentials or null if refresh failed
|
|
74
|
+
*/
|
|
75
|
+
async function refreshTokens(refreshToken, apiUrl) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await request(`${apiUrl}/auth/refresh`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
83
|
+
});
|
|
84
|
+
if (response.statusCode !== 200) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const data = await response.body.json();
|
|
88
|
+
// Decode user info from new token (basic decode, no verification needed here)
|
|
89
|
+
const payload = decodeTokenPayload(data.access_token);
|
|
90
|
+
if (!payload) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// Read existing credentials to preserve team_id and sync_enabled
|
|
94
|
+
const existing = readCredentials();
|
|
95
|
+
return {
|
|
96
|
+
access_token: data.access_token,
|
|
97
|
+
refresh_token: data.refresh_token,
|
|
98
|
+
expires_at: data.expires_at,
|
|
99
|
+
user_id: payload.sub,
|
|
100
|
+
email: payload.email,
|
|
101
|
+
team_id: existing?.team_id,
|
|
102
|
+
sync_enabled: existing?.sync_enabled ?? false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Decode JWT payload without verification (for extracting user info)
|
|
111
|
+
* WARNING: Do not use for authentication - tokens are verified server-side
|
|
112
|
+
*/
|
|
113
|
+
function decodeTokenPayload(token) {
|
|
114
|
+
try {
|
|
115
|
+
const parts = token.split('.');
|
|
116
|
+
if (parts.length !== 3)
|
|
117
|
+
return null;
|
|
118
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
119
|
+
return {
|
|
120
|
+
sub: payload.sub,
|
|
121
|
+
email: payload.email,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get a valid access token, refreshing if necessary
|
|
130
|
+
* @returns Access token or null if not authenticated
|
|
131
|
+
*/
|
|
132
|
+
export async function getAccessToken() {
|
|
133
|
+
const creds = readCredentials();
|
|
134
|
+
if (!creds) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// Check if token needs refresh
|
|
138
|
+
if (isTokenExpiringSoon(creds.expires_at)) {
|
|
139
|
+
const apiUrl = process.env.GROV_API_URL || 'https://api.grov.dev';
|
|
140
|
+
const newCreds = await refreshTokens(creds.refresh_token, apiUrl);
|
|
141
|
+
if (newCreds) {
|
|
142
|
+
writeCredentials(newCreds);
|
|
143
|
+
return newCreds.access_token;
|
|
144
|
+
}
|
|
145
|
+
// Refresh failed - user needs to login again
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return creds.access_token;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Set the team ID for sync
|
|
152
|
+
*/
|
|
153
|
+
export function setTeamId(teamId) {
|
|
154
|
+
const creds = readCredentials();
|
|
155
|
+
if (!creds) {
|
|
156
|
+
throw new Error('Not authenticated. Please run: grov login');
|
|
157
|
+
}
|
|
158
|
+
writeCredentials({
|
|
159
|
+
...creds,
|
|
160
|
+
team_id: teamId,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Enable or disable sync
|
|
165
|
+
*/
|
|
166
|
+
export function setSyncEnabled(enabled) {
|
|
167
|
+
const creds = readCredentials();
|
|
168
|
+
if (!creds) {
|
|
169
|
+
throw new Error('Not authenticated. Please run: grov login');
|
|
170
|
+
}
|
|
171
|
+
writeCredentials({
|
|
172
|
+
...creds,
|
|
173
|
+
sync_enabled: enabled,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get current sync status
|
|
178
|
+
*/
|
|
179
|
+
export function getSyncStatus() {
|
|
180
|
+
const creds = readCredentials();
|
|
181
|
+
if (!creds) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
enabled: creds.sync_enabled,
|
|
186
|
+
teamId: creds.team_id,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get current user info
|
|
191
|
+
*/
|
|
192
|
+
export function getCurrentUser() {
|
|
193
|
+
const creds = readCredentials();
|
|
194
|
+
if (!creds) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
id: creds.user_id,
|
|
199
|
+
email: creds.email,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -58,7 +58,7 @@ export declare function isSummaryAvailable(): boolean;
|
|
|
58
58
|
* Generate session summary for CLEAR operation
|
|
59
59
|
* Reference: plan_proxy_local.md Section 2.3, 4.5
|
|
60
60
|
*/
|
|
61
|
-
export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[]): Promise<string>;
|
|
61
|
+
export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[], maxTokens?: number): Promise<string>;
|
|
62
62
|
/**
|
|
63
63
|
* Task analysis result from Haiku
|
|
64
64
|
*/
|
|
@@ -456,23 +456,30 @@ export function isSummaryAvailable() {
|
|
|
456
456
|
* Generate session summary for CLEAR operation
|
|
457
457
|
* Reference: plan_proxy_local.md Section 2.3, 4.5
|
|
458
458
|
*/
|
|
459
|
-
export async function generateSessionSummary(sessionState, steps
|
|
459
|
+
export async function generateSessionSummary(sessionState, steps, maxTokens = 800 // Default 800, CLEAR mode uses 15000
|
|
460
|
+
) {
|
|
460
461
|
const client = getAnthropicClient();
|
|
462
|
+
// For larger summaries, include more steps
|
|
463
|
+
const stepLimit = maxTokens > 5000 ? 50 : 20;
|
|
464
|
+
const wordLimit = Math.min(Math.floor(maxTokens / 2), 10000); // ~2 tokens per word
|
|
461
465
|
const stepsText = steps
|
|
462
466
|
.filter(s => s.is_validated)
|
|
463
|
-
.slice(-
|
|
467
|
+
.slice(-stepLimit)
|
|
464
468
|
.map(step => {
|
|
465
469
|
let desc = `- ${step.action_type}`;
|
|
466
470
|
if (step.files.length > 0) {
|
|
467
471
|
desc += `: ${step.files.join(', ')}`;
|
|
468
472
|
}
|
|
469
473
|
if (step.command) {
|
|
470
|
-
desc += ` (${step.command.substring(0,
|
|
474
|
+
desc += ` (${step.command.substring(0, 100)})`;
|
|
475
|
+
}
|
|
476
|
+
if (step.reasoning && maxTokens > 5000) {
|
|
477
|
+
desc += `\n Reasoning: ${step.reasoning.substring(0, 200)}`;
|
|
471
478
|
}
|
|
472
479
|
return desc;
|
|
473
480
|
})
|
|
474
481
|
.join('\n');
|
|
475
|
-
const prompt = `Create a concise summary of this coding session for context continuation.
|
|
482
|
+
const prompt = `Create a ${maxTokens > 5000 ? 'comprehensive' : 'concise'} summary of this coding session for context continuation.
|
|
476
483
|
|
|
477
484
|
ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
|
|
478
485
|
|
|
@@ -483,18 +490,19 @@ CONSTRAINTS: ${sessionState.constraints.join(', ') || 'None'}
|
|
|
483
490
|
ACTIONS TAKEN:
|
|
484
491
|
${stepsText || 'No actions recorded'}
|
|
485
492
|
|
|
486
|
-
Create a summary with these sections (keep total under
|
|
487
|
-
1. ORIGINAL GOAL: (1
|
|
488
|
-
2. PROGRESS: (2-3 bullet points of what was accomplished)
|
|
489
|
-
3. KEY DECISIONS: (
|
|
490
|
-
4. FILES MODIFIED: (list of files)
|
|
491
|
-
5. CURRENT STATE: (where the work left off)
|
|
492
|
-
6. NEXT STEPS: (recommended next actions)
|
|
493
|
+
Create a summary with these sections (keep total under ${wordLimit} words):
|
|
494
|
+
1. ORIGINAL GOAL: (1-2 sentences)
|
|
495
|
+
2. PROGRESS: (${maxTokens > 5000 ? '5-10' : '2-3'} bullet points of what was accomplished)
|
|
496
|
+
3. KEY DECISIONS: (important architectural/design choices made, with reasoning)
|
|
497
|
+
4. FILES MODIFIED: (list of files with brief description of changes)
|
|
498
|
+
5. CURRENT STATE: (detailed status of where the work left off)
|
|
499
|
+
6. NEXT STEPS: (recommended next actions to continue)
|
|
500
|
+
${maxTokens > 5000 ? '7. IMPORTANT CONTEXT: (any critical information that must not be lost)' : ''}
|
|
493
501
|
|
|
494
502
|
Format as plain text, not JSON.`;
|
|
495
503
|
const response = await client.messages.create({
|
|
496
504
|
model: 'claude-haiku-4-5-20251001',
|
|
497
|
-
max_tokens:
|
|
505
|
+
max_tokens: maxTokens,
|
|
498
506
|
messages: [{ role: 'user', content: prompt }],
|
|
499
507
|
});
|
|
500
508
|
const content = response.content?.[0];
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface Task {
|
|
|
22
22
|
turn_number?: number;
|
|
23
23
|
tags: string[];
|
|
24
24
|
created_at: string;
|
|
25
|
+
synced_at?: string | null;
|
|
26
|
+
sync_error?: string | null;
|
|
25
27
|
}
|
|
26
28
|
export interface CreateTaskInput {
|
|
27
29
|
project_path: string;
|
|
@@ -89,6 +91,11 @@ interface ProxyFields {
|
|
|
89
91
|
completed_at?: string;
|
|
90
92
|
parent_session_id?: string;
|
|
91
93
|
task_type?: TaskType;
|
|
94
|
+
pending_correction?: string;
|
|
95
|
+
pending_forced_recovery?: string;
|
|
96
|
+
pending_clear_summary?: string;
|
|
97
|
+
cached_injection?: string;
|
|
98
|
+
final_response?: string;
|
|
92
99
|
}
|
|
93
100
|
export interface SessionState extends SessionStateBase, HookFields, ProxyFields {
|
|
94
101
|
}
|
|
@@ -217,6 +224,18 @@ export declare function updateTaskStatus(id: string, status: TaskStatus): void;
|
|
|
217
224
|
* Get task count for a project
|
|
218
225
|
*/
|
|
219
226
|
export declare function getTaskCount(projectPath: string): number;
|
|
227
|
+
/**
|
|
228
|
+
* Get unsynced tasks for a project (synced_at is NULL)
|
|
229
|
+
*/
|
|
230
|
+
export declare function getUnsyncedTasks(projectPath: string, limit?: number): Task[];
|
|
231
|
+
/**
|
|
232
|
+
* Mark a task as synced and clear any previous sync error
|
|
233
|
+
*/
|
|
234
|
+
export declare function markTaskSynced(id: string): void;
|
|
235
|
+
/**
|
|
236
|
+
* Record a sync error for a task
|
|
237
|
+
*/
|
|
238
|
+
export declare function setTaskSyncError(id: string, error: string): void;
|
|
220
239
|
/**
|
|
221
240
|
* Create a new session state.
|
|
222
241
|
* FIXED: Uses INSERT OR IGNORE to handle race conditions safely.
|
|
@@ -306,6 +325,16 @@ export declare function getRecentSteps(sessionId: string, count?: number): StepR
|
|
|
306
325
|
* Get validated steps only (for summary generation)
|
|
307
326
|
*/
|
|
308
327
|
export declare function getValidatedSteps(sessionId: string): StepRecord[];
|
|
328
|
+
/**
|
|
329
|
+
* Get key decision steps for a session (is_key_decision = 1)
|
|
330
|
+
* Used for user message injection - important decisions with reasoning
|
|
331
|
+
*/
|
|
332
|
+
export declare function getKeyDecisions(sessionId: string, limit?: number): StepRecord[];
|
|
333
|
+
/**
|
|
334
|
+
* Get edited files for a session (action_type IN ('edit', 'write'))
|
|
335
|
+
* Used for user message injection - prevent re-work
|
|
336
|
+
*/
|
|
337
|
+
export declare function getEditedFiles(sessionId: string): string[];
|
|
309
338
|
/**
|
|
310
339
|
* Delete steps for a session
|
|
311
340
|
*/
|
|
@@ -346,9 +375,10 @@ export declare function getStepsByKeywords(sessionId: string, keywords: string[]
|
|
|
346
375
|
export declare function getKeyDecisionSteps(sessionId: string, limit?: number): StepRecord[];
|
|
347
376
|
/**
|
|
348
377
|
* Get steps reasoning by file path (for proxy team memory injection)
|
|
349
|
-
* Searches across
|
|
378
|
+
* Searches across sessions, returns file-level reasoning from steps table
|
|
379
|
+
* @param excludeSessionId - Optional session ID to exclude (for filtering current session)
|
|
350
380
|
*/
|
|
351
|
-
export declare function getStepsReasoningByPath(filePath: string, limit?: number): Array<{
|
|
381
|
+
export declare function getStepsReasoningByPath(filePath: string, limit?: number, excludeSessionId?: string): Array<{
|
|
352
382
|
file_path: string;
|
|
353
383
|
reasoning: string;
|
|
354
384
|
anchor?: string;
|