@augmentcode/auggie-sdk 0.1.9 → 0.1.11
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/dist/auggie/sdk-acp-client.d.ts +7 -6
- package/dist/auggie/sdk-acp-client.js +299 -332
- package/dist/auggie/sdk-mcp-server.d.ts +5 -3
- package/dist/auggie/sdk-mcp-server.js +102 -112
- package/dist/context/direct-context.d.ts +19 -15
- package/dist/context/direct-context.js +556 -567
- package/dist/context/filesystem-context.d.ts +5 -3
- package/dist/context/filesystem-context.js +187 -209
- package/dist/context/internal/__mocks__/api-client.d.ts +17 -11
- package/dist/context/internal/__mocks__/api-client.js +104 -91
- package/dist/context/internal/api-client.d.ts +15 -15
- package/dist/context/internal/api-client.js +233 -224
- package/dist/context/internal/blob-name-calculator.d.ts +5 -4
- package/dist/context/internal/blob-name-calculator.js +41 -38
- package/dist/context/internal/chat-utils.d.ts +6 -3
- package/dist/context/internal/chat-utils.js +5 -18
- package/dist/context/internal/credentials.d.ts +5 -4
- package/dist/context/internal/credentials.js +24 -38
- package/dist/context/internal/retry-utils.d.ts +5 -4
- package/dist/context/internal/retry-utils.js +60 -114
- package/dist/context/internal/search-utils.d.ts +3 -2
- package/dist/context/internal/search-utils.js +8 -9
- package/dist/context/internal/session-reader.d.ts +4 -3
- package/dist/context/internal/session-reader.js +14 -22
- package/dist/context/types.d.ts +17 -12
- package/dist/context/types.js +0 -5
- package/dist/index.d.ts +8 -7
- package/dist/index.js +14 -9
- package/dist/version.d.ts +3 -2
- package/dist/version.js +24 -38
- package/package.json +3 -2
- package/dist/auggie/sdk-acp-client.d.ts.map +0 -1
- package/dist/auggie/sdk-acp-client.js.map +0 -1
- package/dist/auggie/sdk-mcp-server.d.ts.map +0 -1
- package/dist/auggie/sdk-mcp-server.js.map +0 -1
- package/dist/context/direct-context.d.ts.map +0 -1
- package/dist/context/direct-context.js.map +0 -1
- package/dist/context/filesystem-context.d.ts.map +0 -1
- package/dist/context/filesystem-context.js.map +0 -1
- package/dist/context/internal/__mocks__/api-client.d.ts.map +0 -1
- package/dist/context/internal/__mocks__/api-client.js.map +0 -1
- package/dist/context/internal/api-client.d.ts.map +0 -1
- package/dist/context/internal/api-client.js.map +0 -1
- package/dist/context/internal/blob-name-calculator.d.ts.map +0 -1
- package/dist/context/internal/blob-name-calculator.js.map +0 -1
- package/dist/context/internal/chat-utils.d.ts.map +0 -1
- package/dist/context/internal/chat-utils.js.map +0 -1
- package/dist/context/internal/credentials.d.ts.map +0 -1
- package/dist/context/internal/credentials.js.map +0 -1
- package/dist/context/internal/retry-utils.d.ts.map +0 -1
- package/dist/context/internal/retry-utils.js.map +0 -1
- package/dist/context/internal/search-utils.d.ts.map +0 -1
- package/dist/context/internal/search-utils.js.map +0 -1
- package/dist/context/internal/session-reader.d.ts.map +0 -1
- package/dist/context/internal/session-reader.js.map +0 -1
- package/dist/context/types.d.ts.map +0 -1
- package/dist/context/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -1,594 +1,583 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { Mutex } from "async-mutex";
|
|
3
|
-
import { ContextAPIClient } from "./internal/api-client";
|
|
4
|
-
import { BlobNameCalculator } from "./internal/blob-name-calculator";
|
|
5
|
-
import { chatWithRetry } from "./internal/chat-utils";
|
|
6
|
-
import { resolveCredentials } from "./internal/credentials";
|
|
7
|
-
import { formatSearchPrompt } from "./internal/search-utils";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
3
|
+
import { ContextAPIClient } from "./internal/api-client.js";
|
|
4
|
+
import { BlobNameCalculator } from "./internal/blob-name-calculator.js";
|
|
5
|
+
import { chatWithRetry } from "./internal/chat-utils.js";
|
|
6
|
+
import { resolveCredentials } from "./internal/credentials.js";
|
|
7
|
+
import { formatSearchPrompt } from "./internal/search-utils.js";
|
|
8
|
+
class DirectContext {
|
|
9
|
+
static MAX_FILE_SIZE_BYTES = 1048576;
|
|
10
|
+
// 1MB
|
|
11
|
+
static MAX_BATCH_UPLOAD_SIZE = 1e3;
|
|
12
|
+
static MAX_BATCH_CONTENT_BYTES = 2 * 1024 * 1024;
|
|
13
|
+
// 2MB
|
|
14
|
+
static MAX_FIND_MISSING_SIZE = 1e3;
|
|
15
|
+
static CHECKPOINT_THRESHOLD = 1e3;
|
|
16
|
+
apiClient;
|
|
17
|
+
blobCalculator;
|
|
18
|
+
debug;
|
|
19
|
+
/**
|
|
20
|
+
* State management:
|
|
21
|
+
*
|
|
22
|
+
* blobMap: Tracks all files that have been uploaded (path -> serverBlobId).
|
|
23
|
+
* This includes both indexed and non-indexed blobs.
|
|
24
|
+
* Also, includes both checkpointed and non-checkpointed blobs.
|
|
25
|
+
* The server blob ID is the authoritative identifier.
|
|
26
|
+
*
|
|
27
|
+
* clientBlobMap: Tracks client-computed blob IDs (path -> clientBlobId).
|
|
28
|
+
* Used for detecting unchanged files on the client side.
|
|
29
|
+
* May differ from serverBlobId due to encoding differences.
|
|
30
|
+
*
|
|
31
|
+
* checkpointId: The current checkpoint ID from the backend.
|
|
32
|
+
* Checkpoints are named snapshots of the blob set for performance.
|
|
33
|
+
* Updated after each successful checkpoint operation.
|
|
34
|
+
*
|
|
35
|
+
* pendingAdded: Blob IDs that have been uploaded but not yet checkpointed.
|
|
36
|
+
* Accumulated until CHECKPOINT_THRESHOLD is reached.
|
|
37
|
+
*
|
|
38
|
+
* pendingDeleted: Blob IDs that have been removed but not yet checkpointed.
|
|
39
|
+
* Accumulated until CHECKPOINT_THRESHOLD is reached.
|
|
40
|
+
*
|
|
41
|
+
* Note: Checkpointing and indexing are independent operations.
|
|
42
|
+
* A blob can be checkpointed but not indexed, indexed but not checkpointed,
|
|
43
|
+
* both, or neither.
|
|
44
|
+
*/
|
|
45
|
+
blobMap = /* @__PURE__ */ new Map();
|
|
46
|
+
clientBlobMap = /* @__PURE__ */ new Map();
|
|
47
|
+
checkpointId;
|
|
48
|
+
pendingAdded = /* @__PURE__ */ new Set();
|
|
49
|
+
pendingDeleted = /* @__PURE__ */ new Set();
|
|
50
|
+
// Mutex for serializing state-modifying operations (upload, checkpoint, etc.)
|
|
51
|
+
mutex = new Mutex();
|
|
52
|
+
// Mutex for serializing polling operations to prevent unbounded concurrent requests
|
|
53
|
+
// to the backend's findMissing endpoint. Without this, concurrent addToIndex() calls
|
|
54
|
+
// could result in many simultaneous polling loops hitting the backend.
|
|
55
|
+
pollingMutex = new Mutex();
|
|
56
|
+
/**
|
|
57
|
+
* Create and initialize a new DirectContext instance
|
|
58
|
+
*
|
|
59
|
+
* Authentication priority:
|
|
60
|
+
* 1. options.apiKey / options.apiUrl
|
|
61
|
+
* 2. AUGMENT_API_TOKEN / AUGMENT_API_URL environment variables
|
|
62
|
+
* 3. ~/.augment/session.json (created by `auggie login`)
|
|
63
|
+
*
|
|
64
|
+
* @param options Configuration options
|
|
65
|
+
* @returns Promise that resolves to a DirectContext instance
|
|
66
|
+
*/
|
|
67
|
+
static async create(options = {}) {
|
|
68
|
+
const { apiKey, apiUrl } = await resolveCredentials(options);
|
|
69
|
+
return new DirectContext(apiKey, apiUrl, options.debug ?? false);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Import a DirectContext instance from a saved state object
|
|
73
|
+
*
|
|
74
|
+
* @param state The state object to restore from
|
|
75
|
+
* @param options Configuration options
|
|
76
|
+
* @returns Promise that resolves to a DirectContext instance with restored state
|
|
77
|
+
*/
|
|
78
|
+
static async import(state, options = {}) {
|
|
79
|
+
const { apiKey, apiUrl } = await resolveCredentials(options);
|
|
80
|
+
const instance = new DirectContext(apiKey, apiUrl, options.debug ?? false);
|
|
81
|
+
await instance.doImport(state);
|
|
82
|
+
return instance;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Import a DirectContext instance from a saved state file (Node.js only)
|
|
86
|
+
*
|
|
87
|
+
* @param filePath Path to the state file
|
|
88
|
+
* @param options Configuration options
|
|
89
|
+
* @returns Promise that resolves to a DirectContext instance with restored state
|
|
90
|
+
*/
|
|
91
|
+
static async importFromFile(filePath, options = {}) {
|
|
92
|
+
const content = await readFile(filePath, "utf-8");
|
|
93
|
+
const state = JSON.parse(content);
|
|
94
|
+
return DirectContext.import(state, options);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Private constructor - use DirectContext.create() instead
|
|
98
|
+
* @param apiKey API key for authentication
|
|
99
|
+
* @param apiUrl API URL for the tenant
|
|
100
|
+
* @param debug Enable debug logging
|
|
101
|
+
*/
|
|
102
|
+
constructor(apiKey, apiUrl, debug) {
|
|
103
|
+
this.debug = debug;
|
|
104
|
+
this.apiClient = new ContextAPIClient({
|
|
105
|
+
apiKey,
|
|
106
|
+
apiUrl,
|
|
107
|
+
debug
|
|
108
|
+
});
|
|
109
|
+
this.blobCalculator = new BlobNameCalculator(
|
|
110
|
+
DirectContext.MAX_FILE_SIZE_BYTES
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Log a debug message if debug mode is enabled
|
|
115
|
+
*/
|
|
116
|
+
log(message) {
|
|
117
|
+
if (this.debug) {
|
|
118
|
+
console.log(`[DirectContext] ${message}`);
|
|
82
119
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return instance;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create a checkpoint if threshold is reached
|
|
123
|
+
*/
|
|
124
|
+
async maybeCheckpoint() {
|
|
125
|
+
const pendingChanges = this.pendingAdded.size + this.pendingDeleted.size;
|
|
126
|
+
if (pendingChanges < DirectContext.CHECKPOINT_THRESHOLD) {
|
|
127
|
+
this.log(
|
|
128
|
+
`Skipping checkpoint: ${pendingChanges} pending changes (threshold: ${DirectContext.CHECKPOINT_THRESHOLD})`
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
95
131
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
const addedBlobs = Array.from(this.pendingAdded);
|
|
133
|
+
const deletedBlobs = Array.from(this.pendingDeleted);
|
|
134
|
+
this.log(
|
|
135
|
+
`Creating checkpoint: ${addedBlobs.length} added, ${deletedBlobs.length} deleted blobs`
|
|
136
|
+
);
|
|
137
|
+
const blobs = {
|
|
138
|
+
checkpointId: this.checkpointId,
|
|
139
|
+
addedBlobs: addedBlobs.sort(),
|
|
140
|
+
deletedBlobs: deletedBlobs.sort()
|
|
141
|
+
};
|
|
142
|
+
const result = await this.apiClient.checkpointBlobs(blobs);
|
|
143
|
+
this.checkpointId = result.newCheckpointId;
|
|
144
|
+
this.log(`Checkpoint created: ${this.checkpointId}`);
|
|
145
|
+
this.pendingAdded.clear();
|
|
146
|
+
this.pendingDeleted.clear();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get the list of indexed file paths
|
|
150
|
+
*/
|
|
151
|
+
getIndexedPaths() {
|
|
152
|
+
return Array.from(this.blobMap.keys());
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Add files to the index by uploading them to the backend.
|
|
156
|
+
*
|
|
157
|
+
* By default, this method waits for the uploaded files to be fully indexed
|
|
158
|
+
* on the backend before returning. Set `waitForIndexing: false` to return
|
|
159
|
+
* immediately after upload completes (indexing will continue asynchronously).
|
|
160
|
+
*
|
|
161
|
+
* @param files - Array of files to add to the index
|
|
162
|
+
* @param options - Optional configuration
|
|
163
|
+
* @param options.waitForIndexing - If true (default), waits for the newly added files to be indexed before returning
|
|
164
|
+
* @returns Result indicating which files were newly uploaded vs already uploaded
|
|
165
|
+
*/
|
|
166
|
+
async addToIndex(files, options) {
|
|
167
|
+
const waitForIndexing = options?.waitForIndexing ?? true;
|
|
168
|
+
const result = await this.mutex.runExclusive(
|
|
169
|
+
() => this.doAddToIndex(files)
|
|
170
|
+
);
|
|
171
|
+
if (waitForIndexing && result.newlyUploaded.length > 0) {
|
|
172
|
+
const newlyUploadedBlobNames = result.newlyUploaded.map((path) => this.blobMap.get(path)).filter((blobName) => blobName !== void 0);
|
|
173
|
+
await this.waitForSpecificBlobs(newlyUploadedBlobNames);
|
|
107
174
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*
|
|
126
|
-
* pendingAdded: Blob names that have been uploaded but not yet checkpointed.
|
|
127
|
-
* Accumulated until CHECKPOINT_THRESHOLD is reached.
|
|
128
|
-
*
|
|
129
|
-
* pendingDeleted: Blob names that have been removed but not yet checkpointed.
|
|
130
|
-
* Accumulated until CHECKPOINT_THRESHOLD is reached.
|
|
131
|
-
*
|
|
132
|
-
* Note: Checkpointing and indexing are independent operations.
|
|
133
|
-
* A blob can be checkpointed but not indexed, indexed but not checkpointed,
|
|
134
|
-
* both, or neither.
|
|
135
|
-
*/
|
|
136
|
-
this.blobMap = new Map();
|
|
137
|
-
this.pendingAdded = new Set();
|
|
138
|
-
this.pendingDeleted = new Set();
|
|
139
|
-
// Mutex for serializing state-modifying operations (upload, checkpoint, etc.)
|
|
140
|
-
this.mutex = new Mutex();
|
|
141
|
-
// Mutex for serializing polling operations to prevent unbounded concurrent requests
|
|
142
|
-
// to the backend's findMissing endpoint. Without this, concurrent addToIndex() calls
|
|
143
|
-
// could result in many simultaneous polling loops hitting the backend.
|
|
144
|
-
this.pollingMutex = new Mutex();
|
|
145
|
-
this.debug = debug;
|
|
146
|
-
// Initialize API client
|
|
147
|
-
this.apiClient = new ContextAPIClient({
|
|
148
|
-
apiKey,
|
|
149
|
-
apiUrl,
|
|
150
|
-
debug,
|
|
151
|
-
});
|
|
152
|
-
// Initialize blob calculator
|
|
153
|
-
this.blobCalculator = new BlobNameCalculator(DirectContext.MAX_FILE_SIZE_BYTES);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Log a debug message if debug mode is enabled
|
|
157
|
-
*/
|
|
158
|
-
log(message) {
|
|
159
|
-
if (this.debug) {
|
|
160
|
-
console.log(`[DirectContext] ${message}`);
|
|
161
|
-
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Find blobs that are missing or not yet indexed on the backend (detailed result)
|
|
179
|
+
* Batches requests in chunks of 1000
|
|
180
|
+
*
|
|
181
|
+
* @param blobNames - Array of blob names to check
|
|
182
|
+
* @returns Object with unknownBlobNames and nonindexedBlobNames arrays
|
|
183
|
+
*/
|
|
184
|
+
async findMissingBlobsDetailed(blobNames) {
|
|
185
|
+
const allUnknown = [];
|
|
186
|
+
const allNonIndexed = [];
|
|
187
|
+
for (let i = 0; i < blobNames.length; i += DirectContext.MAX_FIND_MISSING_SIZE) {
|
|
188
|
+
const batch = blobNames.slice(i, i + DirectContext.MAX_FIND_MISSING_SIZE);
|
|
189
|
+
const result = await this.apiClient.findMissing(batch);
|
|
190
|
+
allUnknown.push(...result.unknownBlobNames);
|
|
191
|
+
allNonIndexed.push(...result.nonindexedBlobNames);
|
|
162
192
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
193
|
+
return {
|
|
194
|
+
unknownBlobNames: allUnknown,
|
|
195
|
+
nonindexedBlobNames: allNonIndexed
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Upload files in batches respecting backend limits
|
|
200
|
+
* Returns a map of client-computed blob IDs to backend-returned blob IDs
|
|
201
|
+
*/
|
|
202
|
+
async batchUploadFiles(filesToUpload) {
|
|
203
|
+
const batches = [];
|
|
204
|
+
let currentBatch = [];
|
|
205
|
+
let currentBatchSize = 0;
|
|
206
|
+
for (const file of filesToUpload) {
|
|
207
|
+
const fileSize = Buffer.byteLength(file.text, "utf-8");
|
|
208
|
+
const wouldExceedCount = currentBatch.length >= DirectContext.MAX_BATCH_UPLOAD_SIZE;
|
|
209
|
+
const wouldExceedSize = currentBatchSize + fileSize > DirectContext.MAX_BATCH_CONTENT_BYTES;
|
|
210
|
+
if (wouldExceedCount || wouldExceedSize) {
|
|
211
|
+
if (currentBatch.length > 0) {
|
|
212
|
+
batches.push(currentBatch);
|
|
171
213
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
addedBlobs: addedBlobs.sort(),
|
|
179
|
-
deletedBlobs: deletedBlobs.sort(),
|
|
180
|
-
};
|
|
181
|
-
const result = await this.apiClient.checkpointBlobs(blobs);
|
|
182
|
-
this.checkpointId = result.newCheckpointId;
|
|
183
|
-
this.log(`Checkpoint created: ${this.checkpointId}`);
|
|
184
|
-
// Clear pending changes after successful checkpoint
|
|
185
|
-
this.pendingAdded.clear();
|
|
186
|
-
this.pendingDeleted.clear();
|
|
214
|
+
currentBatch = [file];
|
|
215
|
+
currentBatchSize = fileSize;
|
|
216
|
+
} else {
|
|
217
|
+
currentBatch.push(file);
|
|
218
|
+
currentBatchSize += fileSize;
|
|
219
|
+
}
|
|
187
220
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*/
|
|
191
|
-
getIndexedPaths() {
|
|
192
|
-
return Array.from(this.blobMap.keys());
|
|
221
|
+
if (currentBatch.length > 0) {
|
|
222
|
+
batches.push(currentBatch);
|
|
193
223
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
* @param options - Optional configuration
|
|
203
|
-
* @param options.waitForIndexing - If true (default), waits for the newly added files to be indexed before returning
|
|
204
|
-
* @returns Result indicating which files were newly uploaded vs already uploaded
|
|
205
|
-
*/
|
|
206
|
-
async addToIndex(files, options) {
|
|
207
|
-
const waitForIndexing = options?.waitForIndexing ?? true;
|
|
208
|
-
const result = await this.mutex.runExclusive(() => this.doAddToIndex(files));
|
|
209
|
-
if (waitForIndexing && result.newlyUploaded.length > 0) {
|
|
210
|
-
// Wait for the newly uploaded files to be indexed
|
|
211
|
-
// These paths are guaranteed to be in blobMap since they were just added in doAddToIndex
|
|
212
|
-
const newlyUploadedBlobNames = result.newlyUploaded
|
|
213
|
-
.map((path) => this.blobMap.get(path))
|
|
214
|
-
.filter((blobName) => blobName !== undefined);
|
|
215
|
-
await this.waitForSpecificBlobs(newlyUploadedBlobNames);
|
|
224
|
+
const blobIdMap = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const batch of batches) {
|
|
226
|
+
const result = await this.apiClient.batchUpload(batch);
|
|
227
|
+
for (const [i, batchItem] of batch.entries()) {
|
|
228
|
+
const clientBlobId = batchItem.blobName;
|
|
229
|
+
const backendBlobId = result.blobNames[i];
|
|
230
|
+
if (backendBlobId) {
|
|
231
|
+
blobIdMap.set(clientBlobId, backendBlobId);
|
|
216
232
|
}
|
|
217
|
-
|
|
233
|
+
}
|
|
218
234
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
allMissing.push(...result.unknownBlobNames);
|
|
233
|
-
if (includeNonIndexed) {
|
|
234
|
-
allMissing.push(...result.nonindexedBlobNames);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return allMissing;
|
|
235
|
+
return blobIdMap;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Validate that all files are within size limits
|
|
239
|
+
*/
|
|
240
|
+
validateFileSizes(files) {
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
const sizeBytes = Buffer.byteLength(file.contents, "utf-8");
|
|
243
|
+
if (sizeBytes > DirectContext.MAX_FILE_SIZE_BYTES) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`File ${file.path} is too large (${sizeBytes} bytes). Maximum size is ${DirectContext.MAX_FILE_SIZE_BYTES} bytes (1MB).`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
238
248
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
currentBatchSize = fileSize;
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
currentBatch.push(file);
|
|
259
|
-
currentBatchSize += fileSize;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (currentBatch.length > 0) {
|
|
263
|
-
batches.push(currentBatch);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Calculate blob names and prepare upload data for files
|
|
252
|
+
* Returns files that need uploading, new blob entries, and already uploaded files
|
|
253
|
+
*/
|
|
254
|
+
prepareBlobsForUpload(files) {
|
|
255
|
+
const filesToUpload = [];
|
|
256
|
+
const newBlobEntries = [];
|
|
257
|
+
const alreadyUploaded = [];
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
const clientBlobId = this.blobCalculator.calculate(file.path, file.contents);
|
|
260
|
+
if (clientBlobId) {
|
|
261
|
+
const existingClientBlobId = this.clientBlobMap.get(file.path);
|
|
262
|
+
if (existingClientBlobId && existingClientBlobId === clientBlobId) {
|
|
263
|
+
alreadyUploaded.push(file.path);
|
|
264
|
+
continue;
|
|
264
265
|
}
|
|
265
|
-
|
|
266
|
-
|
|
266
|
+
const existingServerBlobId = this.blobMap.get(file.path);
|
|
267
|
+
if (existingServerBlobId) {
|
|
268
|
+
this.handleBlobReplacement(existingServerBlobId);
|
|
267
269
|
}
|
|
270
|
+
newBlobEntries.push([file.path, clientBlobId]);
|
|
271
|
+
filesToUpload.push({
|
|
272
|
+
blobName: clientBlobId,
|
|
273
|
+
// API expects 'blobName' field
|
|
274
|
+
pathName: file.path,
|
|
275
|
+
text: file.contents,
|
|
276
|
+
metadata: []
|
|
277
|
+
});
|
|
278
|
+
}
|
|
268
279
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
return { filesToUpload, newBlobEntries, alreadyUploaded };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Handle replacing an existing blob (track deletion of old blob)
|
|
284
|
+
*/
|
|
285
|
+
handleBlobReplacement(oldBlobName) {
|
|
286
|
+
if (this.pendingAdded.has(oldBlobName)) {
|
|
287
|
+
this.pendingAdded.delete(oldBlobName);
|
|
288
|
+
} else {
|
|
289
|
+
this.pendingDeleted.add(oldBlobName);
|
|
279
290
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (blobName) {
|
|
291
|
-
const existingBlobName = this.blobMap.get(file.path);
|
|
292
|
-
if (existingBlobName && existingBlobName === blobName) {
|
|
293
|
-
// File content hasn't changed
|
|
294
|
-
alreadyUploaded.push(file.path);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
// Handle file replacement: if a file with this path already exists
|
|
298
|
-
if (existingBlobName) {
|
|
299
|
-
this.handleBlobReplacement(existingBlobName);
|
|
300
|
-
}
|
|
301
|
-
newBlobEntries.push([file.path, blobName]);
|
|
302
|
-
filesToUpload.push({
|
|
303
|
-
blobName,
|
|
304
|
-
pathName: file.path,
|
|
305
|
-
text: file.contents,
|
|
306
|
-
metadata: [],
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return { filesToUpload, newBlobEntries, alreadyUploaded };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Internal implementation of addToIndex
|
|
294
|
+
*/
|
|
295
|
+
async doAddToIndex(files) {
|
|
296
|
+
this.log(`Adding ${files.length} files to index`);
|
|
297
|
+
this.validateFileSizes(files);
|
|
298
|
+
const { filesToUpload, newBlobEntries, alreadyUploaded } = this.prepareBlobsForUpload(files);
|
|
299
|
+
if (newBlobEntries.length === 0) {
|
|
300
|
+
return { newlyUploaded: [], alreadyUploaded };
|
|
311
301
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
302
|
+
const blobNamesToCheck = newBlobEntries.map(([_, clientBlobId]) => clientBlobId);
|
|
303
|
+
this.log(`Checking ${blobNamesToCheck.length} blobs with server`);
|
|
304
|
+
const result = await this.findMissingBlobsDetailed(blobNamesToCheck);
|
|
305
|
+
const missingBlobSet = /* @__PURE__ */ new Set([
|
|
306
|
+
...result.unknownBlobNames,
|
|
307
|
+
...result.nonindexedBlobNames
|
|
308
|
+
]);
|
|
309
|
+
this.log(`Server is missing ${missingBlobSet.size} blobs`);
|
|
310
|
+
const filesToActuallyUpload = filesToUpload.filter(
|
|
311
|
+
(file) => missingBlobSet.has(file.blobName)
|
|
312
|
+
);
|
|
313
|
+
const blobIdMap = /* @__PURE__ */ new Map();
|
|
314
|
+
if (filesToActuallyUpload.length > 0) {
|
|
315
|
+
this.log(`Uploading ${filesToActuallyUpload.length} files to backend`);
|
|
316
|
+
const uploadedBlobIdMap = await this.batchUploadFiles(filesToActuallyUpload);
|
|
317
|
+
this.log("Upload complete");
|
|
318
|
+
for (const [clientId, serverId] of uploadedBlobIdMap) {
|
|
319
|
+
blobIdMap.set(clientId, serverId);
|
|
320
|
+
}
|
|
325
321
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return { newlyUploaded, alreadyOnServer };
|
|
322
|
+
const newlyUploaded = [];
|
|
323
|
+
const alreadyOnServer = [];
|
|
324
|
+
for (const [path, clientBlobId] of newBlobEntries) {
|
|
325
|
+
const serverBlobId = blobIdMap.get(clientBlobId) ?? clientBlobId;
|
|
326
|
+
this.pendingAdded.add(serverBlobId);
|
|
327
|
+
this.pendingDeleted.delete(serverBlobId);
|
|
328
|
+
this.blobMap.set(path, serverBlobId);
|
|
329
|
+
this.clientBlobMap.set(path, clientBlobId);
|
|
330
|
+
if (missingBlobSet.has(clientBlobId)) {
|
|
331
|
+
newlyUploaded.push(path);
|
|
332
|
+
} else {
|
|
333
|
+
alreadyOnServer.push(path);
|
|
334
|
+
}
|
|
342
335
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
this.log(`Uploading ${filesToActuallyUpload.length} files to backend`);
|
|
367
|
-
await this.batchUploadFiles(filesToActuallyUpload);
|
|
368
|
-
this.log("Upload complete");
|
|
336
|
+
await this.maybeCheckpoint();
|
|
337
|
+
return {
|
|
338
|
+
newlyUploaded,
|
|
339
|
+
alreadyUploaded: [...alreadyUploaded, ...alreadyOnServer]
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Remove paths from the index
|
|
344
|
+
*/
|
|
345
|
+
async removeFromIndex(paths) {
|
|
346
|
+
return await this.mutex.runExclusive(() => this.doRemoveFromIndex(paths));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Internal implementation of removeFromIndex
|
|
350
|
+
*/
|
|
351
|
+
async doRemoveFromIndex(paths) {
|
|
352
|
+
for (const path of paths) {
|
|
353
|
+
const serverBlobId = this.blobMap.get(path);
|
|
354
|
+
if (serverBlobId) {
|
|
355
|
+
if (this.pendingAdded.has(serverBlobId)) {
|
|
356
|
+
this.pendingAdded.delete(serverBlobId);
|
|
357
|
+
} else {
|
|
358
|
+
this.pendingDeleted.add(serverBlobId);
|
|
369
359
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.pendingDeleted.delete(blobName);
|
|
374
|
-
this.blobMap.set(path, blobName);
|
|
375
|
-
}
|
|
376
|
-
await this.maybeCheckpoint();
|
|
377
|
-
return {
|
|
378
|
-
newlyUploaded,
|
|
379
|
-
alreadyUploaded: [...alreadyUploaded, ...alreadyOnServer],
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Remove paths from the index
|
|
384
|
-
*/
|
|
385
|
-
async removeFromIndex(paths) {
|
|
386
|
-
return await this.mutex.runExclusive(() => this.doRemoveFromIndex(paths));
|
|
360
|
+
this.blobMap.delete(path);
|
|
361
|
+
this.clientBlobMap.delete(path);
|
|
362
|
+
}
|
|
387
363
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
364
|
+
await this.maybeCheckpoint();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Clear the entire index
|
|
368
|
+
*/
|
|
369
|
+
async clearIndex() {
|
|
370
|
+
return await this.mutex.runExclusive(() => this.doClearIndex());
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Internal implementation of clearIndex
|
|
374
|
+
*/
|
|
375
|
+
doClearIndex() {
|
|
376
|
+
this.log(`Clearing index (${this.blobMap.size} files)`);
|
|
377
|
+
this.checkpointId = void 0;
|
|
378
|
+
this.blobMap.clear();
|
|
379
|
+
this.clientBlobMap.clear();
|
|
380
|
+
this.pendingAdded.clear();
|
|
381
|
+
this.pendingDeleted.clear();
|
|
382
|
+
this.log("Index cleared");
|
|
383
|
+
return Promise.resolve();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Wait for specific blobs to be indexed on the backend
|
|
387
|
+
*
|
|
388
|
+
* This method is serialized via pollingMutex to prevent unbounded concurrent
|
|
389
|
+
* polling requests to the backend.
|
|
390
|
+
*/
|
|
391
|
+
waitForSpecificBlobs(blobNames) {
|
|
392
|
+
return this.pollingMutex.runExclusive(async () => {
|
|
393
|
+
if (blobNames.length === 0) {
|
|
394
|
+
this.log("No blobs to wait for");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.log(
|
|
398
|
+
`Waiting for ${blobNames.length} blobs to be indexed on backend`
|
|
399
|
+
);
|
|
400
|
+
const initialPollIntervalMs = 3e3;
|
|
401
|
+
const backoffThresholdMs = 6e4;
|
|
402
|
+
const backoffPollIntervalMs = 6e4;
|
|
403
|
+
const maxWaitTimeMs = 6e5;
|
|
404
|
+
const startTime = Date.now();
|
|
405
|
+
while (true) {
|
|
406
|
+
const result = await this.findMissingBlobsDetailed(blobNames);
|
|
407
|
+
const unknownBlobs = result.unknownBlobNames.filter(
|
|
408
|
+
(id) => this.pendingAdded.has(id)
|
|
409
|
+
);
|
|
410
|
+
if (unknownBlobs.length > 0) {
|
|
411
|
+
this.log(
|
|
412
|
+
`WARNING: Backend doesn't recognize ${unknownBlobs.length} blob IDs. This may indicate a blob ID mismatch.`
|
|
413
|
+
);
|
|
414
|
+
this.log(`Unknown blob IDs: ${unknownBlobs.join(", ")}`);
|
|
415
|
+
this.log(
|
|
416
|
+
"This is unexpected but not necessarily an error. Continuing to poll..."
|
|
417
|
+
);
|
|
403
418
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Internal implementation of clearIndex
|
|
414
|
-
*/
|
|
415
|
-
doClearIndex() {
|
|
416
|
-
this.log(`Clearing index (${this.blobMap.size} files)`);
|
|
417
|
-
this.checkpointId = undefined;
|
|
418
|
-
this.blobMap.clear();
|
|
419
|
-
this.pendingAdded.clear();
|
|
420
|
-
this.pendingDeleted.clear();
|
|
421
|
-
this.log("Index cleared");
|
|
422
|
-
return Promise.resolve();
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Wait for specific blobs to be indexed on the backend
|
|
426
|
-
*
|
|
427
|
-
* This method is serialized via pollingMutex to prevent unbounded concurrent
|
|
428
|
-
* polling requests to the backend.
|
|
429
|
-
*/
|
|
430
|
-
waitForSpecificBlobs(blobNames) {
|
|
431
|
-
return this.pollingMutex.runExclusive(async () => {
|
|
432
|
-
if (blobNames.length === 0) {
|
|
433
|
-
this.log("No blobs to wait for");
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
this.log(`Waiting for ${blobNames.length} blobs to be indexed on backend`);
|
|
437
|
-
const initialPollIntervalMs = 3000;
|
|
438
|
-
const backoffThresholdMs = 60000;
|
|
439
|
-
const backoffPollIntervalMs = 60000;
|
|
440
|
-
const maxWaitTimeMs = 600000;
|
|
441
|
-
const startTime = Date.now();
|
|
442
|
-
while (true) {
|
|
443
|
-
// Check for blobs that are not yet indexed (either not uploaded or uploaded but not indexed)
|
|
444
|
-
const stillPending = await this.findMissingBlobs(blobNames, true);
|
|
445
|
-
if (stillPending.length === 0) {
|
|
446
|
-
this.log("All blobs indexed successfully");
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
const elapsedMs = Date.now() - startTime;
|
|
450
|
-
this.log(`Still waiting for ${stillPending.length} blobs to be indexed (elapsed: ${Math.round(elapsedMs / 1000)}s)`);
|
|
451
|
-
if (elapsedMs >= maxWaitTimeMs) {
|
|
452
|
-
throw new Error(`Indexing timeout: Backend did not finish indexing within ${maxWaitTimeMs / 1000} seconds`);
|
|
453
|
-
}
|
|
454
|
-
const pollIntervalMs = elapsedMs < backoffThresholdMs
|
|
455
|
-
? initialPollIntervalMs
|
|
456
|
-
: backoffPollIntervalMs;
|
|
457
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Wait for all indexed files to be fully indexed on the backend.
|
|
463
|
-
*
|
|
464
|
-
* This method polls the backend until all files that have been added to the index
|
|
465
|
-
* are confirmed to be indexed and searchable.
|
|
466
|
-
*
|
|
467
|
-
* @returns Promise that resolves when all files are indexed
|
|
468
|
-
* @throws Error if indexing times out (default: 10 minutes)
|
|
469
|
-
*/
|
|
470
|
-
async waitForIndexing() {
|
|
471
|
-
const blobNames = Array.from(this.blobMap.values());
|
|
472
|
-
await this.waitForSpecificBlobs(blobNames);
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Search the codebase using natural language and return formatted results.
|
|
476
|
-
*
|
|
477
|
-
* The results are returned as a formatted string designed for use in LLM prompts.
|
|
478
|
-
* The format includes file paths, line numbers, and code content in a structured,
|
|
479
|
-
* readable format that can be passed directly to LLM APIs like `generate()`.
|
|
480
|
-
*
|
|
481
|
-
* Note: This method does not wait for indexing. Ensure files are indexed before
|
|
482
|
-
* searching by either:
|
|
483
|
-
* - Using `addToIndex()` with `waitForIndexing: true` (default)
|
|
484
|
-
* - Calling `waitForIndexing()` explicitly before searching
|
|
485
|
-
*
|
|
486
|
-
* @param query - The search query describing what code you're looking for
|
|
487
|
-
* @param options - Optional search options
|
|
488
|
-
* @param options.maxOutputLength - Maximum character length of the formatted output (default: 20000, max: 80000)
|
|
489
|
-
* @returns A formatted string containing the search results, ready for LLM consumption
|
|
490
|
-
*/
|
|
491
|
-
async search(query, options) {
|
|
492
|
-
this.log(`Searching for: "${query}"`);
|
|
493
|
-
if (this.blobMap.size === 0) {
|
|
494
|
-
throw new Error("Index not initialized. Add files to index first using addToIndex().");
|
|
419
|
+
const stillPending = [
|
|
420
|
+
...result.unknownBlobNames,
|
|
421
|
+
...result.nonindexedBlobNames
|
|
422
|
+
];
|
|
423
|
+
if (stillPending.length === 0) {
|
|
424
|
+
this.log("All blobs indexed successfully");
|
|
425
|
+
return;
|
|
495
426
|
}
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
return result.formattedRetrieval;
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Search the indexed codebase and ask an LLM a question about the results.
|
|
508
|
-
*
|
|
509
|
-
* This is a convenience method that combines search() with an LLM call to answer
|
|
510
|
-
* questions about your codebase.
|
|
511
|
-
*
|
|
512
|
-
* Note: This method does not wait for indexing. Ensure files are indexed before
|
|
513
|
-
* searching by either:
|
|
514
|
-
* - Using `addToIndex()` with `waitForIndexing: true` (default)
|
|
515
|
-
* - Calling `waitForIndexing()` explicitly before searching
|
|
516
|
-
*
|
|
517
|
-
* @param searchQuery - The semantic search query to find relevant code (also used as the prompt if no separate prompt is provided)
|
|
518
|
-
* @param prompt - Optional prompt to ask the LLM about the search results. If not provided, searchQuery is used as the prompt.
|
|
519
|
-
* @returns The LLM's answer to your question
|
|
520
|
-
*
|
|
521
|
-
* @example
|
|
522
|
-
* ```typescript
|
|
523
|
-
* const answer = await context.searchAndAsk(
|
|
524
|
-
* "How does the authentication flow work?"
|
|
525
|
-
* );
|
|
526
|
-
* console.log(answer);
|
|
527
|
-
* ```
|
|
528
|
-
*/
|
|
529
|
-
async searchAndAsk(searchQuery, prompt) {
|
|
530
|
-
const results = await this.search(searchQuery);
|
|
531
|
-
const llmPrompt = formatSearchPrompt(prompt ?? searchQuery, results);
|
|
532
|
-
return await chatWithRetry(this.apiClient, llmPrompt, this.debug);
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Export the current state to a JSON object
|
|
536
|
-
*/
|
|
537
|
-
export() {
|
|
538
|
-
// Convert blobMap to array of [blobName, path] tuples
|
|
539
|
-
const blobs = [];
|
|
540
|
-
for (const [path, blobName] of this.blobMap.entries()) {
|
|
541
|
-
blobs.push([blobName, path]);
|
|
427
|
+
const elapsedMs = Date.now() - startTime;
|
|
428
|
+
this.log(
|
|
429
|
+
`Still waiting for ${stillPending.length} blobs to be indexed (elapsed: ${Math.round(elapsedMs / 1e3)}s)`
|
|
430
|
+
);
|
|
431
|
+
if (elapsedMs >= maxWaitTimeMs) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Indexing timeout: Backend did not finish indexing within ${maxWaitTimeMs / 1e3} seconds`
|
|
434
|
+
);
|
|
542
435
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
436
|
+
const pollIntervalMs = elapsedMs < backoffThresholdMs ? initialPollIntervalMs : backoffPollIntervalMs;
|
|
437
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Wait for all indexed files to be fully indexed on the backend.
|
|
443
|
+
*
|
|
444
|
+
* This method polls the backend until all files that have been added to the index
|
|
445
|
+
* are confirmed to be indexed and searchable.
|
|
446
|
+
*
|
|
447
|
+
* @returns Promise that resolves when all files are indexed
|
|
448
|
+
* @throws Error if indexing times out (default: 10 minutes)
|
|
449
|
+
*/
|
|
450
|
+
async waitForIndexing() {
|
|
451
|
+
const blobNames = Array.from(this.blobMap.values());
|
|
452
|
+
await this.waitForSpecificBlobs(blobNames);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Search the codebase using natural language and return formatted results.
|
|
456
|
+
*
|
|
457
|
+
* The results are returned as a formatted string designed for use in LLM prompts.
|
|
458
|
+
* The format includes file paths, line numbers, and code content in a structured,
|
|
459
|
+
* readable format that can be passed directly to LLM APIs like `generate()`.
|
|
460
|
+
*
|
|
461
|
+
* Note: This method does not wait for indexing. Ensure files are indexed before
|
|
462
|
+
* searching by either:
|
|
463
|
+
* - Using `addToIndex()` with `waitForIndexing: true` (default)
|
|
464
|
+
* - Calling `waitForIndexing()` explicitly before searching
|
|
465
|
+
*
|
|
466
|
+
* @param query - The search query describing what code you're looking for
|
|
467
|
+
* @param options - Optional search options
|
|
468
|
+
* @param options.maxOutputLength - Maximum character length of the formatted output (default: 20000, max: 80000)
|
|
469
|
+
* @returns A formatted string containing the search results, ready for LLM consumption
|
|
470
|
+
*/
|
|
471
|
+
async search(query, options) {
|
|
472
|
+
this.log(`Searching for: "${query}"`);
|
|
473
|
+
if (this.blobMap.size === 0) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
"Index not initialized. Add files to index first using addToIndex()."
|
|
476
|
+
);
|
|
553
477
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
478
|
+
const blobs = {
|
|
479
|
+
checkpointId: this.checkpointId,
|
|
480
|
+
addedBlobs: Array.from(this.pendingAdded).sort(),
|
|
481
|
+
deletedBlobs: Array.from(this.pendingDeleted).sort()
|
|
482
|
+
};
|
|
483
|
+
this.log(
|
|
484
|
+
`Executing search with checkpoint ${this.checkpointId || "(none)"}, ${this.blobMap.size} indexed files`
|
|
485
|
+
);
|
|
486
|
+
const result = await this.apiClient.agentCodebaseRetrieval(
|
|
487
|
+
query,
|
|
488
|
+
blobs,
|
|
489
|
+
options?.maxOutputLength
|
|
490
|
+
);
|
|
491
|
+
this.log("Search completed successfully");
|
|
492
|
+
return result.formattedRetrieval;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Search the indexed codebase and ask an LLM a question about the results.
|
|
496
|
+
*
|
|
497
|
+
* This is a convenience method that combines search() with an LLM call to answer
|
|
498
|
+
* questions about your codebase.
|
|
499
|
+
*
|
|
500
|
+
* Note: This method does not wait for indexing. Ensure files are indexed before
|
|
501
|
+
* searching by either:
|
|
502
|
+
* - Using `addToIndex()` with `waitForIndexing: true` (default)
|
|
503
|
+
* - Calling `waitForIndexing()` explicitly before searching
|
|
504
|
+
*
|
|
505
|
+
* @param searchQuery - The semantic search query to find relevant code (also used as the prompt if no separate prompt is provided)
|
|
506
|
+
* @param prompt - Optional prompt to ask the LLM about the search results. If not provided, searchQuery is used as the prompt.
|
|
507
|
+
* @returns The LLM's answer to your question
|
|
508
|
+
*
|
|
509
|
+
* @example
|
|
510
|
+
* ```typescript
|
|
511
|
+
* const answer = await context.searchAndAsk(
|
|
512
|
+
* "How does the authentication flow work?"
|
|
513
|
+
* );
|
|
514
|
+
* console.log(answer);
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
async searchAndAsk(searchQuery, prompt) {
|
|
518
|
+
const results = await this.search(searchQuery);
|
|
519
|
+
const llmPrompt = formatSearchPrompt(prompt ?? searchQuery, results);
|
|
520
|
+
return await chatWithRetry(this.apiClient, llmPrompt, this.debug);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Export the current state to a JSON object
|
|
524
|
+
*/
|
|
525
|
+
export() {
|
|
526
|
+
const blobs = [];
|
|
527
|
+
for (const [path, serverBlobId] of this.blobMap.entries()) {
|
|
528
|
+
const clientBlobId = this.clientBlobMap.get(path);
|
|
529
|
+
if (clientBlobId && clientBlobId !== serverBlobId) {
|
|
530
|
+
blobs.push([serverBlobId, path, clientBlobId]);
|
|
531
|
+
} else {
|
|
532
|
+
blobs.push([serverBlobId, path]);
|
|
533
|
+
}
|
|
587
534
|
}
|
|
535
|
+
const addedBlobs = Array.from(this.pendingAdded);
|
|
536
|
+
const deletedBlobs = Array.from(this.pendingDeleted);
|
|
537
|
+
return {
|
|
538
|
+
checkpointId: this.checkpointId,
|
|
539
|
+
addedBlobs,
|
|
540
|
+
deletedBlobs,
|
|
541
|
+
blobs
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Internal method to import state from a JSON object
|
|
546
|
+
*/
|
|
547
|
+
async doImport(state) {
|
|
548
|
+
return await this.mutex.runExclusive(() => {
|
|
549
|
+
this.checkpointId = state.checkpointId;
|
|
550
|
+
this.blobMap.clear();
|
|
551
|
+
this.clientBlobMap.clear();
|
|
552
|
+
this.pendingAdded.clear();
|
|
553
|
+
this.pendingDeleted.clear();
|
|
554
|
+
if (state.blobs) {
|
|
555
|
+
for (const entry of state.blobs) {
|
|
556
|
+
const [serverBlobId, path, clientBlobId] = entry;
|
|
557
|
+
this.blobMap.set(path, serverBlobId);
|
|
558
|
+
this.clientBlobMap.set(path, clientBlobId ?? serverBlobId);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (state.addedBlobs && state.addedBlobs.length > 0) {
|
|
562
|
+
this.pendingAdded = new Set(state.addedBlobs);
|
|
563
|
+
}
|
|
564
|
+
if (state.deletedBlobs && state.deletedBlobs.length > 0) {
|
|
565
|
+
this.pendingDeleted = new Set(state.deletedBlobs);
|
|
566
|
+
}
|
|
567
|
+
this.log(
|
|
568
|
+
`State imported: checkpoint ${this.checkpointId}, ${this.blobMap.size} files, ${this.pendingAdded.size} pending added, ${this.pendingDeleted.size} pending deleted`
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Export state to a file (Node.js only)
|
|
574
|
+
*/
|
|
575
|
+
async exportToFile(filePath) {
|
|
576
|
+
const state = this.export();
|
|
577
|
+
await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
578
|
+
this.log(`State saved to ${filePath}`);
|
|
579
|
+
}
|
|
588
580
|
}
|
|
589
|
-
|
|
590
|
-
DirectContext
|
|
591
|
-
|
|
592
|
-
DirectContext.MAX_FIND_MISSING_SIZE = 1000;
|
|
593
|
-
DirectContext.CHECKPOINT_THRESHOLD = 1000;
|
|
594
|
-
//# sourceMappingURL=direct-context.js.map
|
|
581
|
+
export {
|
|
582
|
+
DirectContext
|
|
583
|
+
};
|