tarsk 0.3.37 → 0.3.39

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
@@ -1131,6 +1131,118 @@ class ProjectManagerImpl {
1131
1131
  import { randomUUID as randomUUID2 } from "crypto";
1132
1132
  import { join as join4 } from "path";
1133
1133
 
1134
+ // src/utils/random-words.ts
1135
+ var WORD_LIST = [
1136
+ "panda",
1137
+ "koala",
1138
+ "penguin",
1139
+ "bunny",
1140
+ "kitten",
1141
+ "puppy",
1142
+ "hamster",
1143
+ "otter",
1144
+ "hedgehog",
1145
+ "sloth",
1146
+ "dolphin",
1147
+ "owl",
1148
+ "fox",
1149
+ "deer",
1150
+ "koala",
1151
+ "walrus",
1152
+ "bouncy",
1153
+ "sparkly",
1154
+ "fluffy",
1155
+ "jolly",
1156
+ "zesty",
1157
+ "peppy",
1158
+ "snappy",
1159
+ "perky",
1160
+ "breezy",
1161
+ "chirpy",
1162
+ "dizzy",
1163
+ "fizzy",
1164
+ "glossy",
1165
+ "groggy",
1166
+ "loopy",
1167
+ "quirky",
1168
+ "umbrella",
1169
+ "taco",
1170
+ "pickle",
1171
+ "waffle",
1172
+ "bubble",
1173
+ "puzzle",
1174
+ "rocket",
1175
+ "tornado",
1176
+ "volcano",
1177
+ "rainbow",
1178
+ "diamond",
1179
+ "crystal",
1180
+ "lantern",
1181
+ "treasure",
1182
+ "whisper",
1183
+ "echo",
1184
+ "boing",
1185
+ "splat",
1186
+ "whoosh",
1187
+ "crunch",
1188
+ "splash",
1189
+ "jingle",
1190
+ "wiggle",
1191
+ "giggle",
1192
+ "scramble",
1193
+ "tumble",
1194
+ "fumble",
1195
+ "jumble",
1196
+ "rumble",
1197
+ "mumble",
1198
+ "stumble",
1199
+ "hustle",
1200
+ "castle",
1201
+ "island",
1202
+ "mountain",
1203
+ "forest",
1204
+ "meadow",
1205
+ "lagoon",
1206
+ "canyon",
1207
+ "geyser",
1208
+ "oasis",
1209
+ "glacier",
1210
+ "waterfall",
1211
+ "volcano",
1212
+ "tundra",
1213
+ "savanna",
1214
+ "jungle",
1215
+ "desert",
1216
+ "magic",
1217
+ "dream",
1218
+ "wonder",
1219
+ "spark",
1220
+ "glow",
1221
+ "shimmer",
1222
+ "twinkle",
1223
+ "radiance",
1224
+ "harmony",
1225
+ "melody",
1226
+ "rhythm",
1227
+ "symphony",
1228
+ "adventure",
1229
+ "journey",
1230
+ "quest",
1231
+ "odyssey"
1232
+ ];
1233
+ function getRandomWord() {
1234
+ return WORD_LIST[Math.floor(Math.random() * WORD_LIST.length)];
1235
+ }
1236
+ function generateRandomThreadName() {
1237
+ const word1 = getRandomWord();
1238
+ let word2 = getRandomWord();
1239
+ while (word2 === word1) {
1240
+ word2 = getRandomWord();
1241
+ }
1242
+ return `${word1}-${word2}`;
1243
+ }
1244
+
1245
+ // src/managers/thread-manager.ts
1134
1246
  class ThreadManagerImpl {
1135
1247
  metadataManager;
1136
1248
  gitManager;
@@ -1364,17 +1476,14 @@ class ThreadManagerImpl {
1364
1476
  return join4(projectPath, threadId);
1365
1477
  }
1366
1478
  generateThreadTitle(existingTitles) {
1367
- const threadNumRegex = /^thread-(\d+)$/i;
1368
- let maxNum = 0;
1369
- for (const title of existingTitles) {
1370
- const match = title.match(threadNumRegex);
1371
- if (match) {
1372
- const n = parseInt(match[1], 10);
1373
- if (n > maxNum)
1374
- maxNum = n;
1375
- }
1479
+ let threadTitle = generateRandomThreadName();
1480
+ let attempts = 0;
1481
+ const maxAttempts = 100;
1482
+ while (existingTitles.has(threadTitle) && attempts < maxAttempts) {
1483
+ threadTitle = generateRandomThreadName();
1484
+ attempts++;
1376
1485
  }
1377
- return `Thread ${maxNum + 1}`;
1486
+ return threadTitle;
1378
1487
  }
1379
1488
  async listFiles(threadId) {
1380
1489
  const thread = await this.getThread(threadId);
@@ -3826,6 +3935,9 @@ class EventQueue {
3826
3935
  this.resolver = null;
3827
3936
  }
3828
3937
  }
3938
+ notify() {
3939
+ this.notifyResolver();
3940
+ }
3829
3941
  }
3830
3942
 
3831
3943
  // src/managers/pi-prompt-loader.ts
@@ -3986,7 +4098,7 @@ class PiExecutorImpl {
3986
4098
  });
3987
4099
  const promptDone = agent.prompt(userPrompt).then(() => {
3988
4100
  done = true;
3989
- eventQueue.setResolver(() => {});
4101
+ eventQueue.notify();
3990
4102
  }).catch((err) => {
3991
4103
  if (!eventQueue.errorOccurred) {
3992
4104
  eventQueue.errorOccurred = true;
@@ -4004,7 +4116,7 @@ class PiExecutorImpl {
4004
4116
  });
4005
4117
  }
4006
4118
  done = true;
4007
- eventQueue.setResolver(() => {});
4119
+ eventQueue.notify();
4008
4120
  });
