tarsk 0.3.35 → 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 open2 from "open";
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 new Promise((resolve, reject) => {
2588
- if (signal?.aborted) {
2589
- reject(new Error("Operation aborted"));
2590
- return;
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
- let aborted = false;
2593
- const onAbort = () => {
2594
- aborted = true;
2595
- reject(new Error("Operation aborted"));
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 new Promise((resolve, reject) => {
3151
- if (signal?.aborted) {
3152
- reject(new Error("Operation aborted"));
3153
- return;
3154
- }
3155
- let aborted = false;
3156
- const onAbort = () => {
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
- const totalFileLines = allLines.length;
3173
- const startLine = offset ? Math.max(0, offset - 1) : 0;
3174
- const startLineDisplay = startLine + 1;
3175
- if (startLine >= allLines.length) {
3176
- throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
3177
- }
3178
- let selectedContent;
3179
- let userLimitedLines;
3180
- if (limit !== undefined) {
3181
- const endLine = Math.min(startLine + limit, allLines.length);
3182
- selectedContent = allLines.slice(startLine, endLine).join(`
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
- userLimitedLines = endLine - startLine;
3185
- } else {
3186
- selectedContent = allLines.slice(startLine).join(`
3163
+ userLimitedLines = endLine - startLine;
3164
+ } else {
3165
+ selectedContent = allLines.slice(startLine).join(`
3187
3166
  `);
3188
- }
3189
- const truncation = truncateHead(selectedContent);
3190
- let outputText;
3191
- let details;
3192
- if (truncation.firstLineExceedsLimit) {
3193
- 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}]`;
3194
- details = { truncation };
3195
- } else if (truncation.truncated) {
3196
- const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
3197
- const nextOffset = endLineDisplay + 1;
3198
- outputText = truncation.content;
3199
- if (truncation.truncatedBy === "lines") {
3200
- outputText += `
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
- } else {
3204
- outputText += `
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
- details = { truncation };
3209
- } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
3210
- const remaining = allLines.length - (startLine + userLimitedLines);
3211
- const nextOffset = startLine + userLimitedLines + 1;
3212
- outputText = truncation.content;
3213
- outputText += `
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
- } else {
3217
- outputText = truncation.content;
3218
- }
3219
- if (aborted)
3220
- return;
3221
- if (signal)
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
- return [
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 join7 } from "path";
3691
+ import { join as join9 } from "path";
3497
3692
  import { homedir as homedir2 } from "os";
3498
- var APP_SUPPORT_DIR = join7(homedir2(), "Library", "Application Support", "Tarsk");
3499
- var DATA_DIR = join7(APP_SUPPORT_DIR, "data");
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-executor.ts
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
- var getErrorCode = (error) => {
3556
- if (error && typeof error === "object" && "code" in error) {
3557
- const code = error.code;
3558
- if (typeof code === "string")
3559
- return code;
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
- return;
3562
- };
3563
- function loadAgentSystemPrompt() {
3564
- const 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.";
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
- return content.trim();
3844
+ basePrompt = content.trim();
3572
3845
  } catch (error) {
3573
- console.warn("[ai] Could not load agents.md, using default prompt:", error);
3574
- return DEFAULT_PROMPT;
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 : resolve(getDataDir(), 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 queue = [];
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
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
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
- if (resolver) {
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
- queue.push({
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
- if (resolver) {
3714
- resolver();
3715
- resolver = null;
3716
- }
4007
+ eventQueue.setResolver(() => {});
3717
4008
  });
3718
4009
  try {
3719
- while (!done || queue.length > 0) {
3720
- if (queue.length > 0) {
3721
- yield queue.shift();
4010
+ while (!done || eventQueue.length > 0) {
4011
+ if (eventQueue.length > 0) {
4012
+ yield eventQueue.shift();
3722
4013
  } else {
3723
- await new Promise((r) => {
3724
- resolver = r;
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
- let errCode;
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 join8, dirname as dirname6 } from "path";
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 = join8(this.metadataDir, "conversations");
3864
- return join8(conversationsDir, threadId, CONVERSATION_FILE);
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "gitUrl is required and must be a string", 400);
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
- const { response, statusCode } = ResponseBuilder.error("REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("LIST_PROJECTS_ERROR", "Failed to list projects", 500, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.error("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, 404);
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
- const { response, statusCode } = ResponseBuilder.error("GET_PROJECT_ERROR", "Failed to get project", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Project deleted successfully" });
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("PROJECT_NOT_FOUND", errorMessage, 404);
4011
- return c.json(response2, statusCode2);
4280
+ return errorResponse(c, "PROJECT_NOT_FOUND", errorMessage, 404);
4012
4281
  }
4013
- const { response, statusCode } = ResponseBuilder.error("DELETE_PROJECT_ERROR", "Failed to delete project", 500, errorMessage);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
4025
- return c.json(response2, statusCode2);
4292
+ return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
4026
4293
  }
4027
4294
  if (!program) {
4028
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Program is required", 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", `Invalid program. Must be one of: ${AVAILABLE_PROGRAMS.join(", ")}`, 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, 404);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: `Project opened in ${program}` });
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
- const { response, statusCode } = ResponseBuilder.error("OPEN_PROGRAM_ERROR", "Failed to open project", 500, errorMessage);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("PROJECT_NOT_FOUND", `Project not found: ${projectId}`, 404);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Program cannot be empty", 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", `Invalid program. Must be one of: ${AVAILABLE_PROGRAMS.join(", ")}`, 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", `Commit method must be one of: "${validMethods}"`, 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project name is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Project updated" });
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
- const { response, statusCode } = ResponseBuilder.error("UPDATE_PROJECT_ERROR", "Failed to update project", 500, errorMessage);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Command saved" });
4120
- return c.json(response, statusCode);
4372
+ return successResponse(c, { success: true, message: "Command saved" });
4121
4373
  } catch (error) {
4122
- const { response, statusCode } = ResponseBuilder.error("SAVE_COMMAND_ERROR", "Failed to save command", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID and Command ID are required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Command deleted" });
4136
- return c.json(response, statusCode);
4385
+ return successResponse(c, { success: true, message: "Command deleted" });
4137
4386
  } catch (error) {
4138
- const { response, statusCode } = ResponseBuilder.error("DELETE_COMMAND_ERROR", "Failed to delete command", 500, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID and commandLine are required", 400);
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
- const { response, statusCode } = ResponseBuilder.error("RUN_COMMAND_ERROR", "Failed to run command", 500, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.error("START_PROCESS_ERROR", "Failed to start dev server", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ runCommand });
4178
- return c.json(response, statusCode);
4420
+ return successResponse(c, { runCommand });
4179
4421
  } catch (error) {
4180
- const { response, statusCode } = ResponseBuilder.error("SUGGEST_RUN_COMMAND_ERROR", "Failed to suggest run command", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Dev server stopped" });
4193
- return c.json(response, statusCode);
4432
+ return successResponse(c, { success: true, message: "Dev server stopped" });
4194
4433
  } catch (error) {
4195
- const { response, statusCode } = ResponseBuilder.error("STOP_PROCESS_ERROR", "Failed to stop dev server", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ isRunning });
4208
- return c.json(response, statusCode);
4444
+ return successResponse(c, { isRunning });
4209
4445
  } catch (error) {
4210
- const { response, statusCode } = ResponseBuilder.error("CHECK_PROCESS_ERROR", "Failed to check process status", 500, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "projectId is required and must be a string", 400);
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "title must be a string if provided", 400);
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", nameError, 400);
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
- const { response, statusCode } = ResponseBuilder.error("DUPLICATE_BRANCH", `A thread with branch name "${sanitizedNewName}" already exists`, 409);
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
- const { response, statusCode } = ResponseBuilder.error("REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "projectId query parameter is required", 400);
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
- const { response, statusCode } = ResponseBuilder.error("LIST_THREADS_ERROR", "Failed to list threads", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Thread deleted successfully" });
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("THREAD_NOT_FOUND", errorMessage, 404);
4436
- return c.json(response2, statusCode2);
4662
+ return errorResponse(c, "THREAD_NOT_FOUND", errorMessage, 404);
4437
4663
  }
4438
- const { response, statusCode } = ResponseBuilder.error("DELETE_THREAD_ERROR", "Failed to delete thread", 500, errorMessage);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID is required", 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Thread selected successfully", threadId });
4456
- return c.json(response, statusCode);
4678
+ return successResponse(c, { success: true, message: "Thread selected successfully", threadId });
4457
4679
  } catch (error) {
4458
- const { response, statusCode } = ResponseBuilder.error("SELECT_THREAD_ERROR", "Failed to select thread", 500, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.error("THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
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
- const { response, statusCode } = ResponseBuilder.error("GET_THREAD_MESSAGES_ERROR", "Failed to load thread messages", 500, error instanceof Error ? error.message : String(error));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.error("LIST_THREAD_FILES_ERROR", "Failed to list thread files", 500, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID is required", 400);
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
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Thread updated successfully" });
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
- const { response, statusCode } = ResponseBuilder.error("UPDATE_THREAD_ERROR", "Failed to update thread", 500, errorMessage);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Thread ID is required", 400);
4549
- return c.json(response2, statusCode2);
4769
+ return errorResponse(c, "INVALID_REQUEST", "Thread ID is required", 400);
4550
4770
  }
4551
4771
  if (!program) {
4552
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Program is required", 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", `Invalid program. Must be one of: ${AVAILABLE_PROGRAMS2.join(", ")}`, 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
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
- if (program === "VS Code") {
4568
- spawn7("code", [threadPath], { detached: true });
4569
- } else if (program === "Cursor") {
4570
- spawn7("cursor", [threadPath], { detached: true });
4571
- } else if (program === "Windsurf") {
4572
- spawn7("windsurf", [threadPath], { detached: true });
4573
- } else if (program === "Xcode") {
4574
- spawn7("open", ["-a", "Xcode", threadPath], { detached: true });
4575
- } else if (program === "Android Studio") {
4576
- spawn7("open", ["-a", "Android Studio", threadPath], { detached: true });
4577
- } else if (program === "Kiro") {
4578
- spawn7("kiro", [threadPath], { detached: true });
4579
- }
4580
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: `Thread opened in ${program}` });
4581
- return c.json(response, statusCode);
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
- const { response, statusCode } = ResponseBuilder.error("OPEN_PROGRAM_ERROR", "Failed to open thread", 500, errorMessage);
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((resolve2) => setTimeout(resolve2, ms));
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "threadId is required and must be a string", 400);
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "content is required and must be a string", 400);
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
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "model is required and must be a string", 400);
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
- const { response, statusCode } = ResponseBuilder.error("THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
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
- const { response: response2, statusCode } = ResponseBuilder.error("NO_HISTORY", "No previous conversation found to replay", 404);
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
- const { response: response2, statusCode } = ResponseBuilder.error("NO_RESPONSE", "Previous message has no completed response to replay", 404);
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
- const { response, statusCode } = ResponseBuilder.error("REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "threadId is required and must be a string", 400);
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
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
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
- const { response, statusCode } = ResponseBuilder.success({ message: "Chat history cleared" }, 200);
4761
- return c.json(response, statusCode);
5239
+ return successResponse(c, { message: "Chat history cleared" }, 200);
4762
5240
  } catch (error) {
4763
- const { response, statusCode } = ResponseBuilder.error("CLEAR_CHAT_ERROR", "Failed to clear chat history", 500, error instanceof Error ? error.message : String(error));
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 join9 } from "path";
5414
+ import { join as join12 } from "path";
4938
5415
  async function updateEnvFile(keyNames) {
4939
- const envPath = join9(process.cwd(), ".env");
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 = join9(process.cwd(), ".env");
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 spawn7 } from "child_process";
5757
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
5758
- import { join as join10 } from "path";
5759
- import { isAbsolute as isAbsolute3, normalize, resolve as resolve2 } from "path";
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 : resolve2(getDataDir(), repoPath);
5885
- return normalize(base);
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 = spawn7("git", ["rev-parse", "--show-toplevel"], { cwd });
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((resolve3, reject) => {
5913
- const proc = spawn7("git", ["config", "user.name"]);
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
- resolve3(out.trim());
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 (!existsSync6(absolutePath)) {
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((resolve3) => {
5963
- const proc = spawn7("git", ["status", "--porcelain"], { cwd: gitRoot });
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
- resolve3({
6448
+ resolve4({
5972
6449
  hasChanges: lines.length > 0,
5973
6450
  changedFilesCount: lines.length
5974
6451
  });
5975
6452
  });
5976
- proc.on("error", () => resolve3({ hasChanges: false, changedFilesCount: 0 }));
6453
+ proc.on("error", () => resolve4({ hasChanges: false, changedFilesCount: 0 }));
5977
6454
  });
5978
- const currentBranch = await new Promise((resolve3) => {
5979
- const proc = spawn7("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
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
- resolve3(out.trim());
6462
+ resolve4(out.trim());
5986
6463
  });
5987
- proc.on("error", () => resolve3(""));
6464
+ proc.on("error", () => resolve4(""));
5988
6465
  });
5989
- const hasUnpushedCommits = await new Promise((resolve3) => {
5990
- const proc = spawn7("git", ["log", `origin/${currentBranch}..HEAD`, "--oneline"], { cwd: gitRoot });
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
- resolve3(out.trim().length > 0);
6473
+ resolve4(out.trim().length > 0);
5997
6474
  });
5998
- proc.on("error", () => resolve3(false));
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((resolve3) => {
6029
- const proc = spawn7("git", ["status", "--porcelain"], { cwd: gitRoot });
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", () => resolve3(out));
6035
- proc.on("error", () => resolve3(""));
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((resolve3) => {
6070
- const proc = spawn7("git", ["show", `HEAD:${file.path}`], { cwd: gitRoot });
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", () => resolve3(out));
6076
- proc.on("error", () => resolve3(""));
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(resolve2(gitRoot, file.path.replace("(new) ", "")), "utf-8");
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((resolve3) => {
6088
- const proc = spawn7("git", file.status === "added" ? ["diff", "--cached", "--", file.path] : ["diff", "HEAD", "--", file.path], { cwd: gitRoot });
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", () => resolve3(out));
6094
- proc.on("error", () => resolve3(""));
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((resolve3) => {
6102
- const proc = spawn7("git", ["show", `HEAD:${file.path}`], { cwd: gitRoot });
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", () => resolve3(out));
6108
- proc.on("error", () => resolve3(""));
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(resolve2(gitRoot, file.path), "utf-8");
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 (!existsSync6(absolutePath)) {
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 = spawn7("git", ["diff"], { cwd: gitRoot });
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 = spawn7("git", ["diff", "--cached"], { cwd: gitRoot });
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 = spawn7("git", ["status", "--porcelain"], { cwd: gitRoot });
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 = join10(gitRoot, relPath);
6290
- if (!existsSync6(fullPath))
6766
+ const fullPath = join13(gitRoot, relPath);
6767
+ if (!existsSync9(fullPath))
6291
6768
  continue;
6292
6769
  try {
6293
- if (statSync3(fullPath).isDirectory())
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((resolve3, reject) => {
6357
- const proc = spawn7("git", ["add", "-A"], { cwd: gitRoot });
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
- resolve3();
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((resolve3, reject) => {
6367
- const proc = spawn7("git", ["commit", "-m", message], { cwd: gitRoot });
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
- resolve3();
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((resolve3, reject) => {
6401
- const proc = spawn7("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
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", () => resolve3(out.trim()));
6883
+ proc.on("close", () => resolve4(out.trim()));
6407
6884
  proc.on("error", reject);
6408
6885
  });
6409
- await new Promise((resolve3, reject) => {
6410
- const proc = spawn7("git", ["push", "-u", "origin", currentBranch], { cwd: gitRoot });
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
- resolve3();
6894
+ resolve4();
6418
6895
  else
6419
6896
  reject(new Error(err || "Failed to push changes"));
6420
6897
  });
@@ -6426,6 +6903,44 @@ new file mode 100644
6426
6903
  return c.json({ error: message }, 500);
6427
6904
  }
6428
6905
  });
6906
+ router.post("/fetch/:threadId", async (c) => {
6907
+ try {
6908
+ const threadId = c.req.param("threadId");
6909
+ const thread = await metadataManager.loadThreads().then((threads) => threads.find((t) => t.id === threadId));
6910
+ if (!thread) {
6911
+ return c.json({ error: "Thread not found" }, 404);
6912
+ }
6913
+ const repoPath = thread.path;
6914
+ if (!repoPath) {
6915
+ return c.json({ error: "Thread path not found" }, 404);
6916
+ }
6917
+ const absolutePath = resolveThreadPath(repoPath);
6918
+ let gitRoot;
6919
+ try {
6920
+ gitRoot = await getGitRoot(absolutePath);
6921
+ } catch {
6922
+ return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
6923
+ }
6924
+ await new Promise((resolve4, reject) => {
6925
+ const proc = spawn8("git", ["fetch", "origin"], { cwd: gitRoot });
6926
+ let err = "";
6927
+ proc.stderr.on("data", (d) => {
6928
+ err += d.toString();
6929
+ });
6930
+ proc.on("close", (code) => {
6931
+ if (code === 0)
6932
+ resolve4();
6933
+ else
6934
+ reject(new Error(err || "Failed to fetch from origin"));
6935
+ });
6936
+ proc.on("error", reject);
6937
+ });
6938
+ return c.json({ success: true });
6939
+ } catch (error) {
6940
+ const message = error instanceof Error ? error.message : "Failed to fetch from origin";
6941
+ return c.json({ error: message }, 500);
6942
+ }
6943
+ });
6429
6944
  router.post("/generate-pr-info/:threadId", async (c) => {
6430
6945
  try {
6431
6946
  const threadId = c.req.param("threadId");
@@ -6448,7 +6963,7 @@ new file mode 100644
6448
6963
  }
6449
6964
  const diff = await new Promise((resolveDiff, reject) => {
6450
6965
  const runPlainDiff = () => {
6451
- const proc2 = spawn7("git", ["diff"], { cwd: gitRoot });
6966
+ const proc2 = spawn8("git", ["diff"], { cwd: gitRoot });
6452
6967
  let out2 = "";
6453
6968
  proc2.stdout.on("data", (d) => {
6454
6969
  out2 += d.toString();
@@ -6456,7 +6971,7 @@ new file mode 100644
6456
6971
  proc2.on("close", () => resolveDiff(out2));
6457
6972
  proc2.on("error", reject);
6458
6973
  };
6459
- const proc = spawn7("git", ["diff", "HEAD"], { cwd: gitRoot });
6974
+ const proc = spawn8("git", ["diff", "HEAD"], { cwd: gitRoot });
6460
6975
  let out = "";
6461
6976
  let err = "";
6462
6977
  proc.stdout.on("data", (d) => {
@@ -6540,8 +7055,8 @@ DESCRIPTION: <description here>`;
6540
7055
  } catch {
6541
7056
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
6542
7057
  }
6543
- const commits = await new Promise((resolve3, reject) => {
6544
- const proc = spawn7("git", ["log", "--oneline", "-n", limit.toString(), "--format=%H|%s|%an|%ai"], { cwd: gitRoot });
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 });
6545
7060
  let out = "";
6546
7061
  let err = "";
6547
7062
  proc.stdout.on("data", (d) => {
@@ -6563,7 +7078,7 @@ DESCRIPTION: <description here>`;
6563
7078
  date: parts[3] || ""
6564
7079
  };
6565
7080
  });
6566
- resolve3(parsed);
7081
+ resolve4(parsed);
6567
7082
  } else {
6568
7083
  reject(new Error(err || "Failed to get git log"));
6569
7084
  }
@@ -6596,18 +7111,18 @@ DESCRIPTION: <description here>`;
6596
7111
  } catch {
6597
7112
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
6598
7113
  }
6599
- const currentBranch = await new Promise((resolve3, reject) => {
6600
- const proc = spawn7("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
7114
+ const currentBranch = await new Promise((resolve4, reject) => {
7115
+ const proc = spawn8("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
6601
7116
  let out = "";
6602
7117
  proc.stdout.on("data", (d) => {
6603
7118
  out += d.toString();
6604
7119
  });
6605
- proc.on("close", () => resolve3(out.trim()));
7120
+ proc.on("close", () => resolve4(out.trim()));
6606
7121
  proc.on("error", reject);
6607
7122
  });
6608
- const prUrl = await new Promise((resolve3, reject) => {
7123
+ const prUrl = await new Promise((resolve4, reject) => {
6609
7124
  const args = ["pr", "create", "--title", title || currentBranch, "--body", description || ""];
6610
- const proc = spawn7("gh", args, { cwd: gitRoot });
7125
+ const proc = spawn8("gh", args, { cwd: gitRoot });
6611
7126
  let out = "";
6612
7127
  let err = "";
6613
7128
  proc.stdout.on("data", (d) => {
@@ -6619,7 +7134,7 @@ DESCRIPTION: <description here>`;
6619
7134
  proc.on("close", (code) => {
6620
7135
  if (code === 0) {
6621
7136
  const urlMatch = out.match(/https:\/\/[^\s]+/);
6622
- resolve3(urlMatch ? urlMatch[0] : out.trim());
7137
+ resolve4(urlMatch ? urlMatch[0] : out.trim());
6623
7138
  } else {
6624
7139
  reject(new Error(err || "Failed to create PR"));
6625
7140
  }
@@ -6643,84 +7158,69 @@ function createRunRoutes(projectManager) {
6643
7158
  try {
6644
7159
  const projectId = c.req.param("id");
6645
7160
  if (!projectId) {
6646
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
6647
- return c.json(response2, statusCode2);
7161
+ return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
6648
7162
  }
6649
7163
  const runCommand = await projectManager.suggestRunCommand(projectId);
6650
- const { response, statusCode } = ResponseBuilder.success({ runCommand });
6651
- return c.json(response, statusCode);
7164
+ return successResponse(c, { runCommand });
6652
7165
  } catch (error) {
6653
7166
  const errorMessage = error instanceof Error ? error.message : String(error);
6654
- const { response, statusCode } = ResponseBuilder.error("SUGGEST_RUN_COMMAND_ERROR", "Failed to suggest run command", 500, errorMessage);
6655
- return c.json(response, statusCode);
7167
+ return errorResponse(c, "SUGGEST_RUN_COMMAND_ERROR", "Failed to suggest run command", 500, errorMessage);
6656
7168
  }
6657
7169
  });
6658
7170
  router.post("/:id/run", async (c) => {
6659
7171
  try {
6660
7172
  const projectId = c.req.param("id");
6661
7173
  if (!projectId) {
6662
- const { response, statusCode } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
6663
- return c.json(response, statusCode);
7174
+ return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
6664
7175
  }
6665
7176
  return streamAsyncGenerator(c, projectManager.startRunningProcess(projectId));
6666
7177
  } catch (error) {
6667
7178
  const errorMessage = error instanceof Error ? error.message : String(error);
6668
- const { response, statusCode } = ResponseBuilder.error("RUN_PROCESS_ERROR", "Failed to start process", 500, errorMessage);
6669
- return c.json(response, statusCode);
7179
+ return errorResponse(c, "RUN_PROCESS_ERROR", "Failed to start process", 500, errorMessage);
6670
7180
  }
6671
7181
  });
6672
7182
  router.post("/:id/stop", async (c) => {
6673
7183
  try {
6674
7184
  const projectId = c.req.param("id");
6675
7185
  if (!projectId) {
6676
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
6677
- return c.json(response2, statusCode2);
7186
+ return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
6678
7187
  }
6679
7188
  const stopped = await projectManager.stopRunningProcess(projectId);
6680
7189
  if (!stopped) {
6681
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("NO_PROCESS_RUNNING", "No process running for this project", 400);
6682
- return c.json(response2, statusCode2);
7190
+ return errorResponse(c, "NO_PROCESS_RUNNING", "No process running for this project", 400);
6683
7191
  }
6684
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Process stopped" });
6685
- return c.json(response, statusCode);
7192
+ return successResponse(c, { success: true, message: "Process stopped" });
6686
7193
  } catch (error) {
6687
7194
  const errorMessage = error instanceof Error ? error.message : String(error);
6688
- const { response, statusCode } = ResponseBuilder.error("STOP_PROCESS_ERROR", "Failed to stop process", 500, errorMessage);
6689
- return c.json(response, statusCode);
7195
+ return errorResponse(c, "STOP_PROCESS_ERROR", "Failed to stop process", 500, errorMessage);
6690
7196
  }
6691
7197
  });
6692
7198
  router.get("/:id/running", async (c) => {
6693
7199
  try {
6694
7200
  const projectId = c.req.param("id");
6695
7201
  if (!projectId) {
6696
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
6697
- return c.json(response2, statusCode2);
7202
+ return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
6698
7203
  }
6699
7204
  const isRunning = await projectManager.isProcessRunning(projectId);
6700
- const { response, statusCode } = ResponseBuilder.success({ isRunning });
6701
- return c.json(response, statusCode);
7205
+ return successResponse(c, { isRunning });
6702
7206
  } catch (error) {
6703
7207
  const errorMessage = error instanceof Error ? error.message : String(error);
6704
- const { response, statusCode } = ResponseBuilder.error("CHECK_RUNNING_ERROR", "Failed to check running status", 500, errorMessage);
6705
- return c.json(response, statusCode);
7208
+ return errorResponse(c, "CHECK_RUNNING_ERROR", "Failed to check running status", 500, errorMessage);
6706
7209
  }
6707
7210
  });
6708
7211
  router.put("/:id/run-command", async (c) => {
6709
7212
  try {
6710
7213
  const projectId = c.req.param("id");
6711
7214
  if (!projectId) {
6712
- const { response: response2, statusCode: statusCode2 } = ResponseBuilder.error("INVALID_REQUEST", "Project ID is required", 400);
6713
- return c.json(response2, statusCode2);
7215
+ return errorResponse(c, "INVALID_REQUEST", "Project ID is required", 400);
6714
7216
  }
6715
7217
  const body = await c.req.json();
6716
7218
  const { runCommand } = body;
6717
7219
  await projectManager.updateRunCommand(projectId, runCommand || null);
6718
- const { response, statusCode } = ResponseBuilder.success({ success: true, message: "Run command updated" });
6719
- return c.json(response, statusCode);
7220
+ return successResponse(c, { success: true, message: "Run command updated" });
6720
7221
  } catch (error) {
6721
7222
  const errorMessage = error instanceof Error ? error.message : String(error);
6722
- const { response, statusCode } = ResponseBuilder.error("UPDATE_RUN_COMMAND_ERROR", "Failed to update run command", 500, errorMessage);
6723
- return c.json(response, statusCode);
7223
+ return errorResponse(c, "UPDATE_RUN_COMMAND_ERROR", "Failed to update run command", 500, errorMessage);
6724
7224
  }
6725
7225
  });
6726
7226
  return router;
@@ -6728,7 +7228,7 @@ function createRunRoutes(projectManager) {
6728
7228
 
6729
7229
  // src/routes/onboarding.ts
6730
7230
  import { Hono as Hono8 } from "hono";
6731
- import { spawn as spawn8 } from "child_process";
7231
+ import { spawn as spawn9 } from "child_process";
6732
7232
  function createOnboardingRoutes(metadataManager) {
6733
7233
  const router = new Hono8;
6734
7234
  router.get("/status", async (c) => {
@@ -6742,17 +7242,17 @@ function createOnboardingRoutes(metadataManager) {
6742
7242
  });
6743
7243
  router.get("/git-check", async (c) => {
6744
7244
  try {
6745
- const gitInstalled = await new Promise((resolve3) => {
6746
- const proc = spawn8("git", ["--version"]);
7245
+ const gitInstalled = await new Promise((resolve4) => {
7246
+ const proc = spawn9("git", ["--version"]);
6747
7247
  let _err = "";
6748
7248
  proc.stderr.on("data", (d) => {
6749
7249
  _err += d.toString();
6750
7250
  });
6751
7251
  proc.on("close", (code) => {
6752
- resolve3(code === 0);
7252
+ resolve4(code === 0);
6753
7253
  });
6754
7254
  proc.on("error", () => {
6755
- resolve3(false);
7255
+ resolve4(false);
6756
7256
  });
6757
7257
  });
6758
7258
  return c.json({ gitInstalled });
@@ -6820,6 +7320,357 @@ function createScaffoldRoutes(projectManager) {
6820
7320
  return router;
6821
7321
  }
6822
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
+
6823
7674
  // src/index.ts
6824
7675
  init_dist();
6825
7676
  var __filename2 = fileURLToPath3(import.meta.url);
@@ -6867,6 +7718,7 @@ app.route("/api/models", createModelRoutes(metadataManager));
6867
7718
  app.route("/api/git", createGitRoutes(metadataManager));
6868
7719
  app.route("/api/onboarding", createOnboardingRoutes(metadataManager));
6869
7720
  app.route("/api/scaffold", createScaffoldRoutes(projectManager));
7721
+ createSlashCommandRoutes(app, threadManager);
6870
7722
  var publicDir = path3.join(__dirname3, "public");
6871
7723
  var staticRoot = path3.relative(process.cwd(), publicDir);
6872
7724
  app.use("/*", async (c, next) => {
@@ -6884,14 +7736,14 @@ app.get("*", async (c, next) => {
6884
7736
  })(c, next);
6885
7737
  });
6886
7738
  app.all("*", (c) => {
6887
- const errorResponse = {
7739
+ const errorResponse2 = {
6888
7740
  error: {
6889
7741
  code: "NOT_FOUND",
6890
7742
  message: `Route not found: ${c.req.method} ${c.req.path}`,
6891
7743
  timestamp: new Date().toISOString()
6892
7744
  }
6893
7745
  };
6894
- return c.json(errorResponse, 404);
7746
+ return c.json(errorResponse2, 404);
6895
7747
  });
6896
7748
  var port = isDebug ? 462 : process.env.PORT ? parseInt(process.env.PORT) : 641;
6897
7749
  var url = `http://localhost:${port}`;
@@ -6903,6 +7755,6 @@ serve({
6903
7755
  }, () => {
6904
7756
  const isDevelopment = process.env.MODE === "development";
6905
7757
  if (shouldOpenBrowser || !isDevelopment) {
6906
- open2(url).catch(() => {});
7758
+ open3(url).catch(() => {});
6907
7759
  }
6908
7760
  });