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/flows/add-repos.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { writeAgentsMd } from "../agents-md";
|
|
|
6
6
|
import { recordRepoHistory } from "../repo-history";
|
|
7
7
|
import { showAddReposPrompt } from "../ui/add-repos";
|
|
8
8
|
import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
|
|
9
|
-
import { ensureReferenceRepo, linkReferenceRepo, ensureReferencesDir } from "../reference-repo";
|
|
10
9
|
import { rm } from "node:fs/promises";
|
|
11
10
|
|
|
12
11
|
export interface AddReposParams {
|
|
@@ -20,10 +19,7 @@ export interface AddReposResult {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
|
|
23
|
-
|
|
24
|
-
const regularRepos = repos.filter((repo) => !repo.reference);
|
|
25
|
-
|
|
26
|
-
return regularRepos.map((repo) => {
|
|
22
|
+
return repos.map((repo) => {
|
|
27
23
|
const url = `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
28
24
|
const targetDir = join(sessionPath, repo.dir);
|
|
29
25
|
const args = repo.branch
|
|
@@ -49,34 +45,6 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
|
|
|
49
45
|
});
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
async function setupReferenceRepos(
|
|
53
|
-
repos: RepoSpec[],
|
|
54
|
-
sessionPath: string,
|
|
55
|
-
): Promise<{ successful: RepoSpec[]; failed: Array<{ repo: RepoSpec; error: string }> }> {
|
|
56
|
-
const referenceRepos = repos.filter((repo) => repo.reference);
|
|
57
|
-
const successful: RepoSpec[] = [];
|
|
58
|
-
const failed: Array<{ repo: RepoSpec; error: string }> = [];
|
|
59
|
-
|
|
60
|
-
if (referenceRepos.length === 0) {
|
|
61
|
-
return { successful, failed };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await ensureReferencesDir();
|
|
65
|
-
|
|
66
|
-
for (const repo of referenceRepos) {
|
|
67
|
-
try {
|
|
68
|
-
await ensureReferenceRepo(repo);
|
|
69
|
-
await linkReferenceRepo(repo, sessionPath);
|
|
70
|
-
successful.push(repo);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
73
|
-
failed.push({ repo, error: errorMsg });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { successful, failed };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
48
|
export async function addReposFlow(params: AddReposParams): Promise<AddReposResult> {
|
|
81
49
|
const { renderer, session } = params;
|
|
82
50
|
|
|
@@ -87,102 +55,70 @@ export async function addReposFlow(params: AddReposParams): Promise<AddReposResu
|
|
|
87
55
|
const newRepos = addResult.repos.filter((r) => !existingSpecs.has(r.spec));
|
|
88
56
|
|
|
89
57
|
if (newRepos.length > 0) {
|
|
90
|
-
// Setup reference repos first (symlinks from cache)
|
|
91
|
-
const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
|
|
92
|
-
newRepos,
|
|
93
|
-
session.path,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
// Report reference repo failures
|
|
97
|
-
if (failedRefs.length > 0) {
|
|
98
|
-
console.error(`\n⚠️ ${failedRefs.length} reference repo(s) failed:`);
|
|
99
|
-
failedRefs.forEach(({ repo, error }) => {
|
|
100
|
-
console.error(` ✗ ${repo.owner}/${repo.name}: ${error}`);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Clone regular repos
|
|
105
58
|
const tasks = reposToCommandTasks(newRepos, session.path);
|
|
106
|
-
let results: Awaited<ReturnType<typeof runCommands>> = [];
|
|
107
|
-
|
|
108
|
-
if (tasks.length > 0) {
|
|
109
|
-
results = await runCommands(renderer, {
|
|
110
|
-
title: "Adding repositories",
|
|
111
|
-
tasks,
|
|
112
|
-
showOutput: true,
|
|
113
|
-
outputLines: 5,
|
|
114
|
-
allowAbort: true,
|
|
115
|
-
allowSkip: tasks.length > 1,
|
|
116
|
-
allowBackground: true,
|
|
117
|
-
sessionName: session.name,
|
|
118
|
-
});
|
|
119
59
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} catch {
|
|
131
|
-
// Ignore cleanup errors
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// Also clean up reference symlinks
|
|
135
|
-
for (const repo of successfulRefs) {
|
|
136
|
-
const repoPath = join(session.path, repo.dir);
|
|
137
|
-
try {
|
|
138
|
-
await rm(repoPath, { recursive: true, force: true });
|
|
139
|
-
} catch {
|
|
140
|
-
// Ignore cleanup errors
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return { session, cancelled: true };
|
|
144
|
-
}
|
|
60
|
+
const results = await runCommands(renderer, {
|
|
61
|
+
title: "Adding repositories",
|
|
62
|
+
tasks,
|
|
63
|
+
showOutput: true,
|
|
64
|
+
outputLines: 5,
|
|
65
|
+
allowAbort: true,
|
|
66
|
+
allowSkip: tasks.length > 1,
|
|
67
|
+
allowBackground: true,
|
|
68
|
+
sessionName: session.name,
|
|
69
|
+
});
|
|
145
70
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
71
|
+
// Handle aborted operation - return without changes
|
|
72
|
+
const wasAborted = results.some((r) => r.outcome === "aborted");
|
|
73
|
+
if (wasAborted) {
|
|
74
|
+
hideCommandRunner(renderer);
|
|
75
|
+
// Clean up any partially cloned repos
|
|
76
|
+
for (const repo of newRepos) {
|
|
77
|
+
const repoPath = join(session.path, repo.dir);
|
|
78
|
+
try {
|
|
79
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore cleanup errors
|
|
158
82
|
}
|
|
159
83
|
}
|
|
84
|
+
return { session, cancelled: true };
|
|
85
|
+
}
|
|
160
86
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
}
|
|
87
|
+
// Clean up skipped repo directories
|
|
88
|
+
const skippedResults = results.filter((r) => r.outcome === "skipped");
|
|
89
|
+
for (const skipped of skippedResults) {
|
|
90
|
+
const repoSpec = skipped.task.label.replace("Cloning ", "");
|
|
91
|
+
const repo = newRepos.find((r) => `${r.owner}/${r.name}` === repoSpec);
|
|
92
|
+
if (repo) {
|
|
93
|
+
const repoPath = join(session.path, repo.dir);
|
|
94
|
+
try {
|
|
95
|
+
await rm(repoPath, { recursive: true, force: true });
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore cleanup errors
|
|
98
|
+
}
|
|
171
99
|
}
|
|
172
100
|
}
|
|
173
101
|
|
|
174
|
-
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
175
|
-
hideCommandRunner(renderer);
|
|
176
|
-
|
|
177
102
|
// Filter to only successfully cloned repos
|
|
178
|
-
const
|
|
179
|
-
if (repo.reference) return false; // Reference repos handled separately
|
|
103
|
+
const successfulRepos = newRepos.filter((repo) => {
|
|
180
104
|
const result = results.find((r) => r.task.label === `Cloning ${repo.owner}/${repo.name}`);
|
|
181
105
|
return result?.outcome === "completed";
|
|
182
106
|
});
|
|
183
107
|
|
|
184
|
-
//
|
|
185
|
-
const
|
|
108
|
+
// Check for errors (not skipped/aborted)
|
|
109
|
+
const errors = results.filter((r) => r.outcome === "error");
|
|
110
|
+
if (errors.length > 0) {
|
|
111
|
+
console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
|
|
112
|
+
errors.forEach((err) => {
|
|
113
|
+
console.error(` ✗ ${err.task.label}`);
|
|
114
|
+
if (err.error) {
|
|
115
|
+
console.error(` ${err.error}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
121
|
+
hideCommandRunner(renderer);
|
|
186
122
|
|
|
187
123
|
if (successfulRepos.length > 0) {
|
|
188
124
|
const allRepos = [...session.repos, ...successfulRepos];
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { streamText, type ToolSet } from "ai";
|
|
2
|
+
import { parseRepoSpec, type RepoSpec } from "../schemas";
|
|
3
|
+
import { INCREMENTAL_CHAT_PROMPT, generateRepoHistorySection } from "../prompts/chat-prompt";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { ConfigBuilder } from "../config-builder";
|
|
6
|
+
import { listRepoHistory } from "../repo-history";
|
|
7
|
+
|
|
8
|
+
export interface ChatMessage {
|
|
9
|
+
role: "user" | "assistant" | "system";
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ChatConfig {
|
|
14
|
+
repos: string[];
|
|
15
|
+
skills: string[];
|
|
16
|
+
goal: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ToolCallResult {
|
|
20
|
+
toolName: string;
|
|
21
|
+
input: unknown;
|
|
22
|
+
output: unknown;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function configToRepoSpecs(config: ChatConfig): RepoSpec[] {
|
|
27
|
+
return config.repos.map((spec) => parseRepoSpec(spec));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// INCREMENTAL CHAT MODE - Tools add config items directly
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export interface IncrementalChatResult {
|
|
35
|
+
response: string;
|
|
36
|
+
toolResults?: ToolCallResult[];
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Process chat with incremental config building.
|
|
42
|
+
* Tools directly modify the ConfigBuilder, which emits events to update UI.
|
|
43
|
+
*/
|
|
44
|
+
export async function chatToConfigIncremental(
|
|
45
|
+
messages: ChatMessage[],
|
|
46
|
+
configBuilder: ConfigBuilder,
|
|
47
|
+
onChunk: (chunk: string) => void,
|
|
48
|
+
): Promise<IncrementalChatResult> {
|
|
49
|
+
const context = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
50
|
+
const userMessage = messages.findLast((m) => m.role === "user")?.content ?? "";
|
|
51
|
+
|
|
52
|
+
const repoHistory = await generateRepoHistorySection();
|
|
53
|
+
|
|
54
|
+
const prompt = INCREMENTAL_CHAT_PROMPT.replace("{{context}}", context)
|
|
55
|
+
.replace("{{userMessage}}", userMessage)
|
|
56
|
+
.replace("{{repoHistory}}", repoHistory);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const toolTimings: Map<string, number> = new Map();
|
|
60
|
+
|
|
61
|
+
const tools: ToolSet = {
|
|
62
|
+
// Config building tools
|
|
63
|
+
add_repo: {
|
|
64
|
+
title: "add_repo",
|
|
65
|
+
description: "Add a repository to the workspace configuration",
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
repo: z.string().describe("Repository in format owner/repo or owner/repo:branch"),
|
|
68
|
+
}),
|
|
69
|
+
execute: async ({ repo }) => {
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
|
|
72
|
+
// Validate repo exists on GitHub using gh CLI
|
|
73
|
+
const repoPath = repo.includes(":") ? repo.split(":")[0] : repo;
|
|
74
|
+
const branch = repo.includes(":") ? repo.split(":")[1] : undefined;
|
|
75
|
+
|
|
76
|
+
const validationProc = Bun.spawn(
|
|
77
|
+
["gh", "repo", "view", repoPath, "--json", "nameWithOwner"],
|
|
78
|
+
{
|
|
79
|
+
stdout: "pipe",
|
|
80
|
+
stderr: "pipe",
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const validationOutput = await new Response(validationProc.stdout).text();
|
|
85
|
+
const validationStderr = await new Response(validationProc.stderr).text();
|
|
86
|
+
|
|
87
|
+
if (validationStderr && !validationOutput) {
|
|
88
|
+
const duration = Date.now() - startTime;
|
|
89
|
+
toolTimings.set("add_repo", duration);
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: `Repository '${repoPath}' not found on GitHub. Please check the owner and repository name.`,
|
|
93
|
+
currentRepos: configBuilder.config.repos,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If branch is specified, validate it exists
|
|
98
|
+
if (branch) {
|
|
99
|
+
const branchProc = Bun.spawn(
|
|
100
|
+
["gh", "api", `repos/${repoPath}/git/refs/heads/${branch}`],
|
|
101
|
+
{
|
|
102
|
+
stdout: "pipe",
|
|
103
|
+
stderr: "pipe",
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const branchOutput = await new Response(branchProc.stdout).text();
|
|
108
|
+
const branchStderr = await new Response(branchProc.stderr).text();
|
|
109
|
+
|
|
110
|
+
if (branchStderr && !branchOutput) {
|
|
111
|
+
const duration = Date.now() - startTime;
|
|
112
|
+
toolTimings.set("add_repo", duration);
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: `Branch '${branch}' not found in repository '${repoPath}'.`,
|
|
116
|
+
currentRepos: configBuilder.config.repos,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const added = configBuilder.addRepo(repo);
|
|
122
|
+
const duration = Date.now() - startTime;
|
|
123
|
+
toolTimings.set("add_repo", duration);
|
|
124
|
+
return {
|
|
125
|
+
success: added,
|
|
126
|
+
message: added ? `Added ${repo}` : `${repo} already in config`,
|
|
127
|
+
currentRepos: configBuilder.config.repos,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
add_skill: {
|
|
132
|
+
title: "add_skill",
|
|
133
|
+
description: "Add a skill or package to install in the workspace",
|
|
134
|
+
inputSchema: z.object({
|
|
135
|
+
skill: z.string().describe("Skill or package name to install"),
|
|
136
|
+
}),
|
|
137
|
+
execute: async ({ skill }) => {
|
|
138
|
+
const startTime = Date.now();
|
|
139
|
+
const added = configBuilder.addSkill(skill);
|
|
140
|
+
const duration = Date.now() - startTime;
|
|
141
|
+
toolTimings.set("add_skill", duration);
|
|
142
|
+
return {
|
|
143
|
+
success: added,
|
|
144
|
+
message: added ? `Added skill ${skill}` : `${skill} already in config`,
|
|
145
|
+
currentSkills: configBuilder.config.skills,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
set_goal: {
|
|
150
|
+
title: "set_goal",
|
|
151
|
+
description: "Set the goal/purpose for this workspace session",
|
|
152
|
+
inputSchema: z.object({
|
|
153
|
+
goal: z.string().describe("Brief description of what the user wants to accomplish"),
|
|
154
|
+
}),
|
|
155
|
+
execute: async ({ goal }) => {
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
configBuilder.setGoal(goal);
|
|
158
|
+
const duration = Date.now() - startTime;
|
|
159
|
+
toolTimings.set("set_goal", duration);
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
message: `Goal set to: ${goal}`,
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
// Research tools (existing)
|
|
167
|
+
list_repos: {
|
|
168
|
+
title: "list_repos",
|
|
169
|
+
description: "List all repositories for a given owner/organization",
|
|
170
|
+
inputSchema: z.object({
|
|
171
|
+
owner: z.string(),
|
|
172
|
+
limit: z.number().optional(),
|
|
173
|
+
topic: z.string().optional(),
|
|
174
|
+
}),
|
|
175
|
+
execute: async ({ owner, limit, topic }) => {
|
|
176
|
+
const startTime = Date.now();
|
|
177
|
+
const args = [
|
|
178
|
+
"repo",
|
|
179
|
+
"list",
|
|
180
|
+
owner,
|
|
181
|
+
"--json",
|
|
182
|
+
"nameWithOwner",
|
|
183
|
+
"--jq",
|
|
184
|
+
".[].nameWithOwner",
|
|
185
|
+
];
|
|
186
|
+
if (limit) {
|
|
187
|
+
args.push("--limit", String(limit));
|
|
188
|
+
}
|
|
189
|
+
if (topic) {
|
|
190
|
+
args.push("--topic", topic);
|
|
191
|
+
}
|
|
192
|
+
const proc = Bun.spawn(["gh", ...args], {
|
|
193
|
+
stdout: "pipe",
|
|
194
|
+
stderr: "pipe",
|
|
195
|
+
});
|
|
196
|
+
const output = await new Response(proc.stdout).text();
|
|
197
|
+
const result = {
|
|
198
|
+
repos: output.trim().split("\n").filter(Boolean),
|
|
199
|
+
};
|
|
200
|
+
const duration = Date.now() - startTime;
|
|
201
|
+
toolTimings.set("list_repos", duration);
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
view_issue: {
|
|
206
|
+
title: "view_issue",
|
|
207
|
+
description:
|
|
208
|
+
"View a GitHub issue. Accepts either a full GitHub URL or owner/repo with issue number.",
|
|
209
|
+
inputSchema: z.object({
|
|
210
|
+
url: z.string().optional().describe("Full GitHub issue URL"),
|
|
211
|
+
owner: z.string().optional().describe("Repository owner"),
|
|
212
|
+
repo: z.string().optional().describe("Repository name"),
|
|
213
|
+
issue: z.number().optional().describe("Issue number"),
|
|
214
|
+
}),
|
|
215
|
+
execute: async ({ url, owner, repo, issue }) => {
|
|
216
|
+
const startTime = Date.now();
|
|
217
|
+
|
|
218
|
+
let repoPath: string;
|
|
219
|
+
let issueNumber: number;
|
|
220
|
+
|
|
221
|
+
if (url) {
|
|
222
|
+
// Parse URL like https://github.com/owner/repo/issues/123
|
|
223
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
224
|
+
if (!match) {
|
|
225
|
+
return { error: "Invalid GitHub issue URL", markdown: "" };
|
|
226
|
+
}
|
|
227
|
+
repoPath = `${match[1]}/${match[2]}`;
|
|
228
|
+
issueNumber = parseInt(match[3], 10);
|
|
229
|
+
} else if (owner && repo && issue) {
|
|
230
|
+
repoPath = `${owner}/${repo}`;
|
|
231
|
+
issueNumber = issue;
|
|
232
|
+
} else {
|
|
233
|
+
return { error: "Provide either a URL or owner/repo/issue", markdown: "" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const proc = Bun.spawn(
|
|
237
|
+
[
|
|
238
|
+
"gh",
|
|
239
|
+
"issue",
|
|
240
|
+
"view",
|
|
241
|
+
String(issueNumber),
|
|
242
|
+
"--repo",
|
|
243
|
+
repoPath,
|
|
244
|
+
"--json",
|
|
245
|
+
"title,body,state,author,labels,createdAt,comments,url",
|
|
246
|
+
],
|
|
247
|
+
{
|
|
248
|
+
stdout: "pipe",
|
|
249
|
+
stderr: "pipe",
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const output = await new Response(proc.stdout).text();
|
|
254
|
+
const stderr = await new Response(proc.stderr).text();
|
|
255
|
+
|
|
256
|
+
if (stderr && !output) {
|
|
257
|
+
return { error: stderr.trim(), markdown: "" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const data = JSON.parse(output);
|
|
262
|
+
const labels = data.labels?.map((l: { name: string }) => l.name).join(", ") || "None";
|
|
263
|
+
const commentCount = data.comments?.length || 0;
|
|
264
|
+
|
|
265
|
+
const markdown = `## ${data.title}
|
|
266
|
+
|
|
267
|
+
**Repository:** ${repoPath}
|
|
268
|
+
**Issue:** #${issueNumber}
|
|
269
|
+
**State:** ${data.state}
|
|
270
|
+
**Author:** ${data.author?.login || "Unknown"}
|
|
271
|
+
**Labels:** ${labels}
|
|
272
|
+
**Created:** ${new Date(data.createdAt).toLocaleDateString()}
|
|
273
|
+
**Comments:** ${commentCount}
|
|
274
|
+
**URL:** ${data.url}
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
${data.body || "*No description provided*"}`;
|
|
279
|
+
|
|
280
|
+
const duration = Date.now() - startTime;
|
|
281
|
+
toolTimings.set("view_issue", duration);
|
|
282
|
+
|
|
283
|
+
return { markdown, title: data.title, state: data.state, url: data.url, repoPath };
|
|
284
|
+
} catch {
|
|
285
|
+
return { error: "Failed to parse issue data", markdown: "" };
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
list_repo_history: {
|
|
290
|
+
title: "list_repo_history",
|
|
291
|
+
description:
|
|
292
|
+
"List previously used repositories from history. Useful when user mentions 'my repos' or wants to reuse repos.",
|
|
293
|
+
inputSchema: z.object({
|
|
294
|
+
owner: z.string().optional().describe("Filter by repository owner/organization"),
|
|
295
|
+
limit: z.number().optional().describe("Maximum number of results to return"),
|
|
296
|
+
}),
|
|
297
|
+
execute: async ({ owner, limit }) => {
|
|
298
|
+
const startTime = Date.now();
|
|
299
|
+
const items = await listRepoHistory(limit ?? 50);
|
|
300
|
+
const filtered = owner
|
|
301
|
+
? items.filter((item) => item.owner.toLowerCase() === owner.toLowerCase())
|
|
302
|
+
: items;
|
|
303
|
+
const duration = Date.now() - startTime;
|
|
304
|
+
toolTimings.set("list_repo_history", duration);
|
|
305
|
+
return {
|
|
306
|
+
repos: filtered.map((item) => ({
|
|
307
|
+
spec: item.spec,
|
|
308
|
+
owner: item.owner,
|
|
309
|
+
name: item.name,
|
|
310
|
+
branch: item.branch,
|
|
311
|
+
timesUsed: item.timesUsed,
|
|
312
|
+
lastUsed: item.lastUsed,
|
|
313
|
+
})),
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const result = streamText({
|
|
320
|
+
model: "moonshotai/kimi-k2.5",
|
|
321
|
+
temperature: 0.7,
|
|
322
|
+
prompt,
|
|
323
|
+
tools,
|
|
324
|
+
maxSteps: 100,
|
|
325
|
+
} as any);
|
|
326
|
+
|
|
327
|
+
let response = "";
|
|
328
|
+
const toolResults: ToolCallResult[] = [];
|
|
329
|
+
|
|
330
|
+
for await (const chunk of result.fullStream) {
|
|
331
|
+
switch (chunk.type) {
|
|
332
|
+
case "text-delta": {
|
|
333
|
+
const text = (chunk as any).text ?? (chunk as any).textDelta ?? "";
|
|
334
|
+
response += text;
|
|
335
|
+
onChunk(text);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "tool-call": {
|
|
339
|
+
// Tool is being called - show indicator to user
|
|
340
|
+
onChunk(`\n[${chunk.toolName}...]\n`);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "tool-result": {
|
|
344
|
+
const durationMs = toolTimings.get(chunk.toolName) || 0;
|
|
345
|
+
const chunkAny = chunk as any;
|
|
346
|
+
toolResults.push({
|
|
347
|
+
toolName: chunk.toolName,
|
|
348
|
+
input: chunkAny.input ?? chunkAny.args,
|
|
349
|
+
output: chunkAny.output ?? chunkAny.result,
|
|
350
|
+
durationMs,
|
|
351
|
+
});
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "error": {
|
|
355
|
+
console.error("Stream error:", chunk.error);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
response = response.trim();
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
response,
|
|
365
|
+
toolResults: toolResults.length > 0 ? toolResults : undefined,
|
|
366
|
+
};
|
|
367
|
+
} catch (llmError) {
|
|
368
|
+
return {
|
|
369
|
+
response: "",
|
|
370
|
+
error: `LLM error: ${llmError instanceof Error ? llmError.message : String(llmError)}`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|