letmecook 0.0.21 → 0.0.23
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/index.ts +90 -256
- package/package.json +7 -2
- package/src/agents-md.ts +12 -15
- package/src/chat-logger.ts +220 -0
- package/src/chat-mode.ts +465 -0
- package/src/cli-mode.ts +366 -0
- package/src/config-builder.ts +147 -0
- package/src/env.ts +76 -0
- package/src/flows/add-repos.ts +51 -115
- package/src/flows/chat-to-config.ts +373 -0
- package/src/flows/new-session.ts +69 -145
- package/src/flows/resume-session.ts +33 -37
- package/src/git.ts +39 -77
- package/src/naming.ts +2 -2
- package/src/prompts/chat-prompt.ts +143 -0
- package/src/schemas.ts +82 -0
- package/src/splash.ts +199 -0
- package/src/tui-mode.ts +41 -0
- package/src/types.ts +16 -78
- package/src/ui/add-repos.ts +34 -26
- package/src/ui/agent-proposal.ts +13 -1
- package/src/ui/chat-confirmation.ts +151 -0
- package/src/ui/chat-with-sidebar.ts +524 -0
- package/src/ui/common/clipboard.ts +105 -0
- package/src/ui/common/keyboard.ts +7 -0
- package/src/ui/common/repo-formatter.ts +4 -4
- package/src/ui/cooking-indicator.ts +88 -0
- package/src/ui/main-menu.ts +8 -0
- package/src/ui/new-session.ts +2 -2
- package/src/ui/progress.ts +1 -1
- package/src/ui/renderer.ts +7 -14
- package/src/ui/session-settings.ts +4 -3
- package/src/validation.ts +152 -0
- package/src/reference-repo.ts +0 -288
package/src/cli-mode.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { parseRepoSpec, type RepoSpec } from "./types";
|
|
2
|
+
import {
|
|
3
|
+
listSessions,
|
|
4
|
+
getSession,
|
|
5
|
+
updateLastAccessed,
|
|
6
|
+
deleteAllSessions,
|
|
7
|
+
deleteSession,
|
|
8
|
+
} from "./sessions";
|
|
9
|
+
import { createRenderer, destroyRenderer } from "./ui/renderer";
|
|
10
|
+
import { showNewSessionPrompt } from "./ui/new-session";
|
|
11
|
+
import { showSessionList } from "./ui/list";
|
|
12
|
+
import { showNukeConfirm } from "./ui/confirm-nuke";
|
|
13
|
+
import { createNewSession, resumeSession } from "./flows";
|
|
14
|
+
import { ChatLogger } from "./chat-logger";
|
|
15
|
+
|
|
16
|
+
export function printCLIUsage(): void {
|
|
17
|
+
console.log(`
|
|
18
|
+
letmecook CLI mode
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
letmecook --cli <owner/repo> [owner/repo:branch...] Create or resume a session
|
|
22
|
+
letmecook --cli --list List all sessions
|
|
23
|
+
letmecook --cli --resume <session-name> Resume a session
|
|
24
|
+
letmecook --cli --delete <session-name> Delete a session
|
|
25
|
+
letmecook --cli --nuke [--yes] Nuke everything
|
|
26
|
+
letmecook --cli --logs List chat logs
|
|
27
|
+
letmecook --cli --logs --view <log-id> View chat log
|
|
28
|
+
letmecook --cli --logs --delete <log-id> Delete chat log
|
|
29
|
+
letmecook --cli --logs --nuke [--yes] Delete all chat logs
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
letmecook --cli microsoft/playwright
|
|
33
|
+
letmecook --cli facebook/react openai/agents
|
|
34
|
+
letmecook --cli --resume playwright-agent-tests
|
|
35
|
+
letmecook --cli --logs
|
|
36
|
+
letmecook --cli --logs --view 1700000000000-abc123
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function handleNewSessionCLI(repos: RepoSpec[]): Promise<void> {
|
|
41
|
+
const renderer = await createRenderer();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const { goal, cancelled } = await showNewSessionPrompt(renderer, repos);
|
|
45
|
+
|
|
46
|
+
if (cancelled) {
|
|
47
|
+
destroyRenderer();
|
|
48
|
+
console.log("\nCancelled.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await createNewSession(renderer, {
|
|
53
|
+
repos,
|
|
54
|
+
goal,
|
|
55
|
+
mode: "cli",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!result) {
|
|
59
|
+
destroyRenderer();
|
|
60
|
+
console.log("\nCancelled.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { session, skipped } = result;
|
|
65
|
+
|
|
66
|
+
if (skipped) {
|
|
67
|
+
destroyRenderer();
|
|
68
|
+
console.log(`\nResuming existing session: ${session.name}\n`);
|
|
69
|
+
await resumeSession(renderer, {
|
|
70
|
+
session,
|
|
71
|
+
mode: "cli",
|
|
72
|
+
initialRefresh: true,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
destroyRenderer();
|
|
78
|
+
console.log(`\nSession created: ${session.name}`);
|
|
79
|
+
console.log(`Path: ${session.path}\n`);
|
|
80
|
+
|
|
81
|
+
await resumeSession(renderer, {
|
|
82
|
+
session,
|
|
83
|
+
mode: "cli",
|
|
84
|
+
initialRefresh: false,
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
destroyRenderer();
|
|
88
|
+
console.error("\nError:", error instanceof Error ? error.message : error);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function handleList(): Promise<void> {
|
|
94
|
+
const renderer = await createRenderer();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const sessions = await listSessions();
|
|
99
|
+
const action = await showSessionList(renderer, sessions);
|
|
100
|
+
|
|
101
|
+
switch (action.type) {
|
|
102
|
+
case "resume":
|
|
103
|
+
destroyRenderer();
|
|
104
|
+
await updateLastAccessed(action.session.name);
|
|
105
|
+
console.log(`\nResuming session: ${action.session.name}\n`);
|
|
106
|
+
await resumeSession(renderer, {
|
|
107
|
+
session: action.session,
|
|
108
|
+
mode: "cli",
|
|
109
|
+
initialRefresh: true,
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
|
|
113
|
+
case "delete":
|
|
114
|
+
console.log("[TODO] Delete session flow");
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case "nuke": {
|
|
118
|
+
const choice = await showNukeConfirm(renderer, sessions.length);
|
|
119
|
+
if (choice === "confirm") {
|
|
120
|
+
const count = await deleteAllSessions();
|
|
121
|
+
destroyRenderer();
|
|
122
|
+
console.log(`\nNuked ${count} session(s) and all data.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "quit":
|
|
129
|
+
destroyRenderer();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
destroyRenderer();
|
|
135
|
+
console.error("\nError:", error instanceof Error ? error.message : error);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function handleResume(sessionName: string): Promise<void> {
|
|
141
|
+
const session = await getSession(sessionName);
|
|
142
|
+
|
|
143
|
+
if (!session) {
|
|
144
|
+
console.error(`Session not found: ${sessionName}`);
|
|
145
|
+
console.log("\nAvailable sessions:");
|
|
146
|
+
const sessions = await listSessions();
|
|
147
|
+
if (sessions.length === 0) {
|
|
148
|
+
console.log(" (none)");
|
|
149
|
+
} else {
|
|
150
|
+
sessions.forEach((s) => console.log(` - ${s.name}`));
|
|
151
|
+
}
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await updateLastAccessed(session.name);
|
|
156
|
+
console.log(`\nResuming session: ${session.name}\n`);
|
|
157
|
+
|
|
158
|
+
const renderer = await createRenderer();
|
|
159
|
+
await resumeSession(renderer, {
|
|
160
|
+
session,
|
|
161
|
+
mode: "cli",
|
|
162
|
+
initialRefresh: true,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function handleDelete(sessionName: string): Promise<void> {
|
|
167
|
+
const session = await getSession(sessionName);
|
|
168
|
+
|
|
169
|
+
if (!session) {
|
|
170
|
+
console.error(`Session not found: ${sessionName}`);
|
|
171
|
+
const sessions = await listSessions();
|
|
172
|
+
if (sessions.length > 0) {
|
|
173
|
+
console.log("\nAvailable sessions:");
|
|
174
|
+
sessions.forEach((s) => console.log(` - ${s.name}`));
|
|
175
|
+
}
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const deleted = await deleteSession(sessionName);
|
|
180
|
+
if (deleted) {
|
|
181
|
+
console.log(`Deleted session: ${sessionName}`);
|
|
182
|
+
} else {
|
|
183
|
+
console.error(`Failed to delete session: ${sessionName}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function handleNuke(skipConfirm = false): Promise<void> {
|
|
189
|
+
const sessions = await listSessions();
|
|
190
|
+
if (sessions.length === 0) {
|
|
191
|
+
console.log("Nothing to nuke.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Skip confirmation if --yes flag or non-interactive (piped input)
|
|
196
|
+
if (skipConfirm || !process.stdin.isTTY) {
|
|
197
|
+
const count = await deleteAllSessions();
|
|
198
|
+
console.log(`Nuked ${count} session(s) and all data.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const renderer = await createRenderer();
|
|
203
|
+
const choice = await showNukeConfirm(renderer, sessions.length);
|
|
204
|
+
destroyRenderer();
|
|
205
|
+
|
|
206
|
+
if (choice === "confirm") {
|
|
207
|
+
const count = await deleteAllSessions();
|
|
208
|
+
console.log(`Nuked ${count} session(s) and all data.`);
|
|
209
|
+
} else {
|
|
210
|
+
console.log("Cancelled.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function handleChatLogsList(): Promise<void> {
|
|
215
|
+
const logs = await ChatLogger.listLogs();
|
|
216
|
+
if (logs.length === 0) {
|
|
217
|
+
console.log("No chat logs found.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log("Chat Logs:");
|
|
222
|
+
for (const log of logs) {
|
|
223
|
+
console.log(` ID: ${log.id}`);
|
|
224
|
+
console.log(` Created: ${log.createdAt}`);
|
|
225
|
+
console.log("");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function handleChatLogView(logId: string): Promise<void> {
|
|
230
|
+
const log = await ChatLogger.getLog(logId);
|
|
231
|
+
if (!log) {
|
|
232
|
+
console.error(`Chat log not found: ${logId}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(JSON.stringify(log, null, 2));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function handleChatLogDelete(logId: string): Promise<void> {
|
|
240
|
+
const log = await ChatLogger.getLog(logId);
|
|
241
|
+
if (!log) {
|
|
242
|
+
console.error(`Chat log not found: ${logId}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const deleted = await ChatLogger.deleteLog(logId);
|
|
247
|
+
if (deleted) {
|
|
248
|
+
console.log(`Deleted chat log: ${logId}`);
|
|
249
|
+
} else {
|
|
250
|
+
console.error(`Failed to delete chat log: ${logId}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function handleChatLogsNuke(skipConfirm = false): Promise<void> {
|
|
256
|
+
const logs = await ChatLogger.listLogs();
|
|
257
|
+
if (logs.length === 0) {
|
|
258
|
+
console.log("No chat logs to delete.");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Skip confirmation if --yes flag or non-interactive (piped input)
|
|
263
|
+
if (skipConfirm || !process.stdin.isTTY) {
|
|
264
|
+
const count = await ChatLogger.deleteAllLogs();
|
|
265
|
+
console.log(`Deleted ${count} chat log(s).`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Prompt for confirmation
|
|
270
|
+
process.stdout.write(`Delete all ${logs.length} chat logs? [y/N] `);
|
|
271
|
+
const response = await new Promise<string>((resolve) => {
|
|
272
|
+
process.stdin.once("data", (data) => {
|
|
273
|
+
resolve(data.toString().trim().toLowerCase());
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (response === "y" || response === "yes") {
|
|
278
|
+
const count = await ChatLogger.deleteAllLogs();
|
|
279
|
+
console.log(`\nDeleted ${count} chat log(s).`);
|
|
280
|
+
} else {
|
|
281
|
+
console.log("Cancelled.");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function parseRepos(args: string[]): RepoSpec[] {
|
|
286
|
+
const repos: RepoSpec[] = [];
|
|
287
|
+
|
|
288
|
+
for (const arg of args) {
|
|
289
|
+
if (!arg || arg.startsWith("-")) continue;
|
|
290
|
+
|
|
291
|
+
if (!arg.includes("/")) {
|
|
292
|
+
throw new Error(`Invalid repo format: ${arg} (expected owner/repo)`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const repo = parseRepoSpec(arg);
|
|
296
|
+
repos.push(repo);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return repos;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function handleCLIMode(args: string[]): Promise<void> {
|
|
303
|
+
const firstArg = args[0];
|
|
304
|
+
|
|
305
|
+
if (firstArg === "--list" || firstArg === "-l") {
|
|
306
|
+
await handleList();
|
|
307
|
+
} else if (firstArg === "--resume" || firstArg === "-r") {
|
|
308
|
+
const sessionName = args[1];
|
|
309
|
+
if (!sessionName) {
|
|
310
|
+
console.error("Missing session name. Usage: letmecook --cli --resume <session-name>");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
await handleResume(sessionName);
|
|
314
|
+
} else if (firstArg === "--delete" || firstArg === "-d") {
|
|
315
|
+
const sessionName = args[1];
|
|
316
|
+
if (!sessionName) {
|
|
317
|
+
console.error("Missing session name. Usage: letmecook --cli --delete <session-name>");
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
await handleDelete(sessionName);
|
|
321
|
+
} else if (firstArg === "--nuke") {
|
|
322
|
+
const hasYes = args.includes("--yes") || args.includes("-y");
|
|
323
|
+
await handleNuke(hasYes);
|
|
324
|
+
} else if (firstArg === "--logs") {
|
|
325
|
+
const secondArg = args[1];
|
|
326
|
+
if (secondArg === "--view") {
|
|
327
|
+
const logId = args[2];
|
|
328
|
+
if (!logId) {
|
|
329
|
+
console.error("Missing log ID. Usage: letmecook --cli --logs --view <log-id>");
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
await handleChatLogView(logId);
|
|
333
|
+
} else if (secondArg === "--delete") {
|
|
334
|
+
const logId = args[2];
|
|
335
|
+
if (!logId) {
|
|
336
|
+
console.error("Missing log ID. Usage: letmecook --cli --logs --delete <log-id>");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
await handleChatLogDelete(logId);
|
|
340
|
+
} else if (secondArg === "--nuke") {
|
|
341
|
+
const hasYes = args.includes("--yes") || args.includes("-y");
|
|
342
|
+
await handleChatLogsNuke(hasYes);
|
|
343
|
+
} else {
|
|
344
|
+
await handleChatLogsList();
|
|
345
|
+
}
|
|
346
|
+
} else if (!firstArg || firstArg.startsWith("-")) {
|
|
347
|
+
// No args or unknown flag after --cli
|
|
348
|
+
if (firstArg?.startsWith("-")) {
|
|
349
|
+
console.error(`Unknown CLI option: ${firstArg}`);
|
|
350
|
+
}
|
|
351
|
+
printCLIUsage();
|
|
352
|
+
process.exit(firstArg ? 1 : 0);
|
|
353
|
+
} else {
|
|
354
|
+
try {
|
|
355
|
+
const repos = parseRepos(args);
|
|
356
|
+
if (repos.length === 0) {
|
|
357
|
+
printCLIUsage();
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
await handleNewSessionCLI(repos);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type { ChatConfig } from "./flows/chat-to-config";
|
|
3
|
+
|
|
4
|
+
export interface PartialConfig {
|
|
5
|
+
repos: string[];
|
|
6
|
+
skills: string[];
|
|
7
|
+
goal: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ConfigBuilderEvents {
|
|
11
|
+
"config-changed": (config: PartialConfig) => void;
|
|
12
|
+
"repo-added": (repo: string) => void;
|
|
13
|
+
"repo-removed": (repo: string) => void;
|
|
14
|
+
"skill-added": (skill: string) => void;
|
|
15
|
+
"goal-set": (goal: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ConfigBuilder extends EventEmitter {
|
|
19
|
+
private _config: PartialConfig;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this._config = {
|
|
24
|
+
repos: [],
|
|
25
|
+
skills: [],
|
|
26
|
+
goal: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get config(): PartialConfig {
|
|
31
|
+
return { ...this._config };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Add a repository to the config
|
|
36
|
+
* @returns true if added, false if already exists
|
|
37
|
+
*/
|
|
38
|
+
addRepo(repo: string): boolean {
|
|
39
|
+
const normalized = repo.trim();
|
|
40
|
+
if (!normalized) return false;
|
|
41
|
+
|
|
42
|
+
// Check if already exists
|
|
43
|
+
if (this._config.repos.includes(normalized)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this._config.repos.push(normalized);
|
|
48
|
+
this.emit("repo-added", normalized);
|
|
49
|
+
this.emit("config-changed", this.config);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove a repository from the config
|
|
55
|
+
* @returns true if removed, false if not found
|
|
56
|
+
*/
|
|
57
|
+
removeRepo(repo: string): boolean {
|
|
58
|
+
const normalized = repo.trim();
|
|
59
|
+
const index = this._config.repos.indexOf(normalized);
|
|
60
|
+
|
|
61
|
+
if (index === -1) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._config.repos.splice(index, 1);
|
|
66
|
+
this.emit("repo-removed", normalized);
|
|
67
|
+
this.emit("config-changed", this.config);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add a skill to the config
|
|
73
|
+
* @returns true if added, false if already exists
|
|
74
|
+
*/
|
|
75
|
+
addSkill(skill: string): boolean {
|
|
76
|
+
const normalized = skill.trim();
|
|
77
|
+
if (!normalized) return false;
|
|
78
|
+
|
|
79
|
+
// Check if already exists
|
|
80
|
+
if (this._config.skills.includes(normalized)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this._config.skills.push(normalized);
|
|
85
|
+
this.emit("skill-added", normalized);
|
|
86
|
+
this.emit("config-changed", this.config);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove a skill from the config
|
|
92
|
+
* @returns true if removed, false if not found
|
|
93
|
+
*/
|
|
94
|
+
removeSkill(skill: string): boolean {
|
|
95
|
+
const normalized = skill.trim();
|
|
96
|
+
const index = this._config.skills.indexOf(normalized);
|
|
97
|
+
|
|
98
|
+
if (index === -1) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._config.skills.splice(index, 1);
|
|
103
|
+
this.emit("config-changed", this.config);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set the goal for the config
|
|
109
|
+
*/
|
|
110
|
+
setGoal(goal: string): void {
|
|
111
|
+
const normalized = goal.trim();
|
|
112
|
+
this._config.goal = normalized || null;
|
|
113
|
+
this.emit("goal-set", normalized);
|
|
114
|
+
this.emit("config-changed", this.config);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if the config has minimum requirements to proceed
|
|
119
|
+
* Currently requires at least one repo
|
|
120
|
+
*/
|
|
121
|
+
isReady(): boolean {
|
|
122
|
+
return this._config.repos.length > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert to final ChatConfig format
|
|
127
|
+
*/
|
|
128
|
+
toFinalConfig(): ChatConfig {
|
|
129
|
+
return {
|
|
130
|
+
repos: [...this._config.repos],
|
|
131
|
+
skills: [...this._config.skills],
|
|
132
|
+
goal: this._config.goal || "",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reset the config to initial state
|
|
138
|
+
*/
|
|
139
|
+
reset(): void {
|
|
140
|
+
this._config = {
|
|
141
|
+
repos: [],
|
|
142
|
+
skills: [],
|
|
143
|
+
goal: null,
|
|
144
|
+
};
|
|
145
|
+
this.emit("config-changed", this.config);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const LETMECOOK_DIR = join(homedir(), ".letmecook");
|
|
5
|
+
|
|
6
|
+
export async function loadEnvAsync(): Promise<void> {
|
|
7
|
+
// Check if AI_GATEWAY_API_KEY is already set
|
|
8
|
+
if (process.env.AI_GATEWAY_API_KEY) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Try loading from ~/.letmecook/.env
|
|
13
|
+
const globalEnvPath = join(LETMECOOK_DIR, ".env");
|
|
14
|
+
const globalEnvFile = Bun.file(globalEnvPath);
|
|
15
|
+
|
|
16
|
+
// Try loading from ./.env (current directory)
|
|
17
|
+
const localEnvPath = join(process.cwd(), ".env");
|
|
18
|
+
const localEnvFile = Bun.file(localEnvPath);
|
|
19
|
+
|
|
20
|
+
// Load global first, then local (local overrides)
|
|
21
|
+
try {
|
|
22
|
+
if (await globalEnvFile.exists()) {
|
|
23
|
+
const content = await globalEnvFile.text();
|
|
24
|
+
parseAndSetEnv(content);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore errors
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (await localEnvFile.exists()) {
|
|
32
|
+
const content = await localEnvFile.text();
|
|
33
|
+
parseAndSetEnv(content);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadEnv(): void {
|
|
41
|
+
// Synchronous version - fire and forget
|
|
42
|
+
loadEnvAsync().catch(() => {
|
|
43
|
+
// Ignore errors
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseAndSetEnv(content: string): void {
|
|
48
|
+
for (const line of content.split("\n")) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
|
|
51
|
+
// Skip comments and empty lines
|
|
52
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const equalIndex = trimmed.indexOf("=");
|
|
57
|
+
if (equalIndex === -1) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const key = trimmed.slice(0, equalIndex).trim();
|
|
62
|
+
let value = trimmed.slice(equalIndex + 1).trim();
|
|
63
|
+
|
|
64
|
+
// Remove quotes if present
|
|
65
|
+
if (
|
|
66
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
67
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
68
|
+
) {
|
|
69
|
+
value = value.slice(1, -1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (key) {
|
|
73
|
+
process.env[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|