tarsk 0.3.36 → 0.3.37
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/index.js
CHANGED
|
@@ -96,7 +96,7 @@ import { cors } from "hono/cors";
|
|
|
96
96
|
import { serve } from "@hono/node-server";
|
|
97
97
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
98
98
|
import path3 from "path";
|
|
99
|
-
import
|
|
99
|
+
import open3 from "open";
|
|
100
100
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
101
101
|
|
|
102
102
|
// src/managers/project-manager.ts
|
|
@@ -1908,12 +1908,7 @@ class MetadataManager {
|
|
|
1908
1908
|
|
|
1909
1909
|
// src/managers/pi-executor.ts
|
|
1910
1910
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
1911
|
-
import {
|
|
1912
|
-
getModel
|
|
1913
|
-
} from "@mariozechner/pi-ai";
|
|
1914
|
-
import { resolve, dirname as dirname5, isAbsolute as isAbsolute2 } from "path";
|
|
1915
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
1916
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1911
|
+
import { resolve as resolve2, isAbsolute as isAbsolute2 } from "path";
|
|
1917
1912
|
|
|
1918
1913
|
// src/tools/bash.ts
|
|
1919
1914
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -2563,6 +2558,38 @@ function resolveReadPath(filePath, cwd) {
|
|
|
2563
2558
|
return resolved;
|
|
2564
2559
|
}
|
|
2565
2560
|
|
|
2561
|
+
// src/tools/tool-helpers.ts
|
|
2562
|
+
async function withAbortSignal(signal, operation) {
|
|
2563
|
+
return new Promise((resolve, reject) => {
|
|
2564
|
+
if (signal?.aborted) {
|
|
2565
|
+
reject(new Error("Operation aborted"));
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
let aborted = false;
|
|
2569
|
+
const onAbort = () => {
|
|
2570
|
+
aborted = true;
|
|
2571
|
+
reject(new Error("Operation aborted"));
|
|
2572
|
+
};
|
|
2573
|
+
if (signal) {
|
|
2574
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2575
|
+
}
|
|
2576
|
+
const cleanup = () => {
|
|
2577
|
+
if (signal)
|
|
2578
|
+
signal.removeEventListener("abort", onAbort);
|
|
2579
|
+
};
|
|
2580
|
+
const abortCheck = () => aborted;
|
|
2581
|
+
operation(abortCheck).then((result) => {
|
|
2582
|
+
cleanup();
|
|
2583
|
+
if (!aborted)
|
|
2584
|
+
resolve(result);
|
|
2585
|
+
}).catch((error) => {
|
|
2586
|
+
cleanup();
|
|
2587
|
+
if (!aborted)
|
|
2588
|
+
reject(error);
|
|
2589
|
+
});
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2566
2593
|
// src/tools/edit.ts
|
|
2567
2594
|
var editSchema = Type2.Object({
|
|
2568
2595
|
path: Type2.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
@@ -2584,83 +2611,49 @@ function createEditTool(cwd, options) {
|
|
|
2584
2611
|
execute: async (_toolCallId, { path, oldText, newText }, signal) => {
|
|
2585
2612
|
const absolutePath = resolveToCwd(path, cwd);
|
|
2586
2613
|
validatePathWithinCwd(absolutePath, cwd);
|
|
2587
|
-
return
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2614
|
+
return withAbortSignal(signal, async (isAborted) => {
|
|
2615
|
+
try {
|
|
2616
|
+
await ops.access(absolutePath);
|
|
2617
|
+
} catch {
|
|
2618
|
+
throw new Error(`File not found: ${path}`);
|
|
2591
2619
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2620
|
+
if (isAborted())
|
|
2621
|
+
return { content: [], details: undefined };
|
|
2622
|
+
const buffer = await ops.readFile(absolutePath);
|
|
2623
|
+
const rawContent = buffer.toString("utf-8");
|
|
2624
|
+
if (isAborted())
|
|
2625
|
+
return { content: [], details: undefined };
|
|
2626
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
2627
|
+
const originalEnding = detectLineEnding(content);
|
|
2628
|
+
const normalizedContent = normalizeToLF(content);
|
|
2629
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
2630
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
2631
|
+
const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
|
|
2632
|
+
if (!matchResult.found) {
|
|
2633
|
+
throw new Error(`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`);
|
|
2634
|
+
}
|
|
2635
|
+
const fuzzyContent = normalizeForFuzzyMatch(normalizedContent);
|
|
2636
|
+
const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
|
|
2637
|
+
const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
|
|
2638
|
+
if (occurrences > 1) {
|
|
2639
|
+
throw new Error(`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`);
|
|
2640
|
+
}
|
|
2641
|
+
if (isAborted())
|
|
2642
|
+
return { content: [], details: undefined };
|
|
2643
|
+
const baseContent = matchResult.contentForReplacement;
|
|
2644
|
+
const newContent = baseContent.substring(0, matchResult.index) + normalizedNewText + baseContent.substring(matchResult.index + matchResult.matchLength);
|
|
2645
|
+
if (baseContent === newContent) {
|
|
2646
|
+
throw new Error(`No changes made to ${path}. The replacement produced identical content.`);
|
|
2647
|
+
}
|
|
2648
|
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
2649
|
+
await ops.writeFile(absolutePath, finalContent);
|
|
2650
|
+
if (isAborted())
|
|
2651
|
+
return { content: [], details: undefined };
|
|
2652
|
+
const diffResult = generateDiffString(baseContent, newContent);
|
|
2653
|
+
return {
|
|
2654
|
+
content: [{ type: "text", text: `Successfully replaced text in ${path}.` }],
|
|
2655
|
+
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
|
|
2596
2656
|
};
|
|
2597
|
-
if (signal)
|
|
2598
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2599
|
-
(async () => {
|
|
2600
|
-
try {
|
|
2601
|
-
try {
|
|
2602
|
-
await ops.access(absolutePath);
|
|
2603
|
-
} catch {
|
|
2604
|
-
if (signal)
|
|
2605
|
-
signal.removeEventListener("abort", onAbort);
|
|
2606
|
-
reject(new Error(`File not found: ${path}`));
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
if (aborted)
|
|
2610
|
-
return;
|
|
2611
|
-
const buffer = await ops.readFile(absolutePath);
|
|
2612
|
-
const rawContent = buffer.toString("utf-8");
|
|
2613
|
-
if (aborted)
|
|
2614
|
-
return;
|
|
2615
|
-
const { bom, text: content } = stripBom(rawContent);
|
|
2616
|
-
const originalEnding = detectLineEnding(content);
|
|
2617
|
-
const normalizedContent = normalizeToLF(content);
|
|
2618
|
-
const normalizedOldText = normalizeToLF(oldText);
|
|
2619
|
-
const normalizedNewText = normalizeToLF(newText);
|
|
2620
|
-
const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
|
|
2621
|
-
if (!matchResult.found) {
|
|
2622
|
-
if (signal)
|
|
2623
|
-
signal.removeEventListener("abort", onAbort);
|
|
2624
|
-
reject(new Error(`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`));
|
|
2625
|
-
return;
|
|
2626
|
-
}
|
|
2627
|
-
const fuzzyContent = normalizeForFuzzyMatch(normalizedContent);
|
|
2628
|
-
const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
|
|
2629
|
-
const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
|
|
2630
|
-
if (occurrences > 1) {
|
|
2631
|
-
if (signal)
|
|
2632
|
-
signal.removeEventListener("abort", onAbort);
|
|
2633
|
-
reject(new Error(`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`));
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
if (aborted)
|
|
2637
|
-
return;
|
|
2638
|
-
const baseContent = matchResult.contentForReplacement;
|
|
2639
|
-
const newContent = baseContent.substring(0, matchResult.index) + normalizedNewText + baseContent.substring(matchResult.index + matchResult.matchLength);
|
|
2640
|
-
if (baseContent === newContent) {
|
|
2641
|
-
if (signal)
|
|
2642
|
-
signal.removeEventListener("abort", onAbort);
|
|
2643
|
-
reject(new Error(`No changes made to ${path}. The replacement produced identical content.`));
|
|
2644
|
-
return;
|
|
2645
|
-
}
|
|
2646
|
-
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
2647
|
-
await ops.writeFile(absolutePath, finalContent);
|
|
2648
|
-
if (aborted)
|
|
2649
|
-
return;
|
|
2650
|
-
if (signal)
|
|
2651
|
-
signal.removeEventListener("abort", onAbort);
|
|
2652
|
-
const diffResult = generateDiffString(baseContent, newContent);
|
|
2653
|
-
resolve({
|
|
2654
|
-
content: [{ type: "text", text: `Successfully replaced text in ${path}.` }],
|
|
2655
|
-
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
|
|
2656
|
-
});
|
|
2657
|
-
} catch (error) {
|
|
2658
|
-
if (signal)
|
|
2659
|
-
signal.removeEventListener("abort", onAbort);
|
|
2660
|
-
if (!aborted)
|
|
2661
|
-
reject(error);
|
|
2662
|
-
}
|
|
2663
|
-
})();
|
|
2664
2657
|
});
|
|
2665
2658
|
}
|
|
2666
2659
|
};
|
|
@@ -3147,87 +3140,64 @@ function createReadTool(cwd, options) {
|
|
|
3147
3140
|
execute: async (_toolCallId, { path: path3, offset, limit }, signal) => {
|
|
3148
3141
|
const absolutePath = resolveReadPath(path3, cwd);
|
|
3149
3142
|
validatePathWithinCwd(absolutePath, cwd);
|
|
3150
|
-
return
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
return;
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
const
|
|
3157
|
-
aborted = true;
|
|
3158
|
-
reject(new Error("Operation aborted"));
|
|
3159
|
-
};
|
|
3160
|
-
if (signal) {
|
|
3161
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
3162
|
-
}
|
|
3163
|
-
(async () => {
|
|
3164
|
-
try {
|
|
3165
|
-
await ops.access(absolutePath);
|
|
3166
|
-
if (aborted)
|
|
3167
|
-
return;
|
|
3168
|
-
const buffer = await ops.readFile(absolutePath);
|
|
3169
|
-
const textContent = buffer.toString("utf-8");
|
|
3170
|
-
const allLines = textContent.split(`
|
|
3143
|
+
return withAbortSignal(signal, async (isAborted) => {
|
|
3144
|
+
await ops.access(absolutePath);
|
|
3145
|
+
if (isAborted())
|
|
3146
|
+
return { content: [], details: undefined };
|
|
3147
|
+
const buffer = await ops.readFile(absolutePath);
|
|
3148
|
+
const textContent = buffer.toString("utf-8");
|
|
3149
|
+
const allLines = textContent.split(`
|
|
3171
3150
|
`);
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3151
|
+
const totalFileLines = allLines.length;
|
|
3152
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
3153
|
+
const startLineDisplay = startLine + 1;
|
|
3154
|
+
if (startLine >= allLines.length) {
|
|
3155
|
+
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
|
|
3156
|
+
}
|
|
3157
|
+
let selectedContent;
|
|
3158
|
+
let userLimitedLines;
|
|
3159
|
+
if (limit !== undefined) {
|
|
3160
|
+
const endLine = Math.min(startLine + limit, allLines.length);
|
|
3161
|
+
selectedContent = allLines.slice(startLine, endLine).join(`
|
|
3183
3162
|
`);
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3163
|
+
userLimitedLines = endLine - startLine;
|
|
3164
|
+
} else {
|
|
3165
|
+
selectedContent = allLines.slice(startLine).join(`
|
|
3187
3166
|
`);
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3167
|
+
}
|
|
3168
|
+
const truncation = truncateHead(selectedContent);
|
|
3169
|
+
let outputText;
|
|
3170
|
+
let details;
|
|
3171
|
+
if (truncation.firstLineExceedsLimit) {
|
|
3172
|
+
outputText = `[Line ${startLineDisplay} is ${formatSize(Buffer.byteLength(allLines[startLine], "utf-8"))}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path3} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
3173
|
+
details = { truncation };
|
|
3174
|
+
} else if (truncation.truncated) {
|
|
3175
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
3176
|
+
const nextOffset = endLineDisplay + 1;
|
|
3177
|
+
outputText = truncation.content;
|
|
3178
|
+
if (truncation.truncatedBy === "lines") {
|
|
3179
|
+
outputText += `
|
|
3201
3180
|
|
|
3202
3181
|
[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
|
3203
|
-
|
|
3204
|
-
|
|
3182
|
+
} else {
|
|
3183
|
+
outputText += `
|
|
3205
3184
|
|
|
3206
3185
|
[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3186
|
+
}
|
|
3187
|
+
details = { truncation };
|
|
3188
|
+
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
3189
|
+
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
3190
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
3191
|
+
outputText = truncation.content;
|
|
3192
|
+
outputText += `
|
|
3214
3193
|
|
|
3215
3194
|
[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
signal.removeEventListener("abort", onAbort);
|
|
3223
|
-
resolve({ content: [{ type: "text", text: outputText }], details });
|
|
3224
|
-
} catch (error) {
|
|
3225
|
-
if (signal)
|
|
3226
|
-
signal.removeEventListener("abort", onAbort);
|
|
3227
|
-
if (!aborted)
|
|
3228
|
-
reject(error);
|
|
3229
|
-
}
|
|
3230
|
-
})();
|
|
3195
|
+
} else {
|
|
3196
|
+
outputText = truncation.content;
|
|
3197
|
+
}
|
|
3198
|
+
if (isAborted())
|
|
3199
|
+
return { content: [], details: undefined };
|
|
3200
|
+
return { content: [{ type: "text", text: outputText }], details };
|
|
3231
3201
|
});
|
|
3232
3202
|
}
|
|
3233
3203
|
};
|
|
@@ -3296,14 +3266,239 @@ function createWriteTool(cwd, options) {
|
|
|
3296
3266
|
}
|
|
3297
3267
|
var writeTool = createWriteTool(process.cwd());
|
|
3298
3268
|
|
|
3269
|
+
// src/tools/skill-tool.ts
|
|
3270
|
+
import { readdir } from "fs/promises";
|
|
3271
|
+
import { join as join7, extname } from "path";
|
|
3272
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
3273
|
+
import { Type as Type8 } from "@sinclair/typebox";
|
|
3274
|
+
import { spawn as spawn7 } from "child_process";
|
|
3275
|
+
function getInterpreter(scriptPath) {
|
|
3276
|
+
const ext = extname(scriptPath);
|
|
3277
|
+
switch (ext) {
|
|
3278
|
+
case ".sh":
|
|
3279
|
+
case ".bash":
|
|
3280
|
+
return { command: "bash", args: [scriptPath] };
|
|
3281
|
+
case ".py":
|
|
3282
|
+
return { command: "python3", args: [scriptPath] };
|
|
3283
|
+
case ".js":
|
|
3284
|
+
return { command: "node", args: [scriptPath] };
|
|
3285
|
+
case ".ts":
|
|
3286
|
+
return { command: "bun", args: ["run", scriptPath] };
|
|
3287
|
+
default:
|
|
3288
|
+
try {
|
|
3289
|
+
const stats = statSync3(scriptPath);
|
|
3290
|
+
if (stats.mode & 73) {
|
|
3291
|
+
return { command: scriptPath, args: [] };
|
|
3292
|
+
}
|
|
3293
|
+
} catch {}
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
async function executeScript(scriptPath, args, cwd, timeout = 30) {
|
|
3298
|
+
const interpreter = getInterpreter(scriptPath);
|
|
3299
|
+
if (!interpreter) {
|
|
3300
|
+
throw new Error(`Unsupported script type: ${scriptPath}`);
|
|
3301
|
+
}
|
|
3302
|
+
return new Promise((resolve, reject) => {
|
|
3303
|
+
const child = spawn7(interpreter.command, [...interpreter.args, ...args], {
|
|
3304
|
+
cwd,
|
|
3305
|
+
timeout: timeout * 1000
|
|
3306
|
+
});
|
|
3307
|
+
let stdout = "";
|
|
3308
|
+
let stderr = "";
|
|
3309
|
+
let timedOut = false;
|
|
3310
|
+
if (child.stdout) {
|
|
3311
|
+
child.stdout.on("data", (data) => {
|
|
3312
|
+
stdout += data.toString();
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
if (child.stderr) {
|
|
3316
|
+
child.stderr.on("data", (data) => {
|
|
3317
|
+
stderr += data.toString();
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
const timeoutHandle = setTimeout(() => {
|
|
3321
|
+
timedOut = true;
|
|
3322
|
+
child.kill("SIGTERM");
|
|
3323
|
+
}, timeout * 1000);
|
|
3324
|
+
child.on("error", (err) => {
|
|
3325
|
+
clearTimeout(timeoutHandle);
|
|
3326
|
+
reject(err);
|
|
3327
|
+
});
|
|
3328
|
+
child.on("close", (code) => {
|
|
3329
|
+
clearTimeout(timeoutHandle);
|
|
3330
|
+
if (timedOut) {
|
|
3331
|
+
reject(new Error(`Script execution timed out after ${timeout}s`));
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
resolve({ stdout, stderr, exitCode: code });
|
|
3335
|
+
});
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
var skillScriptSchema = Type8.Object({
|
|
3339
|
+
skillName: Type8.String({ description: "Name of the skill" }),
|
|
3340
|
+
scriptName: Type8.String({ description: "Name of the script to execute (without path)" }),
|
|
3341
|
+
args: Type8.Optional(Type8.Array(Type8.String(), { description: "Arguments to pass to the script" })),
|
|
3342
|
+
timeout: Type8.Optional(Type8.Number({ description: "Timeout in seconds (default: 30)" }))
|
|
3343
|
+
});
|
|
3344
|
+
function createSkillScriptTool(skills, cwd) {
|
|
3345
|
+
return {
|
|
3346
|
+
name: "execute_skill_script",
|
|
3347
|
+
description: "Execute a script from an activated skill. Skills provide executable scripts in their scripts/ directory.",
|
|
3348
|
+
input_schema: skillScriptSchema,
|
|
3349
|
+
execute: async (input) => {
|
|
3350
|
+
const { skillName, scriptName, args = [], timeout = 30 } = input;
|
|
3351
|
+
const skill = skills.find((s) => s.name === skillName);
|
|
3352
|
+
if (!skill) {
|
|
3353
|
+
return {
|
|
3354
|
+
error: `Skill not found: ${skillName}. Available skills: ${skills.map((s) => s.name).join(", ")}`
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
const scriptsDir = join7(skill.skillPath, "scripts");
|
|
3358
|
+
if (!existsSync6(scriptsDir)) {
|
|
3359
|
+
return {
|
|
3360
|
+
error: `Skill '${skillName}' has no scripts directory`
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
const scriptPath = join7(scriptsDir, scriptName);
|
|
3364
|
+
if (!existsSync6(scriptPath)) {
|
|
3365
|
+
try {
|
|
3366
|
+
const availableScripts = await readdir(scriptsDir);
|
|
3367
|
+
return {
|
|
3368
|
+
error: `Script '${scriptName}' not found in skill '${skillName}'. Available scripts: ${availableScripts.join(", ")}`
|
|
3369
|
+
};
|
|
3370
|
+
} catch {
|
|
3371
|
+
return {
|
|
3372
|
+
error: `Script '${scriptName}' not found in skill '${skillName}'`
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
try {
|
|
3377
|
+
const result = await executeScript(scriptPath, args, cwd, timeout);
|
|
3378
|
+
let output = "";
|
|
3379
|
+
if (result.stdout) {
|
|
3380
|
+
output += `STDOUT:
|
|
3381
|
+
${result.stdout}
|
|
3382
|
+
`;
|
|
3383
|
+
}
|
|
3384
|
+
if (result.stderr) {
|
|
3385
|
+
output += `STDERR:
|
|
3386
|
+
${result.stderr}
|
|
3387
|
+
`;
|
|
3388
|
+
}
|
|
3389
|
+
output += `Exit code: ${result.exitCode}
|
|
3390
|
+
`;
|
|
3391
|
+
return output;
|
|
3392
|
+
} catch (error) {
|
|
3393
|
+
return {
|
|
3394
|
+
error: `Failed to execute script: ${error instanceof Error ? error.message : String(error)}`
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// src/tools/skill-reference-tool.ts
|
|
3402
|
+
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
3403
|
+
import { join as join8, normalize, relative as relative3 } from "path";
|
|
3404
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3405
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
3406
|
+
function isPathSafe(basePath, requestedPath) {
|
|
3407
|
+
const normalized = normalize(requestedPath);
|
|
3408
|
+
const fullPath = join8(basePath, normalized);
|
|
3409
|
+
const relativePath = relative3(basePath, fullPath);
|
|
3410
|
+
return !relativePath.startsWith("..") && !relativePath.startsWith("/");
|
|
3411
|
+
}
|
|
3412
|
+
var skillReferenceSchema = Type9.Object({
|
|
3413
|
+
skillName: Type9.String({ description: "Name of the skill" }),
|
|
3414
|
+
referencePath: Type9.String({
|
|
3415
|
+
description: `Path to the reference file relative to the skill's references/ directory (e.g., "api-reference.md" or "guides/setup.md")`
|
|
3416
|
+
})
|
|
3417
|
+
});
|
|
3418
|
+
function createSkillReferenceTool(skills) {
|
|
3419
|
+
return {
|
|
3420
|
+
name: "read_skill_reference",
|
|
3421
|
+
description: "Read a reference file from an activated skill. Skills provide additional documentation in their references/ directory.",
|
|
3422
|
+
input_schema: skillReferenceSchema,
|
|
3423
|
+
execute: async (input) => {
|
|
3424
|
+
const { skillName, referencePath } = input;
|
|
3425
|
+
const skill = skills.find((s) => s.name === skillName);
|
|
3426
|
+
if (!skill) {
|
|
3427
|
+
return {
|
|
3428
|
+
error: `Skill not found: ${skillName}. Available skills: ${skills.map((s) => s.name).join(", ")}`
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
const referencesDir = join8(skill.skillPath, "references");
|
|
3432
|
+
if (!existsSync7(referencesDir)) {
|
|
3433
|
+
return {
|
|
3434
|
+
error: `Skill '${skillName}' has no references directory`
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
if (!isPathSafe(referencesDir, referencePath)) {
|
|
3438
|
+
return {
|
|
3439
|
+
error: `Invalid reference path: ${referencePath}. Path must be within the skill's references directory.`
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
const fullPath = join8(referencesDir, referencePath);
|
|
3443
|
+
if (!existsSync7(fullPath)) {
|
|
3444
|
+
try {
|
|
3445
|
+
const availableRefs = await listReferencesRecursive(referencesDir);
|
|
3446
|
+
return {
|
|
3447
|
+
error: `Reference file '${referencePath}' not found in skill '${skillName}'. Available references:
|
|
3448
|
+
${availableRefs.join(`
|
|
3449
|
+
`)}`
|
|
3450
|
+
};
|
|
3451
|
+
} catch {
|
|
3452
|
+
return {
|
|
3453
|
+
error: `Reference file '${referencePath}' not found in skill '${skillName}'`
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
const content = await readFile2(fullPath, "utf-8");
|
|
3459
|
+
return content;
|
|
3460
|
+
} catch (error) {
|
|
3461
|
+
return {
|
|
3462
|
+
error: `Failed to read reference file: ${error instanceof Error ? error.message : String(error)}`
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
async function listReferencesRecursive(dir, prefix = "") {
|
|
3469
|
+
const files = [];
|
|
3470
|
+
try {
|
|
3471
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
3472
|
+
for (const entry of entries) {
|
|
3473
|
+
const relativePath = prefix ? join8(prefix, entry.name) : entry.name;
|
|
3474
|
+
if (entry.isDirectory()) {
|
|
3475
|
+
const subFiles = await listReferencesRecursive(join8(dir, entry.name), relativePath);
|
|
3476
|
+
files.push(...subFiles);
|
|
3477
|
+
} else {
|
|
3478
|
+
files.push(relativePath);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
} catch (error) {
|
|
3482
|
+
console.error(`Failed to list references in ${dir}:`, error);
|
|
3483
|
+
}
|
|
3484
|
+
return files;
|
|
3485
|
+
}
|
|
3299
3486
|
// src/tools/index.ts
|
|
3300
3487
|
function createCodingTools(cwd, options) {
|
|
3301
|
-
|
|
3488
|
+
const baseTools = [
|
|
3302
3489
|
createReadTool(cwd, options?.read),
|
|
3303
3490
|
createBashTool(cwd, options?.bash),
|
|
3304
3491
|
createEditTool(cwd),
|
|
3305
3492
|
createWriteTool(cwd)
|
|
3306
3493
|
];
|
|
3494
|
+
if (options?.skills && options.skills.length > 0) {
|
|
3495
|
+
const skillTools = [
|
|
3496
|
+
createSkillScriptTool(options.skills, cwd),
|
|
3497
|
+
createSkillReferenceTool(options.skills)
|
|
3498
|
+
];
|
|
3499
|
+
return [...baseTools, ...skillTools];
|
|
3500
|
+
}
|
|
3501
|
+
return baseTools;
|
|
3307
3502
|
}
|
|
3308
3503
|
|
|
3309
3504
|
// src/provider.ts
|
|
@@ -3493,15 +3688,16 @@ var PROVIDERS = [
|
|
|
3493
3688
|
];
|
|
3494
3689
|
|
|
3495
3690
|
// src/paths.ts
|
|
3496
|
-
import { join as
|
|
3691
|
+
import { join as join9 } from "path";
|
|
3497
3692
|
import { homedir as homedir2 } from "os";
|
|
3498
|
-
var APP_SUPPORT_DIR =
|
|
3499
|
-
var DATA_DIR =
|
|
3693
|
+
var APP_SUPPORT_DIR = join9(homedir2(), "Library", "Application Support", "Tarsk");
|
|
3694
|
+
var DATA_DIR = join9(APP_SUPPORT_DIR, "data");
|
|
3500
3695
|
function getDataDir() {
|
|
3501
3696
|
return DATA_DIR;
|
|
3502
3697
|
}
|
|
3503
3698
|
|
|
3504
|
-
// src/managers/pi-
|
|
3699
|
+
// src/managers/pi-model-resolver.ts
|
|
3700
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
3505
3701
|
var PROVIDER_NAME_TO_PI = {
|
|
3506
3702
|
anthropic: "anthropic",
|
|
3507
3703
|
openai: "openai",
|
|
@@ -3549,32 +3745,185 @@ function resolveModel(providerName, modelId, providerConfig) {
|
|
|
3549
3745
|
maxTokens: 16384
|
|
3550
3746
|
};
|
|
3551
3747
|
}
|
|
3748
|
+
|
|
3749
|
+
// src/managers/pi-event-transformer.ts
|
|
3552
3750
|
function extractTextFromAssistantMessage(msg) {
|
|
3553
3751
|
return msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
3554
3752
|
}
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3753
|
+
|
|
3754
|
+
class EventQueue {
|
|
3755
|
+
queue = [];
|
|
3756
|
+
resolver = null;
|
|
3757
|
+
finalContent = "";
|
|
3758
|
+
errorOccurred = false;
|
|
3759
|
+
handlePiEvent(event) {
|
|
3760
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
3761
|
+
const delta = event.assistantMessageEvent.delta;
|
|
3762
|
+
this.finalContent += delta;
|
|
3763
|
+
this.push({
|
|
3764
|
+
type: "message",
|
|
3765
|
+
role: "assistant",
|
|
3766
|
+
content: delta
|
|
3767
|
+
});
|
|
3768
|
+
} else if (event.type === "tool_execution_start") {
|
|
3769
|
+
this.push({
|
|
3770
|
+
type: "message",
|
|
3771
|
+
role: "tool",
|
|
3772
|
+
content: JSON.stringify([
|
|
3773
|
+
{
|
|
3774
|
+
type: "tool_use",
|
|
3775
|
+
name: event.toolName,
|
|
3776
|
+
id: event.toolCallId,
|
|
3777
|
+
input: event.args
|
|
3778
|
+
}
|
|
3779
|
+
])
|
|
3780
|
+
});
|
|
3781
|
+
} else if (event.type === "tool_execution_end") {
|
|
3782
|
+
const resultContent = event.result?.content?.filter((b) => b.type === "text").map((b) => b.text || "").join("") || JSON.stringify(event.result);
|
|
3783
|
+
this.push({
|
|
3784
|
+
type: "message",
|
|
3785
|
+
role: "tool",
|
|
3786
|
+
content: JSON.stringify([
|
|
3787
|
+
{
|
|
3788
|
+
type: "tool-result",
|
|
3789
|
+
toolCallId: event.toolCallId,
|
|
3790
|
+
toolName: event.toolName,
|
|
3791
|
+
content: resultContent,
|
|
3792
|
+
isError: event.isError
|
|
3793
|
+
}
|
|
3794
|
+
])
|
|
3795
|
+
});
|
|
3796
|
+
} else if (event.type === "agent_end") {
|
|
3797
|
+
const messages = event.messages;
|
|
3798
|
+
const lastAssistant = [...messages].reverse().find((m) => ("role" in m) && m.role === "assistant");
|
|
3799
|
+
if (lastAssistant) {
|
|
3800
|
+
const extracted = extractTextFromAssistantMessage(lastAssistant);
|
|
3801
|
+
if (extracted && extracted !== this.finalContent) {
|
|
3802
|
+
this.finalContent = extracted;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
this.notifyResolver();
|
|
3560
3807
|
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3808
|
+
push(event) {
|
|
3809
|
+
this.queue.push(event);
|
|
3810
|
+
}
|
|
3811
|
+
shift() {
|
|
3812
|
+
return this.queue.shift();
|
|
3813
|
+
}
|
|
3814
|
+
get length() {
|
|
3815
|
+
return this.queue.length;
|
|
3816
|
+
}
|
|
3817
|
+
setResolver(resolver) {
|
|
3818
|
+
this.resolver = resolver;
|
|
3819
|
+
}
|
|
3820
|
+
clearResolver() {
|
|
3821
|
+
this.resolver = null;
|
|
3822
|
+
}
|
|
3823
|
+
notifyResolver() {
|
|
3824
|
+
if (this.resolver) {
|
|
3825
|
+
this.resolver();
|
|
3826
|
+
this.resolver = null;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
// src/managers/pi-prompt-loader.ts
|
|
3832
|
+
import { resolve, dirname as dirname5 } from "path";
|
|
3833
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3834
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3835
|
+
var DEFAULT_PROMPT = "You are a helpful coding assistant. You have access to read, bash, edit, and write tools. Use them to explore and modify the codebase as needed.";
|
|
3836
|
+
function loadAgentSystemPrompt(skills) {
|
|
3837
|
+
let basePrompt;
|
|
3565
3838
|
try {
|
|
3566
3839
|
const __filename2 = fileURLToPath2(import.meta.url);
|
|
3567
3840
|
const __dirname3 = dirname5(__filename2);
|
|
3568
3841
|
const agentsPath = resolve(__dirname3, "../../agents.md");
|
|
3569
3842
|
const content = readFileSync3(agentsPath, "utf-8");
|
|
3570
3843
|
console.log("[ai] Successfully loaded agents.md for system prompt");
|
|
3571
|
-
|
|
3844
|
+
basePrompt = content.trim();
|
|
3572
3845
|
} catch (error) {
|
|
3573
|
-
|
|
3574
|
-
|
|
3846
|
+
const isFileNotFound = error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
3847
|
+
if (isFileNotFound) {
|
|
3848
|
+
console.log("[ai] agents.md not found, using default system prompt");
|
|
3849
|
+
} else {
|
|
3850
|
+
console.warn("[ai] Error loading agents.md, using default prompt:", error instanceof Error ? error.message : String(error));
|
|
3851
|
+
}
|
|
3852
|
+
basePrompt = DEFAULT_PROMPT;
|
|
3853
|
+
}
|
|
3854
|
+
if (!skills || skills.length === 0) {
|
|
3855
|
+
return basePrompt;
|
|
3856
|
+
}
|
|
3857
|
+
let skillsSection = `
|
|
3858
|
+
|
|
3859
|
+
# Available Skills
|
|
3860
|
+
|
|
3861
|
+
`;
|
|
3862
|
+
skillsSection += `The following skills are available for this session. Use them to enhance your capabilities:
|
|
3863
|
+
|
|
3864
|
+
`;
|
|
3865
|
+
for (const skill of skills) {
|
|
3866
|
+
skillsSection += `## ${skill.name}
|
|
3867
|
+
|
|
3868
|
+
`;
|
|
3869
|
+
skillsSection += `**Description**: ${skill.description}
|
|
3870
|
+
|
|
3871
|
+
`;
|
|
3872
|
+
if (skill.compatibility) {
|
|
3873
|
+
skillsSection += `**Compatibility**: ${skill.compatibility}
|
|
3874
|
+
|
|
3875
|
+
`;
|
|
3876
|
+
}
|
|
3877
|
+
skillsSection += skill.instructions;
|
|
3878
|
+
skillsSection += `
|
|
3879
|
+
|
|
3880
|
+
---
|
|
3881
|
+
|
|
3882
|
+
`;
|
|
3883
|
+
}
|
|
3884
|
+
console.log(`[ai] Injected ${skills.length} skill(s) into system prompt: ${skills.map((s) => s.name).join(", ")}`);
|
|
3885
|
+
return basePrompt + skillsSection;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// src/managers/pi-error-utils.ts
|
|
3889
|
+
function getErrorCode(error) {
|
|
3890
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
3891
|
+
const code = error.code;
|
|
3892
|
+
if (typeof code === "string")
|
|
3893
|
+
return code;
|
|
3575
3894
|
}
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
function formatErrorEvent(error) {
|
|
3898
|
+
let errCode;
|
|
3899
|
+
let errMessage;
|
|
3900
|
+
let errStack;
|
|
3901
|
+
let errDetails;
|
|
3902
|
+
if (error instanceof Error) {
|
|
3903
|
+
errMessage = error.message || "Unknown error";
|
|
3904
|
+
errStack = error.stack;
|
|
3905
|
+
errCode = getErrorCode(error);
|
|
3906
|
+
} else if (typeof error === "object" && error !== null) {
|
|
3907
|
+
errMessage = error.message ? String(error.message) : JSON.stringify(error);
|
|
3908
|
+
errCode = getErrorCode(error);
|
|
3909
|
+
errDetails = error;
|
|
3910
|
+
} else if (typeof error === "string") {
|
|
3911
|
+
errMessage = error;
|
|
3912
|
+
} else {
|
|
3913
|
+
errMessage = String(error);
|
|
3914
|
+
}
|
|
3915
|
+
return {
|
|
3916
|
+
type: "error",
|
|
3917
|
+
content: errMessage,
|
|
3918
|
+
error: {
|
|
3919
|
+
code: errCode || "EXECUTION_ERROR",
|
|
3920
|
+
message: errMessage || "An error occurred during execution",
|
|
3921
|
+
details: { stack: errStack, originalError: errDetails }
|
|
3922
|
+
}
|
|
3923
|
+
};
|
|
3576
3924
|
}
|
|
3577
3925
|
|
|
3926
|
+
// src/managers/pi-executor.ts
|
|
3578
3927
|
class PiExecutorImpl {
|
|
3579
3928
|
metadataManager;
|
|
3580
3929
|
constructor(metadataManager) {
|
|
@@ -3591,7 +3940,7 @@ class PiExecutorImpl {
|
|
|
3591
3940
|
throw new Error("No model specified in execution context");
|
|
3592
3941
|
}
|
|
3593
3942
|
model = model.replace(`${providerName.toLowerCase()}/`, "");
|
|
3594
|
-
const cwd = isAbsolute2(context.threadPath) ? context.threadPath :
|
|
3943
|
+
const cwd = isAbsolute2(context.threadPath) ? context.threadPath : resolve2(getDataDir(), context.threadPath);
|
|
3595
3944
|
console.log("[ai] Execution context:", {
|
|
3596
3945
|
model,
|
|
3597
3946
|
cwd,
|
|
@@ -3619,9 +3968,9 @@ class PiExecutorImpl {
|
|
|
3619
3968
|
api: resolvedModel.api,
|
|
3620
3969
|
provider: resolvedModel.provider
|
|
3621
3970
|
});
|
|
3622
|
-
const tools = createCodingTools(cwd);
|
|
3623
|
-
const systemPrompt = loadAgentSystemPrompt();
|
|
3624
|
-
console.log("[ai] System prompt loaded from agents.md");
|
|
3971
|
+
const tools = createCodingTools(cwd, { skills: context.skills });
|
|
3972
|
+
const systemPrompt = loadAgentSystemPrompt(context.skills);
|
|
3973
|
+
console.log("[ai] System prompt loaded from agents.md", context.skills && context.skills.length > 0 ? `with ${context.skills.length} skill(s)` : "");
|
|
3625
3974
|
const agent = new Agent({
|
|
3626
3975
|
initialState: {
|
|
3627
3976
|
systemPrompt,
|
|
@@ -3630,74 +3979,19 @@ class PiExecutorImpl {
|
|
|
3630
3979
|
},
|
|
3631
3980
|
getApiKey: async () => apiKey
|
|
3632
3981
|
});
|
|
3633
|
-
const
|
|
3634
|
-
let resolver = null;
|
|
3982
|
+
const eventQueue = new EventQueue;
|
|
3635
3983
|
let done = false;
|
|
3636
|
-
let finalContent = "";
|
|
3637
|
-
let errorOccurred = false;
|
|
3638
3984
|
const unsubscribe = agent.subscribe((event) => {
|
|
3639
|
-
|
|
3640
|
-
const delta = event.assistantMessageEvent.delta;
|
|
3641
|
-
finalContent += delta;
|
|
3642
|
-
queue.push({
|
|
3643
|
-
type: "message",
|
|
3644
|
-
role: "assistant",
|
|
3645
|
-
content: delta
|
|
3646
|
-
});
|
|
3647
|
-
} else if (event.type === "tool_execution_start") {
|
|
3648
|
-
queue.push({
|
|
3649
|
-
type: "message",
|
|
3650
|
-
role: "tool",
|
|
3651
|
-
content: JSON.stringify([
|
|
3652
|
-
{
|
|
3653
|
-
type: "tool_use",
|
|
3654
|
-
name: event.toolName,
|
|
3655
|
-
id: event.toolCallId,
|
|
3656
|
-
input: event.args
|
|
3657
|
-
}
|
|
3658
|
-
])
|
|
3659
|
-
});
|
|
3660
|
-
} else if (event.type === "tool_execution_end") {
|
|
3661
|
-
const resultContent = event.result?.content?.filter((b) => b.type === "text").map((b) => b.text || "").join("") || JSON.stringify(event.result);
|
|
3662
|
-
queue.push({
|
|
3663
|
-
type: "message",
|
|
3664
|
-
role: "tool",
|
|
3665
|
-
content: JSON.stringify([
|
|
3666
|
-
{
|
|
3667
|
-
type: "tool-result",
|
|
3668
|
-
toolCallId: event.toolCallId,
|
|
3669
|
-
toolName: event.toolName,
|
|
3670
|
-
content: resultContent,
|
|
3671
|
-
isError: event.isError
|
|
3672
|
-
}
|
|
3673
|
-
])
|
|
3674
|
-
});
|
|
3675
|
-
} else if (event.type === "agent_end") {
|
|
3676
|
-
const messages = event.messages;
|
|
3677
|
-
const lastAssistant = [...messages].reverse().find((m) => ("role" in m) && m.role === "assistant");
|
|
3678
|
-
if (lastAssistant) {
|
|
3679
|
-
const extracted = extractTextFromAssistantMessage(lastAssistant);
|
|
3680
|
-
if (extracted && extracted !== finalContent) {
|
|
3681
|
-
finalContent = extracted;
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
if (resolver) {
|
|
3686
|
-
resolver();
|
|
3687
|
-
resolver = null;
|
|
3688
|
-
}
|
|
3985
|
+
eventQueue.handlePiEvent(event);
|
|
3689
3986
|
});
|
|
3690
3987
|
const promptDone = agent.prompt(userPrompt).then(() => {
|
|
3691
3988
|
done = true;
|
|
3692
|
-
|
|
3693
|
-
resolver();
|
|
3694
|
-
resolver = null;
|
|
3695
|
-
}
|
|
3989
|
+
eventQueue.setResolver(() => {});
|
|
3696
3990
|
}).catch((err) => {
|
|
3697
|
-
if (!errorOccurred) {
|
|
3698
|
-
errorOccurred = true;
|
|
3991
|
+
if (!eventQueue.errorOccurred) {
|
|
3992
|
+
eventQueue.errorOccurred = true;
|
|
3699
3993
|
const errMessage = err instanceof Error ? err.message : String(err);
|
|
3700
|
-
|
|
3994
|
+
eventQueue.push({
|
|
3701
3995
|
type: "error",
|
|
3702
3996
|
content: errMessage,
|
|
3703
3997
|
error: {
|
|
@@ -3710,68 +4004,40 @@ class PiExecutorImpl {
|
|
|
3710
4004
|
});
|
|
3711
4005
|
}
|
|
3712
4006
|
done = true;
|
|
3713
|
-
|
|
3714
|
-
resolver();
|
|
3715
|
-
resolver = null;
|
|
3716
|
-
}
|
|
4007
|
+
eventQueue.setResolver(() => {});
|
|
3717
4008
|
});
|
|
3718
4009
|
try {
|
|
3719
|
-
while (!done ||
|
|
3720
|
-
if (
|
|
3721
|
-
yield
|
|
4010
|
+
while (!done || eventQueue.length > 0) {
|
|
4011
|
+
if (eventQueue.length > 0) {
|
|
4012
|
+
yield eventQueue.shift();
|
|
3722
4013
|
} else {
|
|
3723
|
-
await new Promise((
|
|
3724
|
-
|
|
4014
|
+
await new Promise((resolve3) => {
|
|
4015
|
+
eventQueue.setResolver(resolve3);
|
|
3725
4016
|
});
|
|
3726
4017
|
}
|
|
3727
4018
|
}
|
|
3728
4019
|
await promptDone;
|
|
3729
|
-
if (!errorOccurred && finalContent) {
|
|
4020
|
+
if (!eventQueue.errorOccurred && eventQueue.finalContent) {
|
|
3730
4021
|
yield {
|
|
3731
4022
|
type: "result",
|
|
3732
|
-
content: finalContent
|
|
4023
|
+
content: eventQueue.finalContent
|
|
3733
4024
|
};
|
|
3734
4025
|
}
|
|
3735
4026
|
} finally {
|
|
3736
4027
|
unsubscribe();
|
|
3737
4028
|
agent.abort();
|
|
4029
|
+
eventQueue.clearResolver();
|
|
3738
4030
|
}
|
|
3739
4031
|
} catch (error) {
|
|
3740
4032
|
console.error("[ai] Error during execution:", error);
|
|
3741
|
-
|
|
3742
|
-
let errMessage;
|
|
3743
|
-
let errStack;
|
|
3744
|
-
let errDetails;
|
|
3745
|
-
if (error instanceof Error) {
|
|
3746
|
-
errMessage = error.message || "Unknown error";
|
|
3747
|
-
errStack = error.stack;
|
|
3748
|
-
errCode = getErrorCode(error);
|
|
3749
|
-
} else if (typeof error === "object" && error !== null) {
|
|
3750
|
-
errMessage = error.message ? String(error.message) : JSON.stringify(error);
|
|
3751
|
-
errCode = getErrorCode(error);
|
|
3752
|
-
errDetails = error;
|
|
3753
|
-
} else if (typeof error === "string") {
|
|
3754
|
-
errMessage = error;
|
|
3755
|
-
} else {
|
|
3756
|
-
errMessage = String(error);
|
|
3757
|
-
}
|
|
3758
|
-
console.error("[ai] Error details:", { errCode, errMessage, errStack });
|
|
3759
|
-
yield {
|
|
3760
|
-
type: "error",
|
|
3761
|
-
content: errMessage,
|
|
3762
|
-
error: {
|
|
3763
|
-
code: errCode || "EXECUTION_ERROR",
|
|
3764
|
-
message: errMessage || "An error occurred during execution",
|
|
3765
|
-
details: { stack: errStack, originalError: errDetails }
|
|
3766
|
-
}
|
|
3767
|
-
};
|
|
4033
|
+
yield formatErrorEvent(error);
|
|
3768
4034
|
}
|
|
3769
4035
|
}
|
|
3770
4036
|
}
|
|
3771
4037
|
|
|
3772
4038
|
// src/managers/conversation-manager.ts
|
|
3773
4039
|
import { promises as fs2 } from "fs";
|
|
3774
|
-
import { join as
|
|
4040
|
+
import { join as join10, dirname as dirname6 } from "path";
|
|
3775
4041
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3776
4042
|
var CONVERSATION_FILE = "conversation-history.json";
|
|
3777
4043
|
|
|
@@ -3860,8 +4126,8 @@ class ConversationManagerImpl {
|
|
|
3860
4126
|
}
|
|
3861
4127
|
getConversationFilePath(threadPath) {
|
|
3862
4128
|
const threadId = threadPath.split(/[\\/]/).pop() || "unknown";
|
|
3863
|
-
const conversationsDir =
|
|
3864
|
-
return
|
|
4129
|
+
const conversationsDir = join10(this.metadataDir, "conversations");
|
|
4130
|
+
return join10(conversationsDir, threadId, CONVERSATION_FILE);
|
|
3865
4131
|
}
|
|
3866
4132
|
}
|
|
3867
4133
|
|
|
@@ -3908,6 +4174,9 @@ function streamAsyncGenerator(c, generator, options) {
|
|
|
3908
4174
|
});
|
|
3909
4175
|
}
|
|
3910
4176
|
|
|
4177
|
+
// src/routes/projects.ts
|
|
4178
|
+
init_dist();
|
|
4179
|
+
|
|
3911
4180
|
// src/lib/response-builder.ts
|
|
3912
4181
|
class ResponseBuilder {
|
|
3913
4182
|
static error(code, message, statusCode, details) {
|
|
@@ -3927,8 +4196,17 @@ class ResponseBuilder {
|
|
|
3927
4196
|
}
|
|
3928
4197
|
}
|
|
3929
4198
|
|
|
4199
|
+
// src/lib/route-helpers.ts
|
|
4200
|
+
function errorResponse(c, code, message, statusCode, details) {
|
|
4201
|
+
const { response, statusCode: status } = ResponseBuilder.error(code, message, statusCode, details);
|
|
4202
|
+
return c.json(response, status);
|
|
4203
|
+
}
|
|
4204
|
+
function successResponse(c, data, statusCode = 200) {
|
|
4205
|
+
const { response, statusCode: status } = ResponseBuilder.success(data, statusCode);
|
|
4206
|
+
return c.json(response, status);
|
|
4207
|
+
}
|
|
4208
|
+
|
|
3930
4209
|
// src/routes/projects.ts
|
|
3931
|
-
init_dist();
|
|
3932
4210
|
function createProjectRoutes(projectManager, threadManager) {
|
|
3933
4211
|
const router = new Hono;
|
|
3934
4212
|
router.post("/", async (c) => {
|
|
@@ -3936,14 +4214,12 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
3936
4214
|
const body = await c.req.json();
|
|
3937
4215
|
const { gitUrl, commitMethod } = body;
|
|
3938
4216
|
if (!gitUrl || typeof gitUrl !== "string") {
|
|
3939
|
-
|
|
3940
|
-
return c.json(response, statusCode);
|
|
4217
|
+
return errorResponse(c, "INVALID_REQUEST", "gitUrl is required and must be a string", 400);
|
|
3941
4218
|
}
|
|
3942
4219
|
const method = isValidCommitMethod(commitMethod) ? commitMethod : CommitMethods.PullRequest;
|
|
3943
4220
|
return streamAsyncGenerator(c, projectManager.createProject(gitUrl, method));
|
|
3944
4221
|
} catch (error) {
|
|
3945
|
-
|
|
3946
|
-
return c.json(response, statusCode);
|
|
4222
|
+
return errorResponse(c, "REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
|
|
3947
4223
|
}
|
|
3948
4224
|
});
|
|
3949
4225
|
router.get("/", async (c) => {
|
|
@@ -3972,46 +4248,38 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
3972
4248
|
}));
|
|
3973
4249
|
return c.json(expandedProjects);
|
|
3974
4250
|
} catch (error) {
|
|
3975
|
-
|
|
3976
|
-
return c.json(response, statusCode);
|
|
4251
|
+
return errorResponse(c, "LIST_PROJECTS_ERROR", "Failed to list projects", 500, error instanceof Error ? error.message : String(error));
|
|
3977
4252
|
}
|
|
3978
4253
|
});
|
|
3979
4254
|
router.get("/:id", async (c) => {
|
|
3980
4255
|
try {
|
|
3981
4256
|
const projectId = c.req.param("id");
|
|
3982
4257
|
if (!projectId) {
|
|
3983
|
-
|
|
3984
|
-
return c.json(response, statusCode);
|
|
4258
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
3985
4259
|
}
|
|
3986
4260
|
const project = await projectManager.getProject(projectId);
|
|
3987
4261
|
if (!project) {
|
|
3988
|
-
|
|
3989
|
-
return c.json(response, statusCode);
|
|
4262
|
+
return errorResponse(c, "PROJECT_NOT_FOUND", `Project not found: ${projectId}`, 404);
|
|
3990
4263
|
}
|
|
3991
4264
|
return c.json(project);
|
|
3992
4265
|
} catch (error) {
|
|
3993
|
-
|
|
3994
|
-
return c.json(response, statusCode);
|
|
4266
|
+
return errorResponse(c, "GET_PROJECT_ERROR", "Failed to get project", 500, error instanceof Error ? error.message : String(error));
|
|
3995
4267
|
}
|
|
3996
4268
|
});
|
|
3997
4269
|
router.delete("/:id", async (c) => {
|
|
3998
4270
|
try {
|
|
3999
4271
|
const projectId = c.req.param("id");
|
|
4000
4272
|
if (!projectId) {
|
|
4001
|
-
|
|
4002
|
-
return c.json(response2, statusCode2);
|
|
4273
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4003
4274
|
}
|
|
4004
4275
|
await projectManager.deleteProject(projectId);
|
|
4005
|
-
|
|
4006
|
-
return c.json(response, statusCode);
|
|
4276
|
+
return successResponse(c, { success: true, message: "Project deleted successfully" });
|
|
4007
4277
|
} catch (error) {
|
|
4008
4278
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4009
4279
|
if (errorMessage.includes("not found")) {
|
|
4010
|
-
|
|
4011
|
-
return c.json(response2, statusCode2);
|
|
4280
|
+
return errorResponse(c, "PROJECT_NOT_FOUND", errorMessage, 404);
|
|
4012
4281
|
}
|
|
4013
|
-
|
|
4014
|
-
return c.json(response, statusCode);
|
|
4282
|
+
return errorResponse(c, "DELETE_PROJECT_ERROR", "Failed to delete project", 500, errorMessage);
|
|
4015
4283
|
}
|
|
4016
4284
|
});
|
|
4017
4285
|
router.post("/:id/open", async (c) => {
|
|
@@ -4021,31 +4289,25 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
4021
4289
|
const { program } = body;
|
|
4022
4290
|
console.log("[POST /api/projects/:id/open] projectId=%s program=%s", projectId, program);
|
|
4023
4291
|
if (!projectId) {
|
|
4024
|
-
|
|
4025
|
-
return c.json(response2, statusCode2);
|
|
4292
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4026
4293
|
}
|
|
4027
4294
|
if (!program) {
|
|
4028
|
-
|
|
4029
|
-
return c.json(response2, statusCode2);
|
|
4295
|
+
return errorResponse(c, "INVALID_REQUEST", "Program is required", 400);
|
|
4030
4296
|
}
|
|
4031
4297
|
if (!AVAILABLE_PROGRAMS.includes(program)) {
|
|
4032
|
-
|
|
4033
|
-
return c.json(response2, statusCode2);
|
|
4298
|
+
return errorResponse(c, "INVALID_REQUEST", `Invalid program. Must be one of: ${AVAILABLE_PROGRAMS.join(", ")}`, 400);
|
|
4034
4299
|
}
|
|
4035
4300
|
const project = await projectManager.getProject(projectId);
|
|
4036
4301
|
if (!project) {
|
|
4037
|
-
|
|
4038
|
-
return c.json(response2, statusCode2);
|
|
4302
|
+
return errorResponse(c, "PROJECT_NOT_FOUND", `Project not found: ${projectId}`, 404);
|
|
4039
4303
|
}
|
|
4040
4304
|
console.log("[POST /api/projects/:id/open] Calling projectManager.openWith");
|
|
4041
4305
|
await projectManager.openWith(projectId, program);
|
|
4042
4306
|
console.log("[POST /api/projects/:id/open] openWith completed successfully");
|
|
4043
|
-
|
|
4044
|
-
return c.json(response, statusCode);
|
|
4307
|
+
return successResponse(c, { success: true, message: `Project opened in ${program}` });
|
|
4045
4308
|
} catch (error) {
|
|
4046
4309
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4047
|
-
|
|
4048
|
-
return c.json(response, statusCode);
|
|
4310
|
+
return errorResponse(c, "OPEN_PROGRAM_ERROR", "Failed to open project", 500, errorMessage);
|
|
4049
4311
|
}
|
|
4050
4312
|
});
|
|
4051
4313
|
router.put("/:id", async (c) => {
|
|
@@ -4054,22 +4316,18 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
4054
4316
|
const body = await c.req.json();
|
|
4055
4317
|
const { program, setupScript, name, runCommand, commitMethod, runNow } = body;
|
|
4056
4318
|
if (!projectId) {
|
|
4057
|
-
|
|
4058
|
-
return c.json(response2, statusCode2);
|
|
4319
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4059
4320
|
}
|
|
4060
4321
|
const project = await projectManager.getProject(projectId);
|
|
4061
4322
|
if (!project) {
|
|
4062
|
-
|
|
4063
|
-
return c.json(response2, statusCode2);
|
|
4323
|
+
return errorResponse(c, "PROJECT_NOT_FOUND", `Project not found: ${projectId}`, 404);
|
|
4064
4324
|
}
|
|
4065
4325
|
if (program !== undefined) {
|
|
4066
4326
|
if (!program) {
|
|
4067
|
-
|
|
4068
|
-
return c.json(response2, statusCode2);
|
|
4327
|
+
return errorResponse(c, "INVALID_REQUEST", "Program cannot be empty", 400);
|
|
4069
4328
|
}
|
|
4070
4329
|
if (!AVAILABLE_PROGRAMS.includes(program)) {
|
|
4071
|
-
|
|
4072
|
-
return c.json(response2, statusCode2);
|
|
4330
|
+
return errorResponse(c, "INVALID_REQUEST", `Invalid program. Must be one of: ${AVAILABLE_PROGRAMS.join(", ")}`, 400);
|
|
4073
4331
|
}
|
|
4074
4332
|
await projectManager.updateOpenWith(projectId, program);
|
|
4075
4333
|
}
|
|
@@ -4087,24 +4345,20 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
4087
4345
|
if (commitMethod !== undefined) {
|
|
4088
4346
|
if (!isValidCommitMethod(commitMethod)) {
|
|
4089
4347
|
const validMethods = Object.values(CommitMethods).join('", "');
|
|
4090
|
-
|
|
4091
|
-
return c.json(response2, statusCode2);
|
|
4348
|
+
return errorResponse(c, "INVALID_REQUEST", `Commit method must be one of: "${validMethods}"`, 400);
|
|
4092
4349
|
}
|
|
4093
4350
|
await projectManager.updateCommitMethod(projectId, commitMethod);
|
|
4094
4351
|
}
|
|
4095
4352
|
if (name !== undefined) {
|
|
4096
4353
|
if (!name || !name.trim()) {
|
|
4097
|
-
|
|
4098
|
-
return c.json(response2, statusCode2);
|
|
4354
|
+
return errorResponse(c, "INVALID_REQUEST", "Project name is required", 400);
|
|
4099
4355
|
}
|
|
4100
4356
|
await projectManager.updateProjectName(projectId, name);
|
|
4101
4357
|
}
|
|
4102
|
-
|
|
4103
|
-
return c.json(response, statusCode);
|
|
4358
|
+
return successResponse(c, { success: true, message: "Project updated" });
|
|
4104
4359
|
} catch (error) {
|
|
4105
4360
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4106
|
-
|
|
4107
|
-
return c.json(response, statusCode);
|
|
4361
|
+
return errorResponse(c, "UPDATE_PROJECT_ERROR", "Failed to update project", 500, errorMessage);
|
|
4108
4362
|
}
|
|
4109
4363
|
});
|
|
4110
4364
|
router.post("/:id/commands", async (c) => {
|
|
@@ -4112,15 +4366,12 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
4112
4366
|
const projectId = c.req.param("id");
|
|
4113
4367
|
const command = await c.req.json();
|
|
4114
4368
|
if (!projectId) {
|
|
4115
|
-
|
|
4116
|
-
return c.json(response2, statusCode2);
|
|
4369
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4117
4370
|
}
|
|
4118
4371
|
await projectManager.saveCommand(projectId, command);
|
|
4119
|
-
|
|
4120
|
-
return c.json(response, statusCode);
|
|
4372
|
+
return successResponse(c, { success: true, message: "Command saved" });
|
|
4121
4373
|
} catch (error) {
|
|
4122
|
-
|
|
4123
|
-
return c.json(response, statusCode);
|
|
4374
|
+
return errorResponse(c, "SAVE_COMMAND_ERROR", "Failed to save command", 500, error instanceof Error ? error.message : String(error));
|
|
4124
4375
|
}
|
|
4125
4376
|
});
|
|
4126
4377
|
router.delete("/:id/commands/:commandId", async (c) => {
|
|
@@ -4128,15 +4379,12 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
4128
4379
|
const projectId = c.req.param("id");
|
|
4129
4380
|
const commandId = c.req.param("commandId");
|
|
4130
4381
|
if (!projectId || !commandId) {
|
|
4131
|
-
|
|
4132
|
-
return c.json(response2, statusCode2);
|
|
4382
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID and Command ID are required", 400);
|
|
4133
4383
|
}
|
|
4134
4384
|
await projectManager.deleteCommand(projectId, commandId);
|
|
4135
|
-
|
|
4136
|
-
return c.json(response, statusCode);
|
|
4385
|
+
return successResponse(c, { success: true, message: "Command deleted" });
|
|
4137
4386
|
} catch (error) {
|
|
4138
|
-
|
|
4139
|
-
return c.json(response, statusCode);
|
|
4387
|
+
return errorResponse(c, "DELETE_COMMAND_ERROR", "Failed to delete command", 500, error instanceof Error ? error.message : String(error));
|
|
4140
4388
|
}
|
|
4141
4389
|
});
|
|
4142
4390
|
router.post("/:projectId/threads/:threadId/commands/run", async (c) => {
|
|
@@ -4144,71 +4392,58 @@ function createProjectRoutes(projectManager, threadManager) {
|
|
|
4144
4392
|
const threadId = c.req.param("threadId");
|
|
4145
4393
|
const { commandLine, cwd } = await c.req.json();
|
|
4146
4394
|
if (!threadId || !commandLine) {
|
|
4147
|
-
|
|
4148
|
-
return c.json(response, statusCode);
|
|
4395
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID and commandLine are required", 400);
|
|
4149
4396
|
}
|
|
4150
4397
|
return streamAsyncGenerator(c, projectManager.runCommand(threadId, commandLine, cwd));
|
|
4151
4398
|
} catch (error) {
|
|
4152
|
-
|
|
4153
|
-
return c.json(response, statusCode);
|
|
4399
|
+
return errorResponse(c, "RUN_COMMAND_ERROR", "Failed to run command", 500, error instanceof Error ? error.message : String(error));
|
|
4154
4400
|
}
|
|
4155
4401
|
});
|
|
4156
4402
|
router.post("/:id/run", async (c) => {
|
|
4157
4403
|
try {
|
|
4158
4404
|
const projectId = c.req.param("id");
|
|
4159
4405
|
if (!projectId) {
|
|
4160
|
-
|
|
4161
|
-
return c.json(response, statusCode);
|
|
4406
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4162
4407
|
}
|
|
4163
4408
|
return streamAsyncGenerator(c, projectManager.startRunningProcess(projectId));
|
|
4164
4409
|
} catch (error) {
|
|
4165
|
-
|
|
4166
|
-
return c.json(response, statusCode);
|
|
4410
|
+
return errorResponse(c, "START_PROCESS_ERROR", "Failed to start dev server", 500, error instanceof Error ? error.message : String(error));
|
|
4167
4411
|
}
|
|
4168
4412
|
});
|
|
4169
4413
|
router.post("/:id/run-suggest", async (c) => {
|
|
4170
4414
|
try {
|
|
4171
4415
|
const projectId = c.req.param("id");
|
|
4172
4416
|
if (!projectId) {
|
|
4173
|
-
|
|
4174
|
-
return c.json(response2, statusCode2);
|
|
4417
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4175
4418
|
}
|
|
4176
4419
|
const runCommand = await projectManager.suggestRunCommand(projectId);
|
|
4177
|
-
|
|
4178
|
-
return c.json(response, statusCode);
|
|
4420
|
+
return successResponse(c, { runCommand });
|
|
4179
4421
|
} catch (error) {
|
|
4180
|
-
|
|
4181
|
-
return c.json(response, statusCode);
|
|
4422
|
+
return errorResponse(c, "SUGGEST_RUN_COMMAND_ERROR", "Failed to suggest run command", 500, error instanceof Error ? error.message : String(error));
|
|
4182
4423
|
}
|
|
4183
4424
|
});
|
|
4184
4425
|
router.post("/:id/stop", async (c) => {
|
|
4185
4426
|
try {
|
|
4186
4427
|
const projectId = c.req.param("id");
|
|
4187
4428
|
if (!projectId) {
|
|
4188
|
-
|
|
4189
|
-
return c.json(response2, statusCode2);
|
|
4429
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4190
4430
|
}
|
|
4191
4431
|
await projectManager.stopRunningProcess(projectId);
|
|
4192
|
-
|
|
4193
|
-
return c.json(response, statusCode);
|
|
4432
|
+
return successResponse(c, { success: true, message: "Dev server stopped" });
|
|
4194
4433
|
} catch (error) {
|
|
4195
|
-
|
|
4196
|
-
return c.json(response, statusCode);
|
|
4434
|
+
return errorResponse(c, "STOP_PROCESS_ERROR", "Failed to stop dev server", 500, error instanceof Error ? error.message : String(error));
|
|
4197
4435
|
}
|
|
4198
4436
|
});
|
|
4199
4437
|
router.get("/:id/running", async (c) => {
|
|
4200
4438
|
try {
|
|
4201
4439
|
const projectId = c.req.param("id");
|
|
4202
4440
|
if (!projectId) {
|
|
4203
|
-
|
|
4204
|
-
return c.json(response2, statusCode2);
|
|
4441
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
4205
4442
|
}
|
|
4206
4443
|
const isRunning = await projectManager.isProcessRunning(projectId);
|
|
4207
|
-
|
|
4208
|
-
return c.json(response, statusCode);
|
|
4444
|
+
return successResponse(c, { isRunning });
|
|
4209
4445
|
} catch (error) {
|
|
4210
|
-
|
|
4211
|
-
return c.json(response, statusCode);
|
|
4446
|
+
return errorResponse(c, "CHECK_PROCESS_ERROR", "Failed to check process status", 500, error instanceof Error ? error.message : String(error));
|
|
4212
4447
|
}
|
|
4213
4448
|
});
|
|
4214
4449
|
return router;
|
|
@@ -4328,6 +4563,7 @@ function extractAssistantContent(events, fallback) {
|
|
|
4328
4563
|
}
|
|
4329
4564
|
|
|
4330
4565
|
// src/routes/threads.ts
|
|
4566
|
+
import open2 from "open";
|
|
4331
4567
|
var buildStructuredContentFromEvents = (events, fallbackContent) => {
|
|
4332
4568
|
if (!events || events.length === 0) {
|
|
4333
4569
|
return fallbackContent;
|
|
@@ -4378,98 +4614,81 @@ function createThreadRoutes(threadManager, gitManager, conversationManager) {
|
|
|
4378
4614
|
const body = await c.req.json();
|
|
4379
4615
|
const { projectId, title } = body;
|
|
4380
4616
|
if (!projectId || typeof projectId !== "string") {
|
|
4381
|
-
|
|
4382
|
-
return c.json(response, statusCode);
|
|
4617
|
+
return errorResponse(c, "INVALID_REQUEST", "projectId is required and must be a string", 400);
|
|
4383
4618
|
}
|
|
4384
4619
|
if (title !== undefined && typeof title !== "string") {
|
|
4385
|
-
|
|
4386
|
-
return c.json(response, statusCode);
|
|
4620
|
+
return errorResponse(c, "INVALID_REQUEST", "title must be a string if provided", 400);
|
|
4387
4621
|
}
|
|
4388
4622
|
if (title && title.trim() !== "") {
|
|
4389
4623
|
const nameError = validateThreadName(title);
|
|
4390
4624
|
if (nameError) {
|
|
4391
|
-
|
|
4392
|
-
return c.json(response, statusCode);
|
|
4625
|
+
return errorResponse(c, "INVALID_REQUEST", nameError, 400);
|
|
4393
4626
|
}
|
|
4394
4627
|
const existingThreads = await threadManager.listThreads(projectId);
|
|
4395
4628
|
const sanitizedNewName = gitManager.sanitizeBranchName(title);
|
|
4396
4629
|
const existingSanitized = existingThreads.map((t) => gitManager.sanitizeBranchName(t.title));
|
|
4397
4630
|
if (existingSanitized.includes(sanitizedNewName)) {
|
|
4398
|
-
|
|
4399
|
-
return c.json(response, statusCode);
|
|
4631
|
+
return errorResponse(c, "DUPLICATE_BRANCH", `A thread with branch name "${sanitizedNewName}" already exists`, 409);
|
|
4400
4632
|
}
|
|
4401
4633
|
}
|
|
4402
4634
|
return streamAsyncGenerator(c, threadManager.createThread(projectId, title));
|
|
4403
4635
|
} catch (error) {
|
|
4404
|
-
|
|
4405
|
-
return c.json(response, statusCode);
|
|
4636
|
+
return errorResponse(c, "REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
|
|
4406
4637
|
}
|
|
4407
4638
|
});
|
|
4408
4639
|
router.get("/", async (c) => {
|
|
4409
4640
|
try {
|
|
4410
4641
|
const projectId = c.req.query("projectId");
|
|
4411
4642
|
if (!projectId) {
|
|
4412
|
-
|
|
4413
|
-
return c.json(response, statusCode);
|
|
4643
|
+
return errorResponse(c, "INVALID_REQUEST", "projectId query parameter is required", 400);
|
|
4414
4644
|
}
|
|
4415
4645
|
const threads = await threadManager.listThreads(projectId);
|
|
4416
4646
|
return c.json(threads);
|
|
4417
4647
|
} catch (error) {
|
|
4418
|
-
|
|
4419
|
-
return c.json(response, statusCode);
|
|
4648
|
+
return errorResponse(c, "LIST_THREADS_ERROR", "Failed to list threads", 500, error instanceof Error ? error.message : String(error));
|
|
4420
4649
|
}
|
|
4421
4650
|
});
|
|
4422
4651
|
router.delete("/:id", async (c) => {
|
|
4423
4652
|
try {
|
|
4424
4653
|
const threadId = c.req.param("id");
|
|
4425
4654
|
if (!threadId) {
|
|
4426
|
-
|
|
4427
|
-
return c.json(response2, statusCode2);
|
|
4655
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
|
|
4428
4656
|
}
|
|
4429
4657
|
await threadManager.deleteThread(threadId);
|
|
4430
|
-
|
|
4431
|
-
return c.json(response, statusCode);
|
|
4658
|
+
return successResponse(c, { success: true, message: "Thread deleted successfully" });
|
|
4432
4659
|
} catch (error) {
|
|
4433
4660
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4434
4661
|
if (errorMessage.includes("not found")) {
|
|
4435
|
-
|
|
4436
|
-
return c.json(response2, statusCode2);
|
|
4662
|
+
return errorResponse(c, "THREAD_NOT_FOUND", errorMessage, 404);
|
|
4437
4663
|
}
|
|
4438
|
-
|
|
4439
|
-
return c.json(response, statusCode);
|
|
4664
|
+
return errorResponse(c, "DELETE_THREAD_ERROR", "Failed to delete thread", 500, errorMessage);
|
|
4440
4665
|
}
|
|
4441
4666
|
});
|
|
4442
4667
|
router.post("/:id/select", async (c) => {
|
|
4443
4668
|
try {
|
|
4444
4669
|
const threadId = c.req.param("id");
|
|
4445
4670
|
if (!threadId) {
|
|
4446
|
-
|
|
4447
|
-
return c.json(response2, statusCode2);
|
|
4671
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
|
|
4448
4672
|
}
|
|
4449
4673
|
const thread = await threadManager.getThread(threadId);
|
|
4450
4674
|
if (!thread) {
|
|
4451
|
-
|
|
4452
|
-
return c.json(response2, statusCode2);
|
|
4675
|
+
return errorResponse(c, "THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
|
|
4453
4676
|
}
|
|
4454
4677
|
await threadManager.selectThread(threadId);
|
|
4455
|
-
|
|
4456
|
-
return c.json(response, statusCode);
|
|
4678
|
+
return successResponse(c, { success: true, message: "Thread selected successfully", threadId });
|
|
4457
4679
|
} catch (error) {
|
|
4458
|
-
|
|
4459
|
-
return c.json(response, statusCode);
|
|
4680
|
+
return errorResponse(c, "SELECT_THREAD_ERROR", "Failed to select thread", 500, error instanceof Error ? error.message : String(error));
|
|
4460
4681
|
}
|
|
4461
4682
|
});
|
|
4462
4683
|
router.get("/:id/messages", async (c) => {
|
|
4463
4684
|
try {
|
|
4464
4685
|
const threadId = c.req.param("id");
|
|
4465
4686
|
if (!threadId) {
|
|
4466
|
-
|
|
4467
|
-
return c.json(response, statusCode);
|
|
4687
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
|
|
4468
4688
|
}
|
|
4469
4689
|
const thread = await threadManager.getThread(threadId);
|
|
4470
4690
|
if (!thread) {
|
|
4471
|
-
|
|
4472
|
-
return c.json(response, statusCode);
|
|
4691
|
+
return errorResponse(c, "THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
|
|
4473
4692
|
}
|
|
4474
4693
|
const history = await conversationManager.getConversationHistory(thread.path);
|
|
4475
4694
|
if (!history || history.messages.length === 0) {
|
|
@@ -4504,22 +4723,19 @@ function createThreadRoutes(threadManager, gitManager, conversationManager) {
|
|
|
4504
4723
|
});
|
|
4505
4724
|
return c.json(messages);
|
|
4506
4725
|
} catch (error) {
|
|
4507
|
-
|
|
4508
|
-
return c.json(response, statusCode);
|
|
4726
|
+
return errorResponse(c, "GET_THREAD_MESSAGES_ERROR", "Failed to load thread messages", 500, error instanceof Error ? error.message : String(error));
|
|
4509
4727
|
}
|
|
4510
4728
|
});
|
|
4511
4729
|
router.get("/:id/files", async (c) => {
|
|
4512
4730
|
try {
|
|
4513
4731
|
const threadId = c.req.param("id");
|
|
4514
4732
|
if (!threadId) {
|
|
4515
|
-
|
|
4516
|
-
return c.json(response, statusCode);
|
|
4733
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
|
|
4517
4734
|
}
|
|
4518
4735
|
const files = await threadManager.listFiles(threadId);
|
|
4519
4736
|
return c.json(files);
|
|
4520
4737
|
} catch (error) {
|
|
4521
|
-
|
|
4522
|
-
return c.json(response, statusCode);
|
|
4738
|
+
return errorResponse(c, "LIST_THREAD_FILES_ERROR", "Failed to list thread files", 500, error instanceof Error ? error.message : String(error));
|
|
4523
4739
|
}
|
|
4524
4740
|
});
|
|
4525
4741
|
router.patch("/:id", async (c) => {
|
|
@@ -4527,62 +4743,62 @@ function createThreadRoutes(threadManager, gitManager, conversationManager) {
|
|
|
4527
4743
|
const threadId = c.req.param("id");
|
|
4528
4744
|
const body = await c.req.json();
|
|
4529
4745
|
if (!threadId) {
|
|
4530
|
-
|
|
4531
|
-
return c.json(response2, statusCode2);
|
|
4746
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
|
|
4532
4747
|
}
|
|
4533
4748
|
await threadManager.updateThread(threadId, body);
|
|
4534
|
-
|
|
4535
|
-
return c.json(response, statusCode);
|
|
4749
|
+
return successResponse(c, { success: true, message: "Thread updated successfully" });
|
|
4536
4750
|
} catch (error) {
|
|
4537
4751
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4538
|
-
|
|
4539
|
-
return c.json(response, statusCode);
|
|
4752
|
+
return errorResponse(c, "UPDATE_THREAD_ERROR", "Failed to update thread", 500, errorMessage);
|
|
4540
4753
|
}
|
|
4541
4754
|
});
|
|
4755
|
+
const OPEN_APP_NAMES = {
|
|
4756
|
+
"VS Code": ["Visual Studio Code", "code"],
|
|
4757
|
+
Cursor: ["Cursor", "cursor"],
|
|
4758
|
+
Windsurf: ["Windsurf", "windsurf"],
|
|
4759
|
+
Xcode: "Xcode",
|
|
4760
|
+
"Android Studio": ["Android Studio", "studio"],
|
|
4761
|
+
Kiro: ["Kiro", "kiro"]
|
|
4762
|
+
};
|
|
4542
4763
|
router.post("/:id/open", async (c) => {
|
|
4543
4764
|
try {
|
|
4544
4765
|
const threadId = c.req.param("id");
|
|
4545
4766
|
const body = await c.req.json();
|
|
4546
4767
|
const { program } = body;
|
|
4547
4768
|
if (!threadId) {
|
|
4548
|
-
|
|
4549
|
-
return c.json(response2, statusCode2);
|
|
4769
|
+
return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
|
|
4550
4770
|
}
|
|
4551
4771
|
if (!program) {
|
|
4552
|
-
|
|
4553
|
-
return c.json(response2, statusCode2);
|
|
4772
|
+
return errorResponse(c, "INVALID_REQUEST", "Program is required", 400);
|
|
4554
4773
|
}
|
|
4555
4774
|
const { AVAILABLE_PROGRAMS: AVAILABLE_PROGRAMS2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
4556
4775
|
if (!AVAILABLE_PROGRAMS2.includes(program)) {
|
|
4557
|
-
|
|
4558
|
-
return c.json(response2, statusCode2);
|
|
4776
|
+
return errorResponse(c, "INVALID_REQUEST", `Invalid program. Must be one of: ${AVAILABLE_PROGRAMS2.join(", ")}`, 400);
|
|
4559
4777
|
}
|
|
4560
4778
|
const thread = await threadManager.getThread(threadId);
|
|
4561
4779
|
if (!thread) {
|
|
4562
|
-
|
|
4563
|
-
return c.json(response2, statusCode2);
|
|
4780
|
+
return errorResponse(c, "THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
|
|
4564
4781
|
}
|
|
4565
|
-
const { spawn: spawn7 } = await import("child_process");
|
|
4566
4782
|
const threadPath = thread.path;
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
}
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4783
|
+
const appNames = OPEN_APP_NAMES[program];
|
|
4784
|
+
if (!appNames) {
|
|
4785
|
+
return errorResponse(c, "OPEN_PROGRAM_ERROR", `Unsupported program: ${program}`, 500);
|
|
4786
|
+
}
|
|
4787
|
+
console.log("[POST /api/threads/:id/open] threadId=%s program=%s threadPath=%s", threadId, program, threadPath);
|
|
4788
|
+
try {
|
|
4789
|
+
await open2(threadPath, {
|
|
4790
|
+
app: { name: appNames }
|
|
4791
|
+
});
|
|
4792
|
+
console.log("[POST /api/threads/:id/open] Successfully opened thread in %s", program);
|
|
4793
|
+
} catch (openError) {
|
|
4794
|
+
const message = openError instanceof Error ? openError.message : String(openError);
|
|
4795
|
+
console.error("[POST /api/threads/:id/open] Failed to open thread:", message);
|
|
4796
|
+
return errorResponse(c, "OPEN_PROGRAM_ERROR", `Failed to open ${program}: ${message}`, 500);
|
|
4797
|
+
}
|
|
4798
|
+
return successResponse(c, { success: true, message: `Thread opened in ${program}` });
|
|
4582
4799
|
} catch (error) {
|
|
4583
4800
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4584
|
-
|
|
4585
|
-
return c.json(response, statusCode);
|
|
4801
|
+
return errorResponse(c, "OPEN_PROGRAM_ERROR", "Failed to open thread", 500, errorMessage);
|
|
4586
4802
|
}
|
|
4587
4803
|
});
|
|
4588
4804
|
return router;
|
|
@@ -4591,9 +4807,274 @@ function createThreadRoutes(threadManager, gitManager, conversationManager) {
|
|
|
4591
4807
|
// src/routes/chat.ts
|
|
4592
4808
|
import { Hono as Hono3 } from "hono";
|
|
4593
4809
|
|
|
4810
|
+
// src/managers/skill-manager.ts
|
|
4811
|
+
import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
|
|
4812
|
+
import { join as join11 } from "path";
|
|
4813
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4814
|
+
import { homedir as homedir3 } from "os";
|
|
4815
|
+
function parseFrontmatter(markdown) {
|
|
4816
|
+
const lines = markdown.split(`
|
|
4817
|
+
`);
|
|
4818
|
+
if (lines[0]?.trim() !== "---") {
|
|
4819
|
+
return {
|
|
4820
|
+
metadata: {},
|
|
4821
|
+
content: markdown.trim()
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
let endIndex = -1;
|
|
4825
|
+
for (let i = 1;i < lines.length; i++) {
|
|
4826
|
+
if (lines[i]?.trim() === "---") {
|
|
4827
|
+
endIndex = i;
|
|
4828
|
+
break;
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
if (endIndex === -1) {
|
|
4832
|
+
return {
|
|
4833
|
+
metadata: {},
|
|
4834
|
+
content: markdown.trim()
|
|
4835
|
+
};
|
|
4836
|
+
}
|
|
4837
|
+
const frontmatterLines = lines.slice(1, endIndex);
|
|
4838
|
+
const metadata = {};
|
|
4839
|
+
for (const line of frontmatterLines) {
|
|
4840
|
+
const colonIndex = line.indexOf(":");
|
|
4841
|
+
if (colonIndex === -1)
|
|
4842
|
+
continue;
|
|
4843
|
+
const key = line.slice(0, colonIndex).trim();
|
|
4844
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
4845
|
+
if (key === "name") {
|
|
4846
|
+
metadata.name = value;
|
|
4847
|
+
} else if (key === "description") {
|
|
4848
|
+
metadata.description = value;
|
|
4849
|
+
} else if (key === "license") {
|
|
4850
|
+
metadata.license = value;
|
|
4851
|
+
} else if (key === "compatibility") {
|
|
4852
|
+
metadata.compatibility = value;
|
|
4853
|
+
} else if (key === "allowed-tools") {
|
|
4854
|
+
metadata.allowedTools = value;
|
|
4855
|
+
} else if (key === "metadata") {}
|
|
4856
|
+
}
|
|
4857
|
+
const content = lines.slice(endIndex + 1).join(`
|
|
4858
|
+
`).trim();
|
|
4859
|
+
return { metadata, content };
|
|
4860
|
+
}
|
|
4861
|
+
function getGlobalSkillsDir() {
|
|
4862
|
+
return join11(homedir3(), ".tarsk", "skills");
|
|
4863
|
+
}
|
|
4864
|
+
function getProjectSkillsDir(threadPath) {
|
|
4865
|
+
return join11(threadPath, ".tarsk", "skills");
|
|
4866
|
+
}
|
|
4867
|
+
function validateSkillName(name) {
|
|
4868
|
+
if (!name || name.length === 0 || name.length > 64) {
|
|
4869
|
+
return false;
|
|
4870
|
+
}
|
|
4871
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
4872
|
+
return false;
|
|
4873
|
+
}
|
|
4874
|
+
if (name.startsWith("-") || name.endsWith("-")) {
|
|
4875
|
+
return false;
|
|
4876
|
+
}
|
|
4877
|
+
if (name.includes("--")) {
|
|
4878
|
+
return false;
|
|
4879
|
+
}
|
|
4880
|
+
return true;
|
|
4881
|
+
}
|
|
4882
|
+
function validateDescription(description) {
|
|
4883
|
+
return description && description.length > 0 && description.length <= 1024;
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
class SkillManager {
|
|
4887
|
+
async loadSkills(threadPath) {
|
|
4888
|
+
const skills = new Map;
|
|
4889
|
+
const globalDir = getGlobalSkillsDir();
|
|
4890
|
+
if (existsSync8(globalDir)) {
|
|
4891
|
+
const globalSkills = await this.loadSkillsFromDir(globalDir, "global");
|
|
4892
|
+
for (const skill of globalSkills) {
|
|
4893
|
+
skills.set(skill.name, skill);
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
const projectDir = getProjectSkillsDir(threadPath);
|
|
4897
|
+
if (existsSync8(projectDir)) {
|
|
4898
|
+
const projectSkills = await this.loadSkillsFromDir(projectDir, "project");
|
|
4899
|
+
for (const skill of projectSkills) {
|
|
4900
|
+
skills.set(skill.name, skill);
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
return Array.from(skills.values());
|
|
4904
|
+
}
|
|
4905
|
+
async loadSkillsFromDir(dir, scope) {
|
|
4906
|
+
const skills = [];
|
|
4907
|
+
try {
|
|
4908
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
4909
|
+
for (const entry of entries) {
|
|
4910
|
+
if (!entry.isDirectory())
|
|
4911
|
+
continue;
|
|
4912
|
+
const skillDirName = entry.name;
|
|
4913
|
+
const skillPath = join11(dir, skillDirName);
|
|
4914
|
+
const skillFilePath = join11(skillPath, "SKILL.md");
|
|
4915
|
+
if (!existsSync8(skillFilePath)) {
|
|
4916
|
+
console.warn(`Skipping skill directory ${skillDirName}: SKILL.md not found`);
|
|
4917
|
+
continue;
|
|
4918
|
+
}
|
|
4919
|
+
try {
|
|
4920
|
+
const fileContent = await readFile3(skillFilePath, "utf-8");
|
|
4921
|
+
const { metadata, content } = parseFrontmatter(fileContent);
|
|
4922
|
+
if (!metadata.name) {
|
|
4923
|
+
console.warn(`Skipping skill in ${skillDirName}: missing 'name' in frontmatter`);
|
|
4924
|
+
continue;
|
|
4925
|
+
}
|
|
4926
|
+
if (!metadata.description) {
|
|
4927
|
+
console.warn(`Skipping skill in ${skillDirName}: missing 'description' in frontmatter`);
|
|
4928
|
+
continue;
|
|
4929
|
+
}
|
|
4930
|
+
if (!validateSkillName(metadata.name)) {
|
|
4931
|
+
console.warn(`Skipping skill in ${skillDirName}: invalid name format '${metadata.name}'. ` + `Name must be 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens.`);
|
|
4932
|
+
continue;
|
|
4933
|
+
}
|
|
4934
|
+
if (!validateDescription(metadata.description)) {
|
|
4935
|
+
console.warn(`Skipping skill in ${skillDirName}: description must be 1-1024 characters`);
|
|
4936
|
+
continue;
|
|
4937
|
+
}
|
|
4938
|
+
if (metadata.name !== skillDirName) {
|
|
4939
|
+
console.warn(`Skipping skill in ${skillDirName}: directory name must match frontmatter name '${metadata.name}'`);
|
|
4940
|
+
continue;
|
|
4941
|
+
}
|
|
4942
|
+
const skill = {
|
|
4943
|
+
name: metadata.name,
|
|
4944
|
+
description: metadata.description,
|
|
4945
|
+
scope,
|
|
4946
|
+
skillPath,
|
|
4947
|
+
instructions: content
|
|
4948
|
+
};
|
|
4949
|
+
if (metadata.license) {
|
|
4950
|
+
skill.license = metadata.license;
|
|
4951
|
+
}
|
|
4952
|
+
if (metadata.compatibility) {
|
|
4953
|
+
skill.compatibility = metadata.compatibility;
|
|
4954
|
+
}
|
|
4955
|
+
if (metadata.metadata) {
|
|
4956
|
+
skill.metadata = metadata.metadata;
|
|
4957
|
+
}
|
|
4958
|
+
if (metadata.allowedTools) {
|
|
4959
|
+
skill.allowedTools = metadata.allowedTools;
|
|
4960
|
+
}
|
|
4961
|
+
skills.push(skill);
|
|
4962
|
+
} catch (error) {
|
|
4963
|
+
console.error(`Failed to load skill from ${skillDirName}:`, error);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
} catch (error) {
|
|
4967
|
+
console.error(`Failed to load skills from ${dir}:`, error);
|
|
4968
|
+
}
|
|
4969
|
+
return skills;
|
|
4970
|
+
}
|
|
4971
|
+
async getSkill(name, threadPath) {
|
|
4972
|
+
const skills = await this.loadSkills(threadPath);
|
|
4973
|
+
return skills.find((skill) => skill.name === name) || null;
|
|
4974
|
+
}
|
|
4975
|
+
validateSkillName(name) {
|
|
4976
|
+
return validateSkillName(name);
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
// src/managers/skill-activation.ts
|
|
4981
|
+
var STOP_WORDS = new Set([
|
|
4982
|
+
"a",
|
|
4983
|
+
"an",
|
|
4984
|
+
"and",
|
|
4985
|
+
"are",
|
|
4986
|
+
"as",
|
|
4987
|
+
"at",
|
|
4988
|
+
"be",
|
|
4989
|
+
"by",
|
|
4990
|
+
"for",
|
|
4991
|
+
"from",
|
|
4992
|
+
"has",
|
|
4993
|
+
"he",
|
|
4994
|
+
"in",
|
|
4995
|
+
"is",
|
|
4996
|
+
"it",
|
|
4997
|
+
"its",
|
|
4998
|
+
"of",
|
|
4999
|
+
"on",
|
|
5000
|
+
"that",
|
|
5001
|
+
"the",
|
|
5002
|
+
"to",
|
|
5003
|
+
"was",
|
|
5004
|
+
"will",
|
|
5005
|
+
"with",
|
|
5006
|
+
"when",
|
|
5007
|
+
"where",
|
|
5008
|
+
"which",
|
|
5009
|
+
"who",
|
|
5010
|
+
"use",
|
|
5011
|
+
"used",
|
|
5012
|
+
"using",
|
|
5013
|
+
"this",
|
|
5014
|
+
"can",
|
|
5015
|
+
"do",
|
|
5016
|
+
"does",
|
|
5017
|
+
"how",
|
|
5018
|
+
"what",
|
|
5019
|
+
"i",
|
|
5020
|
+
"you",
|
|
5021
|
+
"we",
|
|
5022
|
+
"they",
|
|
5023
|
+
"them",
|
|
5024
|
+
"their",
|
|
5025
|
+
"my",
|
|
5026
|
+
"your",
|
|
5027
|
+
"our"
|
|
5028
|
+
]);
|
|
5029
|
+
function extractKeywords(text) {
|
|
5030
|
+
const keywords = new Set;
|
|
5031
|
+
const words = text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 3);
|
|
5032
|
+
for (const word of words) {
|
|
5033
|
+
if (!STOP_WORDS.has(word)) {
|
|
5034
|
+
keywords.add(word);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
return keywords;
|
|
5038
|
+
}
|
|
5039
|
+
function calculateRelevanceScore(taskKeywords, skillDescription) {
|
|
5040
|
+
const skillKeywords = extractKeywords(skillDescription);
|
|
5041
|
+
if (skillKeywords.size === 0 || taskKeywords.size === 0) {
|
|
5042
|
+
return 0;
|
|
5043
|
+
}
|
|
5044
|
+
let matches = 0;
|
|
5045
|
+
for (const keyword of taskKeywords) {
|
|
5046
|
+
if (skillKeywords.has(keyword)) {
|
|
5047
|
+
matches++;
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
return matches / taskKeywords.size;
|
|
5051
|
+
}
|
|
5052
|
+
function autoDiscoverSkills(allSkills, taskDescription, relevanceThreshold = 0.1, maxSkills = 5) {
|
|
5053
|
+
const taskKeywords = extractKeywords(taskDescription);
|
|
5054
|
+
const scoredSkills = allSkills.map((skill) => ({
|
|
5055
|
+
skill,
|
|
5056
|
+
score: calculateRelevanceScore(taskKeywords, skill.description)
|
|
5057
|
+
}));
|
|
5058
|
+
const relevantSkills = scoredSkills.filter(({ score }) => score >= relevanceThreshold).sort((a, b) => b.score - a.score).slice(0, maxSkills).map(({ skill }) => skill);
|
|
5059
|
+
return relevantSkills;
|
|
5060
|
+
}
|
|
5061
|
+
async function activateSkills(allSkills, taskDescription, thread, options) {
|
|
5062
|
+
const { relevanceThreshold = 0.1, maxSkills = 5 } = options || {};
|
|
5063
|
+
if (thread?.enabledSkills && thread.enabledSkills.length > 0) {
|
|
5064
|
+
const enabledSet = new Set(thread.enabledSkills);
|
|
5065
|
+
return allSkills.filter((skill) => enabledSet.has(skill.name));
|
|
5066
|
+
}
|
|
5067
|
+
const autoDiscovered = autoDiscoverSkills(allSkills, taskDescription, relevanceThreshold, maxSkills);
|
|
5068
|
+
if (thread?.disabledSkills && thread.disabledSkills.length > 0) {
|
|
5069
|
+
const disabledSet = new Set(thread.disabledSkills);
|
|
5070
|
+
return autoDiscovered.filter((skill) => !disabledSet.has(skill.name));
|
|
5071
|
+
}
|
|
5072
|
+
return autoDiscovered;
|
|
5073
|
+
}
|
|
5074
|
+
|
|
4594
5075
|
// src/utils.ts
|
|
4595
5076
|
function delay(ms) {
|
|
4596
|
-
return new Promise((
|
|
5077
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4597
5078
|
}
|
|
4598
5079
|
|
|
4599
5080
|
// src/routes/chat.ts
|
|
@@ -4611,34 +5092,28 @@ function createChatRoutes(threadManager, agentExecutor, conversationManager, pro
|
|
|
4611
5092
|
}
|
|
4612
5093
|
}
|
|
4613
5094
|
if (!threadId || typeof threadId !== "string") {
|
|
4614
|
-
|
|
4615
|
-
return c.json(response, statusCode);
|
|
5095
|
+
return errorResponse(c, "INVALID_REQUEST", "threadId is required and must be a string", 400);
|
|
4616
5096
|
}
|
|
4617
5097
|
if (!content || typeof content !== "string") {
|
|
4618
|
-
|
|
4619
|
-
return c.json(response, statusCode);
|
|
5098
|
+
return errorResponse(c, "INVALID_REQUEST", "content is required and must be a string", 400);
|
|
4620
5099
|
}
|
|
4621
5100
|
if (!model || typeof model !== "string") {
|
|
4622
|
-
|
|
4623
|
-
return c.json(response, statusCode);
|
|
5101
|
+
return errorResponse(c, "INVALID_REQUEST", "model is required and must be a string", 400);
|
|
4624
5102
|
}
|
|
4625
5103
|
const thread = await threadManager.getThread(threadId);
|
|
4626
5104
|
if (!thread) {
|
|
4627
|
-
|
|
4628
|
-
return c.json(response, statusCode);
|
|
5105
|
+
return errorResponse(c, "THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
|
|
4629
5106
|
}
|
|
4630
5107
|
const threadPath = thread.path;
|
|
4631
5108
|
if (content.trim().toLowerCase() === "replay") {
|
|
4632
5109
|
console.log("[ChatRoute] Replay request detected");
|
|
4633
5110
|
const lastMessages = await conversationManager.getLastMessages(threadPath, 1);
|
|
4634
5111
|
if (lastMessages.length === 0) {
|
|
4635
|
-
|
|
4636
|
-
return c.json(response2, statusCode);
|
|
5112
|
+
return errorResponse(c, "NO_HISTORY", "No previous conversation found to replay", 404);
|
|
4637
5113
|
}
|
|
4638
5114
|
const lastMessage = lastMessages[0];
|
|
4639
5115
|
if (!lastMessage.response) {
|
|
4640
|
-
|
|
4641
|
-
return c.json(response2, statusCode);
|
|
5116
|
+
return errorResponse(c, "NO_RESPONSE", "Previous message has no completed response to replay", 404);
|
|
4642
5117
|
}
|
|
4643
5118
|
console.log("[ChatRoute] Replaying message:", {
|
|
4644
5119
|
messageId: lastMessage.id,
|
|
@@ -4669,13 +5144,20 @@ function createChatRoutes(threadManager, agentExecutor, conversationManager, pro
|
|
|
4669
5144
|
}
|
|
4670
5145
|
return streamAsyncGenerator(c, replayGenerator());
|
|
4671
5146
|
}
|
|
5147
|
+
const skillManager = new SkillManager;
|
|
5148
|
+
const allSkills = await skillManager.loadSkills(threadPath);
|
|
5149
|
+
const activatedSkills = await activateSkills(allSkills, content, thread);
|
|
5150
|
+
if (activatedSkills.length > 0) {
|
|
5151
|
+
console.log("[ChatRoute] Activated skills:", activatedSkills.map((s) => s.name).join(", "));
|
|
5152
|
+
}
|
|
4672
5153
|
const context = {
|
|
4673
5154
|
threadId,
|
|
4674
5155
|
threadPath,
|
|
4675
5156
|
model,
|
|
4676
5157
|
provider,
|
|
4677
5158
|
attachments,
|
|
4678
|
-
planMode
|
|
5159
|
+
planMode,
|
|
5160
|
+
skills: activatedSkills
|
|
4679
5161
|
};
|
|
4680
5162
|
console.log("[ChatRoute] Execution context:", {
|
|
4681
5163
|
threadId,
|
|
@@ -4740,28 +5222,23 @@ User: ${content}` : content;
|
|
|
4740
5222
|
}
|
|
4741
5223
|
return streamAsyncGenerator(c, chatExecutionGenerator());
|
|
4742
5224
|
} catch (error) {
|
|
4743
|
-
|
|
4744
|
-
return c.json(response, statusCode);
|
|
5225
|
+
return errorResponse(c, "REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
|
|
4745
5226
|
}
|
|
4746
5227
|
});
|
|
4747
5228
|
router.delete("/:threadId", async (c) => {
|
|
4748
5229
|
try {
|
|
4749
5230
|
const threadId = c.req.param("threadId");
|
|
4750
5231
|
if (!threadId || typeof threadId !== "string") {
|
|
4751
|
-
|
|
4752
|
-
return c.json(response2, statusCode2);
|
|
5232
|
+
return errorResponse(c, "INVALID_REQUEST", "threadId is required and must be a string", 400);
|
|
4753
5233
|
}
|
|
4754
5234
|
const thread = await threadManager.getThread(threadId);
|
|
4755
5235
|
if (!thread) {
|
|
4756
|
-
|
|
4757
|
-
return c.json(response2, statusCode2);
|
|
5236
|
+
return errorResponse(c, "THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
|
|
4758
5237
|
}
|
|
4759
5238
|
await conversationManager.clearChatHistory(thread.path);
|
|
4760
|
-
|
|
4761
|
-
return c.json(response, statusCode);
|
|
5239
|
+
return successResponse(c, { message: "Chat history cleared" }, 200);
|
|
4762
5240
|
} catch (error) {
|
|
4763
|
-
|
|
4764
|
-
return c.json(response, statusCode);
|
|
5241
|
+
return errorResponse(c, "CLEAR_CHAT_ERROR", "Failed to clear chat history", 500, error instanceof Error ? error.message : String(error));
|
|
4765
5242
|
}
|
|
4766
5243
|
});
|
|
4767
5244
|
return router;
|
|
@@ -4934,9 +5411,9 @@ async function getAIHubMixCredits(apiKey) {
|
|
|
4934
5411
|
|
|
4935
5412
|
// src/utils/env-manager.ts
|
|
4936
5413
|
import { promises as fs3 } from "fs";
|
|
4937
|
-
import { join as
|
|
5414
|
+
import { join as join12 } from "path";
|
|
4938
5415
|
async function updateEnvFile(keyNames) {
|
|
4939
|
-
const envPath =
|
|
5416
|
+
const envPath = join12(process.cwd(), ".env");
|
|
4940
5417
|
let content = "";
|
|
4941
5418
|
try {
|
|
4942
5419
|
content = await fs3.readFile(envPath, "utf-8");
|
|
@@ -4967,7 +5444,7 @@ async function updateEnvFile(keyNames) {
|
|
|
4967
5444
|
`, "utf-8");
|
|
4968
5445
|
}
|
|
4969
5446
|
async function readEnvFile() {
|
|
4970
|
-
const envPath =
|
|
5447
|
+
const envPath = join12(process.cwd(), ".env");
|
|
4971
5448
|
const envMap = {};
|
|
4972
5449
|
try {
|
|
4973
5450
|
const content = await fs3.readFile(envPath, "utf-8");
|
|
@@ -5753,10 +6230,10 @@ function createModelRoutes(metadataManager) {
|
|
|
5753
6230
|
|
|
5754
6231
|
// src/routes/git.ts
|
|
5755
6232
|
import { Hono as Hono6 } from "hono";
|
|
5756
|
-
import { spawn as
|
|
5757
|
-
import { existsSync as
|
|
5758
|
-
import { join as
|
|
5759
|
-
import { isAbsolute as isAbsolute3, normalize, resolve as
|
|
6233
|
+
import { spawn as spawn8 } from "child_process";
|
|
6234
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
6235
|
+
import { join as join13 } from "path";
|
|
6236
|
+
import { isAbsolute as isAbsolute3, normalize as normalize2, resolve as resolve3 } from "path";
|
|
5760
6237
|
import {
|
|
5761
6238
|
completeSimple,
|
|
5762
6239
|
getModel as getModel2
|
|
@@ -5881,12 +6358,12 @@ Generate only the commit message, nothing else:`;
|
|
|
5881
6358
|
}
|
|
5882
6359
|
}
|
|
5883
6360
|
function resolveThreadPath(repoPath) {
|
|
5884
|
-
const base = isAbsolute3(repoPath) ? repoPath :
|
|
5885
|
-
return
|
|
6361
|
+
const base = isAbsolute3(repoPath) ? repoPath : resolve3(getDataDir(), repoPath);
|
|
6362
|
+
return normalize2(base);
|
|
5886
6363
|
}
|
|
5887
6364
|
function getGitRoot(cwd) {
|
|
5888
6365
|
return new Promise((resolveRoot, reject) => {
|
|
5889
|
-
const proc =
|
|
6366
|
+
const proc = spawn8("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
5890
6367
|
let out = "";
|
|
5891
6368
|
let err = "";
|
|
5892
6369
|
proc.stdout.on("data", (d) => {
|
|
@@ -5909,8 +6386,8 @@ function createGitRoutes(metadataManager) {
|
|
|
5909
6386
|
const router = new Hono6;
|
|
5910
6387
|
router.get("/username", async (c) => {
|
|
5911
6388
|
try {
|
|
5912
|
-
const name = await new Promise((
|
|
5913
|
-
const proc =
|
|
6389
|
+
const name = await new Promise((resolve4, reject) => {
|
|
6390
|
+
const proc = spawn8("git", ["config", "user.name"]);
|
|
5914
6391
|
let out = "";
|
|
5915
6392
|
let err = "";
|
|
5916
6393
|
proc.stdout.on("data", (d) => {
|
|
@@ -5921,7 +6398,7 @@ function createGitRoutes(metadataManager) {
|
|
|
5921
6398
|
});
|
|
5922
6399
|
proc.on("close", (code) => {
|
|
5923
6400
|
if (code === 0) {
|
|
5924
|
-
|
|
6401
|
+
resolve4(out.trim());
|
|
5925
6402
|
} else {
|
|
5926
6403
|
reject(new Error(err.trim() || `git exited ${code}`));
|
|
5927
6404
|
}
|
|
@@ -5945,7 +6422,7 @@ function createGitRoutes(metadataManager) {
|
|
|
5945
6422
|
return c.json({ error: "Thread path not found" }, 404);
|
|
5946
6423
|
}
|
|
5947
6424
|
const absolutePath = resolveThreadPath(repoPath);
|
|
5948
|
-
if (!
|
|
6425
|
+
if (!existsSync9(absolutePath)) {
|
|
5949
6426
|
return c.json({
|
|
5950
6427
|
error: `Thread repo path does not exist: ${absolutePath}. Check that the project folder is present.`
|
|
5951
6428
|
}, 400);
|
|
@@ -5959,8 +6436,8 @@ function createGitRoutes(metadataManager) {
|
|
|
5959
6436
|
error: `Path is not a git repository: ${absolutePath}. ${msg}`
|
|
5960
6437
|
}, 400);
|
|
5961
6438
|
}
|
|
5962
|
-
const { hasChanges, changedFilesCount } = await new Promise((
|
|
5963
|
-
const proc =
|
|
6439
|
+
const { hasChanges, changedFilesCount } = await new Promise((resolve4) => {
|
|
6440
|
+
const proc = spawn8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: gitRoot });
|
|
5964
6441
|
let out = "";
|
|
5965
6442
|
proc.stdout.on("data", (d) => {
|
|
5966
6443
|
out += d.toString();
|
|
@@ -5968,34 +6445,34 @@ function createGitRoutes(metadataManager) {
|
|
|
5968
6445
|
proc.on("close", () => {
|
|
5969
6446
|
const lines = out.trim().split(`
|
|
5970
6447
|
`).filter((line) => line.length > 0);
|
|
5971
|
-
|
|
6448
|
+
resolve4({
|
|
5972
6449
|
hasChanges: lines.length > 0,
|
|
5973
6450
|
changedFilesCount: lines.length
|
|
5974
6451
|
});
|
|
5975
6452
|
});
|
|
5976
|
-
proc.on("error", () =>
|
|
6453
|
+
proc.on("error", () => resolve4({ hasChanges: false, changedFilesCount: 0 }));
|
|
5977
6454
|
});
|
|
5978
|
-
const currentBranch = await new Promise((
|
|
5979
|
-
const proc =
|
|
6455
|
+
const currentBranch = await new Promise((resolve4) => {
|
|
6456
|
+
const proc = spawn8("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
|
|
5980
6457
|
let out = "";
|
|
5981
6458
|
proc.stdout.on("data", (d) => {
|
|
5982
6459
|
out += d.toString();
|
|
5983
6460
|
});
|
|
5984
6461
|
proc.on("close", () => {
|
|
5985
|
-
|
|
6462
|
+
resolve4(out.trim());
|
|
5986
6463
|
});
|
|
5987
|
-
proc.on("error", () =>
|
|
6464
|
+
proc.on("error", () => resolve4(""));
|
|
5988
6465
|
});
|
|
5989
|
-
const hasUnpushedCommits = await new Promise((
|
|
5990
|
-
const proc =
|
|
6466
|
+
const hasUnpushedCommits = await new Promise((resolve4) => {
|
|
6467
|
+
const proc = spawn8("git", ["log", `origin/${currentBranch}..HEAD`, "--oneline"], { cwd: gitRoot });
|
|
5991
6468
|
let out = "";
|
|
5992
6469
|
proc.stdout.on("data", (d) => {
|
|
5993
6470
|
out += d.toString();
|
|
5994
6471
|
});
|
|
5995
6472
|
proc.on("close", () => {
|
|
5996
|
-
|
|
6473
|
+
resolve4(out.trim().length > 0);
|
|
5997
6474
|
});
|
|
5998
|
-
proc.on("error", () =>
|
|
6475
|
+
proc.on("error", () => resolve4(false));
|
|
5999
6476
|
});
|
|
6000
6477
|
return c.json({
|
|
6001
6478
|
hasChanges,
|
|
@@ -6025,14 +6502,14 @@ function createGitRoutes(metadataManager) {
|
|
|
6025
6502
|
} catch {
|
|
6026
6503
|
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6027
6504
|
}
|
|
6028
|
-
const statusOutput = await new Promise((
|
|
6029
|
-
const proc =
|
|
6505
|
+
const statusOutput = await new Promise((resolve4) => {
|
|
6506
|
+
const proc = spawn8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: gitRoot });
|
|
6030
6507
|
let out = "";
|
|
6031
6508
|
proc.stdout.on("data", (d) => {
|
|
6032
6509
|
out += d.toString();
|
|
6033
6510
|
});
|
|
6034
|
-
proc.on("close", () =>
|
|
6035
|
-
proc.on("error", () =>
|
|
6511
|
+
proc.on("close", () => resolve4(out));
|
|
6512
|
+
proc.on("error", () => resolve4(""));
|
|
6036
6513
|
});
|
|
6037
6514
|
const changedFiles = [];
|
|
6038
6515
|
const statusLines = statusOutput.trim().split(`
|
|
@@ -6066,51 +6543,51 @@ function createGitRoutes(metadataManager) {
|
|
|
6066
6543
|
let newContent = "";
|
|
6067
6544
|
const hunks = [];
|
|
6068
6545
|
if (file.status === "deleted") {
|
|
6069
|
-
const content = await new Promise((
|
|
6070
|
-
const proc =
|
|
6546
|
+
const content = await new Promise((resolve4) => {
|
|
6547
|
+
const proc = spawn8("git", ["show", `HEAD:${file.path}`], { cwd: gitRoot });
|
|
6071
6548
|
let out = "";
|
|
6072
6549
|
proc.stdout.on("data", (d) => {
|
|
6073
6550
|
out += d.toString();
|
|
6074
6551
|
});
|
|
6075
|
-
proc.on("close", () =>
|
|
6076
|
-
proc.on("error", () =>
|
|
6552
|
+
proc.on("close", () => resolve4(out));
|
|
6553
|
+
proc.on("error", () => resolve4(""));
|
|
6077
6554
|
});
|
|
6078
6555
|
oldContent = content;
|
|
6079
6556
|
} else if (file.status === "added" && file.path.startsWith("(new)")) {
|
|
6080
6557
|
const fs4 = await import("fs");
|
|
6081
6558
|
try {
|
|
6082
|
-
newContent = fs4.readFileSync(
|
|
6559
|
+
newContent = fs4.readFileSync(resolve3(gitRoot, file.path.replace("(new) ", "")), "utf-8");
|
|
6083
6560
|
} catch {
|
|
6084
6561
|
newContent = "";
|
|
6085
6562
|
}
|
|
6086
6563
|
} else {
|
|
6087
|
-
const diffOutput = await new Promise((
|
|
6088
|
-
const proc =
|
|
6564
|
+
const diffOutput = await new Promise((resolve4) => {
|
|
6565
|
+
const proc = spawn8("git", file.status === "added" ? ["diff", "--cached", "--", file.path] : ["diff", "HEAD", "--", file.path], { cwd: gitRoot });
|
|
6089
6566
|
let out = "";
|
|
6090
6567
|
proc.stdout.on("data", (d) => {
|
|
6091
6568
|
out += d.toString();
|
|
6092
6569
|
});
|
|
6093
|
-
proc.on("close", () =>
|
|
6094
|
-
proc.on("error", () =>
|
|
6570
|
+
proc.on("close", () => resolve4(out));
|
|
6571
|
+
proc.on("error", () => resolve4(""));
|
|
6095
6572
|
});
|
|
6096
6573
|
const lines = diffOutput.split(`
|
|
6097
6574
|
`);
|
|
6098
6575
|
let currentHunk = null;
|
|
6099
6576
|
let oldLineNum = 0;
|
|
6100
6577
|
let newLineNum = 0;
|
|
6101
|
-
const oldContentOutput = await new Promise((
|
|
6102
|
-
const proc =
|
|
6578
|
+
const oldContentOutput = await new Promise((resolve4) => {
|
|
6579
|
+
const proc = spawn8("git", ["show", `HEAD:${file.path}`], { cwd: gitRoot });
|
|
6103
6580
|
let out = "";
|
|
6104
6581
|
proc.stdout.on("data", (d) => {
|
|
6105
6582
|
out += d.toString();
|
|
6106
6583
|
});
|
|
6107
|
-
proc.on("close", () =>
|
|
6108
|
-
proc.on("error", () =>
|
|
6584
|
+
proc.on("close", () => resolve4(out));
|
|
6585
|
+
proc.on("error", () => resolve4(""));
|
|
6109
6586
|
});
|
|
6110
6587
|
oldContent = oldContentOutput;
|
|
6111
6588
|
const fs4 = await import("fs");
|
|
6112
6589
|
try {
|
|
6113
|
-
newContent = fs4.readFileSync(
|
|
6590
|
+
newContent = fs4.readFileSync(resolve3(gitRoot, file.path), "utf-8");
|
|
6114
6591
|
} catch {
|
|
6115
6592
|
newContent = "";
|
|
6116
6593
|
}
|
|
@@ -6193,7 +6670,7 @@ function createGitRoutes(metadataManager) {
|
|
|
6193
6670
|
const absolutePath = resolveThreadPath(repoPath);
|
|
6194
6671
|
process.stdout.write(`[generate-commit-message] resolved path: ${absolutePath}
|
|
6195
6672
|
`);
|
|
6196
|
-
if (!
|
|
6673
|
+
if (!existsSync9(absolutePath)) {
|
|
6197
6674
|
process.stdout.write(`[generate-commit-message] path does not exist: ${absolutePath}
|
|
6198
6675
|
`);
|
|
6199
6676
|
return c.json({
|
|
@@ -6217,7 +6694,7 @@ function createGitRoutes(metadataManager) {
|
|
|
6217
6694
|
const runUnstagedDiff = () => {
|
|
6218
6695
|
process.stdout.write(`[generate-commit-message] using unstaged diff (git diff)
|
|
6219
6696
|
`);
|
|
6220
|
-
const proc2 =
|
|
6697
|
+
const proc2 = spawn8("git", ["diff"], { cwd: gitRoot });
|
|
6221
6698
|
let out2 = "";
|
|
6222
6699
|
proc2.stdout.on("data", (d) => {
|
|
6223
6700
|
out2 += d.toString();
|
|
@@ -6227,7 +6704,7 @@ function createGitRoutes(metadataManager) {
|
|
|
6227
6704
|
});
|
|
6228
6705
|
proc2.on("error", reject);
|
|
6229
6706
|
};
|
|
6230
|
-
const proc =
|
|
6707
|
+
const proc = spawn8("git", ["diff", "--cached"], { cwd: gitRoot });
|
|
6231
6708
|
let out = "";
|
|
6232
6709
|
let err = "";
|
|
6233
6710
|
proc.stdout.on("data", (d) => {
|
|
@@ -6262,7 +6739,7 @@ function createGitRoutes(metadataManager) {
|
|
|
6262
6739
|
});
|
|
6263
6740
|
if (!diff.trim()) {
|
|
6264
6741
|
const statusOut = await new Promise((resolveStatus) => {
|
|
6265
|
-
const proc =
|
|
6742
|
+
const proc = spawn8("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: gitRoot });
|
|
6266
6743
|
let out = "";
|
|
6267
6744
|
proc.stdout.on("data", (d) => {
|
|
6268
6745
|
out += d.toString();
|
|
@@ -6286,11 +6763,11 @@ function createGitRoutes(metadataManager) {
|
|
|
6286
6763
|
const parts = [];
|
|
6287
6764
|
const maxFileSize = 1e5;
|
|
6288
6765
|
for (const relPath of untrackedPaths) {
|
|
6289
|
-
const fullPath =
|
|
6290
|
-
if (!
|
|
6766
|
+
const fullPath = join13(gitRoot, relPath);
|
|
6767
|
+
if (!existsSync9(fullPath))
|
|
6291
6768
|
continue;
|
|
6292
6769
|
try {
|
|
6293
|
-
if (
|
|
6770
|
+
if (statSync4(fullPath).isDirectory())
|
|
6294
6771
|
continue;
|
|
6295
6772
|
const content = readFileSync4(fullPath, "utf-8");
|
|
6296
6773
|
const safeContent = content.length > maxFileSize ? content.slice(0, maxFileSize) + `
|
|
@@ -6353,21 +6830,21 @@ new file mode 100644
|
|
|
6353
6830
|
} catch {
|
|
6354
6831
|
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6355
6832
|
}
|
|
6356
|
-
await new Promise((
|
|
6357
|
-
const proc =
|
|
6833
|
+
await new Promise((resolve4, reject) => {
|
|
6834
|
+
const proc = spawn8("git", ["add", "-A"], { cwd: gitRoot });
|
|
6358
6835
|
proc.on("close", (code) => {
|
|
6359
6836
|
if (code === 0)
|
|
6360
|
-
|
|
6837
|
+
resolve4();
|
|
6361
6838
|
else
|
|
6362
6839
|
reject(new Error("Failed to stage changes"));
|
|
6363
6840
|
});
|
|
6364
6841
|
proc.on("error", reject);
|
|
6365
6842
|
});
|
|
6366
|
-
await new Promise((
|
|
6367
|
-
const proc =
|
|
6843
|
+
await new Promise((resolve4, reject) => {
|
|
6844
|
+
const proc = spawn8("git", ["commit", "-m", message], { cwd: gitRoot });
|
|
6368
6845
|
proc.on("close", (code) => {
|
|
6369
6846
|
if (code === 0)
|
|
6370
|
-
|
|
6847
|
+
resolve4();
|
|
6371
6848
|
else
|
|
6372
6849
|
reject(new Error("Failed to commit changes"));
|
|
6373
6850
|
});
|
|
@@ -6397,24 +6874,24 @@ new file mode 100644
|
|
|
6397
6874
|
} catch {
|
|
6398
6875
|
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6399
6876
|
}
|
|
6400
|
-
const currentBranch = await new Promise((
|
|
6401
|
-
const proc =
|
|
6877
|
+
const currentBranch = await new Promise((resolve4, reject) => {
|
|
6878
|
+
const proc = spawn8("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
|
|
6402
6879
|
let out = "";
|
|
6403
6880
|
proc.stdout.on("data", (d) => {
|
|
6404
6881
|
out += d.toString();
|
|
6405
6882
|
});
|
|
6406
|
-
proc.on("close", () =>
|
|
6883
|
+
proc.on("close", () => resolve4(out.trim()));
|
|
6407
6884
|
proc.on("error", reject);
|
|
6408
6885
|
});
|
|
6409
|
-
await new Promise((
|
|
6410
|
-
const proc =
|
|
6886
|
+
await new Promise((resolve4, reject) => {
|
|
6887
|
+
const proc = spawn8("git", ["push", "-u", "origin", currentBranch], { cwd: gitRoot });
|
|
6411
6888
|
let err = "";
|
|
6412
6889
|
proc.stderr.on("data", (d) => {
|
|
6413
6890
|
err += d.toString();
|
|
6414
6891
|
});
|
|
6415
6892
|
proc.on("close", (code) => {
|
|
6416
6893
|
if (code === 0)
|
|
6417
|
-
|
|
6894
|
+
resolve4();
|
|
6418
6895
|
else
|
|
6419
6896
|
reject(new Error(err || "Failed to push changes"));
|
|
6420
6897
|
});
|
|
@@ -6444,15 +6921,15 @@ new file mode 100644
|
|
|
6444
6921
|
} catch {
|
|
6445
6922
|
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6446
6923
|
}
|
|
6447
|
-
await new Promise((
|
|
6448
|
-
const proc =
|
|
6924
|
+
await new Promise((resolve4, reject) => {
|
|
6925
|
+
const proc = spawn8("git", ["fetch", "origin"], { cwd: gitRoot });
|
|
6449
6926
|
let err = "";
|
|
6450
6927
|
proc.stderr.on("data", (d) => {
|
|
6451
6928
|
err += d.toString();
|
|
6452
6929
|
});
|
|
6453
6930
|
proc.on("close", (code) => {
|
|
6454
6931
|
if (code === 0)
|
|
6455
|
-
|
|
6932
|
+
resolve4();
|
|
6456
6933
|
else
|
|
6457
6934
|
reject(new Error(err || "Failed to fetch from origin"));
|
|
6458
6935
|
});
|
|
@@ -6486,7 +6963,7 @@ new file mode 100644
|
|
|
6486
6963
|
}
|
|
6487
6964
|
const diff = await new Promise((resolveDiff, reject) => {
|
|
6488
6965
|
const runPlainDiff = () => {
|
|
6489
|
-
const proc2 =
|
|
6966
|
+
const proc2 = spawn8("git", ["diff"], { cwd: gitRoot });
|
|
6490
6967
|
let out2 = "";
|
|
6491
6968
|
proc2.stdout.on("data", (d) => {
|
|
6492
6969
|
out2 += d.toString();
|
|
@@ -6494,7 +6971,7 @@ new file mode 100644
|
|
|
6494
6971
|
proc2.on("close", () => resolveDiff(out2));
|
|
6495
6972
|
proc2.on("error", reject);
|
|
6496
6973
|
};
|
|
6497
|
-
const proc =
|
|
6974
|
+
const proc = spawn8("git", ["diff", "HEAD"], { cwd: gitRoot });
|
|
6498
6975
|
let out = "";
|
|
6499
6976
|
let err = "";
|
|
6500
6977
|
proc.stdout.on("data", (d) => {
|
|
@@ -6578,8 +7055,8 @@ DESCRIPTION: <description here>`;
|
|
|
6578
7055
|
} catch {
|
|
6579
7056
|
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6580
7057
|
}
|
|
6581
|
-
const commits = await new Promise((
|
|
6582
|
-
const proc =
|
|
7058
|
+
const commits = await new Promise((resolve4, reject) => {
|
|
7059
|
+
const proc = spawn8("git", ["log", "--oneline", "-n", limit.toString(), "--format=%H|%s|%an|%ai"], { cwd: gitRoot });
|
|
6583
7060
|
let out = "";
|
|
6584
7061
|
let err = "";
|
|
6585
7062
|
proc.stdout.on("data", (d) => {
|
|
@@ -6601,7 +7078,7 @@ DESCRIPTION: <description here>`;
|
|
|
6601
7078
|
date: parts[3] || ""
|
|
6602
7079
|
};
|
|
6603
7080
|
});
|
|
6604
|
-
|
|
7081
|
+
resolve4(parsed);
|
|
6605
7082
|
} else {
|
|
6606
7083
|
reject(new Error(err || "Failed to get git log"));
|
|
6607
7084
|
}
|
|
@@ -6634,18 +7111,18 @@ DESCRIPTION: <description here>`;
|
|
|
6634
7111
|
} catch {
|
|
6635
7112
|
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6636
7113
|
}
|
|
6637
|
-
const currentBranch = await new Promise((
|
|
6638
|
-
const proc =
|
|
7114
|
+
const currentBranch = await new Promise((resolve4, reject) => {
|
|
7115
|
+
const proc = spawn8("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
|
|
6639
7116
|
let out = "";
|
|
6640
7117
|
proc.stdout.on("data", (d) => {
|
|
6641
7118
|
out += d.toString();
|
|
6642
7119
|
});
|
|
6643
|
-
proc.on("close", () =>
|
|
7120
|
+
proc.on("close", () => resolve4(out.trim()));
|
|
6644
7121
|
proc.on("error", reject);
|
|
6645
7122
|
});
|
|
6646
|
-
const prUrl = await new Promise((
|
|
7123
|
+
const prUrl = await new Promise((resolve4, reject) => {
|
|
6647
7124
|
const args = ["pr", "create", "--title", title || currentBranch, "--body", description || ""];
|
|
6648
|
-
const proc =
|
|
7125
|
+
const proc = spawn8("gh", args, { cwd: gitRoot });
|
|
6649
7126
|
let out = "";
|
|
6650
7127
|
let err = "";
|
|
6651
7128
|
proc.stdout.on("data", (d) => {
|
|
@@ -6657,7 +7134,7 @@ DESCRIPTION: <description here>`;
|
|
|
6657
7134
|
proc.on("close", (code) => {
|
|
6658
7135
|
if (code === 0) {
|
|
6659
7136
|
const urlMatch = out.match(/https:\/\/[^\s]+/);
|
|
6660
|
-
|
|
7137
|
+
resolve4(urlMatch ? urlMatch[0] : out.trim());
|
|
6661
7138
|
} else {
|
|
6662
7139
|
reject(new Error(err || "Failed to create PR"));
|
|
6663
7140
|
}
|
|
@@ -6681,84 +7158,69 @@ function createRunRoutes(projectManager) {
|
|
|
6681
7158
|
try {
|
|
6682
7159
|
const projectId = c.req.param("id");
|
|
6683
7160
|
if (!projectId) {
|
|
6684
|
-
|
|
6685
|
-
return c.json(response2, statusCode2);
|
|
7161
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
6686
7162
|
}
|
|
6687
7163
|
const runCommand = await projectManager.suggestRunCommand(projectId);
|
|
6688
|
-
|
|
6689
|
-
return c.json(response, statusCode);
|
|
7164
|
+
return successResponse(c, { runCommand });
|
|
6690
7165
|
} catch (error) {
|
|
6691
7166
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6692
|
-
|
|
6693
|
-
return c.json(response, statusCode);
|
|
7167
|
+
return errorResponse(c, "SUGGEST_RUN_COMMAND_ERROR", "Failed to suggest run command", 500, errorMessage);
|
|
6694
7168
|
}
|
|
6695
7169
|
});
|
|
6696
7170
|
router.post("/:id/run", async (c) => {
|
|
6697
7171
|
try {
|
|
6698
7172
|
const projectId = c.req.param("id");
|
|
6699
7173
|
if (!projectId) {
|
|
6700
|
-
|
|
6701
|
-
return c.json(response, statusCode);
|
|
7174
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
6702
7175
|
}
|
|
6703
7176
|
return streamAsyncGenerator(c, projectManager.startRunningProcess(projectId));
|
|
6704
7177
|
} catch (error) {
|
|
6705
7178
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6706
|
-
|
|
6707
|
-
return c.json(response, statusCode);
|
|
7179
|
+
return errorResponse(c, "RUN_PROCESS_ERROR", "Failed to start process", 500, errorMessage);
|
|
6708
7180
|
}
|
|
6709
7181
|
});
|
|
6710
7182
|
router.post("/:id/stop", async (c) => {
|
|
6711
7183
|
try {
|
|
6712
7184
|
const projectId = c.req.param("id");
|
|
6713
7185
|
if (!projectId) {
|
|
6714
|
-
|
|
6715
|
-
return c.json(response2, statusCode2);
|
|
7186
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
6716
7187
|
}
|
|
6717
7188
|
const stopped = await projectManager.stopRunningProcess(projectId);
|
|
6718
7189
|
if (!stopped) {
|
|
6719
|
-
|
|
6720
|
-
return c.json(response2, statusCode2);
|
|
7190
|
+
return errorResponse(c, "NO_PROCESS_RUNNING", "No process running for this project", 400);
|
|
6721
7191
|
}
|
|
6722
|
-
|
|
6723
|
-
return c.json(response, statusCode);
|
|
7192
|
+
return successResponse(c, { success: true, message: "Process stopped" });
|
|
6724
7193
|
} catch (error) {
|
|
6725
7194
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6726
|
-
|
|
6727
|
-
return c.json(response, statusCode);
|
|
7195
|
+
return errorResponse(c, "STOP_PROCESS_ERROR", "Failed to stop process", 500, errorMessage);
|
|
6728
7196
|
}
|
|
6729
7197
|
});
|
|
6730
7198
|
router.get("/:id/running", async (c) => {
|
|
6731
7199
|
try {
|
|
6732
7200
|
const projectId = c.req.param("id");
|
|
6733
7201
|
if (!projectId) {
|
|
6734
|
-
|
|
6735
|
-
return c.json(response2, statusCode2);
|
|
7202
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
6736
7203
|
}
|
|
6737
7204
|
const isRunning = await projectManager.isProcessRunning(projectId);
|
|
6738
|
-
|
|
6739
|
-
return c.json(response, statusCode);
|
|
7205
|
+
return successResponse(c, { isRunning });
|
|
6740
7206
|
} catch (error) {
|
|
6741
7207
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6742
|
-
|
|
6743
|
-
return c.json(response, statusCode);
|
|
7208
|
+
return errorResponse(c, "CHECK_RUNNING_ERROR", "Failed to check running status", 500, errorMessage);
|
|
6744
7209
|
}
|
|
6745
7210
|
});
|
|
6746
7211
|
router.put("/:id/run-command", async (c) => {
|
|
6747
7212
|
try {
|
|
6748
7213
|
const projectId = c.req.param("id");
|
|
6749
7214
|
if (!projectId) {
|
|
6750
|
-
|
|
6751
|
-
return c.json(response2, statusCode2);
|
|
7215
|
+
return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
|
|
6752
7216
|
}
|
|
6753
7217
|
const body = await c.req.json();
|
|
6754
7218
|
const { runCommand } = body;
|
|
6755
7219
|
await projectManager.updateRunCommand(projectId, runCommand || null);
|
|
6756
|
-
|
|
6757
|
-
return c.json(response, statusCode);
|
|
7220
|
+
return successResponse(c, { success: true, message: "Run command updated" });
|
|
6758
7221
|
} catch (error) {
|
|
6759
7222
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6760
|
-
|
|
6761
|
-
return c.json(response, statusCode);
|
|
7223
|
+
return errorResponse(c, "UPDATE_RUN_COMMAND_ERROR", "Failed to update run command", 500, errorMessage);
|
|
6762
7224
|
}
|
|
6763
7225
|
});
|
|
6764
7226
|
return router;
|
|
@@ -6766,7 +7228,7 @@ function createRunRoutes(projectManager) {
|
|
|
6766
7228
|
|
|
6767
7229
|
// src/routes/onboarding.ts
|
|
6768
7230
|
import { Hono as Hono8 } from "hono";
|
|
6769
|
-
import { spawn as
|
|
7231
|
+
import { spawn as spawn9 } from "child_process";
|
|
6770
7232
|
function createOnboardingRoutes(metadataManager) {
|
|
6771
7233
|
const router = new Hono8;
|
|
6772
7234
|
router.get("/status", async (c) => {
|
|
@@ -6780,17 +7242,17 @@ function createOnboardingRoutes(metadataManager) {
|
|
|
6780
7242
|
});
|
|
6781
7243
|
router.get("/git-check", async (c) => {
|
|
6782
7244
|
try {
|
|
6783
|
-
const gitInstalled = await new Promise((
|
|
6784
|
-
const proc =
|
|
7245
|
+
const gitInstalled = await new Promise((resolve4) => {
|
|
7246
|
+
const proc = spawn9("git", ["--version"]);
|
|
6785
7247
|
let _err = "";
|
|
6786
7248
|
proc.stderr.on("data", (d) => {
|
|
6787
7249
|
_err += d.toString();
|
|
6788
7250
|
});
|
|
6789
7251
|
proc.on("close", (code) => {
|
|
6790
|
-
|
|
7252
|
+
resolve4(code === 0);
|
|
6791
7253
|
});
|
|
6792
7254
|
proc.on("error", () => {
|
|
6793
|
-
|
|
7255
|
+
resolve4(false);
|
|
6794
7256
|
});
|
|
6795
7257
|
});
|
|
6796
7258
|
return c.json({ gitInstalled });
|
|
@@ -6858,6 +7320,357 @@ function createScaffoldRoutes(projectManager) {
|
|
|
6858
7320
|
return router;
|
|
6859
7321
|
}
|
|
6860
7322
|
|
|
7323
|
+
// src/managers/slash-command-manager.ts
|
|
7324
|
+
import { readdir as readdir4, readFile as readFile4, mkdir as mkdir2, writeFile, unlink } from "fs/promises";
|
|
7325
|
+
import { join as join14, basename, extname as extname2 } from "path";
|
|
7326
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7327
|
+
import { homedir as homedir4 } from "os";
|
|
7328
|
+
function slugify(filename) {
|
|
7329
|
+
return filename.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
7330
|
+
}
|
|
7331
|
+
function parseFrontmatter2(markdown) {
|
|
7332
|
+
const lines = markdown.split(`
|
|
7333
|
+
`);
|
|
7334
|
+
if (lines[0]?.trim() !== "---") {
|
|
7335
|
+
return {
|
|
7336
|
+
metadata: {},
|
|
7337
|
+
content: markdown.trim()
|
|
7338
|
+
};
|
|
7339
|
+
}
|
|
7340
|
+
let endIndex = -1;
|
|
7341
|
+
for (let i = 1;i < lines.length; i++) {
|
|
7342
|
+
if (lines[i]?.trim() === "---") {
|
|
7343
|
+
endIndex = i;
|
|
7344
|
+
break;
|
|
7345
|
+
}
|
|
7346
|
+
}
|
|
7347
|
+
if (endIndex === -1) {
|
|
7348
|
+
return {
|
|
7349
|
+
metadata: {},
|
|
7350
|
+
content: markdown.trim()
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
const frontmatterLines = lines.slice(1, endIndex);
|
|
7354
|
+
const metadata = {};
|
|
7355
|
+
for (const line of frontmatterLines) {
|
|
7356
|
+
const colonIndex = line.indexOf(":");
|
|
7357
|
+
if (colonIndex === -1)
|
|
7358
|
+
continue;
|
|
7359
|
+
const key = line.slice(0, colonIndex).trim();
|
|
7360
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
7361
|
+
if (key === "description") {
|
|
7362
|
+
metadata.description = value;
|
|
7363
|
+
} else if (key === "argument-hint") {
|
|
7364
|
+
metadata.argumentHint = value;
|
|
7365
|
+
} else if (key === "mode") {
|
|
7366
|
+
metadata.mode = value;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
const content = lines.slice(endIndex + 1).join(`
|
|
7370
|
+
`).trim();
|
|
7371
|
+
return { metadata, content };
|
|
7372
|
+
}
|
|
7373
|
+
function getGlobalCommandsDir() {
|
|
7374
|
+
return join14(homedir4(), ".tarsk", "commands");
|
|
7375
|
+
}
|
|
7376
|
+
function getProjectCommandsDir(threadPath) {
|
|
7377
|
+
return join14(threadPath, ".tarsk", "commands");
|
|
7378
|
+
}
|
|
7379
|
+
|
|
7380
|
+
class SlashCommandManager {
|
|
7381
|
+
static BUILT_IN_COMMANDS = new Set(["init"]);
|
|
7382
|
+
async loadCommands(threadPath) {
|
|
7383
|
+
const commands = new Map;
|
|
7384
|
+
const globalDir = getGlobalCommandsDir();
|
|
7385
|
+
if (existsSync10(globalDir)) {
|
|
7386
|
+
const globalCommands = await this.loadCommandsFromDir(globalDir, "global");
|
|
7387
|
+
for (const cmd of globalCommands) {
|
|
7388
|
+
commands.set(cmd.name, cmd);
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
const projectDir = getProjectCommandsDir(threadPath);
|
|
7392
|
+
if (existsSync10(projectDir)) {
|
|
7393
|
+
const projectCommands = await this.loadCommandsFromDir(projectDir, "project");
|
|
7394
|
+
for (const cmd of projectCommands) {
|
|
7395
|
+
if (!SlashCommandManager.BUILT_IN_COMMANDS.has(cmd.name)) {
|
|
7396
|
+
commands.set(cmd.name, cmd);
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
return Array.from(commands.values());
|
|
7401
|
+
}
|
|
7402
|
+
async loadCommandsFromDir(dir, scope) {
|
|
7403
|
+
const commands = [];
|
|
7404
|
+
try {
|
|
7405
|
+
const files = await readdir4(dir);
|
|
7406
|
+
for (const file of files) {
|
|
7407
|
+
if (!file.endsWith(".md"))
|
|
7408
|
+
continue;
|
|
7409
|
+
const filePath = join14(dir, file);
|
|
7410
|
+
const fileContent = await readFile4(filePath, "utf-8");
|
|
7411
|
+
const { metadata, content } = parseFrontmatter2(fileContent);
|
|
7412
|
+
const nameWithoutExt = basename(file, extname2(file));
|
|
7413
|
+
const commandName = slugify(nameWithoutExt);
|
|
7414
|
+
if (!commandName)
|
|
7415
|
+
continue;
|
|
7416
|
+
commands.push({
|
|
7417
|
+
name: commandName,
|
|
7418
|
+
content,
|
|
7419
|
+
metadata,
|
|
7420
|
+
scope,
|
|
7421
|
+
filePath
|
|
7422
|
+
});
|
|
7423
|
+
}
|
|
7424
|
+
} catch (error) {
|
|
7425
|
+
console.error(`Failed to load commands from ${dir}:`, error);
|
|
7426
|
+
}
|
|
7427
|
+
return commands;
|
|
7428
|
+
}
|
|
7429
|
+
async createCommand(name, content, metadata, scope, threadPath) {
|
|
7430
|
+
if (SlashCommandManager.BUILT_IN_COMMANDS.has(name)) {
|
|
7431
|
+
throw new Error(`Cannot create built-in command: ${name}`);
|
|
7432
|
+
}
|
|
7433
|
+
const dir = scope === "global" ? getGlobalCommandsDir() : threadPath ? getProjectCommandsDir(threadPath) : null;
|
|
7434
|
+
if (!dir) {
|
|
7435
|
+
throw new Error("threadPath required for project-scoped commands");
|
|
7436
|
+
}
|
|
7437
|
+
if (!existsSync10(dir)) {
|
|
7438
|
+
await mkdir2(dir, { recursive: true });
|
|
7439
|
+
}
|
|
7440
|
+
const filename = `${name}.md`;
|
|
7441
|
+
const filePath = join14(dir, filename);
|
|
7442
|
+
if (existsSync10(filePath)) {
|
|
7443
|
+
throw new Error(`Command already exists: ${name}`);
|
|
7444
|
+
}
|
|
7445
|
+
let markdown = "";
|
|
7446
|
+
if (metadata.description || metadata.argumentHint || metadata.mode) {
|
|
7447
|
+
markdown += `---
|
|
7448
|
+
`;
|
|
7449
|
+
if (metadata.description) {
|
|
7450
|
+
markdown += `description: ${metadata.description}
|
|
7451
|
+
`;
|
|
7452
|
+
}
|
|
7453
|
+
if (metadata.argumentHint) {
|
|
7454
|
+
markdown += `argument-hint: ${metadata.argumentHint}
|
|
7455
|
+
`;
|
|
7456
|
+
}
|
|
7457
|
+
if (metadata.mode) {
|
|
7458
|
+
markdown += `mode: ${metadata.mode}
|
|
7459
|
+
`;
|
|
7460
|
+
}
|
|
7461
|
+
markdown += `---
|
|
7462
|
+
|
|
7463
|
+
`;
|
|
7464
|
+
}
|
|
7465
|
+
markdown += content;
|
|
7466
|
+
await writeFile(filePath, markdown, "utf-8");
|
|
7467
|
+
return {
|
|
7468
|
+
name,
|
|
7469
|
+
content,
|
|
7470
|
+
metadata,
|
|
7471
|
+
scope,
|
|
7472
|
+
filePath
|
|
7473
|
+
};
|
|
7474
|
+
}
|
|
7475
|
+
async updateCommand(filePath, content, metadata) {
|
|
7476
|
+
if (!existsSync10(filePath)) {
|
|
7477
|
+
throw new Error(`Command file not found: ${filePath}`);
|
|
7478
|
+
}
|
|
7479
|
+
let markdown = "";
|
|
7480
|
+
if (metadata.description || metadata.argumentHint || metadata.mode) {
|
|
7481
|
+
markdown += `---
|
|
7482
|
+
`;
|
|
7483
|
+
if (metadata.description) {
|
|
7484
|
+
markdown += `description: ${metadata.description}
|
|
7485
|
+
`;
|
|
7486
|
+
}
|
|
7487
|
+
if (metadata.argumentHint) {
|
|
7488
|
+
markdown += `argument-hint: ${metadata.argumentHint}
|
|
7489
|
+
`;
|
|
7490
|
+
}
|
|
7491
|
+
if (metadata.mode) {
|
|
7492
|
+
markdown += `mode: ${metadata.mode}
|
|
7493
|
+
`;
|
|
7494
|
+
}
|
|
7495
|
+
markdown += `---
|
|
7496
|
+
|
|
7497
|
+
`;
|
|
7498
|
+
}
|
|
7499
|
+
markdown += content;
|
|
7500
|
+
await writeFile(filePath, markdown, "utf-8");
|
|
7501
|
+
}
|
|
7502
|
+
async deleteCommand(filePath) {
|
|
7503
|
+
if (!existsSync10(filePath)) {
|
|
7504
|
+
throw new Error(`Command file not found: ${filePath}`);
|
|
7505
|
+
}
|
|
7506
|
+
await unlink(filePath);
|
|
7507
|
+
}
|
|
7508
|
+
async getCommand(name, threadPath) {
|
|
7509
|
+
const commands = await this.loadCommands(threadPath);
|
|
7510
|
+
return commands.find((cmd) => cmd.name === name) || null;
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
|
|
7514
|
+
// src/routes/slash-commands.ts
|
|
7515
|
+
var slashCommandManager = new SlashCommandManager;
|
|
7516
|
+
var skillManager = new SkillManager;
|
|
7517
|
+
function createSlashCommandRoutes(router, threadManager) {
|
|
7518
|
+
router.get("/api/slash-commands", async (c) => {
|
|
7519
|
+
const threadId = c.req.query("threadId");
|
|
7520
|
+
if (!threadId) {
|
|
7521
|
+
return c.json({
|
|
7522
|
+
error: {
|
|
7523
|
+
code: "MISSING_THREAD_ID",
|
|
7524
|
+
message: "threadId query parameter is required",
|
|
7525
|
+
timestamp: new Date().toISOString()
|
|
7526
|
+
}
|
|
7527
|
+
}, 400);
|
|
7528
|
+
}
|
|
7529
|
+
try {
|
|
7530
|
+
const thread = await threadManager.getThread(threadId);
|
|
7531
|
+
if (!thread) {
|
|
7532
|
+
return c.json({
|
|
7533
|
+
error: {
|
|
7534
|
+
code: "THREAD_NOT_FOUND",
|
|
7535
|
+
message: `Thread not found: ${threadId}`,
|
|
7536
|
+
timestamp: new Date().toISOString()
|
|
7537
|
+
}
|
|
7538
|
+
}, 404);
|
|
7539
|
+
}
|
|
7540
|
+
const commands = await slashCommandManager.loadCommands(thread.path);
|
|
7541
|
+
const skills = await skillManager.loadSkills(thread.path);
|
|
7542
|
+
const response = {
|
|
7543
|
+
commands,
|
|
7544
|
+
skills
|
|
7545
|
+
};
|
|
7546
|
+
return c.json(response);
|
|
7547
|
+
} catch (error) {
|
|
7548
|
+
console.error("Failed to load slash commands:", error);
|
|
7549
|
+
return c.json({
|
|
7550
|
+
error: {
|
|
7551
|
+
code: "LOAD_FAILED",
|
|
7552
|
+
message: error instanceof Error ? error.message : "Failed to load slash commands",
|
|
7553
|
+
details: error,
|
|
7554
|
+
timestamp: new Date().toISOString()
|
|
7555
|
+
}
|
|
7556
|
+
}, 500);
|
|
7557
|
+
}
|
|
7558
|
+
});
|
|
7559
|
+
router.post("/api/slash-commands", async (c) => {
|
|
7560
|
+
try {
|
|
7561
|
+
const body = await c.req.json();
|
|
7562
|
+
const { name, content, metadata, scope, threadId } = body;
|
|
7563
|
+
if (!name || !content) {
|
|
7564
|
+
return c.json({
|
|
7565
|
+
error: {
|
|
7566
|
+
code: "MISSING_FIELDS",
|
|
7567
|
+
message: "name and content are required",
|
|
7568
|
+
timestamp: new Date().toISOString()
|
|
7569
|
+
}
|
|
7570
|
+
}, 400);
|
|
7571
|
+
}
|
|
7572
|
+
if (scope !== "global" && scope !== "project") {
|
|
7573
|
+
return c.json({
|
|
7574
|
+
error: {
|
|
7575
|
+
code: "INVALID_SCOPE",
|
|
7576
|
+
message: 'scope must be either "global" or "project"',
|
|
7577
|
+
timestamp: new Date().toISOString()
|
|
7578
|
+
}
|
|
7579
|
+
}, 400);
|
|
7580
|
+
}
|
|
7581
|
+
let threadPath;
|
|
7582
|
+
if (scope === "project") {
|
|
7583
|
+
if (!threadId) {
|
|
7584
|
+
return c.json({
|
|
7585
|
+
error: {
|
|
7586
|
+
code: "MISSING_THREAD_ID",
|
|
7587
|
+
message: "threadId is required for project-scoped commands",
|
|
7588
|
+
timestamp: new Date().toISOString()
|
|
7589
|
+
}
|
|
7590
|
+
}, 400);
|
|
7591
|
+
}
|
|
7592
|
+
const thread = await threadManager.getThread(threadId);
|
|
7593
|
+
if (!thread) {
|
|
7594
|
+
return c.json({
|
|
7595
|
+
error: {
|
|
7596
|
+
code: "THREAD_NOT_FOUND",
|
|
7597
|
+
message: `Thread not found: ${threadId}`,
|
|
7598
|
+
timestamp: new Date().toISOString()
|
|
7599
|
+
}
|
|
7600
|
+
}, 404);
|
|
7601
|
+
}
|
|
7602
|
+
threadPath = thread.path;
|
|
7603
|
+
}
|
|
7604
|
+
const command = await slashCommandManager.createCommand(name, content, metadata || {}, scope, threadPath);
|
|
7605
|
+
return c.json(command, 201);
|
|
7606
|
+
} catch (error) {
|
|
7607
|
+
console.error("Failed to create slash command:", error);
|
|
7608
|
+
return c.json({
|
|
7609
|
+
error: {
|
|
7610
|
+
code: "CREATE_FAILED",
|
|
7611
|
+
message: error instanceof Error ? error.message : "Failed to create slash command",
|
|
7612
|
+
details: error,
|
|
7613
|
+
timestamp: new Date().toISOString()
|
|
7614
|
+
}
|
|
7615
|
+
}, 500);
|
|
7616
|
+
}
|
|
7617
|
+
});
|
|
7618
|
+
router.put("/api/slash-commands", async (c) => {
|
|
7619
|
+
try {
|
|
7620
|
+
const body = await c.req.json();
|
|
7621
|
+
const { filePath, content, metadata } = body;
|
|
7622
|
+
if (!filePath || !content) {
|
|
7623
|
+
return c.json({
|
|
7624
|
+
error: {
|
|
7625
|
+
code: "MISSING_FIELDS",
|
|
7626
|
+
message: "filePath and content are required",
|
|
7627
|
+
timestamp: new Date().toISOString()
|
|
7628
|
+
}
|
|
7629
|
+
}, 400);
|
|
7630
|
+
}
|
|
7631
|
+
await slashCommandManager.updateCommand(filePath, content, metadata || {});
|
|
7632
|
+
return c.json({ success: true });
|
|
7633
|
+
} catch (error) {
|
|
7634
|
+
console.error("Failed to update slash command:", error);
|
|
7635
|
+
return c.json({
|
|
7636
|
+
error: {
|
|
7637
|
+
code: "UPDATE_FAILED",
|
|
7638
|
+
message: error instanceof Error ? error.message : "Failed to update slash command",
|
|
7639
|
+
details: error,
|
|
7640
|
+
timestamp: new Date().toISOString()
|
|
7641
|
+
}
|
|
7642
|
+
}, 500);
|
|
7643
|
+
}
|
|
7644
|
+
});
|
|
7645
|
+
router.delete("/api/slash-commands", async (c) => {
|
|
7646
|
+
try {
|
|
7647
|
+
const body = await c.req.json();
|
|
7648
|
+
const { filePath } = body;
|
|
7649
|
+
if (!filePath) {
|
|
7650
|
+
return c.json({
|
|
7651
|
+
error: {
|
|
7652
|
+
code: "MISSING_FILE_PATH",
|
|
7653
|
+
message: "filePath is required",
|
|
7654
|
+
timestamp: new Date().toISOString()
|
|
7655
|
+
}
|
|
7656
|
+
}, 400);
|
|
7657
|
+
}
|
|
7658
|
+
await slashCommandManager.deleteCommand(filePath);
|
|
7659
|
+
return c.json({ success: true });
|
|
7660
|
+
} catch (error) {
|
|
7661
|
+
console.error("Failed to delete slash command:", error);
|
|
7662
|
+
return c.json({
|
|
7663
|
+
error: {
|
|
7664
|
+
code: "DELETE_FAILED",
|
|
7665
|
+
message: error instanceof Error ? error.message : "Failed to delete slash command",
|
|
7666
|
+
details: error,
|
|
7667
|
+
timestamp: new Date().toISOString()
|
|
7668
|
+
}
|
|
7669
|
+
}, 500);
|
|
7670
|
+
}
|
|
7671
|
+
});
|
|
7672
|
+
}
|
|
7673
|
+
|
|
6861
7674
|
// src/index.ts
|
|
6862
7675
|
init_dist();
|
|
6863
7676
|
var __filename2 = fileURLToPath3(import.meta.url);
|
|
@@ -6905,6 +7718,7 @@ app.route("/api/models", createModelRoutes(metadataManager));
|
|
|
6905
7718
|
app.route("/api/git", createGitRoutes(metadataManager));
|
|
6906
7719
|
app.route("/api/onboarding", createOnboardingRoutes(metadataManager));
|
|
6907
7720
|
app.route("/api/scaffold", createScaffoldRoutes(projectManager));
|
|
7721
|
+
createSlashCommandRoutes(app, threadManager);
|
|
6908
7722
|
var publicDir = path3.join(__dirname3, "public");
|
|
6909
7723
|
var staticRoot = path3.relative(process.cwd(), publicDir);
|
|
6910
7724
|
app.use("/*", async (c, next) => {
|
|
@@ -6922,14 +7736,14 @@ app.get("*", async (c, next) => {
|
|
|
6922
7736
|
})(c, next);
|
|
6923
7737
|
});
|
|
6924
7738
|
app.all("*", (c) => {
|
|
6925
|
-
const
|
|
7739
|
+
const errorResponse2 = {
|
|
6926
7740
|
error: {
|
|
6927
7741
|
code: "NOT_FOUND",
|
|
6928
7742
|
message: `Route not found: ${c.req.method} ${c.req.path}`,
|
|
6929
7743
|
timestamp: new Date().toISOString()
|
|
6930
7744
|
}
|
|
6931
7745
|
};
|
|
6932
|
-
return c.json(
|
|
7746
|
+
return c.json(errorResponse2, 404);
|
|
6933
7747
|
});
|
|
6934
7748
|
var port = isDebug ? 462 : process.env.PORT ? parseInt(process.env.PORT) : 641;
|
|
6935
7749
|
var url = `http://localhost:${port}`;
|
|
@@ -6941,6 +7755,6 @@ serve({
|
|
|
6941
7755
|
}, () => {
|
|
6942
7756
|
const isDevelopment = process.env.MODE === "development";
|
|
6943
7757
|
if (shouldOpenBrowser || !isDevelopment) {
|
|
6944
|
-
|
|
7758
|
+
open3(url).catch(() => {});
|
|
6945
7759
|
}
|
|
6946
7760
|
});
|