@bobsworkshop/cli 0.1.0 → 0.1.1
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/bin/{analyse-auto-OBCDWYWX.js → analyse-auto-KKWLMLHZ.js} +2 -1
- package/dist/bin/{analyse-results-QSOD3KVC.js → analyse-results-N5QLJNND.js} +2 -1
- package/dist/bin/bob.js +4 -3
- package/dist/bin/{chunk-LHWBSCJ4.js → chunk-WEHSNZKO.js} +2 -0
- package/package.json +11 -5
- package/bin/bob.ts +0 -74
- package/dist/bin/analyse-auto-AAWSETKY.js +0 -540
- package/dist/bin/analyse-auto-FQ62GYPV.js +0 -533
- package/dist/bin/analyse-auto-JAD24IQ5.js +0 -511
- package/dist/bin/analyse-auto-R4MZA7SX.js +0 -511
- package/dist/bin/analyse-auto-V3SH4MS7.js +0 -524
- package/dist/bin/analyse-auto-WQUK5YPO.js +0 -531
- package/dist/bin/analyse-auto-WVAY6467.js +0 -280
- package/dist/bin/analyse-results-3NSD6MAY.js +0 -363
- package/dist/bin/analyse-results-B7LONUXU.js +0 -265
- package/dist/bin/analyse-results-E6NBAVDB.js +0 -265
- package/dist/bin/analyse-results-FIDS4635.js +0 -9
- package/dist/bin/analyse-results-LMGVKAUX.js +0 -342
- package/dist/bin/analyse-results-LSMLUEIB.js +0 -338
- package/dist/bin/analyse-results-MOCLBCD7.js +0 -326
- package/dist/bin/analyse-results-NLAEAOOP.js +0 -10
- package/dist/bin/analyse-results-PYQIKWYL.js +0 -9
- package/dist/bin/analyse-results-R3MG5H7G.js +0 -329
- package/dist/bin/analyse-results-UYZZSBHB.js +0 -9
- package/dist/bin/analyse-results-YYGHIK2Q.js +0 -9
- package/dist/bin/analysis-tracker-N5VANTLH.js +0 -12
- package/dist/bin/chunk-3RSDDQE2.js +0 -420
- package/dist/bin/chunk-6KWC4HDO.js +0 -97
- package/dist/bin/chunk-6W7WDF4Q.js +0 -589
- package/dist/bin/chunk-7CXM3RLM.js +0 -287
- package/dist/bin/chunk-FGYL6SWO.js +0 -465
- package/dist/bin/chunk-J4BSKFCW.js +0 -624
- package/dist/bin/chunk-KWOQFI6L.js +0 -287
- package/dist/bin/chunk-OOGLZ2QB.js +0 -322
- package/dist/bin/chunk-TEVQLSGD.js +0 -287
- package/dist/bin/chunk-VUS7R7SO.js +0 -479
|
@@ -1,624 +0,0 @@
|
|
|
1
|
-
// src/core/config-store.ts
|
|
2
|
-
import Conf from "conf";
|
|
3
|
-
|
|
4
|
-
// src/types/config.ts
|
|
5
|
-
var DEFAULT_CONFIG = {
|
|
6
|
-
tier: "local",
|
|
7
|
-
loggedIn: false,
|
|
8
|
-
email: null,
|
|
9
|
-
uid: null,
|
|
10
|
-
authToken: null,
|
|
11
|
-
refreshToken: null,
|
|
12
|
-
provider: null,
|
|
13
|
-
providerKey: null,
|
|
14
|
-
localEndpoint: null,
|
|
15
|
-
personalizationMode: false,
|
|
16
|
-
consultantMode: false,
|
|
17
|
-
autoMode: false,
|
|
18
|
-
idrp: false,
|
|
19
|
-
idrpFilter: "free",
|
|
20
|
-
activeProject: null,
|
|
21
|
-
conversationId: null,
|
|
22
|
-
activePersona: null,
|
|
23
|
-
hasSeenWelcome: false
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// src/core/config-store.ts
|
|
27
|
-
var store = new Conf({
|
|
28
|
-
projectName: "bob-cli",
|
|
29
|
-
defaults: DEFAULT_CONFIG
|
|
30
|
-
});
|
|
31
|
-
function getConfig() {
|
|
32
|
-
return {
|
|
33
|
-
tier: store.get("tier"),
|
|
34
|
-
loggedIn: store.get("loggedIn"),
|
|
35
|
-
email: store.get("email"),
|
|
36
|
-
uid: store.get("uid"),
|
|
37
|
-
authToken: store.get("authToken"),
|
|
38
|
-
refreshToken: store.get("refreshToken"),
|
|
39
|
-
provider: store.get("provider"),
|
|
40
|
-
providerKey: store.get("providerKey"),
|
|
41
|
-
localEndpoint: store.get("localEndpoint"),
|
|
42
|
-
personalizationMode: store.get("personalizationMode"),
|
|
43
|
-
consultantMode: store.get("consultantMode"),
|
|
44
|
-
idrp: store.get("idrp"),
|
|
45
|
-
idrpFilter: store.get("idrpFilter"),
|
|
46
|
-
activeProject: store.get("activeProject"),
|
|
47
|
-
conversationId: store.get("conversationId"),
|
|
48
|
-
activePersona: store.get("activePersona"),
|
|
49
|
-
hasSeenWelcome: store.get("hasSeenWelcome"),
|
|
50
|
-
autoMode: store.get("autoMode")
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function setConfigValue(key, value) {
|
|
54
|
-
store.set(key, value);
|
|
55
|
-
}
|
|
56
|
-
function getConfigPath() {
|
|
57
|
-
return store.path;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/commands/login.ts
|
|
61
|
-
import chalk from "chalk";
|
|
62
|
-
import http from "http";
|
|
63
|
-
import open from "open";
|
|
64
|
-
import axios from "axios";
|
|
65
|
-
import { URL } from "url";
|
|
66
|
-
import * as readline from "readline";
|
|
67
|
-
var CLI_AUTH_URL = "https://bobs-workshop.web.app/cli-auth";
|
|
68
|
-
var CALLBACK_PORT = 9876;
|
|
69
|
-
var FIREBASE_API_KEY = "AIzaSyB-hUZEonRIzbExVDwuneJaDjJZBvHdIps";
|
|
70
|
-
function registerLoginCommand(program) {
|
|
71
|
-
program.command("login").description("Authenticate with Bob's Workshop via browser").action(async () => {
|
|
72
|
-
console.log("");
|
|
73
|
-
console.log(chalk.bold.cyan(" \u{1F510} Bob CLI \u2014 Login"));
|
|
74
|
-
console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
75
|
-
console.log("");
|
|
76
|
-
console.log(chalk.yellow(" \u26A0\uFE0F Important:"));
|
|
77
|
-
console.log(chalk.gray(" \u2022 Local conversations (Tier 1) will NOT sync to the platform."));
|
|
78
|
-
console.log(chalk.gray(" \u2022 Only NEW conversations created after login will save to Firebase."));
|
|
79
|
-
console.log(chalk.gray(" \u2022 Your local history stays in ~/.bob/projects/ (backup via `bob backup`)."));
|
|
80
|
-
console.log(chalk.gray(" \u2022 Logging in upgrades you to Tier 3 (Platform) with full features."));
|
|
81
|
-
console.log("");
|
|
82
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
83
|
-
const answer = await new Promise((resolve2) => {
|
|
84
|
-
rl.question(chalk.cyan(" Continue with login? (y/n): "), resolve2);
|
|
85
|
-
});
|
|
86
|
-
rl.close();
|
|
87
|
-
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
88
|
-
console.log("");
|
|
89
|
-
console.log(chalk.gray(" Login cancelled."));
|
|
90
|
-
console.log("");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
console.log("");
|
|
94
|
-
console.log(chalk.gray(" Opening browser for authentication..."));
|
|
95
|
-
console.log("");
|
|
96
|
-
try {
|
|
97
|
-
const result = await startAuthFlow();
|
|
98
|
-
if (result) {
|
|
99
|
-
const exchangeResult = await exchangeCustomToken(result.token);
|
|
100
|
-
setConfigValue("authToken", exchangeResult.idToken);
|
|
101
|
-
setConfigValue("refreshToken", exchangeResult.refreshToken);
|
|
102
|
-
setConfigValue("email", result.email);
|
|
103
|
-
setConfigValue("uid", result.uid);
|
|
104
|
-
setConfigValue("loggedIn", true);
|
|
105
|
-
setConfigValue("tier", "platform");
|
|
106
|
-
console.log("");
|
|
107
|
-
console.log(chalk.green(` \u2705 Logged in as ${result.email}`));
|
|
108
|
-
console.log(chalk.gray(" Tier: Platform (Tier 3)"));
|
|
109
|
-
console.log(chalk.gray(" All platform features are now available."));
|
|
110
|
-
console.log("");
|
|
111
|
-
}
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.log(chalk.red(` \u274C Login failed: ${error.message}`));
|
|
114
|
-
console.log("");
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
program.command("logout").description("Sign out and clear stored credentials").action(() => {
|
|
118
|
-
setConfigValue("authToken", null);
|
|
119
|
-
setConfigValue("refreshToken", null);
|
|
120
|
-
setConfigValue("email", null);
|
|
121
|
-
setConfigValue("uid", null);
|
|
122
|
-
setConfigValue("loggedIn", false);
|
|
123
|
-
setConfigValue("tier", "local");
|
|
124
|
-
console.log("");
|
|
125
|
-
console.log(chalk.gray(" \u{1F44B} Logged out. Switched to Tier 1 (local-first)."));
|
|
126
|
-
console.log("");
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async function exchangeCustomToken(customToken) {
|
|
130
|
-
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${FIREBASE_API_KEY}`;
|
|
131
|
-
const response = await axios.post(url, {
|
|
132
|
-
token: customToken,
|
|
133
|
-
returnSecureToken: true
|
|
134
|
-
});
|
|
135
|
-
if (!response.data?.idToken || !response.data?.refreshToken) {
|
|
136
|
-
throw new Error("Token exchange failed \u2014 no ID token returned.");
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
idToken: response.data.idToken,
|
|
140
|
-
refreshToken: response.data.refreshToken
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
async function refreshAuthToken(refreshToken) {
|
|
144
|
-
const url = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
|
|
145
|
-
const response = await axios.post(url, {
|
|
146
|
-
grant_type: "refresh_token",
|
|
147
|
-
refresh_token: refreshToken
|
|
148
|
-
});
|
|
149
|
-
if (!response.data?.id_token) {
|
|
150
|
-
throw new Error("Token refresh failed.");
|
|
151
|
-
}
|
|
152
|
-
setConfigValue("authToken", response.data.id_token);
|
|
153
|
-
return response.data.id_token;
|
|
154
|
-
}
|
|
155
|
-
function startAuthFlow() {
|
|
156
|
-
return new Promise((resolve2, reject) => {
|
|
157
|
-
const timeout = setTimeout(() => {
|
|
158
|
-
server.close();
|
|
159
|
-
reject(new Error("Login timed out after 120 seconds. Please try again."));
|
|
160
|
-
}, 12e4);
|
|
161
|
-
const server = http.createServer((req, res) => {
|
|
162
|
-
if (!req.url?.startsWith("/callback")) {
|
|
163
|
-
res.writeHead(404);
|
|
164
|
-
res.end("Not found");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
169
|
-
const token = url.searchParams.get("token");
|
|
170
|
-
const email = url.searchParams.get("email");
|
|
171
|
-
const uid = url.searchParams.get("uid");
|
|
172
|
-
if (!token || !email || !uid) {
|
|
173
|
-
res.writeHead(400);
|
|
174
|
-
res.end("Missing parameters");
|
|
175
|
-
reject(new Error("Invalid callback \u2014 missing token, email, or uid."));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
179
|
-
res.end(`
|
|
180
|
-
<html>
|
|
181
|
-
<body style="background: #0a0a0a; color: white; font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
|
|
182
|
-
<div style="text-align: center;">
|
|
183
|
-
<h1>\u2705 Authenticated!</h1>
|
|
184
|
-
<p style="color: #888;">You can close this tab and return to your terminal.</p>
|
|
185
|
-
</div>
|
|
186
|
-
</body>
|
|
187
|
-
</html>
|
|
188
|
-
`);
|
|
189
|
-
clearTimeout(timeout);
|
|
190
|
-
server.close();
|
|
191
|
-
resolve2({ token, email, uid });
|
|
192
|
-
} catch (e) {
|
|
193
|
-
res.writeHead(500);
|
|
194
|
-
res.end("Error");
|
|
195
|
-
reject(e);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
server.listen(CALLBACK_PORT, () => {
|
|
199
|
-
console.log(chalk.gray(` \u{1F310} Waiting for authentication (port ${CALLBACK_PORT})...`));
|
|
200
|
-
console.log(chalk.gray(" If your browser doesn't open, visit:"));
|
|
201
|
-
console.log(chalk.cyan(` ${CLI_AUTH_URL}`));
|
|
202
|
-
console.log("");
|
|
203
|
-
open(CLI_AUTH_URL).catch(() => {
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
server.on("error", (err) => {
|
|
207
|
-
clearTimeout(timeout);
|
|
208
|
-
if (err.code === "EADDRINUSE") {
|
|
209
|
-
reject(new Error("Port 9876 is already in use. Close other instances and try again."));
|
|
210
|
-
} else {
|
|
211
|
-
reject(err);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// src/core/api-client.ts
|
|
218
|
-
import axios2 from "axios";
|
|
219
|
-
var FUNCTIONS_BASE = "https://us-central1-seedlingapp.cloudfunctions.net";
|
|
220
|
-
async function callCloudFunction(functionName, data) {
|
|
221
|
-
const config = getConfig();
|
|
222
|
-
if (!config.authToken) {
|
|
223
|
-
throw new Error("Not authenticated. Run `bob login` first.");
|
|
224
|
-
}
|
|
225
|
-
try {
|
|
226
|
-
const response = await axios2.post(
|
|
227
|
-
`${FUNCTIONS_BASE}/${functionName}`,
|
|
228
|
-
{ data },
|
|
229
|
-
{
|
|
230
|
-
headers: {
|
|
231
|
-
"Content-Type": "application/json",
|
|
232
|
-
"Authorization": `Bearer ${config.authToken}`
|
|
233
|
-
},
|
|
234
|
-
timeout: 18e4
|
|
235
|
-
}
|
|
236
|
-
);
|
|
237
|
-
return response.data?.result || response.data;
|
|
238
|
-
} catch (error) {
|
|
239
|
-
let status = void 0;
|
|
240
|
-
const hasResponse = typeof error === "object" && error !== null && "response" in error && typeof error.response === "object" && error.response !== null;
|
|
241
|
-
if (hasResponse) {
|
|
242
|
-
status = error.response?.status;
|
|
243
|
-
}
|
|
244
|
-
if (status === 401 && config.refreshToken) {
|
|
245
|
-
try {
|
|
246
|
-
const newToken = await refreshAuthToken(config.refreshToken);
|
|
247
|
-
const retryResponse = await axios2.post(
|
|
248
|
-
`${FUNCTIONS_BASE}/${functionName}`,
|
|
249
|
-
{ data },
|
|
250
|
-
{
|
|
251
|
-
headers: {
|
|
252
|
-
"Content-Type": "application/json",
|
|
253
|
-
"Authorization": `Bearer ${newToken}`
|
|
254
|
-
},
|
|
255
|
-
timeout: 18e4
|
|
256
|
-
}
|
|
257
|
-
);
|
|
258
|
-
return retryResponse.data?.result || retryResponse.data;
|
|
259
|
-
} catch (refreshError) {
|
|
260
|
-
setConfigValue("loggedIn", false);
|
|
261
|
-
throw new Error("Session expired. Run `bob login` again.");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (status === 404) {
|
|
265
|
-
throw new Error(`Function "${functionName}" not found. Is it deployed?`);
|
|
266
|
-
}
|
|
267
|
-
if (status === 403) {
|
|
268
|
-
throw new Error("Permission denied. You may not have access to this feature.");
|
|
269
|
-
}
|
|
270
|
-
if (status === 500) {
|
|
271
|
-
const safeError2 = error;
|
|
272
|
-
const serverMsg = safeError2.response?.data?.error?.message || safeError2.response?.data?.error || "Internal server error";
|
|
273
|
-
throw new Error(`Server error: ${serverMsg}`);
|
|
274
|
-
}
|
|
275
|
-
if (status === 429) {
|
|
276
|
-
throw new Error("Rate limited. Please wait a moment and try again.");
|
|
277
|
-
}
|
|
278
|
-
let errorMsg;
|
|
279
|
-
const safeError = error;
|
|
280
|
-
if (safeError.response?.data?.error?.message) {
|
|
281
|
-
errorMsg = safeError.response.data.error.message;
|
|
282
|
-
} else if (typeof error === "object" && error !== null && "message" in error) {
|
|
283
|
-
errorMsg = error.message;
|
|
284
|
-
} else {
|
|
285
|
-
errorMsg = `Request failed with status ${status ?? "unknown"}`;
|
|
286
|
-
}
|
|
287
|
-
throw new Error(errorMsg);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// src/ai/providers/local.ts
|
|
292
|
-
import axios3 from "axios";
|
|
293
|
-
async function callLocalModel(endpoint, messages) {
|
|
294
|
-
try {
|
|
295
|
-
const response = await axios3.post(
|
|
296
|
-
endpoint,
|
|
297
|
-
{
|
|
298
|
-
model: "bob-local-dna:latest",
|
|
299
|
-
messages,
|
|
300
|
-
stream: false
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
headers: { "Content-Type": "application/json" },
|
|
304
|
-
timeout: 18e4
|
|
305
|
-
}
|
|
306
|
-
);
|
|
307
|
-
if (response.data?.message?.content) {
|
|
308
|
-
return response.data.message.content;
|
|
309
|
-
}
|
|
310
|
-
const choice = response.data?.choices?.[0];
|
|
311
|
-
if (choice?.message?.content) {
|
|
312
|
-
return choice.message.content;
|
|
313
|
-
}
|
|
314
|
-
if (typeof response.data?.response === "string") {
|
|
315
|
-
return response.data.response;
|
|
316
|
-
}
|
|
317
|
-
return "No response received from local model.";
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error.code === "ECONNREFUSED") {
|
|
320
|
-
throw new Error("Cannot connect to local model. Is Ollama running? Check your endpoint: " + endpoint);
|
|
321
|
-
}
|
|
322
|
-
throw new Error("Local model error: " + (error.response?.status ? `Status ${error.response.status}` : error.message));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// src/core/context-builder.ts
|
|
327
|
-
import * as fs from "fs";
|
|
328
|
-
import * as path from "path";
|
|
329
|
-
var IGNORE_DIRS = ["node_modules", ".git", "dist", "build", ".dart_tool", ".idea", ".gradle", ".pub-cache"];
|
|
330
|
-
var MAX_DEPTH = 3;
|
|
331
|
-
async function buildLocalContext(rootDir) {
|
|
332
|
-
const tree = await getDirectoryTree(rootDir, 0);
|
|
333
|
-
return `Working Directory: ${rootDir}
|
|
334
|
-
|
|
335
|
-
File Tree:
|
|
336
|
-
${tree}`;
|
|
337
|
-
}
|
|
338
|
-
async function getDirectoryTree(dir, depth) {
|
|
339
|
-
if (depth >= MAX_DEPTH) return "";
|
|
340
|
-
let result = "";
|
|
341
|
-
try {
|
|
342
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
343
|
-
for (const entry of entries) {
|
|
344
|
-
if (IGNORE_DIRS.includes(entry.name)) continue;
|
|
345
|
-
if (entry.name.startsWith(".") && depth === 0) continue;
|
|
346
|
-
const indent = " ".repeat(depth);
|
|
347
|
-
const fullPath = path.join(dir, entry.name);
|
|
348
|
-
if (entry.isDirectory()) {
|
|
349
|
-
result += `${indent}${entry.name}/
|
|
350
|
-
`;
|
|
351
|
-
result += await getDirectoryTree(fullPath, depth + 1);
|
|
352
|
-
} else {
|
|
353
|
-
result += `${indent}${entry.name}
|
|
354
|
-
`;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
} catch (e) {
|
|
358
|
-
}
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
async function readFileContent(filePath) {
|
|
362
|
-
try {
|
|
363
|
-
return await fs.promises.readFile(path.resolve(filePath), "utf-8");
|
|
364
|
-
} catch (e) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// src/core/project-map.ts
|
|
370
|
-
import * as fs2 from "fs/promises";
|
|
371
|
-
import * as path2 from "path";
|
|
372
|
-
import * as os from "os";
|
|
373
|
-
var BOB_DIR = path2.join(os.homedir(), ".bob");
|
|
374
|
-
var PROJECTS_DIR = path2.join(BOB_DIR, "projects");
|
|
375
|
-
function getProjectName(workingDir) {
|
|
376
|
-
return path2.basename(workingDir);
|
|
377
|
-
}
|
|
378
|
-
function getProjectDir(workingDir) {
|
|
379
|
-
const name = getProjectName(workingDir);
|
|
380
|
-
return path2.join(PROJECTS_DIR, name);
|
|
381
|
-
}
|
|
382
|
-
async function ensureProjectStructure(workingDir) {
|
|
383
|
-
const projectDir = getProjectDir(workingDir);
|
|
384
|
-
const conversationsDir = path2.join(projectDir, "conversations");
|
|
385
|
-
const analysisDir = path2.join(projectDir, "analysis");
|
|
386
|
-
const runsDir = path2.join(analysisDir, "runs");
|
|
387
|
-
for (const dir of [BOB_DIR, PROJECTS_DIR, projectDir, conversationsDir, analysisDir, runsDir]) {
|
|
388
|
-
await fs2.mkdir(dir, { recursive: true });
|
|
389
|
-
}
|
|
390
|
-
const metaPath = path2.join(projectDir, "project.json");
|
|
391
|
-
try {
|
|
392
|
-
await fs2.access(metaPath);
|
|
393
|
-
} catch (error) {
|
|
394
|
-
}
|
|
395
|
-
try {
|
|
396
|
-
await fs2.access(metaPath);
|
|
397
|
-
} catch (error) {
|
|
398
|
-
}
|
|
399
|
-
const meta = {
|
|
400
|
-
name: getProjectName(workingDir),
|
|
401
|
-
path: workingDir,
|
|
402
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
403
|
-
lastIndexed: null
|
|
404
|
-
};
|
|
405
|
-
await fs2.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
406
|
-
return { projectDir, conversationsDir, analysisDir, runsDir };
|
|
407
|
-
}
|
|
408
|
-
async function createAnalysisRun(workingDir, files) {
|
|
409
|
-
const { runsDir } = await ensureProjectStructure(workingDir);
|
|
410
|
-
const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
411
|
-
const runDir = path2.join(runsDir, runId);
|
|
412
|
-
const tasksDir = path2.join(runDir, "tasks");
|
|
413
|
-
await fs2.mkdir(runDir, { recursive: true });
|
|
414
|
-
await fs2.mkdir(tasksDir, { recursive: true });
|
|
415
|
-
const manifest = {
|
|
416
|
-
runId,
|
|
417
|
-
status: "in_progress",
|
|
418
|
-
totalFiles: files.length,
|
|
419
|
-
completedFiles: 0,
|
|
420
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
421
|
-
projectPath: workingDir
|
|
422
|
-
};
|
|
423
|
-
await fs2.writeFile(path2.join(runDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
424
|
-
const taskPromises = [];
|
|
425
|
-
for (const filePath of files) {
|
|
426
|
-
const taskId = filePath.replace(/[\/\\]/g, "_");
|
|
427
|
-
const task = {
|
|
428
|
-
filePath,
|
|
429
|
-
status: false,
|
|
430
|
-
summary: null,
|
|
431
|
-
dependencies: [],
|
|
432
|
-
error: null
|
|
433
|
-
};
|
|
434
|
-
taskPromises.push(fs2.writeFile(path2.join(tasksDir, `${taskId}.json`), JSON.stringify(task, null, 2)));
|
|
435
|
-
}
|
|
436
|
-
await Promise.all(taskPromises);
|
|
437
|
-
return { runId, runDir, tasksDir };
|
|
438
|
-
}
|
|
439
|
-
async function completeTask(tasksDir, filePath, summary) {
|
|
440
|
-
const taskId = filePath.replace(/[\/\\]/g, "_");
|
|
441
|
-
const taskPath = path2.join(tasksDir, `${taskId}.json`);
|
|
442
|
-
try {
|
|
443
|
-
await fs2.access(taskPath);
|
|
444
|
-
const content = await fs2.readFile(taskPath, "utf-8");
|
|
445
|
-
const task = JSON.parse(content);
|
|
446
|
-
task.status = true;
|
|
447
|
-
task.summary = summary;
|
|
448
|
-
await fs2.writeFile(taskPath, JSON.stringify(task, null, 2));
|
|
449
|
-
} catch (e) {
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
async function updateManifestProgress(runDir, completedFiles, status) {
|
|
453
|
-
const manifestPath = path2.join(runDir, "manifest.json");
|
|
454
|
-
try {
|
|
455
|
-
await fs2.access(manifestPath);
|
|
456
|
-
const content = await fs2.readFile(manifestPath, "utf-8");
|
|
457
|
-
const manifest = JSON.parse(content);
|
|
458
|
-
manifest.completedFiles = completedFiles;
|
|
459
|
-
if (status) manifest.status = status;
|
|
460
|
-
await fs2.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
461
|
-
} catch (e) {
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
async function saveSummaries(workingDir, summaries) {
|
|
465
|
-
const { analysisDir } = await ensureProjectStructure(workingDir);
|
|
466
|
-
await fs2.writeFile(path2.join(analysisDir, "summaries.json"), JSON.stringify(summaries, null, 2));
|
|
467
|
-
const projectDir = getProjectDir(workingDir);
|
|
468
|
-
const metaPath = path2.join(projectDir, "project.json");
|
|
469
|
-
try {
|
|
470
|
-
await fs2.access(metaPath);
|
|
471
|
-
const content = await fs2.readFile(metaPath, "utf-8");
|
|
472
|
-
const meta = JSON.parse(content);
|
|
473
|
-
meta.lastIndexed = (/* @__PURE__ */ new Date()).toISOString();
|
|
474
|
-
await fs2.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
475
|
-
} catch (e) {
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
async function saveDependencies(workingDir, dependencies) {
|
|
479
|
-
const { analysisDir } = await ensureProjectStructure(workingDir);
|
|
480
|
-
await fs2.writeFile(path2.join(analysisDir, "dependencies.json"), JSON.stringify(dependencies, null, 2));
|
|
481
|
-
}
|
|
482
|
-
async function loadSummaries(workingDir) {
|
|
483
|
-
const { analysisDir } = await ensureProjectStructure(workingDir);
|
|
484
|
-
const summariesPath = path2.join(analysisDir, "summaries.json");
|
|
485
|
-
try {
|
|
486
|
-
await fs2.access(summariesPath);
|
|
487
|
-
const content = await fs2.readFile(summariesPath, "utf-8");
|
|
488
|
-
return JSON.parse(content);
|
|
489
|
-
} catch (e) {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
async function loadDependencies(workingDir) {
|
|
494
|
-
const { analysisDir } = await ensureProjectStructure(workingDir);
|
|
495
|
-
const depsPath = path2.join(analysisDir, "dependencies.json");
|
|
496
|
-
try {
|
|
497
|
-
await fs2.access(depsPath);
|
|
498
|
-
const content = await fs2.readFile(depsPath, "utf-8");
|
|
499
|
-
return JSON.parse(content);
|
|
500
|
-
} catch (e) {
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// src/core/file-writer.ts
|
|
506
|
-
import * as fs3 from "fs";
|
|
507
|
-
import * as path3 from "path";
|
|
508
|
-
import * as readline2 from "readline";
|
|
509
|
-
import chalk2 from "chalk";
|
|
510
|
-
function extractProposedFile(response) {
|
|
511
|
-
const codeBlockRegex = /```[\w]*\n([\s\S]*?)```/;
|
|
512
|
-
const match = response.match(codeBlockRegex);
|
|
513
|
-
if (!match) return null;
|
|
514
|
-
const codeContent = match[1].trim();
|
|
515
|
-
const lines = codeContent.split("\n");
|
|
516
|
-
if (lines.length === 0) return null;
|
|
517
|
-
const firstLine = lines[0].trim();
|
|
518
|
-
let filePathMatch = firstLine.match(/^\/\/\s*File:\s*(.+)$/);
|
|
519
|
-
if (!filePathMatch) {
|
|
520
|
-
filePathMatch = firstLine.match(/^\/\/\s*([\w\-\.\/\\]+\.\w+)\s*$/);
|
|
521
|
-
}
|
|
522
|
-
if (!filePathMatch) {
|
|
523
|
-
filePathMatch = firstLine.match(/^#\s*File:\s*(.+)$/);
|
|
524
|
-
}
|
|
525
|
-
if (!filePathMatch) {
|
|
526
|
-
filePathMatch = firstLine.match(/^#\s*([\w\-\.\/\\]+\.\w+)\s*$/);
|
|
527
|
-
}
|
|
528
|
-
if (!filePathMatch) return null;
|
|
529
|
-
const filePath = filePathMatch[1].trim();
|
|
530
|
-
if (!filePath.includes("/") && !filePath.includes("\\")) return null;
|
|
531
|
-
if (!filePath.includes(".")) return null;
|
|
532
|
-
const fileContent = lines.slice(1).join("\n").trim();
|
|
533
|
-
const absolutePath = path3.join(process.cwd(), filePath);
|
|
534
|
-
const isNew = !fs3.existsSync(absolutePath);
|
|
535
|
-
return {
|
|
536
|
-
filePath,
|
|
537
|
-
content: fileContent,
|
|
538
|
-
isNew
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
function stripCodeBlockFromResponse(response) {
|
|
542
|
-
return response.replace(/```[\w]*\n[\s\S]*?```/g, "").trim();
|
|
543
|
-
}
|
|
544
|
-
async function proposeAndWriteFile(proposed) {
|
|
545
|
-
const absolutePath = path3.join(process.cwd(), proposed.filePath);
|
|
546
|
-
const action = proposed.isNew ? "CREATE" : "UPDATE";
|
|
547
|
-
const icon = proposed.isNew ? "\u{1F4C4}" : "\u270F\uFE0F";
|
|
548
|
-
const color = proposed.isNew ? chalk2.green : chalk2.yellow;
|
|
549
|
-
const totalLines = proposed.content.split("\n").length;
|
|
550
|
-
console.log("");
|
|
551
|
-
console.log(color(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`));
|
|
552
|
-
console.log(color(` \u2502 ${icon} ${action}: ${proposed.filePath} (${totalLines} lines)`));
|
|
553
|
-
console.log(color(` \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524`));
|
|
554
|
-
const previewLines = proposed.content.split("\n").slice(0, 6);
|
|
555
|
-
for (const line of previewLines) {
|
|
556
|
-
console.log(chalk2.gray(` \u2502 ${line}`));
|
|
557
|
-
}
|
|
558
|
-
if (totalLines > 6) {
|
|
559
|
-
console.log(chalk2.gray(` \u2502 ... (${totalLines - 6} more lines)`));
|
|
560
|
-
}
|
|
561
|
-
console.log(color(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`));
|
|
562
|
-
console.log("");
|
|
563
|
-
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
564
|
-
const answer = await new Promise((resolve2) => {
|
|
565
|
-
rl.question(chalk2.cyan(` \u{1F4BE} ${action === "CREATE" ? "Write this file" : "Apply changes"}? (y/n/path): `), resolve2);
|
|
566
|
-
});
|
|
567
|
-
rl.close();
|
|
568
|
-
const trimmed = answer.trim().toLowerCase();
|
|
569
|
-
if (trimmed === "n" || trimmed === "no") {
|
|
570
|
-
console.log(chalk2.gray(" \u23ED\uFE0F Skipped."));
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
let targetPath = absolutePath;
|
|
574
|
-
if (trimmed !== "y" && trimmed !== "yes" && trimmed.length > 0) {
|
|
575
|
-
targetPath = path3.join(process.cwd(), trimmed);
|
|
576
|
-
}
|
|
577
|
-
try {
|
|
578
|
-
const dir = path3.dirname(targetPath);
|
|
579
|
-
if (!fs3.existsSync(dir)) {
|
|
580
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
581
|
-
}
|
|
582
|
-
if (!proposed.isNew && fs3.existsSync(targetPath)) {
|
|
583
|
-
const backupDir = path3.join(process.cwd(), ".bob-backups");
|
|
584
|
-
if (!fs3.existsSync(backupDir)) fs3.mkdirSync(backupDir, { recursive: true });
|
|
585
|
-
const timestamp = Date.now();
|
|
586
|
-
const backupName = proposed.filePath.replace(/[\/\\]/g, "_") + `.${timestamp}.bak`;
|
|
587
|
-
fs3.copyFileSync(targetPath, path3.join(backupDir, backupName));
|
|
588
|
-
}
|
|
589
|
-
fs3.writeFileSync(targetPath, proposed.content, "utf-8");
|
|
590
|
-
const relativePath = path3.relative(process.cwd(), targetPath);
|
|
591
|
-
console.log(chalk2.green(` \u2705 Written: ${relativePath}`));
|
|
592
|
-
if (!proposed.isNew) {
|
|
593
|
-
console.log(chalk2.gray(` \u{1F4E6} Backup saved to .bob-backups/`));
|
|
594
|
-
}
|
|
595
|
-
console.log("");
|
|
596
|
-
return true;
|
|
597
|
-
} catch (error) {
|
|
598
|
-
console.log(chalk2.red(` \u274C Write failed: ${error.message}`));
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export {
|
|
604
|
-
getConfig,
|
|
605
|
-
setConfigValue,
|
|
606
|
-
getConfigPath,
|
|
607
|
-
registerLoginCommand,
|
|
608
|
-
callCloudFunction,
|
|
609
|
-
callLocalModel,
|
|
610
|
-
buildLocalContext,
|
|
611
|
-
readFileContent,
|
|
612
|
-
getProjectName,
|
|
613
|
-
ensureProjectStructure,
|
|
614
|
-
createAnalysisRun,
|
|
615
|
-
completeTask,
|
|
616
|
-
updateManifestProgress,
|
|
617
|
-
saveSummaries,
|
|
618
|
-
saveDependencies,
|
|
619
|
-
loadSummaries,
|
|
620
|
-
loadDependencies,
|
|
621
|
-
extractProposedFile,
|
|
622
|
-
stripCodeBlockFromResponse,
|
|
623
|
-
proposeAndWriteFile
|
|
624
|
-
};
|