@mod-computer/cli 0.2.4 → 0.2.5
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/package.json +3 -3
- package/dist/app.js +0 -227
- package/dist/cli.bundle.js.map +0 -7
- package/dist/cli.js +0 -132
- package/dist/commands/add.js +0 -245
- package/dist/commands/agents-run.js +0 -71
- package/dist/commands/auth.js +0 -259
- package/dist/commands/branch.js +0 -1411
- package/dist/commands/claude-sync.js +0 -772
- package/dist/commands/comment.js +0 -568
- package/dist/commands/diff.js +0 -182
- package/dist/commands/index.js +0 -73
- package/dist/commands/init.js +0 -597
- package/dist/commands/ls.js +0 -135
- package/dist/commands/members.js +0 -687
- package/dist/commands/mv.js +0 -282
- package/dist/commands/recover.js +0 -207
- package/dist/commands/rm.js +0 -257
- package/dist/commands/spec.js +0 -386
- package/dist/commands/status.js +0 -296
- package/dist/commands/sync.js +0 -119
- package/dist/commands/trace.js +0 -1752
- package/dist/commands/workspace.js +0 -447
- package/dist/components/conflict-resolution-ui.js +0 -120
- package/dist/components/messages.js +0 -5
- package/dist/components/thread.js +0 -8
- package/dist/config/features.js +0 -83
- package/dist/containers/branches-container.js +0 -140
- package/dist/containers/directory-container.js +0 -92
- package/dist/containers/thread-container.js +0 -214
- package/dist/containers/threads-container.js +0 -27
- package/dist/containers/workspaces-container.js +0 -27
- package/dist/daemon/conflict-resolution.js +0 -172
- package/dist/daemon/content-hash.js +0 -31
- package/dist/daemon/file-sync.js +0 -985
- package/dist/daemon/index.js +0 -203
- package/dist/daemon/mime-types.js +0 -166
- package/dist/daemon/offline-queue.js +0 -211
- package/dist/daemon/path-utils.js +0 -64
- package/dist/daemon/share-policy.js +0 -83
- package/dist/daemon/wasm-errors.js +0 -189
- package/dist/daemon/worker.js +0 -557
- package/dist/daemon-worker.js +0 -258
- package/dist/errors/workspace-errors.js +0 -48
- package/dist/lib/auth-server.js +0 -216
- package/dist/lib/browser.js +0 -35
- package/dist/lib/diff.js +0 -284
- package/dist/lib/formatters.js +0 -204
- package/dist/lib/git.js +0 -137
- package/dist/lib/local-fs.js +0 -201
- package/dist/lib/prompts.js +0 -56
- package/dist/lib/storage.js +0 -213
- package/dist/lib/trace-formatters.js +0 -314
- package/dist/services/add-service.js +0 -554
- package/dist/services/add-validation.js +0 -124
- package/dist/services/automatic-file-tracker.js +0 -303
- package/dist/services/cli-orchestrator.js +0 -227
- package/dist/services/feature-flags.js +0 -187
- package/dist/services/file-import-service.js +0 -283
- package/dist/services/file-transformation-service.js +0 -218
- package/dist/services/logger.js +0 -44
- package/dist/services/mod-config.js +0 -67
- package/dist/services/modignore-service.js +0 -328
- package/dist/services/sync-daemon.js +0 -244
- package/dist/services/thread-notification-service.js +0 -50
- package/dist/services/thread-service.js +0 -147
- package/dist/stores/use-directory-store.js +0 -96
- package/dist/stores/use-threads-store.js +0 -46
- package/dist/stores/use-workspaces-store.js +0 -54
- package/dist/types/add-types.js +0 -99
- package/dist/types/config.js +0 -16
- package/dist/types/index.js +0 -2
- package/dist/types/workspace-connection.js +0 -53
- package/dist/types.js +0 -1
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import chokidar from 'chokidar';
|
|
4
|
-
import { detectMimeType, mimeTypeToCanvasType } from '@mod/mod-core';
|
|
5
|
-
import { ModIgnoreService } from './modignore-service.js';
|
|
6
|
-
import crypto from 'crypto';
|
|
7
|
-
export class AutomaticFileTracker {
|
|
8
|
-
constructor(repo) {
|
|
9
|
-
this.debounceTimers = new Map();
|
|
10
|
-
this.fileHashes = new Map();
|
|
11
|
-
this.isTracking = false;
|
|
12
|
-
this.watchedFileCount = 0;
|
|
13
|
-
this.maxWatchedFiles = 2000;
|
|
14
|
-
this.filePathToDocumentId = new Map();
|
|
15
|
-
this.repo = repo;
|
|
16
|
-
}
|
|
17
|
-
async enableAutoTracking(options) {
|
|
18
|
-
if (this.isTracking) {
|
|
19
|
-
console.log('Automatic file tracking already enabled');
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const watchDirectory = options.watchDirectory || process.cwd();
|
|
23
|
-
const debounceMs = options.debounceMs || 500;
|
|
24
|
-
const verbose = options.verbose || false;
|
|
25
|
-
const preFilterEnabled = options.preFilterEnabled !== false; // Default true
|
|
26
|
-
const maxWatchedFiles = options.maxWatchedFiles || 2000;
|
|
27
|
-
const resourceMonitoring = options.resourceMonitoring !== false; // Default true
|
|
28
|
-
this.maxWatchedFiles = maxWatchedFiles;
|
|
29
|
-
if (resourceMonitoring) {
|
|
30
|
-
await this.checkFileDescriptorLimits();
|
|
31
|
-
}
|
|
32
|
-
console.log(`🔍 Starting automatic file tracking in: ${watchDirectory}`);
|
|
33
|
-
this.modIgnoreService = new ModIgnoreService(watchDirectory);
|
|
34
|
-
let trackableFiles = [];
|
|
35
|
-
if (preFilterEnabled) {
|
|
36
|
-
const filterResult = await this.modIgnoreService.preFilterDirectory(watchDirectory);
|
|
37
|
-
trackableFiles = filterResult.trackableFiles;
|
|
38
|
-
console.log(`📊 Pre-filter results: ${filterResult.totalFiles} total, ${filterResult.filteredFiles.length} passed filters, ${filterResult.trackableFiles.length} trackable`);
|
|
39
|
-
console.log(`📊 Excluded ${filterResult.excludedCount} files (${((filterResult.excludedCount / filterResult.totalFiles) * 100).toFixed(1)}%)`);
|
|
40
|
-
trackableFiles = this.validateResourceLimits(trackableFiles);
|
|
41
|
-
}
|
|
42
|
-
await this.initializeFileMappings(options.workspaceHandle, watchDirectory);
|
|
43
|
-
if (preFilterEnabled && trackableFiles.length > 0) {
|
|
44
|
-
// Watch specific pre-filtered files instead of entire directory
|
|
45
|
-
this.watcher = chokidar.watch(trackableFiles, {
|
|
46
|
-
ignoreInitial: true,
|
|
47
|
-
persistent: true,
|
|
48
|
-
followSymlinks: false
|
|
49
|
-
});
|
|
50
|
-
this.watchedFileCount = trackableFiles.length;
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
// Fallback to directory watching with ignore patterns
|
|
54
|
-
this.watcher = chokidar.watch(watchDirectory, {
|
|
55
|
-
ignored: (filePath) => {
|
|
56
|
-
return this.modIgnoreService.shouldIgnore(filePath, watchDirectory);
|
|
57
|
-
},
|
|
58
|
-
ignoreInitial: true,
|
|
59
|
-
persistent: true,
|
|
60
|
-
followSymlinks: false
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
this.watcher
|
|
64
|
-
.on('add', filePath => {
|
|
65
|
-
if (verbose)
|
|
66
|
-
console.log(`[tracker] File added: ${filePath}`);
|
|
67
|
-
this.handleFileEvent('add', filePath, options, debounceMs);
|
|
68
|
-
})
|
|
69
|
-
.on('change', filePath => {
|
|
70
|
-
if (verbose)
|
|
71
|
-
console.log(`[tracker] File changed: ${filePath}`);
|
|
72
|
-
this.handleFileEvent('change', filePath, options, debounceMs);
|
|
73
|
-
})
|
|
74
|
-
.on('unlink', filePath => {
|
|
75
|
-
if (verbose)
|
|
76
|
-
console.log(`[tracker] File deleted: ${filePath}`);
|
|
77
|
-
this.handleFileEvent('unlink', filePath, options, debounceMs);
|
|
78
|
-
});
|
|
79
|
-
this.isTracking = true;
|
|
80
|
-
console.log(`✅ Automatic file tracking enabled`);
|
|
81
|
-
}
|
|
82
|
-
async initializeFileMappings(workspaceHandle, watchDirectory) {
|
|
83
|
-
try {
|
|
84
|
-
// Get all files currently tracked in the workspace
|
|
85
|
-
const files = await workspaceHandle.file.list();
|
|
86
|
-
for (const file of files) {
|
|
87
|
-
// Create mapping from file path to document ID
|
|
88
|
-
const filePath = path.join(watchDirectory, file.name);
|
|
89
|
-
this.filePathToDocumentId.set(filePath, file.id);
|
|
90
|
-
// Initialize content hash if file exists on disk
|
|
91
|
-
if (fs.existsSync(filePath)) {
|
|
92
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
93
|
-
const hash = this.getContentHash(content);
|
|
94
|
-
this.fileHashes.set(filePath, hash);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
console.log(`📁 Initialized tracking for ${files.length} files`);
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
console.error('Failed to initialize file mappings:', error);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
handleFileEvent(eventType, filePath, options, debounceMs) {
|
|
104
|
-
// Only track text/code files
|
|
105
|
-
if (!this.isTrackableFile(filePath))
|
|
106
|
-
return;
|
|
107
|
-
// Clear existing timer
|
|
108
|
-
const existingTimer = this.debounceTimers.get(filePath);
|
|
109
|
-
if (existingTimer) {
|
|
110
|
-
clearTimeout(existingTimer);
|
|
111
|
-
}
|
|
112
|
-
// Set new debounced timer
|
|
113
|
-
const timer = setTimeout(async () => {
|
|
114
|
-
try {
|
|
115
|
-
await this.processFileChange(eventType, filePath, options);
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
console.error(`Failed to process ${eventType} for ${filePath}:`, error);
|
|
119
|
-
}
|
|
120
|
-
}, debounceMs);
|
|
121
|
-
this.debounceTimers.set(filePath, timer);
|
|
122
|
-
}
|
|
123
|
-
async processFileChange(eventType, filePath, options) {
|
|
124
|
-
const { workspaceHandle, verbose } = options;
|
|
125
|
-
switch (eventType) {
|
|
126
|
-
case 'add':
|
|
127
|
-
await this.handleFileAdd(filePath, workspaceHandle, verbose);
|
|
128
|
-
break;
|
|
129
|
-
case 'change':
|
|
130
|
-
await this.handleFileChange(filePath, workspaceHandle, verbose);
|
|
131
|
-
break;
|
|
132
|
-
case 'unlink':
|
|
133
|
-
await this.handleFileDelete(filePath, workspaceHandle, verbose);
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async handleFileAdd(filePath, workspaceHandle, verbose) {
|
|
138
|
-
if (!fs.existsSync(filePath))
|
|
139
|
-
return;
|
|
140
|
-
const stat = fs.statSync(filePath);
|
|
141
|
-
if (stat.isDirectory()) {
|
|
142
|
-
if (verbose)
|
|
143
|
-
console.log(`[tracker] Skipping directory: ${filePath}`);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
148
|
-
const fileName = path.basename(filePath);
|
|
149
|
-
const hash = this.getContentHash(content);
|
|
150
|
-
if (verbose) {
|
|
151
|
-
console.log(`[DEBUG AUTO-TRACKER] Creating file ${fileName}...`);
|
|
152
|
-
}
|
|
153
|
-
const mimeType = this.getMimeType(fileName);
|
|
154
|
-
const canvasType = mimeTypeToCanvasType(mimeType);
|
|
155
|
-
const documentData = {
|
|
156
|
-
text: content,
|
|
157
|
-
metadata: {
|
|
158
|
-
type: canvasType, // Required for workspace container routing
|
|
159
|
-
originalFilename: fileName
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
const documentHandle = await workspaceHandle.file.create(documentData, {
|
|
163
|
-
name: fileName,
|
|
164
|
-
mimeType: mimeType
|
|
165
|
-
});
|
|
166
|
-
this.filePathToDocumentId.set(filePath, documentHandle.documentId);
|
|
167
|
-
this.fileHashes.set(filePath, hash);
|
|
168
|
-
if (verbose) {
|
|
169
|
-
console.log(`✅ Added file: ${fileName} (${documentHandle.documentId})`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
console.error(`Failed to add file ${path.basename(filePath)}:`, error);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
async handleFileChange(filePath, workspaceHandle, verbose) {
|
|
177
|
-
if (!fs.existsSync(filePath))
|
|
178
|
-
return;
|
|
179
|
-
try {
|
|
180
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
181
|
-
const newHash = this.getContentHash(content);
|
|
182
|
-
const oldHash = this.fileHashes.get(filePath);
|
|
183
|
-
if (newHash === oldHash)
|
|
184
|
-
return;
|
|
185
|
-
const documentId = this.filePathToDocumentId.get(filePath);
|
|
186
|
-
if (!documentId) {
|
|
187
|
-
// File not tracked yet, treat as new file
|
|
188
|
-
await this.handleFileAdd(filePath, workspaceHandle, verbose);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const fileName = path.basename(filePath);
|
|
192
|
-
const mimeType = this.getMimeType(fileName);
|
|
193
|
-
const canvasType = mimeTypeToCanvasType(mimeType);
|
|
194
|
-
const documentData = {
|
|
195
|
-
text: content,
|
|
196
|
-
metadata: {
|
|
197
|
-
type: canvasType, // Required for workspace container routing
|
|
198
|
-
originalFilename: fileName
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
await workspaceHandle.file.update(documentId, documentData);
|
|
202
|
-
this.fileHashes.set(filePath, newHash);
|
|
203
|
-
if (verbose) {
|
|
204
|
-
const fileName = path.basename(filePath);
|
|
205
|
-
console.log(`🔄 Updated file: ${fileName}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
console.error(`Failed to update file ${path.basename(filePath)}:`, error);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async handleFileDelete(filePath, workspaceHandle, verbose) {
|
|
213
|
-
const documentId = this.filePathToDocumentId.get(filePath);
|
|
214
|
-
if (!documentId)
|
|
215
|
-
return;
|
|
216
|
-
try {
|
|
217
|
-
// For now, clean up local mappings - file.delete() method will be implemented later
|
|
218
|
-
this.filePathToDocumentId.delete(filePath);
|
|
219
|
-
this.fileHashes.delete(filePath);
|
|
220
|
-
if (verbose) {
|
|
221
|
-
const fileName = path.basename(filePath);
|
|
222
|
-
console.log(`🗑️ File deleted: ${fileName} (local cleanup completed)`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
console.error(`Failed to handle deletion of ${path.basename(filePath)}:`, error);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
isTrackableFile(filePath) {
|
|
230
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
231
|
-
const trackableExtensions = [
|
|
232
|
-
'.md', '.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml',
|
|
233
|
-
'.py', '.java', '.cpp', '.c', '.h', '.css', '.scss', '.html', '.xml',
|
|
234
|
-
'.sql', '.go', '.rs', '.php', '.rb', '.swift', '.kt', '.scala'
|
|
235
|
-
];
|
|
236
|
-
return trackableExtensions.includes(ext);
|
|
237
|
-
}
|
|
238
|
-
getMimeType(fileName) {
|
|
239
|
-
// Use centralized MIME type detection from mod-core
|
|
240
|
-
return detectMimeType(fileName);
|
|
241
|
-
}
|
|
242
|
-
getContentHash(content) {
|
|
243
|
-
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
244
|
-
}
|
|
245
|
-
async disableAutoTracking() {
|
|
246
|
-
if (!this.isTracking)
|
|
247
|
-
return;
|
|
248
|
-
if (this.watcher) {
|
|
249
|
-
await this.watcher.close();
|
|
250
|
-
this.watcher = undefined;
|
|
251
|
-
}
|
|
252
|
-
// Clear all timers
|
|
253
|
-
for (const timer of this.debounceTimers.values()) {
|
|
254
|
-
clearTimeout(timer);
|
|
255
|
-
}
|
|
256
|
-
this.debounceTimers.clear();
|
|
257
|
-
this.isTracking = false;
|
|
258
|
-
console.log('🛑 Automatic file tracking disabled');
|
|
259
|
-
}
|
|
260
|
-
getTrackingStatus() {
|
|
261
|
-
const capacityUsed = this.watchedFileCount / this.maxWatchedFiles;
|
|
262
|
-
return {
|
|
263
|
-
isTracking: this.isTracking,
|
|
264
|
-
trackedFiles: this.filePathToDocumentId.size,
|
|
265
|
-
watchedFiles: this.watchedFileCount,
|
|
266
|
-
maxWatchedFiles: this.maxWatchedFiles,
|
|
267
|
-
capacityUsed: Math.round(capacityUsed * 100)
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
getModIgnoreStats() {
|
|
271
|
-
return this.modIgnoreService?.getPatternStats() || null;
|
|
272
|
-
}
|
|
273
|
-
async checkFileDescriptorLimits() {
|
|
274
|
-
try {
|
|
275
|
-
// Get current file descriptor usage (Node.js specific)
|
|
276
|
-
const fs = await import('fs');
|
|
277
|
-
// Check if we're approaching system limits by attempting to open a temporary file
|
|
278
|
-
// This is a simple heuristic since Node.js doesn't expose ulimit directly
|
|
279
|
-
const testFile = '/tmp/.mod-fd-test';
|
|
280
|
-
try {
|
|
281
|
-
fs.writeFileSync(testFile, 'test');
|
|
282
|
-
fs.unlinkSync(testFile);
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
console.warn('⚠️ Warning: Potential file descriptor limit approaching');
|
|
286
|
-
throw new Error('File descriptor limit check failed - consider reducing maxWatchedFiles');
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
catch (error) {
|
|
290
|
-
console.warn('Could not check file descriptor limits:', error);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
validateResourceLimits(trackableFiles) {
|
|
294
|
-
if (trackableFiles.length > this.maxWatchedFiles) {
|
|
295
|
-
console.warn(`⚠️ Warning: ${trackableFiles.length} trackable files exceeds limit of ${this.maxWatchedFiles}`);
|
|
296
|
-
console.warn('⚠️ Consider updating .modignore patterns or increasing maxWatchedFiles limit');
|
|
297
|
-
const limitedFiles = trackableFiles.slice(0, this.maxWatchedFiles);
|
|
298
|
-
console.warn(`⚠️ Limiting tracking to first ${this.maxWatchedFiles} files for performance`);
|
|
299
|
-
return limitedFiles;
|
|
300
|
-
}
|
|
301
|
-
return trackableFiles;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { ThreadService } from '@mod/mod-core/services/thread-service';
|
|
2
|
-
import { BranchableRepo } from '@mod/mod-core/services/branchable-repo';
|
|
3
|
-
import { streamText, stepCountIs } from 'ai';
|
|
4
|
-
import { createOpenAI } from '@ai-sdk/openai';
|
|
5
|
-
import { anthropic } from '@ai-sdk/anthropic';
|
|
6
|
-
import { log } from './logger.js';
|
|
7
|
-
function formatItemsToMessages(items, maxMessages = 20) {
|
|
8
|
-
return items
|
|
9
|
-
.sort((a, b) => {
|
|
10
|
-
const aTime = new Date(a.metadata?.createdAt || 0).getTime();
|
|
11
|
-
const bTime = new Date(b.metadata?.createdAt || 0).getTime();
|
|
12
|
-
if (aTime !== bTime)
|
|
13
|
-
return aTime - bTime;
|
|
14
|
-
return String(a.id || '').localeCompare(String(b.id || ''));
|
|
15
|
-
})
|
|
16
|
-
.slice(-maxMessages)
|
|
17
|
-
.map((item) => ({
|
|
18
|
-
role: item.userType === 'user' ? 'user' : 'assistant',
|
|
19
|
-
content: item.content,
|
|
20
|
-
}));
|
|
21
|
-
}
|
|
22
|
-
export async function* chatWithAgentCli({ repo, threadId, userMessage, user = { id: 'user-1', name: 'You', avatarUrl: '' }, workspace, agent, files = [], images = [], maxMessages = 20, tools = {}, apiKey, provider, modelName, ...opts }) {
|
|
23
|
-
const threadService = new ThreadService(repo);
|
|
24
|
-
// 1. Load thread and history
|
|
25
|
-
const thread = await threadService.getThread(threadId);
|
|
26
|
-
const items = await Promise.all((thread.itemIds || []).map((id) => threadService.getThreadItem(id)));
|
|
27
|
-
// 2. Prepare messages and persist user message
|
|
28
|
-
let messages = formatItemsToMessages(items, maxMessages);
|
|
29
|
-
messages = [...messages, { role: 'user', content: userMessage }];
|
|
30
|
-
// 3. Build session context with branch and task information
|
|
31
|
-
const context = await buildCLISessionContext({
|
|
32
|
-
repo,
|
|
33
|
-
threadId,
|
|
34
|
-
workspace,
|
|
35
|
-
user,
|
|
36
|
-
files,
|
|
37
|
-
images,
|
|
38
|
-
items,
|
|
39
|
-
agent
|
|
40
|
-
});
|
|
41
|
-
const instructions = typeof agent?.instructions === 'function' ? await agent.instructions(context) : (agent?.instructions || `You are ${agent?.name || 'Assistant'}. Help the user with their request.`);
|
|
42
|
-
const resolvedApiKey = (apiKey || process.env.OPENAI_API_KEY || process.env.OPENAI_API_TOKEN || process.env.OPENAI || process.env.ANTHROPIC_API_KEY || '').trim();
|
|
43
|
-
const selectedProvider = provider || opts.provider || 'openai';
|
|
44
|
-
let selectedModel = modelName || opts.modelName || agent?.defaultModel || 'gpt-4o';
|
|
45
|
-
// Normalize common aliases to real provider model IDs
|
|
46
|
-
if (selectedProvider === 'openai' && /^gpt-5\b/i.test(selectedModel)) {
|
|
47
|
-
selectedModel = 'gpt-4o';
|
|
48
|
-
}
|
|
49
|
-
const model = selectedProvider === 'openai'
|
|
50
|
-
? createOpenAI({ apiKey: resolvedApiKey })(selectedModel)
|
|
51
|
-
: anthropic(selectedModel);
|
|
52
|
-
// Merge and wrap tools with context injection (so tool.execute receives context)
|
|
53
|
-
const mergedTools = {
|
|
54
|
-
...(agent?.tools || {}),
|
|
55
|
-
...(tools || {}),
|
|
56
|
-
};
|
|
57
|
-
const aiSDKTools = injectContextIntoTools(mergedTools, context);
|
|
58
|
-
// Debug logs for visibility
|
|
59
|
-
try {
|
|
60
|
-
log('[CLI Orchestrator] Provider:', selectedProvider);
|
|
61
|
-
log('[CLI Orchestrator] Model:', selectedModel);
|
|
62
|
-
log('[CLI Orchestrator] Instructions (first 200):', String(instructions || '').slice(0, 200));
|
|
63
|
-
log('[CLI Orchestrator] Tools:', Object.keys(aiSDKTools));
|
|
64
|
-
log('[CLI Orchestrator] Messages:', messages);
|
|
65
|
-
}
|
|
66
|
-
catch { }
|
|
67
|
-
const maxAgentSteps = Math.max(1, opts?.maxSteps ?? 6);
|
|
68
|
-
let lastAssistantItemId;
|
|
69
|
-
let lastReasoningItemId;
|
|
70
|
-
const processedToolResultIds = new Set();
|
|
71
|
-
async function* processStream(stream) {
|
|
72
|
-
const iter = stream.fullStream ?? stream.textStream ?? stream;
|
|
73
|
-
for await (const part of iter) {
|
|
74
|
-
const partType = part.type;
|
|
75
|
-
if (partType === 'reasoning' ||
|
|
76
|
-
partType === 'reasoning-delta' ||
|
|
77
|
-
partType === 'reasoning-start' ||
|
|
78
|
-
partType === 'reasoning-end' ||
|
|
79
|
-
partType === 'reasoning-part-delta' ||
|
|
80
|
-
partType === 'reasoning-part-finish') {
|
|
81
|
-
const delta = part.text || part.delta || '';
|
|
82
|
-
if (delta && delta.length > 0) {
|
|
83
|
-
if (!lastReasoningItemId) {
|
|
84
|
-
const item = await threadService.addReasoningToThread(threadId, delta, agent?.name || 'Assistant', agent?.iconUrl);
|
|
85
|
-
lastReasoningItemId = item.id;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
await threadService.addChunkToThreadItem(lastReasoningItemId, delta);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
yield { type: 'reasoning', content: delta };
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (part.type === 'text' || typeof part === 'string') {
|
|
95
|
-
const content = typeof part === 'string' ? part : part.text;
|
|
96
|
-
if (content) {
|
|
97
|
-
if (!lastAssistantItemId) {
|
|
98
|
-
const item = await threadService.addAssistantMessageToThread(threadId, content, agent?.name || 'Assistant', agent?.iconUrl);
|
|
99
|
-
lastAssistantItemId = item.id;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
await threadService.addChunkToThreadItem(lastAssistantItemId, content);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
yield { type: 'content', content };
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (part.type === 'tool-call') {
|
|
109
|
-
await threadService.addToolCallToThread(threadId, part, agent?.name || 'Assistant', agent?.iconUrl);
|
|
110
|
-
yield part;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (part.type === 'tool-result') {
|
|
114
|
-
const toolCallId = String(part?.toolCallId || part?.id || `tool-${Date.now()}`);
|
|
115
|
-
if (toolCallId && processedToolResultIds.has(toolCallId)) {
|
|
116
|
-
try {
|
|
117
|
-
log('[CLI Orchestrator] duplicate tool-result skipped', toolCallId);
|
|
118
|
-
}
|
|
119
|
-
catch { }
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
if (toolCallId)
|
|
123
|
-
processedToolResultIds.add(toolCallId);
|
|
124
|
-
await threadService.addToolResultToThread(threadId, part, agent?.name || 'Assistant', agent?.iconUrl);
|
|
125
|
-
}
|
|
126
|
-
yield { type: 'tool-result', output: part };
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (part.type === 'error') {
|
|
130
|
-
yield { type: 'error', error: part.error };
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
const summary = await stream.reasoningText?.catch(() => undefined);
|
|
135
|
-
if (summary && summary.trim().length > 0) {
|
|
136
|
-
if (!lastReasoningItemId) {
|
|
137
|
-
const item = await threadService.addReasoningToThread(threadId, summary, agent?.name || 'Assistant', agent?.iconUrl);
|
|
138
|
-
lastReasoningItemId = item.id;
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
await threadService.addChunkToThreadItem(lastReasoningItemId, summary);
|
|
142
|
-
}
|
|
143
|
-
yield { type: 'reasoning', content: summary };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch { }
|
|
147
|
-
}
|
|
148
|
-
const stream = streamText({
|
|
149
|
-
model,
|
|
150
|
-
system: instructions,
|
|
151
|
-
tools: aiSDKTools,
|
|
152
|
-
messages,
|
|
153
|
-
stopWhen: stepCountIs(maxAgentSteps),
|
|
154
|
-
});
|
|
155
|
-
yield* processStream(stream);
|
|
156
|
-
}
|
|
157
|
-
function injectContextIntoTools(tools, context) {
|
|
158
|
-
const wrapped = {};
|
|
159
|
-
for (const [name, tool] of Object.entries(tools || {})) {
|
|
160
|
-
if (!tool || typeof tool !== 'object')
|
|
161
|
-
continue;
|
|
162
|
-
const exec = tool.execute;
|
|
163
|
-
if (exec && exec.constructor?.name === 'AsyncGeneratorFunction') {
|
|
164
|
-
wrapped[name] = {
|
|
165
|
-
...tool,
|
|
166
|
-
async *execute(args) {
|
|
167
|
-
const result = exec.call(tool, { ...(args || {}), context });
|
|
168
|
-
for await (const chunk of result)
|
|
169
|
-
yield chunk;
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
else if (typeof exec === 'function') {
|
|
174
|
-
wrapped[name] = {
|
|
175
|
-
...tool,
|
|
176
|
-
async execute(args) {
|
|
177
|
-
return exec.call(tool, { ...(args || {}), context });
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
// If it already looks like an ai tool with parameters, keep as-is
|
|
183
|
-
wrapped[name] = tool;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return wrapped;
|
|
187
|
-
}
|
|
188
|
-
async function buildCLISessionContext({ repo, threadId, workspace, user, files = [], images = [], items, agent }) {
|
|
189
|
-
// Get branch context
|
|
190
|
-
const branchableRepo = new BranchableRepo(repo);
|
|
191
|
-
let activeBranch = undefined;
|
|
192
|
-
let activeThread = undefined;
|
|
193
|
-
try {
|
|
194
|
-
const branchCtx = await branchableRepo.getBranchContext(workspace.id);
|
|
195
|
-
if (branchCtx && branchCtx.activeBranchId && branchCtx.branchesDocId) {
|
|
196
|
-
const branchService = branchableRepo.getBranchService();
|
|
197
|
-
activeBranch = await branchService.getBranch(branchCtx.activeBranchId, branchCtx.branchesDocId);
|
|
198
|
-
// Get active task ID for this branch
|
|
199
|
-
const activeTaskId = await branchService.getActiveTaskId(branchCtx.activeBranchId, branchCtx.branchesDocId).catch(() => null);
|
|
200
|
-
// Build active thread context
|
|
201
|
-
activeThread = {
|
|
202
|
-
id: threadId,
|
|
203
|
-
name: 'CLI Thread',
|
|
204
|
-
branchId: activeBranch?.id || branchCtx.activeBranchId,
|
|
205
|
-
tasksDocId: activeTaskId,
|
|
206
|
-
metadata: {
|
|
207
|
-
type: 'thread',
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch (e) {
|
|
213
|
-
console.warn('[CLI SessionContext] Could not fetch active branch:', e);
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
repo,
|
|
217
|
-
user,
|
|
218
|
-
activeWorkspace: workspace, // Expected by tools
|
|
219
|
-
workspace, // For compatibility
|
|
220
|
-
agent,
|
|
221
|
-
files,
|
|
222
|
-
images,
|
|
223
|
-
items,
|
|
224
|
-
...(activeBranch ? { activeBranch } : {}),
|
|
225
|
-
...(activeThread ? { activeThread } : {}),
|
|
226
|
-
};
|
|
227
|
-
}
|