@jixo/cli 0.3.0 → 0.4.0
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/README.md +13 -0
- package/dist/index.js +0 -0
- package/package.json +8 -5
- package/prompts/i18n.md +0 -108
- package/prompts/readme-writer.md +0 -120
- package/prompts/user.md +0 -88
- package/prompts/writer.md +0 -13
- package/scripts/gen-prompts.ts +0 -27
- package/src/cli.ts +0 -96
- package/src/commands/doctor/config.ts +0 -30
- package/src/commands/doctor/doctor.test.ts +0 -17
- package/src/commands/doctor/doctor.ts +0 -151
- package/src/commands/doctor/index.ts +0 -21
- package/src/commands/doctor/types.ts +0 -39
- package/src/commands/init.ts +0 -64
- package/src/commands/prompts/list.ts +0 -14
- package/src/commands/prompts/upgrade.ts +0 -16
- package/src/commands/tasks/ai-tools.ts +0 -65
- package/src/commands/tasks/model-providers.ts +0 -54
- package/src/commands/tasks/run-ai-task.ts +0 -263
- package/src/commands/tasks/run.ts +0 -52
- package/src/config.ts +0 -39
- package/src/env.ts +0 -32
- package/src/helper/find-changes.test.ts +0 -23
- package/src/helper/find-changes.ts +0 -109
- package/src/helper/prompts-loader.ts +0 -33
- package/src/helper/resolve-ai-tasks.ts +0 -126
- package/src/index.ts +0 -8
- package/src/prompts.json +0 -29
- package/tsconfig.json +0 -15
- package/tsconfig.tsbuildinfo +0 -1
@@ -1,263 +0,0 @@
|
|
1
|
-
import {blue, cyan, FileEntry, gray, green, spinner, YAML} from "@gaubee/nodekit";
|
2
|
-
import {func_catch} from "@gaubee/util";
|
3
|
-
import {streamText, type AssistantModelMessage, type ModelMessage, type ToolCallPart} from "ai";
|
4
|
-
import debug from "debug";
|
5
|
-
import os from "node:os";
|
6
|
-
import path from "node:path";
|
7
|
-
import {match, P} from "ts-pattern";
|
8
|
-
import {safeEnv} from "../../env.js";
|
9
|
-
import {getModelMessage, getPromptConfigs} from "../../helper/prompts-loader.js";
|
10
|
-
import type {AiTask} from "../../helper/resolve-ai-tasks.js";
|
11
|
-
import {tools} from "./ai-tools.js";
|
12
|
-
import {providers} from "./model-providers.js";
|
13
|
-
const log = debug("jixo:run-ai-task");
|
14
|
-
|
15
|
-
const getModel = (model?: string) => {
|
16
|
-
return match(model)
|
17
|
-
.with(P.string.startsWith("deepseek-"), (model) => providers.deepseek(model))
|
18
|
-
.with(P.string.startsWith("gemini-"), (model) => providers.google(model))
|
19
|
-
.with(P.string.startsWith("o3-"), P.string.startsWith("o1-"), P.string.startsWith("gpt-"), (model) => providers.openai(model))
|
20
|
-
.with(P.string.startsWith("claude-"), (model) => providers.anthropic(model))
|
21
|
-
.with(P.string.startsWith("grok-"), (model) => providers.xai(model))
|
22
|
-
.with(P.string.includes("/"), (model) => providers.deepinfra(model))
|
23
|
-
.otherwise(() => {
|
24
|
-
if (safeEnv.JIXO_DEEPSEEK_API_KEY) {
|
25
|
-
return providers.deepseek("deepseek-chat");
|
26
|
-
}
|
27
|
-
if (safeEnv.JIXO_GOOGLE_API_KEY) {
|
28
|
-
return providers.google("gemini-2.5-pro-preview-05-06");
|
29
|
-
}
|
30
|
-
if (safeEnv.JIXO_OPENAI_API_KEY) {
|
31
|
-
return providers.openai("o3-mini");
|
32
|
-
}
|
33
|
-
if (safeEnv.JIXO_ANTHROPIC_API_KEY) {
|
34
|
-
return providers.anthropic("claude-4-sonnet-20250514");
|
35
|
-
}
|
36
|
-
if (safeEnv.JIXO_XAI_API_KEY) {
|
37
|
-
return providers.xai("grok-3-beta");
|
38
|
-
}
|
39
|
-
if (safeEnv.JIXO_DEEPINFRA_API_KEY) {
|
40
|
-
return providers.deepinfra("meta-llama/Meta-Llama-3.1-405B-Instruct");
|
41
|
-
}
|
42
|
-
return providers.deepseek("deepseek-reasoner");
|
43
|
-
});
|
44
|
-
};
|
45
|
-
|
46
|
-
export const runAiTask = async (ai_task: AiTask, allFiles: FileEntry[], changedFiles: FileEntry[]) => {
|
47
|
-
const model = getModel(ai_task.model);
|
48
|
-
const availableTools = {
|
49
|
-
...(await tools.fileSystem(ai_task.cwd)),
|
50
|
-
...(await tools.memory(path.join(ai_task.cwd, `.jixo/${ai_task.name}.memory.json`))),
|
51
|
-
// ...(await tools.fetch()),
|
52
|
-
// ...(await tools.git(ai_task.cwd)),
|
53
|
-
};
|
54
|
-
|
55
|
-
const initialMessages: ModelMessage[] = getModelMessage(ai_task.agents);
|
56
|
-
ai_task.startTime;
|
57
|
-
const userPrompt = getPromptConfigs()
|
58
|
-
.user.content //
|
59
|
-
.replace(/\{\{task.(\w+)\}\}/g, (_, key) => {
|
60
|
-
return Reflect.get(ai_task, key);
|
61
|
-
})
|
62
|
-
.replace(/\{\{env.(\w+)\}\}/g, (_, key) => {
|
63
|
-
const envKey = key.toUpperCase();
|
64
|
-
const envValue =
|
65
|
-
Reflect.get(process.env, envKey) ??
|
66
|
-
match(envKey)
|
67
|
-
.with("USER", () => os.userInfo().username)
|
68
|
-
.otherwise(() => "");
|
69
|
-
return envValue;
|
70
|
-
})
|
71
|
-
.replaceAll(
|
72
|
-
"{{allFiles}}",
|
73
|
-
[
|
74
|
-
//
|
75
|
-
`# files dir: ${ai_task.dir}`,
|
76
|
-
`# files count: ${allFiles.length}`,
|
77
|
-
YAML.stringify(allFiles.map((e) => e.relativePath)),
|
78
|
-
].join("\n"),
|
79
|
-
)
|
80
|
-
.replaceAll(
|
81
|
-
"{{changedFiles}}",
|
82
|
-
[
|
83
|
-
//
|
84
|
-
`# files dir: ${ai_task.dir}`,
|
85
|
-
`# files count: ${changedFiles.length}`,
|
86
|
-
YAML.stringify(changedFiles.map((e) => e.relativePath)),
|
87
|
-
].join("\n"),
|
88
|
-
);
|
89
|
-
log("USER PROMPT:", userPrompt);
|
90
|
-
initialMessages.push({
|
91
|
-
role: "user",
|
92
|
-
content: userPrompt,
|
93
|
-
});
|
94
|
-
|
95
|
-
let currentMessages: ModelMessage[] = [...initialMessages];
|
96
|
-
const maxTurns = 10; // Safeguard against infinite loops
|
97
|
-
const loading = spinner("Initializing AI task...");
|
98
|
-
loading.prefixText = "⏳ ";
|
99
|
-
loading.start();
|
100
|
-
|
101
|
-
for (let turn = 0; turn < maxTurns; turn++) {
|
102
|
-
loading.text = turn === 0 ? `Connecting To ${model.provider}...` : `Processing turn ${turn + 1}...`;
|
103
|
-
|
104
|
-
const result = await streamText({
|
105
|
-
model: model,
|
106
|
-
messages: currentMessages,
|
107
|
-
tools: availableTools,
|
108
|
-
toolChoice: "auto", // Changed to auto for more flexibility
|
109
|
-
});
|
110
|
-
|
111
|
-
let reasoning = "";
|
112
|
-
let fulltext = "";
|
113
|
-
let firstStreamPart = true;
|
114
|
-
const requestedToolCalls: ToolCallPart[] = []; // Using any for now, should be ToolCallPart from 'ai'
|
115
|
-
|
116
|
-
const assistantMessageContent: AssistantModelMessage["content"] & unknown[] = [];
|
117
|
-
const _currentAssistantMessage: AssistantModelMessage = {role: "assistant", content: assistantMessageContent};
|
118
|
-
|
119
|
-
const LOOP_SIGNALS = {
|
120
|
-
RETURN: "RETURN",
|
121
|
-
BREAK: "BREAK",
|
122
|
-
CONTINUE: "CONTINUE",
|
123
|
-
} as const;
|
124
|
-
for await (const part of result.fullStream) {
|
125
|
-
if (firstStreamPart) {
|
126
|
-
firstStreamPart = false;
|
127
|
-
loading.text = ""; // Clear initial connecting/processing message
|
128
|
-
}
|
129
|
-
const LOOP_SIGNAL = await match(part)
|
130
|
-
.with({type: "text"}, (textPart) => {
|
131
|
-
loading.prefixText = "🤖 ";
|
132
|
-
let assistantTextPart = assistantMessageContent.findLast((part) => part.type === "text");
|
133
|
-
if (assistantTextPart == null) {
|
134
|
-
assistantTextPart = {type: "text", text: ""};
|
135
|
-
assistantMessageContent.push(assistantTextPart);
|
136
|
-
}
|
137
|
-
assistantTextPart.text += textPart.text;
|
138
|
-
if (fulltext === "") fulltext = "\n"; // For consistent display
|
139
|
-
fulltext += textPart.text;
|
140
|
-
loading.text = fulltext;
|
141
|
-
})
|
142
|
-
.with({type: "tool-call"}, (callPart) => {
|
143
|
-
loading.prefixText = "🛠️ ";
|
144
|
-
loading.text = "Requesting tool:" + blue(callPart.toolName) + gray(": " + YAML.stringify(callPart.args));
|
145
|
-
log("\nQAQ tool-call", callPart);
|
146
|
-
requestedToolCalls.push(callPart);
|
147
|
-
// Update assistant message to include tool calls
|
148
|
-
assistantMessageContent.push({
|
149
|
-
type: "tool-call",
|
150
|
-
toolCallId: callPart.toolCallId,
|
151
|
-
toolName: callPart.toolName,
|
152
|
-
args: callPart.args,
|
153
|
-
});
|
154
|
-
})
|
155
|
-
.with({type: "error"}, (errorPart) => {
|
156
|
-
loading.prefixText = "❌ ";
|
157
|
-
console.error("\nQAQ error", errorPart.error);
|
158
|
-
loading.fail(`Error: ${errorPart.error?.toString()}`);
|
159
|
-
return LOOP_SIGNALS.BREAK; // Stop processing on error
|
160
|
-
})
|
161
|
-
.with({type: "reasoning"}, (reasoningPart) => {
|
162
|
-
loading.prefixText = "🤔 ";
|
163
|
-
if (reasoning === "") loading.text = "";
|
164
|
-
reasoning += reasoningPart.text;
|
165
|
-
loading.text = gray(reasoning.split("\n").slice(-3).join("\n"));
|
166
|
-
})
|
167
|
-
// Add other console logs for debugging if needed, but keep them minimal for production
|
168
|
-
.with({type: "file"}, (p) => log("\nQAQ file", p.file))
|
169
|
-
.with({type: "source"}, (p) => log("\nQAQ source", p))
|
170
|
-
.with({type: "tool-result"}, (p) => log("\nQAQ tool-result", p))
|
171
|
-
.with({type: "tool-call-streaming-start"}, (p) => log("\nQAQ tool-call-streaming-start", p))
|
172
|
-
.with({type: "tool-call-delta"}, (p) => log("\nQAQ tool-call-delta", p))
|
173
|
-
.with({type: "reasoning-part-finish"}, (p) => log("\nQAQ reasoning-part-finish", p))
|
174
|
-
.with({type: "start-step"}, (p) => log("\nQAQ start-step", p))
|
175
|
-
.with({type: "finish-step"}, (p) => log("\nQAQ finish-step", p))
|
176
|
-
.with({type: "start"}, (p) => log("\nQAQ start", p))
|
177
|
-
.with({type: "finish"}, async (finishPart) => {
|
178
|
-
log("\nQAQ finish", finishPart);
|
179
|
-
// Add the assistant's message from this turn to the history
|
180
|
-
currentMessages.push(_currentAssistantMessage);
|
181
|
-
|
182
|
-
if (finishPart.finishReason === "stop" || finishPart.finishReason === "length") {
|
183
|
-
loading.prefixText = "✅ ";
|
184
|
-
loading.text = green(`${cyan(`[${ai_task.name}]`)} Completed`);
|
185
|
-
// Task finished without tool calls or after tool calls that didn't lead to more calls.
|
186
|
-
return LOOP_SIGNALS.RETURN; // Exit the outer loop and function
|
187
|
-
}
|
188
|
-
|
189
|
-
if (finishPart.finishReason === "tool-calls") {
|
190
|
-
if (requestedToolCalls.length === 0) {
|
191
|
-
loading.warn("Finished with 'tool-calls' but no tools were requested.");
|
192
|
-
return LOOP_SIGNALS.RETURN; // Exit, something is off
|
193
|
-
}
|
194
|
-
|
195
|
-
const toolResultMessages: ModelMessage[] = [];
|
196
|
-
for (const toolCall of requestedToolCalls) {
|
197
|
-
const toolToExecute = availableTools[toolCall.toolName];
|
198
|
-
if (!toolToExecute || typeof toolToExecute.execute !== "function") {
|
199
|
-
console.error(`Tool ${toolCall.toolName} not found or not executable.`);
|
200
|
-
toolResultMessages.push({
|
201
|
-
role: "tool",
|
202
|
-
content: [
|
203
|
-
{
|
204
|
-
type: "tool-result",
|
205
|
-
toolCallId: toolCall.toolCallId,
|
206
|
-
toolName: toolCall.toolName,
|
207
|
-
result: JSON.stringify({error: `Tool ${toolCall.toolName} not found or not executable.`}),
|
208
|
-
isError: true,
|
209
|
-
},
|
210
|
-
],
|
211
|
-
});
|
212
|
-
continue;
|
213
|
-
}
|
214
|
-
loading.text = `Executing tool: ${toolCall.toolName}...`;
|
215
|
-
const executionResult = await func_catch(() =>
|
216
|
-
toolToExecute.execute!(toolCall.args, {
|
217
|
-
toolCallId: toolCall.toolCallId,
|
218
|
-
messages: currentMessages,
|
219
|
-
}),
|
220
|
-
)();
|
221
|
-
toolResultMessages.push({
|
222
|
-
role: "tool",
|
223
|
-
content: [
|
224
|
-
{
|
225
|
-
type: "tool-result",
|
226
|
-
toolCallId: toolCall.toolCallId,
|
227
|
-
toolName: toolCall.toolName,
|
228
|
-
isError: !executionResult.success,
|
229
|
-
result: JSON.stringify(executionResult.success ? executionResult.result : executionResult.error),
|
230
|
-
},
|
231
|
-
],
|
232
|
-
});
|
233
|
-
if (executionResult.success) {
|
234
|
-
loading.text = `Tool ${toolCall.toolName} executed.`;
|
235
|
-
} else {
|
236
|
-
loading.text = `Error executing tool ${toolCall.toolName}.`;
|
237
|
-
}
|
238
|
-
}
|
239
|
-
currentMessages.push(...toolResultMessages);
|
240
|
-
// Loop continues for the next turn
|
241
|
-
} else {
|
242
|
-
// Other finish reasons, potentially an error or unexpected state
|
243
|
-
loading.warn(`Task finished with unhandled reason: ${finishPart.finishReason}`);
|
244
|
-
return LOOP_SIGNALS.RETURN;
|
245
|
-
}
|
246
|
-
})
|
247
|
-
.otherwise(() => {}); // Handle any other part types if necessary
|
248
|
-
|
249
|
-
if (LOOP_SIGNAL === LOOP_SIGNALS.RETURN) {
|
250
|
-
break;
|
251
|
-
} else if (LOOP_SIGNAL === LOOP_SIGNALS.BREAK) {
|
252
|
-
break;
|
253
|
-
}
|
254
|
-
}
|
255
|
-
// If the stream finishes without a 'finish' part (e.g. error thrown inside), this loop might exit. Ensure spinner stops.
|
256
|
-
if (turn === maxTurns - 1) {
|
257
|
-
loading.warn("Max interaction turns reached.");
|
258
|
-
return;
|
259
|
-
}
|
260
|
-
}
|
261
|
-
// Fallback spinner stop if loop exits unexpectedly
|
262
|
-
loading.stop();
|
263
|
-
};
|
@@ -1,52 +0,0 @@
|
|
1
|
-
import {FileEntry, Ignore, normalizeFilePath, walkFiles} from "@gaubee/nodekit";
|
2
|
-
import {iter_map_not_null} from "@gaubee/util";
|
3
|
-
import fs from "node:fs";
|
4
|
-
import path from "node:path";
|
5
|
-
import {loadConfig} from "../../config.js";
|
6
|
-
import {loadJixoEnv} from "../../env.js";
|
7
|
-
import {findChangedFilesSinceCommit} from "../../helper/find-changes.js";
|
8
|
-
import {resolveAiTasks} from "../../helper/resolve-ai-tasks.js";
|
9
|
-
import {runAiTask} from "./run-ai-task.js";
|
10
|
-
|
11
|
-
export const run = async (_cwd: string, options: {nameFilter: string[]; dirFilter: string[]}) => {
|
12
|
-
const cwd = normalizeFilePath(_cwd);
|
13
|
-
const config = await loadConfig(cwd);
|
14
|
-
const ai_tasks = resolveAiTasks(cwd, config.tasks);
|
15
|
-
const nameMatcher = options.nameFilter.length ? new Ignore(options.nameFilter, cwd) : {isMatch: () => true};
|
16
|
-
const dirMatcher = options.dirFilter.length ? new Ignore(options.dirFilter, cwd) : {isMatch: () => true};
|
17
|
-
const cwdIgnoreFilepath = path.join(cwd, ".gitignore");
|
18
|
-
let ignore: undefined | string[];
|
19
|
-
if (fs.existsSync(cwdIgnoreFilepath)) {
|
20
|
-
ignore = fs.readFileSync(cwdIgnoreFilepath, "utf-8").split("\n");
|
21
|
-
}
|
22
|
-
|
23
|
-
const allFiles = [...walkFiles(cwd, {ignore})];
|
24
|
-
const changedFiles = (await findChangedFilesSinceCommit("@jixo", cwd)).filter((file) => {
|
25
|
-
return file.path.startsWith(cwd + "/");
|
26
|
-
});
|
27
|
-
|
28
|
-
// const run_tasks: Array<Func> = [];
|
29
|
-
for (const ai_task of ai_tasks) {
|
30
|
-
const {dir: task_dir} = ai_task;
|
31
|
-
if (!dirMatcher.isMatch(task_dir)) {
|
32
|
-
continue;
|
33
|
-
}
|
34
|
-
if (!nameMatcher.isMatch(ai_task.name)) {
|
35
|
-
continue;
|
36
|
-
}
|
37
|
-
|
38
|
-
const task_changedFiles =
|
39
|
-
cwd === task_dir
|
40
|
-
? changedFiles
|
41
|
-
: iter_map_not_null(changedFiles, (file) => {
|
42
|
-
if (file.path.startsWith(task_dir + "/")) {
|
43
|
-
return new FileEntry(file.path, {cwd: task_dir, state: file.stats});
|
44
|
-
}
|
45
|
-
});
|
46
|
-
|
47
|
-
const task_allFiles = cwd === task_dir ? allFiles : [...walkFiles(task_dir, {ignore})];
|
48
|
-
|
49
|
-
loadJixoEnv(cwd);
|
50
|
-
await runAiTask(ai_task, task_allFiles, task_changedFiles);
|
51
|
-
}
|
52
|
-
};
|
package/src/config.ts
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
import {cosmiconfig} from "cosmiconfig";
|
2
|
-
import {defu} from "defu";
|
3
|
-
import z from "zod";
|
4
|
-
|
5
|
-
const zJixoTask = z.union([
|
6
|
-
z.string(),
|
7
|
-
z.object({
|
8
|
-
type: z.literal("file"),
|
9
|
-
name: z.string().optional(),
|
10
|
-
filename: z.string(),
|
11
|
-
}),
|
12
|
-
z.object({
|
13
|
-
type: z.literal("dir"),
|
14
|
-
dirname: z.string(),
|
15
|
-
}),
|
16
|
-
z.object({
|
17
|
-
type: z.literal("prompt"),
|
18
|
-
name: z.string().optional(),
|
19
|
-
content: z.string(),
|
20
|
-
}),
|
21
|
-
]);
|
22
|
-
const zJixoConfig = z.object({
|
23
|
-
tasks: z.union([z.array(zJixoTask), zJixoTask]),
|
24
|
-
});
|
25
|
-
|
26
|
-
const defaultConfig: JixoConfig = {
|
27
|
-
tasks: {type: "dir", dirname: ".jixo"},
|
28
|
-
};
|
29
|
-
export type JixoTask = z.output<typeof zJixoTask>;
|
30
|
-
export type JixoConfig = z.output<typeof zJixoConfig>;
|
31
|
-
export const defineConfig = (config: Partial<JixoConfig>) => {
|
32
|
-
return zJixoConfig.parse(config);
|
33
|
-
};
|
34
|
-
|
35
|
-
export const loadConfig = async (dir: string) => {
|
36
|
-
const explorer = cosmiconfig("jixo");
|
37
|
-
const loaded = await explorer.search(dir);
|
38
|
-
return defu(loaded?.config as JixoConfig, defaultConfig);
|
39
|
-
};
|
package/src/env.ts
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
import {defineEnv} from "@gaubee/node";
|
2
|
-
import fs from "node:fs";
|
3
|
-
import path from "node:path";
|
4
|
-
|
5
|
-
export const loadJixoEnv = (dir: string) => {
|
6
|
-
const cwdJixoEnvFilepath = path.join(dir, ".jixo.env");
|
7
|
-
if (fs.existsSync(cwdJixoEnvFilepath)) {
|
8
|
-
process.loadEnvFile(cwdJixoEnvFilepath);
|
9
|
-
}
|
10
|
-
};
|
11
|
-
loadJixoEnv(process.cwd());
|
12
|
-
|
13
|
-
export const safeEnv = defineEnv("JIXO", {
|
14
|
-
DEEPSEEK_API_KEY: "",
|
15
|
-
DEEPSEEK_BASE_URL: "",
|
16
|
-
|
17
|
-
ANTHROPIC_API_KEY: "",
|
18
|
-
ANTHROPIC_BASE_URL: "",
|
19
|
-
|
20
|
-
OPENAI_API_KEY: "",
|
21
|
-
OPENAI_BASE_URL: "",
|
22
|
-
OPENAI_ORGANIZATION: "",
|
23
|
-
|
24
|
-
GOOGLE_API_KEY: "",
|
25
|
-
GOOGLE_BASE_URL: "",
|
26
|
-
|
27
|
-
XAI_BASE_URL: "",
|
28
|
-
XAI_API_KEY: "",
|
29
|
-
|
30
|
-
DEEPINFRA_BASE_URL: "",
|
31
|
-
DEEPINFRA_API_KEY: "",
|
32
|
-
});
|
@@ -1,23 +0,0 @@
|
|
1
|
-
import {import_meta_ponyfill} from "import-meta-ponyfill";
|
2
|
-
import path from "node:path";
|
3
|
-
import {findChangedFilesSinceCommit} from "./find-changes.js";
|
4
|
-
|
5
|
-
// --- 使用示例 ---
|
6
|
-
async function test() {
|
7
|
-
const messagePattern = "@jixo"; // 你要搜索的 commit message 内容
|
8
|
-
const repoDir = "."; // Git 仓库的路径,默认为当前目录
|
9
|
-
|
10
|
-
console.log(`Searching for changes since commit with message containing "${messagePattern}" in ${path.resolve(repoDir)}...`);
|
11
|
-
const files = await findChangedFilesSinceCommit(messagePattern, repoDir);
|
12
|
-
|
13
|
-
if (files.length > 0) {
|
14
|
-
console.log("\nChanged files (relative to git root):");
|
15
|
-
} else {
|
16
|
-
console.log("\nNo changed files found based on the criteria.");
|
17
|
-
}
|
18
|
-
return files;
|
19
|
-
}
|
20
|
-
if (import_meta_ponyfill(import.meta).main) {
|
21
|
-
const files = await test();
|
22
|
-
console.log(files.map((file) => file.path));
|
23
|
-
}
|
@@ -1,109 +0,0 @@
|
|
1
|
-
import {FileEntry, gray, red, walkFiles} from "@gaubee/nodekit";
|
2
|
-
import {type ExecException, execSync} from "node:child_process";
|
3
|
-
import path from "node:path";
|
4
|
-
|
5
|
-
interface GitError extends ExecException {
|
6
|
-
status?: number | null;
|
7
|
-
// stderr?: Buffer | string;
|
8
|
-
// stdout?: Buffer | string;
|
9
|
-
}
|
10
|
-
|
11
|
-
export async function findChangedFilesSinceCommit(messagePattern: string, cwd: string = "."): Promise<FileEntry[]> {
|
12
|
-
let repoPath = cwd;
|
13
|
-
try {
|
14
|
-
// 获取 Git 仓库位置,同时确保在 Git 仓库中运行
|
15
|
-
repoPath = execSync("git rev-parse --show-toplevel", {cwd, encoding: "utf8", stdio: "pipe"}).toString().trim();
|
16
|
-
} catch (e) {
|
17
|
-
return [...walkFiles(repoPath)];
|
18
|
-
}
|
19
|
-
const gitCommandOptions = {cwd: repoPath, encoding: "utf8" as BufferEncoding, stdio: "pipe" as "pipe"}; // stdio: 'pipe' to access stdout/stderr on error
|
20
|
-
|
21
|
-
let baseCommitForDiff: string | null = null;
|
22
|
-
|
23
|
-
try {
|
24
|
-
// 1. 找到包含特定标记的最新 commit SHA
|
25
|
-
// 使用 --fixed-strings 来精确匹配字符串 "@jixo",而不是作为正则表达式
|
26
|
-
const targetCommitSha = execSync(`git log --grep="${messagePattern}" --fixed-strings -n 1 --pretty=format:%H`, gitCommandOptions).toString().trim();
|
27
|
-
|
28
|
-
if (targetCommitSha) {
|
29
|
-
console.log(`Found target commit SHA: ${targetCommitSha} for message pattern: "${messagePattern}"`);
|
30
|
-
baseCommitForDiff = targetCommitSha;
|
31
|
-
} else {
|
32
|
-
console.warn(gray(`No commit found with message pattern: "${messagePattern}". Only uncommitted changes will be listed if any.`));
|
33
|
-
}
|
34
|
-
} catch (error) {
|
35
|
-
// git log --grep 通常在没有匹配时返回空输出和退出码0。
|
36
|
-
// 如果这里出错,可能是更严重的问题。
|
37
|
-
const gitError = error as GitError;
|
38
|
-
console.warn(red(`Could not determine base commit with pattern "${messagePattern}". Only uncommitted changes will be listed. Details:`));
|
39
|
-
console.error(gitError.message);
|
40
|
-
}
|
41
|
-
|
42
|
-
const changedFiles = new Set<string>();
|
43
|
-
|
44
|
-
// 2. 列出从该 commit 之后到 HEAD (最新已提交) 的所有变更文件
|
45
|
-
if (baseCommitForDiff) {
|
46
|
-
try {
|
47
|
-
const committedChangesOutput = execSync(`git diff ${baseCommitForDiff} HEAD --name-only`, gitCommandOptions).toString().trim();
|
48
|
-
if (committedChangesOutput) {
|
49
|
-
committedChangesOutput.split("\n").forEach((file) => {
|
50
|
-
if (file.trim()) changedFiles.add(file.trim());
|
51
|
-
});
|
52
|
-
}
|
53
|
-
} catch (error) {
|
54
|
-
const gitError = error as GitError;
|
55
|
-
// `git diff` 在有差异时退出码为1,无差异时为0。execSync 默认在非0时抛错。
|
56
|
-
if (gitError.status === 1 && gitError.stdout) {
|
57
|
-
// 有差异
|
58
|
-
const output = gitError.stdout ?? "";
|
59
|
-
if (output) {
|
60
|
-
output.split("\n").forEach((file) => {
|
61
|
-
if (file.trim()) changedFiles.add(file.trim());
|
62
|
-
});
|
63
|
-
}
|
64
|
-
} else if (gitError.status !== 0) {
|
65
|
-
// 其他错误
|
66
|
-
console.error(`Error getting committed changes since ${baseCommitForDiff}: ${gitError.stderr || gitError.message}`);
|
67
|
-
}
|
68
|
-
// status 0 (无差异) 表示没有输出,是正常情况
|
69
|
-
}
|
70
|
-
}
|
71
|
-
|
72
|
-
// 3. 列出工作区中未提交的变更文件
|
73
|
-
const uncommittedCommands = [
|
74
|
-
{cmd: "git diff --name-only --cached", desc: "staged changes"}, // 已暂存
|
75
|
-
{cmd: "git diff --name-only", desc: "unstaged changes"}, // 未暂存 (对比工作区和暂存区)
|
76
|
-
{cmd: "git ls-files --others --exclude-standard", desc: "untracked files"}, // 未跟踪
|
77
|
-
];
|
78
|
-
|
79
|
-
for (const item of uncommittedCommands) {
|
80
|
-
try {
|
81
|
-
const output = execSync(item.cmd, gitCommandOptions).toString().trim();
|
82
|
-
if (output) {
|
83
|
-
output.split("\n").forEach((file) => {
|
84
|
-
if (file.trim()) changedFiles.add(file.trim());
|
85
|
-
});
|
86
|
-
}
|
87
|
-
} catch (error) {
|
88
|
-
const gitError = error as GitError;
|
89
|
-
if (item.cmd.startsWith("git diff") && gitError.status === 1 && gitError.stdout) {
|
90
|
-
// git diff 有差异
|
91
|
-
const output = gitError.stdout ?? "";
|
92
|
-
if (output) {
|
93
|
-
output.split("\n").forEach((file) => {
|
94
|
-
if (file.trim()) changedFiles.add(file.trim());
|
95
|
-
});
|
96
|
-
}
|
97
|
-
} else if (gitError.status !== 0 && !(item.cmd.startsWith("git diff") && gitError.status === 1)) {
|
98
|
-
// 其他错误
|
99
|
-
// 对于 ls-files,如果出错,也记录
|
100
|
-
console.error(`Error getting ${item.desc}: ${gitError.stderr || gitError.message}`);
|
101
|
-
}
|
102
|
-
// `git ls-files` 在没有文件时退出码为0,输出为空。
|
103
|
-
// `git diff` 在没有差异时退出码为0,输出为空。
|
104
|
-
}
|
105
|
-
}
|
106
|
-
|
107
|
-
// 返回相对于仓库根目录的文件路径
|
108
|
-
return [...changedFiles].map((filepath) => new FileEntry(path.resolve(repoPath, filepath), {cwd: repoPath}));
|
109
|
-
}
|
@@ -1,33 +0,0 @@
|
|
1
|
-
import {createResolverByRootFile} from "@gaubee/node";
|
2
|
-
import {readJson} from "@gaubee/nodekit";
|
3
|
-
import {func_remember} from "@gaubee/util";
|
4
|
-
import type {ModelMessage} from "ai";
|
5
|
-
import fs from "node:fs";
|
6
|
-
import defaultPrompts from "../prompts.json" with {type: "json"};
|
7
|
-
const rootResolver = createResolverByRootFile(import.meta.url);
|
8
|
-
|
9
|
-
export const getPromptConfigs = func_remember((): typeof defaultPrompts => {
|
10
|
-
const download_prompts_json_filepath = rootResolver("prompts.json");
|
11
|
-
if (fs.existsSync(download_prompts_json_filepath)) {
|
12
|
-
return readJson(download_prompts_json_filepath);
|
13
|
-
}
|
14
|
-
return defaultPrompts;
|
15
|
-
});
|
16
|
-
type PromptConfigs = typeof defaultPrompts;
|
17
|
-
type PromptItemConfig = PromptConfigs[keyof PromptConfigs];
|
18
|
-
|
19
|
-
export const getModelMessage = (agents: string[]) => {
|
20
|
-
const promptConfigs = getPromptConfigs();
|
21
|
-
const modelMessage: ModelMessage[] = [];
|
22
|
-
const names = agents.slice();
|
23
|
-
for (const name of names) {
|
24
|
-
const promptConfig = name in promptConfigs ? (Reflect.get(promptConfigs, name) as PromptItemConfig) : null;
|
25
|
-
if (!promptConfig) {
|
26
|
-
continue;
|
27
|
-
}
|
28
|
-
modelMessage.unshift({role: "system", content: promptConfig.content});
|
29
|
-
names.push(...promptConfig.data.parent);
|
30
|
-
}
|
31
|
-
|
32
|
-
return modelMessage;
|
33
|
-
};
|
@@ -1,126 +0,0 @@
|
|
1
|
-
import {matter, normalizeFilePath, readMarkdown, walkFiles, writeMarkdown} from "@gaubee/nodekit";
|
2
|
-
import fs from "node:fs";
|
3
|
-
import path from "node:path";
|
4
|
-
import {match, P} from "ts-pattern";
|
5
|
-
import z from "zod";
|
6
|
-
import {type JixoConfig} from "../config.js";
|
7
|
-
|
8
|
-
/**
|
9
|
-
* 将 config.tasks 字段转化成具体的 ai-tasks 信息
|
10
|
-
* @param cwd
|
11
|
-
* @param config_tasks
|
12
|
-
* @returns
|
13
|
-
*/
|
14
|
-
export const resolveAiTasks = (cwd: string, config_tasks: JixoConfig["tasks"]) => {
|
15
|
-
const config_tasks_arr = Array.isArray(config_tasks) ? config_tasks : [config_tasks];
|
16
|
-
type TaskBase = {
|
17
|
-
data: {[key: string]: any};
|
18
|
-
content: string;
|
19
|
-
};
|
20
|
-
type AiTask = TaskBase & {
|
21
|
-
name: string;
|
22
|
-
cwd: string;
|
23
|
-
dir: string;
|
24
|
-
agents: string[];
|
25
|
-
model: string;
|
26
|
-
useMemory: string;
|
27
|
-
useLog: string;
|
28
|
-
log: string;
|
29
|
-
startTime: string;
|
30
|
-
};
|
31
|
-
const tasks: AiTask[] = [];
|
32
|
-
const addTask = (
|
33
|
-
ai_task: TaskBase,
|
34
|
-
options: {
|
35
|
-
defaultName: string;
|
36
|
-
},
|
37
|
-
) => {
|
38
|
-
const {name: inner_task_name, dir: _task_dir = cwd} = ai_task.data;
|
39
|
-
const task_dir = normalizeFilePath(path.resolve(cwd, _task_dir));
|
40
|
-
|
41
|
-
const task_name = inner_task_name || options.defaultName;
|
42
|
-
const useMemory = ai_task.data.useMemory || task_name;
|
43
|
-
const useLog = ai_task.data.useLog || task_name;
|
44
|
-
|
45
|
-
const log_filepath = path.join(cwd, `.jixo/${useLog}.log.md`);
|
46
|
-
if (!fs.existsSync(log_filepath)) {
|
47
|
-
writeMarkdown(log_filepath, ``, {
|
48
|
-
createTime: new Date().toISOString(),
|
49
|
-
updateTime: new Date().toISOString(),
|
50
|
-
});
|
51
|
-
}
|
52
|
-
|
53
|
-
tasks.push({
|
54
|
-
...ai_task,
|
55
|
-
name: task_name,
|
56
|
-
cwd: cwd,
|
57
|
-
dir: task_dir,
|
58
|
-
agents: match(z.union([z.string(), z.string().array()]).safeParse(ai_task.data.agents))
|
59
|
-
.with({success: true, data: P.select()}, (agents) => {
|
60
|
-
return Array.isArray(agents) ? agents : agents.split(/\s+/);
|
61
|
-
})
|
62
|
-
.otherwise(() => []),
|
63
|
-
model: match(z.string().safeParse(ai_task.data.model))
|
64
|
-
.with({success: true, data: P.select()}, (model) => model)
|
65
|
-
.otherwise(() => ""),
|
66
|
-
useMemory,
|
67
|
-
useLog,
|
68
|
-
log: fs.readFileSync(log_filepath, "utf-8"),
|
69
|
-
startTime: new Date().toISOString(),
|
70
|
-
});
|
71
|
-
};
|
72
|
-
|
73
|
-
for (const config_task of config_tasks_arr) {
|
74
|
-
match(config_task)
|
75
|
-
.with(
|
76
|
-
{
|
77
|
-
type: "dir",
|
78
|
-
dirname: P.string.select(),
|
79
|
-
},
|
80
|
-
(dirname) => {
|
81
|
-
for (const entry of walkFiles(path.resolve(cwd, dirname), {
|
82
|
-
matchFile(entry) {
|
83
|
-
return entry.name.endsWith(".task.md");
|
84
|
-
},
|
85
|
-
})) {
|
86
|
-
addTask(readMarkdown(entry.path), {
|
87
|
-
defaultName: entry.name.slice(0, -".task.md".length),
|
88
|
-
});
|
89
|
-
}
|
90
|
-
},
|
91
|
-
)
|
92
|
-
.with(
|
93
|
-
{
|
94
|
-
type: "file",
|
95
|
-
filename: P.string.select("filename"),
|
96
|
-
name: P.string.select("name").optional(),
|
97
|
-
},
|
98
|
-
(m) => {
|
99
|
-
addTask(readMarkdown(m.filename), {
|
100
|
-
defaultName: m.name ?? m.filename.slice(0, -".task.md".length),
|
101
|
-
});
|
102
|
-
},
|
103
|
-
)
|
104
|
-
.with(P.string.select(), (mdContent) => {
|
105
|
-
addTask(matter(mdContent), {
|
106
|
-
defaultName: `${tasks.length + 1}`,
|
107
|
-
});
|
108
|
-
})
|
109
|
-
.with(
|
110
|
-
{
|
111
|
-
type: "prompt",
|
112
|
-
content: P.string.select("content"),
|
113
|
-
name: P.string.select("name").optional(),
|
114
|
-
},
|
115
|
-
(m) => {
|
116
|
-
addTask(matter(m.content), {
|
117
|
-
defaultName: m.name ?? `${tasks.length + 1}`,
|
118
|
-
});
|
119
|
-
},
|
120
|
-
)
|
121
|
-
.exhaustive();
|
122
|
-
}
|
123
|
-
return tasks;
|
124
|
-
};
|
125
|
-
|
126
|
-
export type AiTask = ReturnType<typeof resolveAiTasks>[number];
|