4009
4121
  try {
4010
4122
  while (!done || eventQueue.length > 0) {
@@ -5100,6 +5212,7 @@ function createChatRoutes(threadManager, agentExecutor, conversationManager, pro
5100
5212
  if (!model || typeof model !== "string") {
5101
5213
  return errorResponse(c, "INVALID_REQUEST", "model is required and must be a string", 400);
5102
5214
  }
5215
+ console.log(`[chat] User message (thread: ${threadId}): ${content}`);
5103
5216
  const thread = await threadManager.getThread(threadId);
5104
5217
  if (!thread) {
5105
5218
  return errorResponse(c, "THREAD_NOT_FOUND", `Thread not found: ${threadId}`, 404);
@@ -5183,6 +5296,7 @@ User: ${content}` : content;
5183
5296
  capturedEvents.push(event);
5184
5297
  if (event.type === "message" && event.content) {
5185
5298
  fullContent += event.content;
5299
+ console.log(`[chat] Assistant (role: ${event.role ?? "assistant"}): ${event.content}`);
5186
5300
  }
5187
5301
  if (event.type === "message" && typeof event.content === "string" && (event.role === "tool" || isToolLikeContent(event.content))) {
5188
5302
  yield { type: "thinking", content: event.content };
@@ -5220,7 +5334,11 @@ User: ${content}` : content;
5220
5334
  processingStateManager.clearProcessing(threadId);
5221
5335
  }
5222
5336
  }
5223
- return streamAsyncGenerator(c, chatExecutionGenerator());
5337
+ async function* withCompleteSignal() {
5338
+ yield* chatExecutionGenerator();
5339
+ yield { type: "complete" };
5340
+ }
5341
+ return streamAsyncGenerator(c, withCompleteSignal());
5224
5342
  } catch (error) {
5225
5343
  return errorResponse(c, "REQUEST_PARSE_ERROR", "Failed to parse request body", 400, error instanceof Error ? error.message : String(error));
5226
5344
  }
@@ -5560,32 +5678,6 @@ import { Hono as Hono5 } from "hono";
5560
5678
 
5561
5679
  // src/provider-data.ts
5562
5680
  var PROVIDER_DATA = [
5563
- {
5564
- id: "aihubmix",
5565
- models: [
5566
- "DeepSeek-R1",
5567
- "DeepSeek-V3",
5568
- "claude-3-5-sonnet-20241022",
5569
- "claude-3-7-sonnet-20250219",
5570
- "claude-opus-4-1",
5571
- "claude-opus-4-20250514",
5572
- "claude-sonnet-4-20250514",
5573
- "claude-sonnet-4-5",
5574
- "gemini-2.5-flash",
5575
- "gemini-2.5-flash-lite",
5576
- "gemini-2.5-pro",
5577
- "glm-4.6",
5578
- "gpt-4",
5579
- "gpt-4.1",
5580
- "gpt-4o",
5581
- "gpt-5",
5582
- "gpt-5-mini",
5583
- "kimi-k2-thinking",
5584
- "kimi-k2-turbo-preview",
5585
- "o3-mini",
5586
- "o4-mini"
5587
- ]
5588
- },
5589
5681
  {
5590
5682
  id: "anthropic",
5591
5683
  models: [
@@ -6062,6 +6154,163 @@ async function getModelInfoForProvider(provider, modelIds, apiKey) {
6062
6154
  return result;
6063
6155
  }
6064
6156
 
6157
+ // src/generated-data.ts
6158
+ var GENERATED_DATA = [
6159
+ {
6160
+ id: "aihubmix",
6161
+ models: [
6162
+ "BAAI/bge-large-en-v1.5",
6163
+ "BAAI/bge-large-zh-v1.5",
6164
+ "DeepSeek-R1",
6165
+ "DeepSeek-V3",
6166
+ "DeepSeek-V3-Fast",
6167
+ "DeepSeek-V3.1-Fast",
6168
+ "DeepSeek-V3.1-Terminus",
6169
+ "DeepSeek-V3.1-Think",
6170
+ "DeepSeek-V3.2-Exp",
6171
+ "DeepSeek-V3.2-Exp-Think",
6172
+ "ERNIE-X1.1-Preview",
6173
+ "Kimi-K2-0905",
6174
+ "Qwen/QwQ-32B",
6175
+ "Qwen/Qwen2.5-VL-32B-Instruct",
6176
+ "baidu/ERNIE-4.5-300B-A47B",
6177
+ "bge-large-en",
6178
+ "bge-large-zh",
6179
+ "cc-MiniMax-M2",
6180
+ "cc-deepseek-v3.1",
6181
+ "cc-ernie-4.5-300b-a47b",
6182
+ "cc-kimi-k2-instruct",
6183
+ "cc-kimi-k2-instruct-0905",
6184
+ "cc-minimax-m2",
6185
+ "cc-minimax-m2.1",
6186
+ "cc-minimax-m2.5",
6187
+ "claude-3-5-haiku",
6188
+ "claude-3-5-sonnet",
6189
+ "claude-3-7-sonnet",
6190
+ "claude-haiku-4-5",
6191
+ "claude-opus-4-0",
6192
+ "claude-opus-4-1",
6193
+ "claude-opus-4-5",
6194
+ "claude-opus-4-5-think",
6195
+ "claude-opus-4-6",
6196
+ "claude-opus-4-6-think",
6197
+ "claude-sonnet-4-0",
6198
+ "claude-sonnet-4-5",
6199
+ "claude-sonnet-4-5-think",
6200
+ "claude-sonnet-4-6",
6201
+ "claude-sonnet-4-6-think",
6202
+ "coding-glm-4.6",
6203
+ "coding-glm-4.6-free",
6204
+ "coding-glm-4.7",
6205
+ "coding-glm-4.7-free",
6206
+ "coding-glm-5",
6207
+ "coding-glm-5-free",
6208
+ "coding-minimax-m2",
6209
+ "coding-minimax-m2-free",
6210
+ "coding-minimax-m2.1",
6211
+ "coding-minimax-m2.5",
6212
+ "deepseek-v3.2",
6213
+ "deepseek-v3.2-think",
6214
+ "doubao-seed-1-8",
6215
+ "doubao-seed-2-0-code-preview",
6216
+ "doubao-seed-2-0-lite",
6217
+ "doubao-seed-2-0-mini",
6218
+ "doubao-seed-2-0-pro",
6219
+ "ernie-4.5",
6220
+ "ernie-4.5-0.3b",
6221
+ "ernie-4.5-turbo-128k-preview",
6222
+ "ernie-4.5-turbo-latest",
6223
+ "ernie-4.5-turbo-vl",
6224
+ "ernie-irag-edit",
6225
+ "ernie-x1-turbo",
6226
+ "gemini-2.0-flash",
6227
+ "gemini-2.0-flash-exp-search",
6228
+ "gemini-2.0-flash-free",
6229
+ "gemini-2.5-flash",
6230
+ "gemini-2.5-flash-lite",
6231
+ "gemini-2.5-flash-lite-preview-09-2025",
6232
+ "gemini-2.5-flash-nothink",
6233
+ "gemini-2.5-flash-preview-05-20-nothink",
6234
+ "gemini-2.5-flash-preview-05-20-search",
6235
+ "gemini-2.5-flash-preview-09-2025",
6236
+ "gemini-2.5-flash-search",
6237
+ "gemini-2.5-pro",
6238
+ "gemini-2.5-pro-preview-03-25",
6239
+ "gemini-2.5-pro-preview-03-25-search",
6240
+ "gemini-2.5-pro-preview-06-05",
6241
+ "gemini-2.5-pro-preview-06-05-search",
6242
+ "gemini-2.5-pro-search",
6243
+ "gemini-3-flash-preview",
6244
+ "gemini-3-flash-preview-free",
6245
+ "gemini-3-flash-preview-search",
6246
+ "gemini-3-pro-preview",
6247
+ "gemini-3-pro-preview-search",
6248
+ "gemini-3.1-pro-preview",
6249
+ "gemini-3.1-pro-preview-customtools",
6250
+ "gemini-3.1-pro-preview-search",
6251
+ "glm-4.6",
6252
+ "glm-4.7",
6253
+ "glm-4.7-flash-free",
6254
+ "gpt-4.1",
6255
+ "gpt-4.1-free",
6256
+ "gpt-4.1-mini",
6257
+ "gpt-4.1-mini-free",
6258
+ "gpt-4.1-nano",
6259
+ "gpt-4.1-nano-free",
6260
+ "gpt-4o",
6261
+ "gpt-4o-free",
6262
+ "gpt-5",
6263
+ "gpt-5-codex",
6264
+ "gpt-5-mini",
6265
+ "gpt-5-nano",
6266
+ "gpt-5-pro",
6267
+ "gpt-5.1",
6268
+ "gpt-5.2",
6269
+ "gpt-5.2-high",
6270
+ "gpt-5.2-low",
6271
+ "gpt-5.2-pro",
6272
+ "grok-4-1-fast-non-reasoning",
6273
+ "grok-4-1-fast-reasoning",
6274
+ "grok-code-fast-1",
6275
+ "inclusionAI/Ling-1T",
6276
+ "inclusionAI/Ling-flash-2.0",
6277
+ "inclusionAI/Ling-mini-2.0",
6278
+ "inclusionAI/Ring-1T",
6279
+ "inclusionAI/Ring-flash-2.0",
6280
+ "kimi-for-coding-free",
6281
+ "kimi-k2-0711",
6282
+ "kimi-k2-thinking",
6283
+ "kimi-k2-turbo-preview",
6284
+ "llama-4-maverick",
6285
+ "llama-4-scout",
6286
+ "o3",
6287
+ "o3-pro",
6288
+ "qwen3-235b-a22b",
6289
+ "qwen3-235b-a22b-instruct-2507",
6290
+ "qwen3-235b-a22b-thinking-2507",
6291
+ "qwen3-coder-30b-a3b-instruct",
6292
+ "qwen3-coder-480b-a35b-instruct",
6293
+ "qwen3-coder-flash",
6294
+ "qwen3-coder-next",
6295
+ "qwen3-coder-plus",
6296
+ "qwen3-coder-plus-2025-07-22",
6297
+ "qwen3-max",
6298
+ "qwen3-max-preview",
6299
+ "qwen3-next-80b-a3b-instruct",
6300
+ "qwen3-next-80b-a3b-thinking",
6301
+ "qwen3-vl-235b-a22b-instruct",
6302
+ "qwen3-vl-235b-a22b-thinking",
6303
+ "qwen3-vl-30b-a3b-instruct",
6304
+ "qwen3-vl-30b-a3b-thinking",
6305
+ "qwen3-vl-flash",
6306
+ "qwen3-vl-flash-2026-01-22",
6307
+ "qwen3-vl-plus",
6308
+ "qwen3.5-397b-a17b",
6309
+ "qwen3.5-plus"
6310
+ ]
6311
+ }
6312
+ ];
6313
+
6065
6314
  // src/managers/model-manager.ts
6066
6315
  class ModelManager {
6067
6316
  metadataManager;
@@ -6070,7 +6319,10 @@ class ModelManager {
6070
6319
  }
6071
6320
  async getAvailableModels(provider) {
6072
6321
  const providerId = provider.toLowerCase();
6073
- const providerData = PROVIDER_DATA.find((p) => p.id === providerId);
6322
+ let providerData = PROVIDER_DATA.find((p) => p.id === providerId);
6323
+ if (!providerData) {
6324
+ providerData = GENERATED_DATA.find((p) => p.id === providerId);
6325
+ }
6074
6326
  if (!providerData) {
6075
6327
  throw new Error(`Provider "${provider}" is not supported`);
6076
6328
  }
@@ -6961,6 +7213,15 @@ new file mode 100644
6961
7213
  } catch {
6962
7214
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
6963
7215
  }
7216
+ const currentBranch = await new Promise((resolve4) => {
7217
+ const proc = spawn8("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
7218
+ let out = "";
7219
+ proc.stdout.on("data", (d) => {
7220
+ out += d.toString();
7221
+ });
7222
+ proc.on("close", () => resolve4(out.trim()));
7223
+ proc.on("error", () => resolve4(""));
7224
+ });
6964
7225
  const diff = await new Promise((resolveDiff, reject) => {
6965
7226
  const runPlainDiff = () => {
6966
7227
  const proc2 = spawn8("git", ["diff"], { cwd: gitRoot });
@@ -6993,19 +7254,97 @@ new file mode 100644
6993
7254
  });
6994
7255
  proc.on("error", reject);
6995
7256
  });
6996
- if (!diff.trim()) {
6997
- return c.json({ error: "No changes to generate PR info for. Make sure you have uncommitted changes." }, 400);
7257
+ const baseBranch = await new Promise((resolve4) => {
7258
+ const proc = spawn8("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: gitRoot });
7259
+ let out = "";
7260
+ proc.stdout.on("data", (d) => {
7261
+ out += d.toString();
7262
+ });
7263
+ proc.on("close", (code) => {
7264
+ if (code === 0 && out.trim()) {
7265
+ const match = out.trim().match(/refs\/remotes\/origin\/(.+)/);
7266
+ resolve4(match ? match[1] : null);
7267
+ } else {
7268
+ resolve4(null);
7269
+ }
7270
+ });
7271
+ proc.on("error", () => resolve4(null));
7272
+ });
7273
+ const effectiveBaseBranch = baseBranch || await (async () => {
7274
+ const candidates = ["main", "master", "develop", "staging"];
7275
+ for (const branch of candidates) {
7276
+ const exists = await new Promise((resolve4) => {
7277
+ const proc = spawn8("git", ["rev-parse", "--verify", `origin/${branch}`], { cwd: gitRoot });
7278
+ proc.on("close", (code) => resolve4(code === 0));
7279
+ proc.on("error", () => resolve4(false));
7280
+ });
7281
+ if (exists)
7282
+ return branch;
7283
+ }
7284
+ return null;
7285
+ })();
7286
+ let commitLog = "";
7287
+ if (effectiveBaseBranch && currentBranch !== effectiveBaseBranch) {
7288
+ commitLog = await new Promise((resolve4) => {
7289
+ const proc = spawn8("git", ["log", "--oneline", `${effectiveBaseBranch}..${currentBranch}`], { cwd: gitRoot });
7290
+ let out = "";
7291
+ proc.stdout.on("data", (d) => {
7292
+ out += d.toString();
7293
+ });
7294
+ proc.on("close", () => resolve4(out.trim()));
7295
+ proc.on("error", () => resolve4(""));
7296
+ });
7297
+ }
7298
+ let detailedCommits = "";
7299
+ if (effectiveBaseBranch && currentBranch !== effectiveBaseBranch && commitLog) {
7300
+ detailedCommits = await new Promise((resolve4) => {
7301
+ const proc = spawn8("git", ["log", `${effectiveBaseBranch}..${currentBranch}`, "--format=%h %s%n%b%n---"], { cwd: gitRoot });
7302
+ let out = "";
7303
+ proc.stdout.on("data", (d) => {
7304
+ out += d.toString();
7305
+ });
7306
+ proc.on("close", () => {
7307
+ resolve4(out.length > 2000 ? out.substring(0, 2000) + `
7308
+ ...(more commits)` : out);
7309
+ });
7310
+ proc.on("error", () => resolve4(""));
7311
+ });
6998
7312
  }
6999
- const truncatedDiff = diff.length > 3000 ? diff.substring(0, 3000) + `
7313
+ if (!diff.trim() && !commitLog.trim()) {
7314
+ return c.json({
7315
+ error: effectiveBaseBranch ? `No changes to generate PR info for. The current branch "${currentBranch}" has no uncommitted changes and no commits ahead of "${effectiveBaseBranch}".` : "No changes to generate PR info for. Make sure you have uncommitted changes or commits ahead of the base branch."
7316
+ }, 400);
7317
+ }
7318
+ let promptContent;
7319
+ if (commitLog.trim()) {
7320
+ promptContent = `Based on the following commits in branch "${currentBranch}" compared to "${effectiveBaseBranch}", generate a pull request title and description.
7321
+
7322
+ Commit log:
7323
+ ${commitLog}
7324
+
7325
+ Detailed commit messages:
7326
+ ${detailedCommits || "(no details available)"}
7327
+
7328
+ ${diff.trim() ? `
7329
+ Additionally, there are uncommitted changes:
7330
+ ${diff.length > 1500 ? diff.substring(0, 1500) + `
7331
+ ...(truncated)` : diff}` : ""}`;
7332
+ } else {
7333
+ const truncatedDiff = diff.length > 3000 ? diff.substring(0, 3000) + `
7000
7334
  ...(truncated)` : diff;
7001
- const prompt = `Based on the following git diff, generate a pull request title and description. Follow these guidelines:
7335
+ promptContent = `Based on the following git diff, generate a pull request title and description.
7336
+
7337
+ Git diff:
7338
+ ${truncatedDiff}`;
7339
+ }
7340
+ const prompt = `${promptContent}
7341
+
7342
+ Follow these guidelines:
7002
7343
  - Title: Concise, descriptive, under 72 characters
7003
7344
  - Description: Clear explanation of changes, why they were made, and any relevant context
7004
- - Use markdown formatting for the description
7345
+ - Use markdown formatting for the description (bullet points, headers, etc.)
7005
7346
  - Be professional and clear
7006
-
7007
- Git diff:
7008
- ${truncatedDiff}
7347
+ - If multiple changes, group them logically in the description
7009
7348
 
7010
7349
  Generate the response in this exact format:
7011
7350
  TITLE: <title here>
@@ -7030,7 +7369,7 @@ DESCRIPTION: <description here>`;
7030
7369
  const descriptionMatch = prInfo.match(/DESCRIPTION:\s*(.+?)$/s);
7031
7370
  const title = titleMatch ? titleMatch[1].trim() : "Update";
7032
7371
  const description = descriptionMatch ? descriptionMatch[1].trim() : "";
7033
- return c.json({ title, description });
7372
+ return c.json({ title, description, baseBranch: effectiveBaseBranch, currentBranch });
7034
7373
  } catch (error) {
7035
7374
  const message = error instanceof Error ? error.message : "Failed to generate PR info";
7036
7375
  return c.json({ error: message }, 500);
@@ -7147,6 +7486,148 @@ DESCRIPTION: <description here>`;
7147
7486
  return c.json({ error: message }, 500);
7148
7487
  }
7149
7488
  });
7489
+ router.get("/github-status/:threadId", async (c) => {
7490
+ try {
7491
+ const threadId = c.req.param("threadId");
7492
+ const thread = await metadataManager.loadThreads().then((threads) => threads.find((t) => t.id === threadId));
7493
+ if (!thread) {
7494
+ return c.json({ error: "Thread not found" }, 404);
7495
+ }
7496
+ const repoPath = thread.path;
7497
+ if (!repoPath) {
7498
+ return c.json({ error: "Thread path not found" }, 404);
7499
+ }
7500
+ const absolutePath = resolveThreadPath(repoPath);
7501
+ let gitRoot;
7502
+ try {
7503
+ gitRoot = await getGitRoot(absolutePath);
7504
+ } catch {
7505
+ return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
7506
+ }
7507
+ const remoteUrl = await new Promise((resolve4) => {
7508
+ const proc = spawn8("git", ["remote", "get-url", "origin"], { cwd: gitRoot });
7509
+ let out = "";
7510
+ proc.stdout.on("data", (d) => {
7511
+ out += d.toString();
7512
+ });
7513
+ proc.on("close", (code) => {
7514
+ if (code === 0) {
7515
+ resolve4(out.trim());
7516
+ } else {
7517
+ resolve4("");
7518
+ }
7519
+ });
7520
+ proc.on("error", () => resolve4(""));
7521
+ });
7522
+ const isOnGitHub = remoteUrl.includes("github.com");
7523
+ let repoUrl = "";
7524
+ if (isOnGitHub) {
7525
+ if (remoteUrl.startsWith("https://")) {
7526
+ repoUrl = remoteUrl.replace(/\.git$/, "");
7527
+ } else if (remoteUrl.startsWith("git@")) {
7528
+ const match = remoteUrl.match(/git@github\.com:(.+?)\.git$/);
7529
+ if (match) {
7530
+ repoUrl = `https://github.com/${match[1]}`;
7531
+ }
7532
+ }
7533
+ }
7534
+ return c.json({
7535
+ isOnGitHub,
7536
+ remoteUrl,
7537
+ canCreateRepo: !isOnGitHub && remoteUrl.length === 0,
7538
+ repoUrl
7539
+ });
7540
+ } catch (error) {
7541
+ const message = error instanceof Error ? error.message : "Failed to check GitHub status";
7542
+ return c.json({ error: message }, 500);
7543
+ }
7544
+ });
7545
+ router.post("/create-repo/:threadId", async (c) => {
7546
+ try {
7547
+ const threadId = c.req.param("threadId");
7548
+ const body = await c.req.json().catch(() => ({}));
7549
+ const { repoName, description, isPrivate } = body;
7550
+ const thread = await metadataManager.loadThreads().then((threads) => threads.find((t) => t.id === threadId));
7551
+ if (!thread) {
7552
+ return c.json({ error: "Thread not found" }, 404);
7553
+ }
7554
+ const repoPath = thread.path;
7555
+ if (!repoPath) {
7556
+ return c.json({ error: "Thread path not found" }, 404);
7557
+ }
7558
+ const absolutePath = resolveThreadPath(repoPath);
7559
+ let gitRoot;
7560
+ try {
7561
+ gitRoot = await getGitRoot(absolutePath);
7562
+ } catch {
7563
+ return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
7564
+ }
7565
+ const remoteUrl = await new Promise((resolve4) => {
7566
+ const proc = spawn8("git", ["remote", "get-url", "origin"], { cwd: gitRoot });
7567
+ let out = "";
7568
+ proc.stdout.on("data", (d) => {
7569
+ out += d.toString();
7570
+ });
7571
+ proc.on("close", (code) => {
7572
+ if (code === 0) {
7573
+ resolve4(out.trim());
7574
+ } else {
7575
+ resolve4("");
7576
+ }
7577
+ });
7578
+ proc.on("error", () => resolve4(""));
7579
+ });
7580
+ if (remoteUrl) {
7581
+ return c.json({ error: "Repository already has a remote origin" }, 400);
7582
+ }
7583
+ const _currentBranch = await new Promise((resolve4, reject) => {
7584
+ const proc = spawn8("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
7585
+ let out = "";
7586
+ proc.stdout.on("data", (d) => {
7587
+ out += d.toString();
7588
+ });
7589
+ proc.on("close", () => resolve4(out.trim()));
7590
+ proc.on("error", reject);
7591
+ });
7592
+ const repoUrl = await new Promise((resolve4, reject) => {
7593
+ const args = ["repo", "create"];
7594
+ if (repoName) {
7595
+ args.push(repoName);
7596
+ }
7597
+ if (description) {
7598
+ args.push("--description", description);
7599
+ }
7600
+ if (isPrivate) {
7601
+ args.push("--private");
7602
+ } else {
7603
+ args.push("--public");
7604
+ }
7605
+ args.push("--source", ".", "--push");
7606
+ const proc = spawn8("gh", args, { cwd: gitRoot });
7607
+ let out = "";
7608
+ let err = "";
7609
+ proc.stdout.on("data", (d) => {
7610
+ out += d.toString();
7611
+ });
7612
+ proc.stderr.on("data", (d) => {
7613
+ err += d.toString();
7614
+ });
7615
+ proc.on("close", (code) => {
7616
+ if (code === 0) {
7617
+ const urlMatch = out.match(/https:\/\/[^\s]+/);
7618
+ resolve4(urlMatch ? urlMatch[0] : out.trim());
7619
+ } else {
7620
+ reject(new Error(err || "Failed to create repository"));
7621
+ }
7622
+ });
7623
+ proc.on("error", () => reject(new Error("GitHub CLI (gh) not found. Please install it to create repositories.")));
7624
+ });
7625
+ return c.json({ success: true, repoUrl });
7626
+ } catch (error) {
7627
+ const message = error instanceof Error ? error.message : "Failed to create repository";
7628
+ return c.json({ error: message }, 500);
7629
+ }
7630
+ });
7150
7631
  return router;
7151
7632
  }
7152
7633