tarsk 0.5.43 → 0.5.45

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.
Files changed (91) hide show
  1. package/dist/index.js +1821 -378
  2. package/dist/public/assets/{account-view-xKotpUyx.js → account-view-CQetjhCu.js} +1 -1
  3. package/dist/public/assets/api-BiOks4gS.js +1 -0
  4. package/dist/public/assets/{browser-tab-DxigYzoT.js → browser-tab-CqyQXekj.js} +1 -1
  5. package/dist/public/assets/commit-dialog-CF1xviwe.js +1 -0
  6. package/dist/public/assets/context-menu-DcqiR-vK.js +1 -0
  7. package/dist/public/assets/create-repo-dialog-zVm7eDO3.js +1 -0
  8. package/dist/public/assets/dialogs-config-Cmwn8AP3.js +51 -0
  9. package/dist/public/assets/diff-view-CKaqAShe.js +3 -0
  10. package/dist/public/assets/explorer-tab-view-CDb7lUWX.js +2 -0
  11. package/dist/public/assets/explorer-tree-C-9BuD8n.js +1 -0
  12. package/dist/public/assets/{explorer-view-DIM08sdy.js → explorer-view-txWDc-Tg.js} +1 -1
  13. package/dist/public/assets/git-history-dialog-WU95COhj.js +1 -0
  14. package/dist/public/assets/git-ops-button-eOAENpwR.js +2 -0
  15. package/dist/public/assets/history-view-DTJug8Lv.js +9 -0
  16. package/dist/public/assets/index-CaswO76A.js +89 -0
  17. package/dist/public/assets/index-WIITae9I.css +1 -0
  18. package/dist/public/assets/mcp-server-card-BKJl2_RK.js +1 -0
  19. package/dist/public/assets/merged-pr-dialog-rSHpRiME.js +1 -0
  20. package/dist/public/assets/model-star-rating-DpF6_FVG.js +1 -0
  21. package/dist/public/assets/onboarding-ZCm02KNK.js +1 -0
  22. package/dist/public/assets/project-settings-view-COigmUzh.js +1 -0
  23. package/dist/public/assets/providers-list-view-BWQJtM6g.js +1 -0
  24. package/dist/public/assets/pull-request-dialog-DQVVlS8M.js +1 -0
  25. package/dist/public/assets/pull-with-changes-dialog-DtMvqvd8.js +1 -0
  26. package/dist/public/assets/push-before-pr-dialog-Bi6MHZiO.js +1 -0
  27. package/dist/public/assets/radio-group-s_ij8p3t.js +1 -0
  28. package/dist/public/assets/react-vendor-DMQDEfby.js +16 -0
  29. package/dist/public/assets/settings-general-view-Cgl5v7l-.js +1 -0
  30. package/dist/public/assets/settings-instructions-view-DrIwhrWJ.js +1 -0
  31. package/dist/public/assets/settings-list-B6nYThKU.js +1 -0
  32. package/dist/public/assets/settings-mcp-servers-view-QPaXg_C8.js +5 -0
  33. package/dist/public/assets/{settings-models-skeleton-DPnYbg69.js → settings-models-skeleton-D44p69YI.js} +1 -1
  34. package/dist/public/assets/settings-models-view--0zYzSYj.js +1 -0
  35. package/dist/public/assets/settings-rules-view-DtTJGnIp.js +8 -0
  36. package/dist/public/assets/settings-skills-view-CHT2EsBm.js +3 -0
  37. package/dist/public/assets/settings-slash-commands-view-RamAmlbD.js +1 -0
  38. package/dist/public/assets/settings-subagents-view-BY1e9nax.js +2 -0
  39. package/dist/public/assets/settings-system-prompt-view-DNR6lOSK.js +1 -0
  40. package/dist/public/assets/settings-view-XnpSVwUl.js +2 -0
  41. package/dist/public/assets/skeleton-_77ZLAk1.js +1 -0
  42. package/dist/public/assets/slug-utils-C0Ke4-ko.js +1 -0
  43. package/dist/public/assets/{terminal-panel-DTOx74_o.js → terminal-panel-Cp5-H4GS.js} +1 -1
  44. package/dist/public/assets/{ui-components-Jc6oi6bz.js → ui-components-BMQhWki4.js} +1 -1
  45. package/dist/public/assets/{use-deferred-search-B7EdyRbt.js → use-deferred-search-D0WCS0gz.js} +1 -1
  46. package/dist/public/assets/{utils-tgi5ym_d.js → utils-CkwFiI9G.js} +1 -1
  47. package/dist/public/assets/vosk-speech-CgILeNJR.js +2 -0
  48. package/dist/public/assets/{web-C3vJZ_3_.js → web-CdlbRaqq.js} +1 -1
  49. package/dist/public/assets/{web-CUAWBWPy.js → web-zT0ibfrw.js} +1 -1
  50. package/dist/public/index.html +8 -8
  51. package/package.json +1 -1
  52. package/dist/public/assets/api-D6uLdHBQ.js +0 -1
  53. package/dist/public/assets/commit-dialog-CLQM9ah3.js +0 -1
  54. package/dist/public/assets/context-menu-rC7iWcty.js +0 -1
  55. package/dist/public/assets/create-repo-dialog-C6k5wZPW.js +0 -1
  56. package/dist/public/assets/dialogs-config-CjKh5Rl2.js +0 -51
  57. package/dist/public/assets/diff-view-DWDWI5nl.js +0 -3
  58. package/dist/public/assets/explorer-tab-view-B0kT8Hl6.js +0 -2
  59. package/dist/public/assets/explorer-tree-BC4fBpxi.js +0 -1
  60. package/dist/public/assets/git-history-dialog-CuxOTngT.js +0 -1
  61. package/dist/public/assets/git-ops-button-C04zFAnF.js +0 -2
  62. package/dist/public/assets/history-view-ar7GLZ-R.js +0 -9
  63. package/dist/public/assets/index--HY4BbcM.js +0 -90
  64. package/dist/public/assets/index-DKOXV50p.css +0 -1
  65. package/dist/public/assets/mcp-server-card-Cy4RU2_Q.js +0 -1
  66. package/dist/public/assets/merged-pr-dialog-Bo07VouF.js +0 -1
  67. package/dist/public/assets/model-star-rating-BmkpdXfr.js +0 -1
  68. package/dist/public/assets/onboarding-ClZrOxX7.js +0 -1
  69. package/dist/public/assets/project-settings-view-Dm9pQAp_.js +0 -1
  70. package/dist/public/assets/providers-list-view-D5gHsjl_.js +0 -1
  71. package/dist/public/assets/pull-request-dialog-8AYlOUNX.js +0 -1
  72. package/dist/public/assets/pull-with-changes-dialog-CSa5OE-d.js +0 -1
  73. package/dist/public/assets/push-before-pr-dialog-D5W_xsqv.js +0 -1
  74. package/dist/public/assets/radio-group-CbatNaj1.js +0 -1
  75. package/dist/public/assets/react-vendor-DwQYi7es.js +0 -16
  76. package/dist/public/assets/settings-general-view-BP5ULy9A.js +0 -1
  77. package/dist/public/assets/settings-instructions-view-DMAjbi6E.js +0 -1
  78. package/dist/public/assets/settings-list-B8hiBkBz.js +0 -1
  79. package/dist/public/assets/settings-mcp-servers-view-OimQz-Rd.js +0 -5
  80. package/dist/public/assets/settings-models-view-Fq3WtdKG.js +0 -1
  81. package/dist/public/assets/settings-rules-view-DBk7DzV2.js +0 -8
  82. package/dist/public/assets/settings-skills-view-CmOw-WMM.js +0 -2
  83. package/dist/public/assets/settings-slash-commands-view-FsrF5FkK.js +0 -1
  84. package/dist/public/assets/settings-subagents-view-D98Nxoly.js +0 -2
  85. package/dist/public/assets/settings-system-prompt-view-B6Hy9ZyK.js +0 -1
  86. package/dist/public/assets/settings-view-J-rjoRcU.js +0 -2
  87. package/dist/public/assets/skeleton-BHhGML7J.js +0 -1
  88. package/dist/public/assets/slug-utils-DyRUJ1NS.js +0 -1
  89. package/dist/public/assets/whisper-wasm-CWcbC1MB.js +0 -2
  90. package/dist/public/wasm/libmain-CWYJvMY5.js +0 -3318
  91. package/dist/public/wasm/libmain-D9-QM3iM.mjs +0 -3301
package/dist/index.js CHANGED
@@ -60,11 +60,127 @@ var init_programs = __esm({
60
60
  }
61
61
  });
62
62
 
63
+ // ../shared/dist/model-reasoning.js
64
+ function isReasoningEffortLevel(value) {
65
+ return REASONING_EFFORT_LEVEL_SET.has(value);
66
+ }
67
+ function isModelReasoningSetting(value) {
68
+ return value === REASONING_EFFORT_OFF || isReasoningEffortLevel(value);
69
+ }
70
+ function normalizeModelSettings(settings) {
71
+ if (!settings) {
72
+ return { reasoningEffort: REASONING_EFFORT_OFF };
73
+ }
74
+ if (settings.reasoningEffort !== void 0) {
75
+ return { reasoningEffort: settings.reasoningEffort };
76
+ }
77
+ if (settings.reasoning !== void 0) {
78
+ return {
79
+ reasoningEffort: settings.reasoning ? "medium" : REASONING_EFFORT_OFF
80
+ };
81
+ }
82
+ return { reasoningEffort: REASONING_EFFORT_OFF };
83
+ }
84
+ function isReasoningEnabled(settings) {
85
+ return normalizeModelSettings(settings).reasoningEffort !== REASONING_EFFORT_OFF;
86
+ }
87
+ function getThinkingLevelFromSettings(settings) {
88
+ return normalizeModelSettings(settings).reasoningEffort ?? REASONING_EFFORT_OFF;
89
+ }
90
+ function clampReasoningEffort(effort, supportedEfforts) {
91
+ if (effort === REASONING_EFFORT_OFF || supportedEfforts.length === 0) {
92
+ return REASONING_EFFORT_OFF;
93
+ }
94
+ if (supportedEfforts.includes(effort)) {
95
+ return effort;
96
+ }
97
+ return supportedEfforts.includes("medium") ? "medium" : supportedEfforts[0] ?? REASONING_EFFORT_OFF;
98
+ }
99
+ var REASONING_EFFORT_OFF, REASONING_EFFORT_LEVELS, REASONING_EFFORT_LEVEL_SET;
100
+ var init_model_reasoning = __esm({
101
+ "../shared/dist/model-reasoning.js"() {
102
+ "use strict";
103
+ REASONING_EFFORT_OFF = "off";
104
+ REASONING_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
105
+ REASONING_EFFORT_LEVEL_SET = new Set(REASONING_EFFORT_LEVELS);
106
+ }
107
+ });
108
+
63
109
  // ../shared/dist/models.js
64
110
  var init_models = __esm({
65
111
  "../shared/dist/models.js"() {
66
112
  "use strict";
67
113
  init_programs();
114
+ init_model_reasoning();
115
+ }
116
+ });
117
+
118
+ // ../shared/dist/model-aliases.js
119
+ function isModelAliasName(value) {
120
+ return MODEL_ALIAS_NAME_SET.has(value);
121
+ }
122
+ function normalizeProjectModelAliases(value) {
123
+ if (value == null) {
124
+ return void 0;
125
+ }
126
+ if (typeof value !== "object" || Array.isArray(value)) {
127
+ throw new Error("modelAliases must be an object");
128
+ }
129
+ const normalized = {};
130
+ for (const [key, entry] of Object.entries(value)) {
131
+ if (!isModelAliasName(key)) {
132
+ throw new Error(`Invalid model alias: ${key}`);
133
+ }
134
+ if (entry == null) {
135
+ continue;
136
+ }
137
+ if (typeof entry !== "object" || Array.isArray(entry)) {
138
+ throw new Error(`Invalid model alias target for '${key}'`);
139
+ }
140
+ const model = typeof entry.model === "string" ? entry.model.trim() : "";
141
+ const provider = typeof entry.provider === "string" ? entry.provider.trim() : "";
142
+ if (!model || !provider) {
143
+ throw new Error(`Model alias '${key}' requires both model and provider`);
144
+ }
145
+ normalized[key] = { model, provider };
146
+ }
147
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
148
+ }
149
+ function resolveModelAlias(model, aliases) {
150
+ if (!isModelAliasName(model)) {
151
+ return null;
152
+ }
153
+ return aliases?.[model] ?? null;
154
+ }
155
+ function getEnabledModelIdsForProvider(provider, enabledModelsByProvider) {
156
+ const providerSlug = provider.toLowerCase();
157
+ if (enabledModelsByProvider[providerSlug]) {
158
+ return enabledModelsByProvider[providerSlug];
159
+ }
160
+ const matchedEntry = Object.entries(enabledModelsByProvider).find(([key]) => key.toLowerCase() === providerSlug);
161
+ return matchedEntry?.[1] ?? [];
162
+ }
163
+ function isModelAliasTargetEnabled(target, enabledModelsByProvider) {
164
+ const enabledModelIds = getEnabledModelIdsForProvider(target.provider, enabledModelsByProvider);
165
+ return enabledModelIds.includes(target.model);
166
+ }
167
+ function validateProjectModelAliasesAgainstEnabled(aliases, enabledModelsByProvider) {
168
+ for (const aliasName of MODEL_ALIAS_NAMES) {
169
+ const target = aliases[aliasName];
170
+ if (!target) {
171
+ continue;
172
+ }
173
+ if (!isModelAliasTargetEnabled(target, enabledModelsByProvider)) {
174
+ throw new Error(`Model alias '${aliasName}' must reference an enabled model. '${target.provider}/${target.model}' is not enabled.`);
175
+ }
176
+ }
177
+ }
178
+ var MODEL_ALIAS_NAMES, MODEL_ALIAS_NAME_SET;
179
+ var init_model_aliases = __esm({
180
+ "../shared/dist/model-aliases.js"() {
181
+ "use strict";
182
+ MODEL_ALIAS_NAMES = ["fast", "local", "cheap", "smart"];
183
+ MODEL_ALIAS_NAME_SET = new Set(MODEL_ALIAS_NAMES);
68
184
  }
69
185
  });
70
186
 
@@ -695,13 +811,14 @@ function resolveLocalProviderApiUrl(envValues) {
695
811
  function isLocalProvider(providerId) {
696
812
  return providerId.toLowerCase() === LOCAL_PROVIDER_ID;
697
813
  }
698
- var LOCAL_PROVIDER_ID, LOCAL_PROVIDER_NAME, LOCAL_PROVIDER_DEFAULT_API_URL, LOCAL_API_URL_ENV, LOCAL_API_KEY_ENV;
814
+ var LOCAL_PROVIDER_ID, LOCAL_PROVIDER_NAME, LOCAL_PROVIDER_DEFAULT_API_URL, LOCAL_MODELS_FETCH_TIMEOUT_MS, LOCAL_API_URL_ENV, LOCAL_API_KEY_ENV;
699
815
  var init_providers = __esm({
700
816
  "../shared/dist/providers.js"() {
701
817
  "use strict";
702
818
  LOCAL_PROVIDER_ID = "local";
703
819
  LOCAL_PROVIDER_NAME = "Local";
704
820
  LOCAL_PROVIDER_DEFAULT_API_URL = "http://127.0.0.1:8000/v1";
821
+ LOCAL_MODELS_FETCH_TIMEOUT_MS = 5e3;
705
822
  LOCAL_API_URL_ENV = "LOCAL_API_URL";
706
823
  LOCAL_API_KEY_ENV = "LOCAL_API_KEY";
707
824
  }
@@ -867,11 +984,58 @@ var init_tmp_images = __esm({
867
984
  }
868
985
  });
869
986
 
987
+ // ../shared/dist/git-status-dot.js
988
+ function computeGitStatusDot(status) {
989
+ if (status.hasChanges)
990
+ return "changes";
991
+ if (status.hasUnpushedCommits && status.prExists)
992
+ return "update-pr";
993
+ if (status.prExists && status.prState === "OPEN" && !status.hasUnpushedCommits) {
994
+ return "open-pr";
995
+ }
996
+ return false;
997
+ }
998
+ var init_git_status_dot = __esm({
999
+ "../shared/dist/git-status-dot.js"() {
1000
+ "use strict";
1001
+ }
1002
+ });
1003
+
1004
+ // ../shared/dist/skill-sources.js
1005
+ function isCustomSkillSourceId(sourceId) {
1006
+ return sourceId.startsWith(CUSTOM_SKILL_SOURCE_ID_PREFIX);
1007
+ }
1008
+ function slugifySkillSourceTitle(title) {
1009
+ return title.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "source";
1010
+ }
1011
+ function createCustomSkillSourceId(title, existingIds) {
1012
+ const slug = slugifySkillSourceTitle(title);
1013
+ const base = `${CUSTOM_SKILL_SOURCE_ID_PREFIX}${slug}`;
1014
+ const taken = new Set(existingIds);
1015
+ if (!taken.has(base)) {
1016
+ return base;
1017
+ }
1018
+ let suffix = 2;
1019
+ while (taken.has(`${base}-${suffix}`)) {
1020
+ suffix += 1;
1021
+ }
1022
+ return `${base}-${suffix}`;
1023
+ }
1024
+ var CUSTOM_SKILL_SOURCE_ID_PREFIX;
1025
+ var init_skill_sources = __esm({
1026
+ "../shared/dist/skill-sources.js"() {
1027
+ "use strict";
1028
+ CUSTOM_SKILL_SOURCE_ID_PREFIX = "custom-";
1029
+ }
1030
+ });
1031
+
870
1032
  // ../shared/dist/index.js
871
1033
  var init_dist = __esm({
872
1034
  "../shared/dist/index.js"() {
873
1035
  "use strict";
874
1036
  init_models();
1037
+ init_model_aliases();
1038
+ init_model_reasoning();
875
1039
  init_commit_methods();
876
1040
  init_package_managers();
877
1041
  init_programs();
@@ -889,6 +1053,8 @@ var init_dist = __esm({
889
1053
  init_project_env_vars();
890
1054
  init_browser_tool();
891
1055
  init_tmp_images();
1056
+ init_git_status_dot();
1057
+ init_skill_sources();
892
1058
  }
893
1059
  });
894
1060
 
@@ -1966,6 +2132,14 @@ async function runMigrations(db) {
1966
2132
  tarskDebugLog("[db] Running migration: Adding allowedFetchDomains column to projects");
1967
2133
  await db.execute(`ALTER TABLE projects ADD COLUMN allowedFetchDomains TEXT`);
1968
2134
  }
2135
+ const modelAliasesProjectsInfo = await db.execute(`PRAGMA table_info(projects)`);
2136
+ const hasModelAliases = modelAliasesProjectsInfo.rows.some(
2137
+ (col) => col.name === "modelAliases"
2138
+ );
2139
+ if (!hasModelAliases) {
2140
+ tarskDebugLog("[db] Running migration: Adding modelAliases column to projects");
2141
+ await db.execute(`ALTER TABLE projects ADD COLUMN modelAliases TEXT`);
2142
+ }
1969
2143
  const projectScriptsExists = await db.execute(
1970
2144
  `SELECT name FROM sqlite_master WHERE type='table' AND name='project_scripts'`
1971
2145
  );
@@ -3496,9 +3670,9 @@ function createSimpleGrepTool(cwd, options) {
3496
3670
  const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT2);
3497
3671
  const formatPath = (filePath) => {
3498
3672
  if (isDirectory) {
3499
- const relative8 = path2.relative(searchPath, filePath);
3500
- if (relative8 && !relative8.startsWith("..")) {
3501
- return relative8.replace(/\\/g, "/");
3673
+ const relative9 = path2.relative(searchPath, filePath);
3674
+ if (relative9 && !relative9.startsWith("..")) {
3675
+ return relative9.replace(/\\/g, "/");
3502
3676
  }
3503
3677
  }
3504
3678
  return path2.basename(filePath);
@@ -3838,9 +4012,9 @@ function parseRipgrepOutput(stdout) {
3838
4012
  return stdout.trim().split("\n").map((line) => line.replace(/\r$/, "")).filter(Boolean);
3839
4013
  }
3840
4014
  function toRelativePath(cwd, filePath) {
3841
- const relative8 = path3.relative(cwd, filePath);
3842
- if (relative8 && !relative8.startsWith("..") && !path3.isAbsolute(relative8)) {
3843
- return relative8.replace(/\\/g, "/");
4015
+ const relative9 = path3.relative(cwd, filePath);
4016
+ if (relative9 && !relative9.startsWith("..") && !path3.isAbsolute(relative9)) {
4017
+ return relative9.replace(/\\/g, "/");
3844
4018
  }
3845
4019
  return filePath.replace(/\\/g, "/");
3846
4020
  }
@@ -6282,6 +6456,7 @@ async function getCatalogProvider(providerId) {
6282
6456
  }
6283
6457
 
6284
6458
  // src/features/models/local-models.ts
6459
+ init_dist();
6285
6460
  function normalizeModelsBaseUrl(apiUrl) {
6286
6461
  const trimmed = apiUrl.trim().replace(/\/+$/, "");
6287
6462
  return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
@@ -6296,9 +6471,15 @@ async function fetchLocalModels(apiUrl, apiKey) {
6296
6471
  try {
6297
6472
  response = await fetch(`${baseUrl}/models`, {
6298
6473
  method: "GET",
6299
- headers
6474
+ headers,
6475
+ signal: AbortSignal.timeout(LOCAL_MODELS_FETCH_TIMEOUT_MS)
6300
6476
  });
6301
6477
  } catch (error) {
6478
+ if (error instanceof Error && error.name === "TimeoutError") {
6479
+ throw new Error(
6480
+ `Local API at ${baseUrl}/models did not respond within ${LOCAL_MODELS_FETCH_TIMEOUT_MS / 1e3} seconds`
6481
+ );
6482
+ }
6302
6483
  const detail = error instanceof Error ? error.message : "Unknown error";
6303
6484
  throw new Error(`Could not reach local API at ${baseUrl}/models: ${detail}`);
6304
6485
  }
@@ -6327,6 +6508,119 @@ async function fetchLocalModels(apiUrl, apiKey) {
6327
6508
  }));
6328
6509
  }
6329
6510
 
6511
+ // src/agent/agent.model-resolver.ts
6512
+ init_dist();
6513
+ import {
6514
+ getModel,
6515
+ getSupportedThinkingLevels
6516
+ } from "@earendil-works/pi-ai";
6517
+ var DEFAULT_HEADERS = {
6518
+ "HTTP-Referer": "https://tarsk.io",
6519
+ "X-Title": "Tarsk.io"
6520
+ };
6521
+ var PROVIDER_NAME_TO_PI = {
6522
+ anthropic: "anthropic",
6523
+ openai: "openai",
6524
+ google: "google",
6525
+ groq: "groq",
6526
+ cerebras: "cerebras",
6527
+ xai: "xai",
6528
+ openrouter: "openrouter",
6529
+ "github copilot": "github-copilot",
6530
+ minimax: "minimax",
6531
+ "kimi coding plan": "kimi-coding",
6532
+ "hugging face": "huggingface",
6533
+ codex: "openai-codex",
6534
+ mistral: "mistral"
6535
+ };
6536
+ function determineApiType(providerName, apiUrl) {
6537
+ const slug = providerName.toLowerCase();
6538
+ if (isLocalProvider(slug)) {
6539
+ return "openai-completions";
6540
+ }
6541
+ if (slug === "azure" || apiUrl.includes("openai.azure.com")) {
6542
+ return "azure-openai-responses";
6543
+ }
6544
+ if (apiUrl.includes("/anthropic/")) return "anthropic-messages";
6545
+ if (apiUrl.includes("api.anthropic.com")) return "anthropic-messages";
6546
+ return "openai-completions";
6547
+ }
6548
+ var PI_REGISTRY_PROVIDERS_BY_API = {
6549
+ "azure-openai-responses": "azure-openai-responses"
6550
+ };
6551
+ function getLocalModelResolveOptions(providerName, settings) {
6552
+ if (!isLocalProvider(providerName)) {
6553
+ return void 0;
6554
+ }
6555
+ const normalized = normalizeModelSettings(settings);
6556
+ return {
6557
+ reasoning: isReasoningEnabled(normalized)
6558
+ };
6559
+ }
6560
+ function getSupportedReasoningEfforts(providerName, modelId, providerConfig) {
6561
+ const resolved = resolveModel(providerName, modelId, providerConfig, { reasoning: true });
6562
+ return getSupportedThinkingLevels(resolved).filter(
6563
+ (level) => level !== "off"
6564
+ );
6565
+ }
6566
+ function mergeModelHeaders(model) {
6567
+ return { ...model, headers: { ...model.headers, ...DEFAULT_HEADERS } };
6568
+ }
6569
+ function tryGetRegistryModel(provider, modelId) {
6570
+ const model = getModel(provider, modelId);
6571
+ return model ?? void 0;
6572
+ }
6573
+ function applyReasoningOption(model, options) {
6574
+ if (options?.reasoning === void 0) {
6575
+ return model;
6576
+ }
6577
+ return { ...model, reasoning: options.reasoning };
6578
+ }
6579
+ function resolveModel(providerName, modelId, providerConfig, options) {
6580
+ const piProvider = PROVIDER_NAME_TO_PI[providerName.toLowerCase()];
6581
+ if (piProvider) {
6582
+ const model = tryGetRegistryModel(piProvider, modelId);
6583
+ if (model) {
6584
+ return applyReasoningOption(mergeModelHeaders(model), options);
6585
+ }
6586
+ }
6587
+ const baseUrl = providerConfig.api;
6588
+ if (!baseUrl) {
6589
+ throw new Error(`No API URL configured for provider: ${providerName}`);
6590
+ }
6591
+ const apiType = determineApiType(providerName, baseUrl);
6592
+ const registryProvider = PI_REGISTRY_PROVIDERS_BY_API[apiType];
6593
+ if (registryProvider) {
6594
+ const model = tryGetRegistryModel(registryProvider, modelId);
6595
+ if (model) {
6596
+ return applyReasoningOption(
6597
+ mergeModelHeaders({
6598
+ ...model,
6599
+ provider: providerName.toLowerCase(),
6600
+ baseUrl
6601
+ }),
6602
+ options
6603
+ );
6604
+ }
6605
+ }
6606
+ return applyReasoningOption(
6607
+ {
6608
+ id: modelId,
6609
+ name: modelId,
6610
+ api: apiType,
6611
+ provider: providerName.toLowerCase(),
6612
+ baseUrl,
6613
+ reasoning: false,
6614
+ input: ["text"],
6615
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
6616
+ contextWindow: 128e3,
6617
+ maxTokens: 16384,
6618
+ headers: DEFAULT_HEADERS
6619
+ },
6620
+ options
6621
+ );
6622
+ }
6623
+
6330
6624
  // src/features/models/models.manager.ts
6331
6625
  var ModelManager = class {
6332
6626
  metadataManager;
@@ -6341,10 +6635,40 @@ var ModelManager = class {
6341
6635
  * Get available models for a provider.
6342
6636
  * Uses the remote catalog as the source for model lists, names, and pricing.
6343
6637
  */
6638
+ async applyModelSettings(models, providerId) {
6639
+ const settingsByModel = await this.metadataManager.getProviderModelSettings(providerId);
6640
+ if (!isLocalProvider(providerId)) {
6641
+ return models.map((model) => ({
6642
+ ...model,
6643
+ settings: normalizeModelSettings(settingsByModel[model.id])
6644
+ }));
6645
+ }
6646
+ const envValues = await this.metadataManager.getProviderEnvValues();
6647
+ const apiUrl = resolveLocalProviderApiUrl({
6648
+ ...envValues,
6649
+ [LOCAL_API_URL_ENV]: envValues[LOCAL_API_URL_ENV] ?? process.env[LOCAL_API_URL_ENV]
6650
+ });
6651
+ return models.map((model) => {
6652
+ const supportedReasoningEfforts = getSupportedReasoningEfforts(providerId, model.id, {
6653
+ api: apiUrl || LOCAL_PROVIDER_DEFAULT_API_URL
6654
+ });
6655
+ const normalized = normalizeModelSettings(settingsByModel[model.id]);
6656
+ const reasoningEffort = clampReasoningEffort(
6657
+ normalized.reasoningEffort ?? REASONING_EFFORT_OFF,
6658
+ supportedReasoningEfforts
6659
+ );
6660
+ return {
6661
+ ...model,
6662
+ supportedReasoningEfforts,
6663
+ settings: { reasoningEffort }
6664
+ };
6665
+ });
6666
+ }
6344
6667
  async getAvailableModels(provider) {
6345
6668
  const providerId = provider.toLowerCase();
6346
6669
  if (isLocalProvider(providerId)) {
6347
- return this.getLocalAvailableModels();
6670
+ const models = await this.getLocalAvailableModels();
6671
+ return this.applyModelSettings(models, providerId);
6348
6672
  }
6349
6673
  const catalogProvider = await getCatalogProvider(providerId);
6350
6674
  if (!catalogProvider) {
@@ -6406,6 +6730,9 @@ var ModelManager = class {
6406
6730
  return [];
6407
6731
  }
6408
6732
  }
6733
+ async getModelSettings(provider, modelId) {
6734
+ return this.metadataManager.getModelSettings(provider, modelId);
6735
+ }
6409
6736
  /**
6410
6737
  * Get a specific model by ID from a provider
6411
6738
  * @param provider - Provider name
@@ -6454,17 +6781,24 @@ var ModelManager = class {
6454
6781
  }
6455
6782
  async getAllEnabledImageModels() {
6456
6783
  const enabledByProvider = await this.metadataManager.getAllEnabledImageModels();
6457
- const result = {};
6458
- for (const provider of Object.keys(enabledByProvider)) {
6459
- try {
6460
- const availableModels = await this.getAvailableModels(provider);
6461
- const enabledIds = enabledByProvider[provider];
6462
- const models = availableModels.filter((model) => enabledIds.includes(model.id));
6463
- if (models.length > 0) {
6464
- result[provider] = models;
6784
+ const providers = Object.keys(enabledByProvider);
6785
+ const entries = await Promise.all(
6786
+ providers.map(async (provider) => {
6787
+ try {
6788
+ const availableModels = await this.getAvailableModels(provider);
6789
+ const enabledIds = enabledByProvider[provider];
6790
+ const models = availableModels.filter((model) => enabledIds.includes(model.id));
6791
+ return [provider, models];
6792
+ } catch (error) {
6793
+ console.error(`Failed to get enabled image models for ${provider}:`, error);
6794
+ return [provider, []];
6465
6795
  }
6466
- } catch (error) {
6467
- console.error(`Failed to get enabled image models for ${provider}:`, error);
6796
+ })
6797
+ );
6798
+ const result = {};
6799
+ for (const [provider, models] of entries) {
6800
+ if (models.length > 0) {
6801
+ result[provider] = models;
6468
6802
  }
6469
6803
  }
6470
6804
  return result;
@@ -6475,15 +6809,22 @@ var ModelManager = class {
6475
6809
  */
6476
6810
  async getAllEnabledModels() {
6477
6811
  const enabledByProvider = await this.metadataManager.getAllEnabledModels();
6478
- const result = {};
6479
- for (const provider of Object.keys(enabledByProvider)) {
6480
- try {
6481
- const models = await this.getEnabledModels(provider);
6482
- if (models.length > 0) {
6483
- result[provider] = models;
6812
+ const providers = Object.keys(enabledByProvider);
6813
+ const entries = await Promise.all(
6814
+ providers.map(async (provider) => {
6815
+ try {
6816
+ const models = await this.getEnabledModels(provider);
6817
+ return [provider, models];
6818
+ } catch (error) {
6819
+ console.error(`Failed to get enabled models for ${provider}:`, error);
6820
+ return [provider, []];
6484
6821
  }
6485
- } catch (error) {
6486
- console.error(`Failed to get enabled models for ${provider}:`, error);
6822
+ })
6823
+ );
6824
+ const result = {};
6825
+ for (const [provider, models] of entries) {
6826
+ if (models.length > 0) {
6827
+ result[provider] = models;
6487
6828
  }
6488
6829
  }
6489
6830
  return result;
@@ -8284,65 +8625,6 @@ function getDataDir() {
8284
8625
  return DATA_DIR;
8285
8626
  }
8286
8627
 
8287
- // src/agent/agent.model-resolver.ts
8288
- import { getModel } from "@earendil-works/pi-ai";
8289
- var DEFAULT_HEADERS = {
8290
- "HTTP-Referer": "https://tarsk.io",
8291
- "X-Title": "Tarsk.io"
8292
- };
8293
- var PROVIDER_NAME_TO_PI = {
8294
- anthropic: "anthropic",
8295
- openai: "openai",
8296
- google: "google",
8297
- groq: "groq",
8298
- cerebras: "cerebras",
8299
- xai: "xai",
8300
- openrouter: "openrouter",
8301
- "github copilot": "github-copilot",
8302
- minimax: "minimax",
8303
- "kimi coding plan": "kimi-coding",
8304
- "hugging face": "huggingface",
8305
- codex: "openai-codex",
8306
- mistral: "mistral"
8307
- };
8308
- function determineApiType(providerName, apiUrl) {
8309
- const slug = providerName.toLowerCase();
8310
- if (slug === "azure" || apiUrl.includes("openai.azure.com")) {
8311
- return "azure-openai-responses";
8312
- }
8313
- if (apiUrl.includes("/anthropic/")) return "anthropic-messages";
8314
- if (apiUrl.includes("api.anthropic.com")) return "anthropic-messages";
8315
- return "openai-completions";
8316
- }
8317
- function resolveModel(providerName, modelId, providerConfig) {
8318
- const piProvider = PROVIDER_NAME_TO_PI[providerName.toLowerCase()];
8319
- if (piProvider) {
8320
- try {
8321
- const model = getModel(piProvider, modelId);
8322
- return { ...model, headers: { ...model.headers, ...DEFAULT_HEADERS } };
8323
- } catch {
8324
- }
8325
- }
8326
- const baseUrl = providerConfig.api;
8327
- if (!baseUrl) {
8328
- throw new Error(`No API URL configured for provider: ${providerName}`);
8329
- }
8330
- const apiType = determineApiType(providerName, baseUrl);
8331
- return {
8332
- id: modelId,
8333
- name: modelId,
8334
- api: apiType,
8335
- provider: providerName.toLowerCase(),
8336
- baseUrl,
8337
- reasoning: true,
8338
- input: ["text"],
8339
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
8340
- contextWindow: 128e3,
8341
- maxTokens: 16384,
8342
- headers: DEFAULT_HEADERS
8343
- };
8344
- }
8345
-
8346
8628
  // src/features/git/git.utils.ts
8347
8629
  init_dist();
8348
8630
  import { completeSimple } from "@earendil-works/pi-ai";
@@ -9080,53 +9362,128 @@ function stageAllChanges(gitRoot) {
9080
9362
  proc.on("error", reject);
9081
9363
  });
9082
9364
  }
9083
- function commitChanges(gitRoot, message) {
9084
- return new Promise((resolve8, reject) => {
9085
- const proc = spawnProcess("git", ["commit", "-m", message], { cwd: gitRoot });
9086
- proc.on("close", (code) => {
9087
- if (code === 0) resolve8();
9088
- else reject(new Error("Failed to commit changes"));
9089
- });
9090
- proc.on("error", reject);
9091
- });
9092
- }
9093
- function pushToOrigin(gitRoot, currentBranch) {
9365
+ function getCommitCount(gitRoot) {
9094
9366
  return new Promise((resolve8, reject) => {
9095
- console.log(`[git-push] Executing: git push -u origin ${currentBranch}`);
9096
- const proc = spawnProcess("git", ["push", "-u", "origin", currentBranch], { cwd: gitRoot });
9367
+ const proc = spawnProcess("git", ["rev-list", "--count", "HEAD"], { cwd: gitRoot });
9368
+ let out = "";
9097
9369
  let err = "";
9098
- let _out = "";
9099
9370
  if (proc.stdout) {
9100
9371
  proc.stdout.on("data", (d) => {
9101
- _out += d.toString();
9102
- console.log(`[git-push] stdout: ${d.toString().trim()}`);
9372
+ out += d.toString();
9103
9373
  });
9104
9374
  }
9105
9375
  if (proc.stderr) {
9106
9376
  proc.stderr.on("data", (d) => {
9107
9377
  err += d.toString();
9108
- console.log(`[git-push] stderr: ${d.toString().trim()}`);
9109
9378
  });
9110
9379
  }
9111
9380
  proc.on("close", (code) => {
9112
- console.log(`[git-push] git push exited with code: ${code}`);
9113
9381
  if (code === 0) {
9114
- console.log(`[git-push] \u2713 Push completed successfully`);
9115
- resolve8();
9116
- } else {
9117
- console.log(`[git-push] \u2717 Push failed with error: ${err || "Unknown error"}`);
9118
- reject(new Error(err || "Failed to push changes"));
9382
+ resolve8(parseInt(out.trim(), 10) || 0);
9383
+ return;
9384
+ }
9385
+ if (/unknown revision|ambiguous argument|does not have any commits yet|bad revision|needed a single revision/i.test(
9386
+ err
9387
+ )) {
9388
+ resolve8(0);
9389
+ return;
9119
9390
  }
9391
+ reject(new Error(err.trim() || "Failed to count commits"));
9120
9392
  });
9121
- proc.on("error", (error) => {
9122
- console.log(
9123
- `[git-push] \u2717 Process error: ${error instanceof Error ? error.message : String(error)}`
9124
- );
9125
- reject(error);
9393
+ proc.on("error", reject);
9394
+ });
9395
+ }
9396
+ function checkBranchExists(gitRoot, branchName) {
9397
+ return new Promise((resolve8) => {
9398
+ const proc = spawnProcess(
9399
+ "git",
9400
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
9401
+ {
9402
+ cwd: gitRoot
9403
+ }
9404
+ );
9405
+ proc.on("close", (code) => {
9406
+ resolve8(code === 0);
9407
+ });
9408
+ proc.on("error", () => {
9409
+ resolve8(false);
9126
9410
  });
9127
9411
  });
9128
9412
  }
9129
- function fetchFromOrigin(gitRoot) {
9413
+ function renameCurrentBranch(gitRoot, newBranchName) {
9414
+ return new Promise((resolve8, reject) => {
9415
+ const proc = spawnProcess("git", ["branch", "-m", newBranchName], {
9416
+ cwd: gitRoot
9417
+ });
9418
+ let err = "";
9419
+ if (proc.stderr) {
9420
+ proc.stderr.on("data", (d) => {
9421
+ err += d.toString();
9422
+ });
9423
+ }
9424
+ proc.on("close", (code) => {
9425
+ if (code === 0) {
9426
+ resolve8();
9427
+ } else {
9428
+ reject(new Error(err || `Failed to rename branch to "${newBranchName}"`));
9429
+ }
9430
+ });
9431
+ proc.on("error", reject);
9432
+ });
9433
+ }
9434
+ function commitChanges(gitRoot, message) {
9435
+ return new Promise((resolve8, reject) => {
9436
+ const proc = spawnProcess("git", ["commit", "-m", message], { cwd: gitRoot });
9437
+ let err = "";
9438
+ if (proc.stderr) {
9439
+ proc.stderr.on("data", (d) => {
9440
+ err += d.toString();
9441
+ });
9442
+ }
9443
+ proc.on("close", (code) => {
9444
+ if (code === 0) resolve8({ createdCommit: true });
9445
+ else reject(new Error(err.trim() || "Failed to commit changes"));
9446
+ });
9447
+ proc.on("error", reject);
9448
+ });
9449
+ }
9450
+ function pushToOrigin(gitRoot, currentBranch) {
9451
+ return new Promise((resolve8, reject) => {
9452
+ console.log(`[git-push] Executing: git push -u origin ${currentBranch}`);
9453
+ const proc = spawnProcess("git", ["push", "-u", "origin", currentBranch], { cwd: gitRoot });
9454
+ let err = "";
9455
+ let _out = "";
9456
+ if (proc.stdout) {
9457
+ proc.stdout.on("data", (d) => {
9458
+ _out += d.toString();
9459
+ console.log(`[git-push] stdout: ${d.toString().trim()}`);
9460
+ });
9461
+ }
9462
+ if (proc.stderr) {
9463
+ proc.stderr.on("data", (d) => {
9464
+ err += d.toString();
9465
+ console.log(`[git-push] stderr: ${d.toString().trim()}`);
9466
+ });
9467
+ }
9468
+ proc.on("close", (code) => {
9469
+ console.log(`[git-push] git push exited with code: ${code}`);
9470
+ if (code === 0) {
9471
+ console.log(`[git-push] \u2713 Push completed successfully`);
9472
+ resolve8();
9473
+ } else {
9474
+ console.log(`[git-push] \u2717 Push failed with error: ${err || "Unknown error"}`);
9475
+ reject(new Error(err || "Failed to push changes"));
9476
+ }
9477
+ });
9478
+ proc.on("error", (error) => {
9479
+ console.log(
9480
+ `[git-push] \u2717 Process error: ${error instanceof Error ? error.message : String(error)}`
9481
+ );
9482
+ reject(error);
9483
+ });
9484
+ });
9485
+ }
9486
+ function fetchFromOrigin(gitRoot) {
9130
9487
  return new Promise((resolve8, reject) => {
9131
9488
  const proc = spawnProcess("git", ["fetch", "origin"], { cwd: gitRoot });
9132
9489
  let err = "";
@@ -10785,10 +11142,26 @@ async function* loadCodingToolsWithMcpStatus(cwd, options) {
10785
11142
  }
10786
11143
 
10787
11144
  // src/tools/agent-tool.ts
11145
+ init_dist();
10788
11146
  import { Type as Type21 } from "@sinclair/typebox";
10789
11147
 
11148
+ // src/features/projects/project-model-aliases.ts
11149
+ init_dist();
11150
+ async function validateProjectModelAliasesForSave(metadataManager, aliases) {
11151
+ if (!aliases) {
11152
+ return;
11153
+ }
11154
+ const enabledModelsByProvider = await metadataManager.getAllEnabledModels();
11155
+ validateProjectModelAliasesAgainstEnabled(aliases, enabledModelsByProvider);
11156
+ }
11157
+ async function isProjectModelAliasTargetEnabled(metadataManager, target) {
11158
+ const enabledModelsByProvider = await metadataManager.getAllEnabledModels();
11159
+ return isModelAliasTargetEnabled(target, enabledModelsByProvider);
11160
+ }
11161
+
10790
11162
  // src/agent/agent.subagent-executor.ts
10791
11163
  import { Agent } from "@earendil-works/pi-agent-core";
11164
+ init_dist();
10792
11165
 
10793
11166
  // src/agent/agent.event-transformer.ts
10794
11167
  init_dist();
@@ -11049,7 +11422,14 @@ async function executeSubagent(options) {
11049
11422
  `No API key found for subagent provider: ${providerName}. Set ${providerConfig.apiKeyEnv ?? "an API key"} in environment variables or configuration.`
11050
11423
  );
11051
11424
  }
11052
- const resolvedModel = resolveModel(providerName, model, providerConfig);
11425
+ const localModelSettings = isLocalProvider(providerName) ? await metadataManager.getModelSettings(providerName, model) : {};
11426
+ const thinkingLevel = getThinkingLevelFromSettings(localModelSettings);
11427
+ const resolvedModel = resolveModel(
11428
+ providerName,
11429
+ model,
11430
+ providerConfig,
11431
+ getLocalModelResolveOptions(providerName, localModelSettings)
11432
+ );
11053
11433
  console.log(
11054
11434
  `[subagent] Starting with model=${resolvedModel.id}, tools=${tools.map((t) => t.name).join(",")}`
11055
11435
  );
@@ -11057,7 +11437,8 @@ async function executeSubagent(options) {
11057
11437
  initialState: {
11058
11438
  systemPrompt,
11059
11439
  model: resolvedModel,
11060
- tools
11440
+ tools,
11441
+ thinkingLevel
11061
11442
  },
11062
11443
  streamFn: cachedStreamFn,
11063
11444
  getApiKey: async () => apiKey
@@ -11156,6 +11537,31 @@ function filterTools(baseTools, allowedToolNames) {
11156
11537
  );
11157
11538
  return baseTools.filter((t) => allowed.has(t.name));
11158
11539
  }
11540
+ function resolveSubagentModelSelection(agentDef, parentModel, parentProvider) {
11541
+ if (agentDef?.model) {
11542
+ return {
11543
+ model: agentDef.model,
11544
+ provider: agentDef.provider ?? parentProvider
11545
+ };
11546
+ }
11547
+ return {
11548
+ model: parentModel,
11549
+ provider: parentProvider
11550
+ };
11551
+ }
11552
+ async function resolveSubagentModelSelectionWithAliases(agentDef, parentModel, parentProvider, modelAliases, metadataManager) {
11553
+ if (agentDef?.model && isModelAliasName(agentDef.model)) {
11554
+ const aliasTarget = resolveModelAlias(agentDef.model, modelAliases);
11555
+ if (aliasTarget && await isProjectModelAliasTargetEnabled(metadataManager, aliasTarget)) {
11556
+ return aliasTarget;
11557
+ }
11558
+ return {
11559
+ model: parentModel,
11560
+ provider: parentProvider
11561
+ };
11562
+ }
11563
+ return resolveSubagentModelSelection(agentDef, parentModel, parentProvider);
11564
+ }
11159
11565
  function createAgentTool(options) {
11160
11566
  const {
11161
11567
  agents,
@@ -11165,7 +11571,8 @@ function createAgentTool(options) {
11165
11571
  parentProvider,
11166
11572
  defaultSystemPrompt,
11167
11573
  onEvent,
11168
- signal
11574
+ signal,
11575
+ modelAliases
11169
11576
  } = options;
11170
11577
  return {
11171
11578
  name: "agent",
@@ -11188,8 +11595,13 @@ function createAgentTool(options) {
11188
11595
  }
11189
11596
  const label = description ?? agentName ?? "subagent";
11190
11597
  const systemPrompt = agentDef?.systemPrompt ?? defaultSystemPrompt;
11191
- const model = agentDef?.model ?? parentModel;
11192
- const provider = agentDef?.provider ?? parentProvider;
11598
+ const { model, provider } = await resolveSubagentModelSelectionWithAliases(
11599
+ agentDef,
11600
+ parentModel,
11601
+ parentProvider,
11602
+ modelAliases,
11603
+ metadataManager
11604
+ );
11193
11605
  const subTools = filterTools(baseTools, agentDef?.tools);
11194
11606
  console.log(
11195
11607
  `[agent-tool] Spawning subagent '${label}' with model=${model}, tools=${subTools.map((t) => t.name).join(",")}`
@@ -12475,7 +12887,14 @@ var PiExecutorImpl = class {
12475
12887
  );
12476
12888
  }
12477
12889
  console.log(`[ai] API key source for ${providerName}: ${apiKeySource}`);
12478
- const resolvedModel = resolveModel(providerName, model, providerConfig);
12890
+ const localModelSettings = isLocalProvider(providerName) ? await this.metadataManager.getModelSettings(providerName, model) : {};
12891
+ const thinkingLevel = getThinkingLevelFromSettings(localModelSettings);
12892
+ const resolvedModel = resolveModel(
12893
+ providerName,
12894
+ model,
12895
+ providerConfig,
12896
+ getLocalModelResolveOptions(providerName, localModelSettings)
12897
+ );
12479
12898
  console.log("[ai] Resolved model:", {
12480
12899
  id: resolvedModel.id,
12481
12900
  api: resolvedModel.api,
@@ -12554,7 +12973,8 @@ var PiExecutorImpl = class {
12554
12973
  parentProvider: providerName,
12555
12974
  defaultSystemPrompt: systemPrompt,
12556
12975
  onEvent: (event) => eventQueue.push(event),
12557
- signal
12976
+ signal,
12977
+ modelAliases: context.modelAliases
12558
12978
  });
12559
12979
  tools.push(agentToolInstance);
12560
12980
  }
@@ -12564,7 +12984,8 @@ var PiExecutorImpl = class {
12564
12984
  systemPrompt,
12565
12985
  model: resolvedModel,
12566
12986
  tools,
12567
- messages: historyMessages
12987
+ messages: historyMessages,
12988
+ thinkingLevel
12568
12989
  },
12569
12990
  streamFn: cachedStreamFn,
12570
12991
  getApiKey: async () => apiKey,
@@ -14401,8 +14822,8 @@ async function insertProject(db, project) {
14401
14822
  try {
14402
14823
  await db.execute(
14403
14824
  `
14404
- INSERT INTO projects (id, name, gitUrl, path, createdAt, openWith, commands, setupScript, validationScript, runCommand, commitMethod, planPrompt, testPrompt, reviewPrompt, customSystemPrompt, useCustomSystemPrompt, allowAllCommands, allowedCommandPatterns, allowedFetchDomains)
14405
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
14825
+ INSERT INTO projects (id, name, gitUrl, path, createdAt, openWith, commands, setupScript, validationScript, runCommand, commitMethod, planPrompt, testPrompt, reviewPrompt, customSystemPrompt, useCustomSystemPrompt, allowAllCommands, allowedCommandPatterns, allowedFetchDomains, modelAliases)
14826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
14406
14827
  `,
14407
14828
  [
14408
14829
  project.id,
@@ -14423,7 +14844,8 @@ async function insertProject(db, project) {
14423
14844
  project.useCustomSystemPrompt ? 1 : 0,
14424
14845
  project.allowAllCommands === false ? 0 : 1,
14425
14846
  project.allowedCommandPatterns ? JSON.stringify(project.allowedCommandPatterns) : null,
14426
- project.allowedFetchDomains ? JSON.stringify(project.allowedFetchDomains) : null
14847
+ project.allowedFetchDomains ? JSON.stringify(project.allowedFetchDomains) : null,
14848
+ project.modelAliases ? JSON.stringify(project.modelAliases) : null
14427
14849
  ]
14428
14850
  );
14429
14851
  } catch (error) {
@@ -14507,6 +14929,10 @@ async function updateProject(db, id, updates) {
14507
14929
  updates.allowedFetchDomains.length > 0 ? JSON.stringify(updates.allowedFetchDomains) : null
14508
14930
  );
14509
14931
  }
14932
+ if (updates.modelAliases !== void 0) {
14933
+ fields.push("modelAliases = ?");
14934
+ values.push(updates.modelAliases ? JSON.stringify(updates.modelAliases) : null);
14935
+ }
14510
14936
  if (fields.length === 0) {
14511
14937
  return;
14512
14938
  }
@@ -14614,7 +15040,8 @@ function deserializeProjectWithThreads(row, threadIds) {
14614
15040
  useCustomSystemPrompt: Boolean(row.useCustomSystemPrompt),
14615
15041
  allowAllCommands: row.allowAllCommands === null ? true : Boolean(row.allowAllCommands),
14616
15042
  allowedCommandPatterns: row.allowedCommandPatterns ? JSON.parse(row.allowedCommandPatterns) : void 0,
14617
- allowedFetchDomains: row.allowedFetchDomains ? JSON.parse(row.allowedFetchDomains) : void 0
15043
+ allowedFetchDomains: row.allowedFetchDomains ? JSON.parse(row.allowedFetchDomains) : void 0,
15044
+ modelAliases: row.modelAliases ? JSON.parse(row.modelAliases) : void 0
14618
15045
  };
14619
15046
  }
14620
15047
  async function deserializeProject(db, row) {
@@ -14661,7 +15088,8 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
14661
15088
  attachments,
14662
15089
  planMode,
14663
15090
  ralphMode,
14664
- checkpointRef
15091
+ checkpointRef,
15092
+ conversationId: requestedConversationId
14665
15093
  } = body;
14666
15094
  let model = baseModel;
14667
15095
  if (provider && typeof provider === "string") {
@@ -14758,7 +15186,7 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
14758
15186
  }
14759
15187
  return streamAsyncGenerator(c, replayGenerator());
14760
15188
  }
14761
- let conversationId = thread.currentConversationId;
15189
+ let conversationId = typeof requestedConversationId === "string" && requestedConversationId.length > 0 ? requestedConversationId : thread.currentConversationId;
14762
15190
  if (!conversationId) {
14763
15191
  conversationId = randomUUID8();
14764
15192
  const db2 = await getDatabase();
@@ -14809,7 +15237,8 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
14809
15237
  await updateProject(dbInstance2, thread.projectId, {
14810
15238
  allowedFetchDomains: domains
14811
15239
  });
14812
- }
15240
+ },
15241
+ modelAliases: project?.modelAliases
14813
15242
  };
14814
15243
  console.log("[ChatRoute] Execution context:", {
14815
15244
  threadId,
@@ -16916,7 +17345,8 @@ async function computeContextBreakdown(threadId, conversationId, conversationMan
16916
17345
  parentProvider: thread.modelProvider ?? "",
16917
17346
  defaultSystemPrompt: promptSections.systemPrompt,
16918
17347
  onEvent: () => {
16919
- }
17348
+ },
17349
+ modelAliases: project?.modelAliases
16920
17350
  });
16921
17351
  tools.push(agentToolInstance);
16922
17352
  }
@@ -17328,6 +17758,7 @@ var MetadataManager = class {
17328
17758
  await setState(this.db, "selectedThreadId", state.selectedThreadId);
17329
17759
  await setState(this.db, "enabledModels", state.enabledModels);
17330
17760
  await setState(this.db, "enabledImageModels", state.enabledImageModels);
17761
+ await setState(this.db, "modelSettings", state.modelSettings);
17331
17762
  await setState(this.db, "providerEnvValues", state.providerEnvValues);
17332
17763
  await setState(this.db, "onboardingCompleted", state.onboardingCompleted);
17333
17764
  const encryptedKeys = {};
@@ -17365,6 +17796,7 @@ var MetadataManager = class {
17365
17796
  const enabledImageModels = mergeEnabledModelsByProviderSlug(rawEnabledImageModels);
17366
17797
  const needsEnabledModelsMigration = enabledModelsNeedMigration(rawEnabledModels, enabledModels) || enabledModelsNeedMigration(rawEnabledImageModels, enabledImageModels);
17367
17798
  const providerEnvValues = allState.providerEnvValues || {};
17799
+ const modelSettings = allState.modelSettings || {};
17368
17800
  const encryptedKeys = allState.providerKeys || {};
17369
17801
  const providerKeys = {};
17370
17802
  let needsMigration = false;
@@ -17406,7 +17838,8 @@ var MetadataManager = class {
17406
17838
  providerKeys,
17407
17839
  providerEnvValues,
17408
17840
  enabledModels,
17409
- enabledImageModels
17841
+ enabledImageModels,
17842
+ modelSettings
17410
17843
  };
17411
17844
  } catch (error) {
17412
17845
  throw new Error(`Failed to load state: ${String(error)}`);
@@ -17597,6 +18030,34 @@ var MetadataManager = class {
17597
18030
  }
17598
18031
  await this.saveState(state);
17599
18032
  }
18033
+ async getModelSettings(provider, modelId) {
18034
+ const providerSlug = normalizeProviderSlug(provider);
18035
+ const state = await this.loadState();
18036
+ return state.modelSettings[providerSlug]?.[modelId] ?? {};
18037
+ }
18038
+ async getProviderModelSettings(provider) {
18039
+ const providerSlug = normalizeProviderSlug(provider);
18040
+ const state = await this.loadState();
18041
+ return state.modelSettings[providerSlug] ?? {};
18042
+ }
18043
+ async setModelSettings(provider, modelId, settings) {
18044
+ const providerSlug = normalizeProviderSlug(provider);
18045
+ const state = await this.loadState();
18046
+ if (!state.modelSettings[providerSlug]) {
18047
+ state.modelSettings[providerSlug] = {};
18048
+ }
18049
+ const nextSettings = { ...state.modelSettings[providerSlug][modelId], ...settings };
18050
+ const hasSettings = Object.values(nextSettings).some((value) => value !== void 0);
18051
+ if (hasSettings) {
18052
+ state.modelSettings[providerSlug][modelId] = nextSettings;
18053
+ } else {
18054
+ delete state.modelSettings[providerSlug][modelId];
18055
+ if (Object.keys(state.modelSettings[providerSlug]).length === 0) {
18056
+ delete state.modelSettings[providerSlug];
18057
+ }
18058
+ }
18059
+ await this.saveState(state);
18060
+ }
17600
18061
  // ── Image model methods ─────────────────────────────────────────────────────
17601
18062
  async enableImageModel(provider, modelId) {
17602
18063
  const providerSlug = normalizeProviderSlug(provider);
@@ -17917,6 +18378,38 @@ async function handleGetModelInfo(c, modelManager) {
17917
18378
  }
17918
18379
  }
17919
18380
 
18381
+ // src/features/models/models-model-settings.route.ts
18382
+ init_dist();
18383
+ async function handleModelSettings(c, metadataManager, modelManager) {
18384
+ try {
18385
+ const provider = c.req.param("provider");
18386
+ const modelId = c.req.param("modelId");
18387
+ if (!provider || !modelId) {
18388
+ return c.json({ error: "provider and modelId are required" }, 400);
18389
+ }
18390
+ if (!isLocalProvider(provider)) {
18391
+ return c.json({ error: "Model settings are only supported for the local provider" }, 400);
18392
+ }
18393
+ const body = await c.req.json();
18394
+ if (body.reasoning !== void 0 && typeof body.reasoning !== "boolean") {
18395
+ return c.json({ error: "reasoning must be a boolean" }, 400);
18396
+ }
18397
+ if (body.reasoningEffort !== void 0 && (typeof body.reasoningEffort !== "string" || !isModelReasoningSetting(body.reasoningEffort))) {
18398
+ return c.json({ error: "reasoningEffort must be a supported reasoning level" }, 400);
18399
+ }
18400
+ const model = await modelManager.getModel(provider, modelId);
18401
+ if (!model) {
18402
+ return c.json({ error: `Model "${modelId}" not found in provider "${provider}"` }, 404);
18403
+ }
18404
+ await metadataManager.setModelSettings(provider, modelId, body);
18405
+ const settings = await metadataManager.getModelSettings(provider, modelId);
18406
+ return c.json({ provider, modelId, settings });
18407
+ } catch (error) {
18408
+ const message = error instanceof Error ? error.message : "Unknown error";
18409
+ return c.json({ error: message }, 500);
18410
+ }
18411
+ }
18412
+
17920
18413
  // src/features/models/models.routes.ts
17921
18414
  function createModelRoutes(metadataManager) {
17922
18415
  const router = new Hono6();
@@ -17939,6 +18432,9 @@ function createModelRoutes(metadataManager) {
17939
18432
  router.post("/:provider/:modelId/disable", async (c) => {
17940
18433
  return handleModelDisable(c, modelManager);
17941
18434
  });
18435
+ router.post("/:provider/:modelId/settings", async (c) => {
18436
+ return handleModelSettings(c, metadataManager, modelManager);
18437
+ });
17942
18438
  router.post("/:provider/refresh", async (c) => {
17943
18439
  return handleProviderRefresh(c, modelManager);
17944
18440
  });
@@ -18116,12 +18612,7 @@ async function handleCreateFolderProject(c, projectManager) {
18116
18612
  }
18117
18613
 
18118
18614
  // src/features/projects/projects-list.route.ts
18119
- function computeGitStatusDot(status) {
18120
- if (status.hasChanges) return true;
18121
- if (status.commitsAheadOfDefault && !status.prExists) return true;
18122
- if (status.status === "Behind" || status.status === "Diverged") return true;
18123
- return false;
18124
- }
18615
+ init_dist();
18125
18616
  async function handleListProjects(c, projectManager, threadManager, db) {
18126
18617
  try {
18127
18618
  const projects = await projectManager.listProjects();
@@ -18158,6 +18649,7 @@ async function handleListProjects(c, projectManager, threadManager, db) {
18158
18649
  useCustomSystemPrompt: project.useCustomSystemPrompt ?? false,
18159
18650
  allowAllCommands: project.allowAllCommands !== false,
18160
18651
  allowedCommandPatterns: project.allowedCommandPatterns ?? [],
18652
+ modelAliases: project.modelAliases,
18161
18653
  threads: threads.map((thread) => {
18162
18654
  const cached = statusCache.get(thread.id);
18163
18655
  const gitStatusDot = cached ? computeGitStatusDot(cached) : false;
@@ -18423,7 +18915,7 @@ async function handleOpenProject(c, projectManager) {
18423
18915
 
18424
18916
  // src/features/projects/projects-update.route.ts
18425
18917
  init_dist();
18426
- async function handleUpdateProject(c, projectManager) {
18918
+ async function handleUpdateProject(c, projectManager, metadataManager) {
18427
18919
  try {
18428
18920
  const projectId = c.req.param("id");
18429
18921
  const body = await c.req.json();
@@ -18441,7 +18933,8 @@ async function handleUpdateProject(c, projectManager) {
18441
18933
  customSystemPrompt,
18442
18934
  useCustomSystemPrompt,
18443
18935
  allowAllCommands,
18444
- allowedCommandPatterns
18936
+ allowedCommandPatterns,
18937
+ modelAliases
18445
18938
  } = body;
18446
18939
  if (!projectId) {
18447
18940
  return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Project ID is required", 400);
@@ -18505,6 +18998,17 @@ async function handleUpdateProject(c, projectManager) {
18505
18998
  allowedCommandPatterns
18506
18999
  });
18507
19000
  }
19001
+ if (modelAliases !== void 0) {
19002
+ let normalizedAliases;
19003
+ try {
19004
+ normalizedAliases = normalizeProjectModelAliases(modelAliases);
19005
+ await validateProjectModelAliasesForSave(metadataManager, normalizedAliases);
19006
+ } catch (error) {
19007
+ const message = error instanceof Error ? error.message : String(error);
19008
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, message, 400);
19009
+ }
19010
+ await projectManager.updateModelAliases(projectId, normalizedAliases);
19011
+ }
18508
19012
  if (name !== void 0) {
18509
19013
  if (!name?.trim()) {
18510
19014
  return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Project name is required", 400);
@@ -19360,7 +19864,7 @@ async function handleSyncProjectEnvVars(c, projectManager) {
19360
19864
  }
19361
19865
 
19362
19866
  // src/features/projects/projects.routes.ts
19363
- function createProjectRoutes(projectManager, threadManager) {
19867
+ function createProjectRoutes(projectManager, threadManager, metadataManager) {
19364
19868
  const router = new Hono8();
19365
19869
  router.post("/", async (c) => {
19366
19870
  return handleCreateProject(c, projectManager);
@@ -19386,7 +19890,7 @@ function createProjectRoutes(projectManager, threadManager) {
19386
19890
  return handleOpenProject(c, projectManager);
19387
19891
  });
19388
19892
  router.put("/:id", async (c) => {
19389
- return handleUpdateProject(c, projectManager);
19893
+ return handleUpdateProject(c, projectManager, metadataManager);
19390
19894
  });
19391
19895
  router.post("/:id/commands", async (c) => {
19392
19896
  return handleSaveCommand(c, projectManager);
@@ -19449,7 +19953,8 @@ import { rm as rm4 } from "fs/promises";
19449
19953
  // src/agent/agent.process-manager.ts
19450
19954
  init_utils();
19451
19955
  import { EventEmitter } from "events";
19452
- init_tarsk_debug();
19956
+
19957
+ // src/core/dev-server-url-detector.ts
19453
19958
  var URL_PATTERNS = [
19454
19959
  // http(s)://localhost:PORT
19455
19960
  /https?:\/\/localhost[:/](\d+)/i,
@@ -19462,6 +19967,24 @@ var URL_PATTERNS = [
19462
19967
  // Local address patterns like http://192.168...
19463
19968
  /https?:\/\/(192\.168\.\d+\.\d+):(\d+)/i
19464
19969
  ];
19970
+ function detectDevServerUrl(text) {
19971
+ for (const pattern of URL_PATTERNS) {
19972
+ const match = text.match(pattern);
19973
+ if (match) {
19974
+ if (match[1] && !match[0].includes("://")) {
19975
+ if (match[0].startsWith("127.0.0.1:")) {
19976
+ return `http://${match[0]}`;
19977
+ }
19978
+ return `http://localhost:${match[1]}`;
19979
+ }
19980
+ return match[0];
19981
+ }
19982
+ }
19983
+ return void 0;
19984
+ }
19985
+
19986
+ // src/agent/agent.process-manager.ts
19987
+ init_tarsk_debug();
19465
19988
  var ProcessManager = class extends EventEmitter {
19466
19989
  processes = /* @__PURE__ */ new Map();
19467
19990
  detectedUrls = /* @__PURE__ */ new Map();
@@ -19537,7 +20060,7 @@ var ProcessManager = class extends EventEmitter {
19537
20060
  const text = data.toString();
19538
20061
  urlBuffer += text;
19539
20062
  if (!urlDetected) {
19540
- const detectedUrl = this.detectUrl(urlBuffer);
20063
+ const detectedUrl = detectDevServerUrl(urlBuffer);
19541
20064
  if (detectedUrl) {
19542
20065
  urlDetected = true;
19543
20066
  this.detectedUrls.set(projectId, detectedUrl);
@@ -19551,7 +20074,7 @@ var ProcessManager = class extends EventEmitter {
19551
20074
  const text = data.toString();
19552
20075
  urlBuffer += text;
19553
20076
  if (!urlDetected) {
19554
- const detectedUrl = this.detectUrl(urlBuffer);
20077
+ const detectedUrl = detectDevServerUrl(urlBuffer);
19555
20078
  if (detectedUrl) {
19556
20079
  urlDetected = true;
19557
20080
  this.detectedUrls.set(projectId, detectedUrl);
@@ -19642,23 +20165,6 @@ var ProcessManager = class extends EventEmitter {
19642
20165
  getDetectedUrl(projectId) {
19643
20166
  return this.detectedUrls.get(projectId);
19644
20167
  }
19645
- /**
19646
- * Detect URL from text output
19647
- * @param text - The text to search for URLs
19648
- * @returns The first detected URL or undefined
19649
- */
19650
- detectUrl(text) {
19651
- for (const pattern of URL_PATTERNS) {
19652
- const match = text.match(pattern);
19653
- if (match) {
19654
- if (match[1] && !match[0].includes("://")) {
19655
- return `http://localhost:${match[1]}`;
19656
- }
19657
- return match[0];
19658
- }
19659
- }
19660
- return void 0;
19661
- }
19662
20168
  };
19663
20169
 
19664
20170
  // src/features/projects/projects.creator.ts
@@ -20829,10 +21335,10 @@ async function loadUtils() {
20829
21335
  }
20830
21336
  }
20831
21337
  var ProjectCreator = class {
20832
- constructor(rootFolder, metadataManager, gitManager, processingStateManager, runCommandCallbacks) {
21338
+ constructor(rootFolder, metadataManager, gitManager2, processingStateManager, runCommandCallbacks) {
20833
21339
  this.rootFolder = rootFolder;
20834
21340
  this.metadataManager = metadataManager;
20835
- this.gitManager = gitManager;
21341
+ this.gitManager = gitManager2;
20836
21342
  this.processingStateManager = processingStateManager;
20837
21343
  this.runCommandCallbacks = runCommandCallbacks;
20838
21344
  }
@@ -21526,7 +22032,7 @@ var ProjectManagerImpl = class {
21526
22032
  * - 1.1 - WHEN a user provides a git URL to create a new Project, THE CLI SHALL clone the repository into a folder under the Root_Folder
21527
22033
  * - 1.4 - THE CLI SHALL determine the storage path for each Project folder
21528
22034
  */
21529
- constructor(rootFolder, metadataManager, gitManager, processingStateManager) {
22035
+ constructor(rootFolder, metadataManager, gitManager2, processingStateManager) {
21530
22036
  this.rootFolder = rootFolder;
21531
22037
  this.metadataManager = metadataManager;
21532
22038
  this.processManager = new ProcessManager();
@@ -21535,7 +22041,7 @@ var ProjectManagerImpl = class {
21535
22041
  this.projectCreator = new ProjectCreator(
21536
22042
  rootFolder,
21537
22043
  metadataManager,
21538
- gitManager,
22044
+ gitManager2,
21539
22045
  processingStateManager,
21540
22046
  {
21541
22047
  suggestRunCommand: (id) => this.suggestRunCommand(id),
@@ -21915,6 +22421,15 @@ ___CWD___%s
21915
22421
  }
21916
22422
  await this.metadataManager.saveProjects(projects);
21917
22423
  }
22424
+ async updateModelAliases(projectId, modelAliases) {
22425
+ const projects = await this.metadataManager.loadProjects();
22426
+ const project = projects.find((p) => p.id === projectId);
22427
+ if (!project) {
22428
+ throw new Error(`Project not found: ${projectId}`);
22429
+ }
22430
+ project.modelAliases = modelAliases;
22431
+ await this.metadataManager.saveProjects(projects);
22432
+ }
21918
22433
  /**
21919
22434
  * Starts running a dev server process for a project
21920
22435
  * @param projectId - The project ID
@@ -23326,9 +23841,9 @@ var ThreadManagerImpl = class {
23326
23841
  * - 2.1 - WHEN a user creates a new Thread for a Project, THE CLI SHALL create a new clone of the Project's git repository
23327
23842
  * - 2.4 - THE System SHALL maintain a title for each Thread
23328
23843
  */
23329
- constructor(metadataManager, gitManager, processingStateManager, rootFolder = "") {
23844
+ constructor(metadataManager, gitManager2, processingStateManager, rootFolder = "") {
23330
23845
  this.metadataManager = metadataManager;
23331
- this.gitManager = gitManager;
23846
+ this.gitManager = gitManager2;
23332
23847
  this.processingStateManager = processingStateManager;
23333
23848
  this.rootFolder = rootFolder;
23334
23849
  }
@@ -23807,7 +24322,7 @@ function validateThreadName(name) {
23807
24322
  }
23808
24323
 
23809
24324
  // src/features/threads/threads-create.route.ts
23810
- async function handleCreateThread(c, threadManager, gitManager) {
24325
+ async function handleCreateThread(c, threadManager, gitManager2) {
23811
24326
  try {
23812
24327
  const body = await c.req.json();
23813
24328
  const { projectId, title } = body;
@@ -23833,11 +24348,11 @@ async function handleCreateThread(c, threadManager, gitManager) {
23833
24348
  return errorResponse(c, ErrorCodes.INVALID_REQUEST, nameError, 400);
23834
24349
  }
23835
24350
  const existingThreads = await threadManager.listThreads(projectId);
23836
- const sanitizedNewName = gitManager.sanitizeBranchName(title);
23837
- const existingSanitized = existingThreads.map((t) => gitManager.sanitizeBranchName(t.title));
24351
+ const sanitizedNewName = gitManager2.sanitizeBranchName(title);
24352
+ const existingSanitized = existingThreads.map((t) => gitManager2.sanitizeBranchName(t.title));
23838
24353
  if (existingSanitized.includes(sanitizedNewName)) {
23839
24354
  let fallbackTitle = generateRandomThreadName();
23840
- while (existingSanitized.includes(gitManager.sanitizeBranchName(fallbackTitle))) {
24355
+ while (existingSanitized.includes(gitManager2.sanitizeBranchName(fallbackTitle))) {
23841
24356
  fallbackTitle = generateRandomThreadName();
23842
24357
  }
23843
24358
  return streamAsyncGenerator(c, threadManager.createThread(projectId, fallbackTitle));
@@ -23856,12 +24371,7 @@ async function handleCreateThread(c, threadManager, gitManager) {
23856
24371
  }
23857
24372
 
23858
24373
  // src/features/threads/threads-list.route.ts
23859
- function computeGitStatusDot2(status) {
23860
- if (status.hasChanges) return true;
23861
- if (status.commitsAheadOfDefault && !status.prExists) return true;
23862
- if (status.status === "Behind" || status.status === "Diverged") return true;
23863
- return false;
23864
- }
24374
+ init_dist();
23865
24375
  async function handleListThreads(c, threadManager, db) {
23866
24376
  try {
23867
24377
  const projectId = c.req.query("projectId");
@@ -23883,7 +24393,7 @@ async function handleListThreads(c, threadManager, db) {
23883
24393
  );
23884
24394
  const threadsWithStatus = threads.map((thread) => {
23885
24395
  const cached = statusCache.get(thread.id);
23886
- const gitStatusDot = cached ? computeGitStatusDot2(cached) : false;
24396
+ const gitStatusDot = cached ? computeGitStatusDot(cached) : false;
23887
24397
  return { ...thread, gitStatusDot };
23888
24398
  });
23889
24399
  return c.json(threadsWithStatus);
@@ -24441,9 +24951,11 @@ async function handleGetThreadMessages(c, threadManager, conversationManager) {
24441
24951
  if (!thread) {
24442
24952
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
24443
24953
  }
24444
- const history = thread.currentConversationId ? await conversationManager.getConversationHistoryByConversationId(
24954
+ const requestedConversationId = c.req.query("conversationId");
24955
+ const targetConversationId = typeof requestedConversationId === "string" && requestedConversationId.length > 0 ? requestedConversationId : thread.currentConversationId;
24956
+ const history = targetConversationId ? await conversationManager.getConversationHistoryByConversationId(
24445
24957
  threadId,
24446
- thread.currentConversationId
24958
+ targetConversationId
24447
24959
  ) : await conversationManager.getConversationHistory(threadId);
24448
24960
  if (!history || history.messages.length === 0) {
24449
24961
  return c.json([]);
@@ -25117,20 +25629,43 @@ async function handleFixComments(c, threadManager) {
25117
25629
 
25118
25630
  // src/features/threads/threads-ai-files.route.ts
25119
25631
  init_dist();
25120
- import { readFile as readFile16, writeFile as writeFile8, mkdir as mkdir9, access as access5, rm as rm7, stat as stat6 } from "fs/promises";
25121
- import { join as join33 } from "path";
25632
+ import { readFile as readFile17, writeFile as writeFile8, mkdir as mkdir9, access as access5, rm as rm8, stat as stat6 } from "fs/promises";
25633
+ import { join as join34 } from "path";
25122
25634
  import { existsSync as existsSync19 } from "fs";
25123
25635
 
25124
25636
  // src/features/git/git-download-folder.ts
25125
25637
  import { mkdir as mkdir8, writeFile as writeFile7 } from "fs/promises";
25126
25638
  import { join as join32, dirname as dirname6 } from "path";
25127
- async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25128
- const { token, ref, excludeRelativePrefixes = [] } = options;
25639
+
25640
+ // src/features/git/github-folder.ts
25641
+ function parseGithubRepoUrl(repoUrl) {
25129
25642
  const match = repoUrl.replace(/\.git$/, "").match(/github\.com\/([^/]+)\/([^/]+)/);
25130
25643
  if (!match) {
25131
25644
  throw new Error(`Invalid GitHub URL: ${repoUrl}`);
25132
25645
  }
25133
25646
  const [, owner, repo] = match;
25647
+ return { owner, repo };
25648
+ }
25649
+ function normalizeFolderPrefix(srcPath) {
25650
+ const trimmed = srcPath.replace(/^\/|\/$/g, "");
25651
+ return trimmed.length > 0 ? `${trimmed}/` : "";
25652
+ }
25653
+ function filterFolderBlobPaths(tree, srcPath, excludeRelativePrefixes = []) {
25654
+ const prefix = normalizeFolderPrefix(srcPath);
25655
+ return tree.filter((entry) => {
25656
+ if (entry.type !== "blob") {
25657
+ return false;
25658
+ }
25659
+ if (prefix.length > 0) {
25660
+ if (!entry.path.startsWith(prefix)) {
25661
+ return false;
25662
+ }
25663
+ }
25664
+ const relativePath = prefix.length > 0 ? entry.path.slice(prefix.length) : entry.path;
25665
+ return !excludeRelativePrefixes.some((skipPrefix) => relativePath.startsWith(skipPrefix));
25666
+ }).map((entry) => prefix.length > 0 ? entry.path.slice(prefix.length) : entry.path).sort((a, b) => a.localeCompare(b));
25667
+ }
25668
+ function buildGithubHeaders(token) {
25134
25669
  const headers = {
25135
25670
  Accept: "application/vnd.github+json",
25136
25671
  "X-GitHub-Api-Version": "2022-11-28"
@@ -25138,42 +25673,63 @@ async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25138
25673
  if (token) {
25139
25674
  headers["Authorization"] = `Bearer ${token}`;
25140
25675
  }
25141
- const refParam = ref ?? "HEAD";
25676
+ return headers;
25677
+ }
25678
+ async function fetchGithubTree(owner, repo, refParam, token) {
25142
25679
  const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${refParam}?recursive=1`;
25143
- const treeRes = await fetch(treeUrl, { headers });
25680
+ const treeRes = await fetch(treeUrl, { headers: buildGithubHeaders(token) });
25144
25681
  if (!treeRes.ok) {
25145
25682
  throw new Error(`GitHub API error ${treeRes.status}: ${await treeRes.text()}`);
25146
25683
  }
25147
- const tree = await treeRes.json();
25684
+ return await treeRes.json();
25685
+ }
25686
+ async function listGithubFolderFiles(repoUrl, srcPath, options = {}) {
25687
+ const { token, ref, excludeRelativePrefixes = [] } = options;
25688
+ const { owner, repo } = parseGithubRepoUrl(repoUrl);
25689
+ const refParam = ref ?? "HEAD";
25690
+ const tree = await fetchGithubTree(owner, repo, refParam, token);
25148
25691
  if (tree.truncated) {
25149
25692
  console.warn(
25150
25693
  "Warning: the repository tree was truncated by GitHub. Large repos may be incomplete."
25151
25694
  );
25152
25695
  }
25153
- const prefix = srcPath.replace(/^\/|\/$/g, "") + "/";
25154
- const files = tree.tree.filter((entry) => {
25155
- if (entry.type !== "blob" || !entry.path.startsWith(prefix)) {
25156
- return false;
25157
- }
25158
- const relativePath = entry.path.slice(prefix.length);
25159
- return !excludeRelativePrefixes.some((skipPrefix) => relativePath.startsWith(skipPrefix));
25160
- });
25696
+ const files = filterFolderBlobPaths(tree.tree, srcPath, excludeRelativePrefixes);
25161
25697
  if (files.length === 0) {
25162
25698
  throw new Error(`No files found at path "${srcPath}" in ${owner}/${repo}@${refParam}`);
25163
25699
  }
25700
+ return { files, ref: refParam };
25701
+ }
25702
+ async function fetchGithubFolderFile(repoUrl, srcPath, relativePath, options = {}) {
25703
+ const { token, ref } = options;
25704
+ const { owner, repo } = parseGithubRepoUrl(repoUrl);
25705
+ const refParam = ref ?? "HEAD";
25706
+ const folderPrefix = normalizeFolderPrefix(srcPath);
25707
+ const fullPath = folderPrefix.length > 0 ? `${folderPrefix}${relativePath}` : relativePath;
25708
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${refParam}/${fullPath}`;
25709
+ const fileRes = await fetch(rawUrl, { headers: buildGithubHeaders(token) });
25710
+ if (!fileRes.ok) {
25711
+ throw new Error(`Failed to download ${fullPath}: HTTP ${fileRes.status}`);
25712
+ }
25713
+ let content = Buffer.from(await fileRes.arrayBuffer()).toString("utf8");
25714
+ content = content.replace(/Claude/gi, "Tarsk");
25715
+ return content;
25716
+ }
25717
+
25718
+ // src/features/git/git-download-folder.ts
25719
+ async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25720
+ const { ref, excludeRelativePrefixes = [] } = options;
25721
+ const { files, ref: refParam } = await listGithubFolderFiles(repoUrl, srcPath, options);
25722
+ const { owner, repo } = parseGithubRepoUrl(repoUrl);
25164
25723
  console.log(`Downloading ${files.length} file(s) from ${owner}/${repo}/${srcPath} \u2192 ${destPath}`);
25165
25724
  await Promise.all(
25166
- files.map(async (file) => {
25167
- const relativePath = file.path.slice(prefix.length);
25725
+ files.map(async (relativePath) => {
25168
25726
  const localPath = join32(destPath, relativePath);
25169
25727
  await mkdir8(dirname6(localPath), { recursive: true });
25170
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${refParam}/${file.path}`;
25171
- const fileRes = await fetch(rawUrl, { headers });
25172
- if (!fileRes.ok) {
25173
- throw new Error(`Failed to download ${file.path}: HTTP ${fileRes.status}`);
25174
- }
25175
- let content = Buffer.from(await fileRes.arrayBuffer()).toString("utf8");
25176
- content = content.replace(/Claude/gi, "Tarsk");
25728
+ const content = await fetchGithubFolderFile(repoUrl, srcPath, relativePath, {
25729
+ ...options,
25730
+ ref: ref ?? refParam,
25731
+ excludeRelativePrefixes
25732
+ });
25177
25733
  await writeFile7(localPath, content, "utf8");
25178
25734
  console.log(` \u2713 ${relativePath}`);
25179
25735
  })
@@ -25181,46 +25737,473 @@ async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25181
25737
  console.log("Done.");
25182
25738
  }
25183
25739
 
25184
- // src/features/threads/threads-ai-files.route.ts
25185
- async function handleGetThreadAIFiles(c, threadManager) {
25186
- try {
25187
- const threadId = c.req.param("id");
25188
- if (!threadId) {
25189
- return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
25190
- }
25191
- if (!threadId) {
25192
- return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
25193
- }
25194
- const thread = await threadManager.getThread(threadId);
25195
- if (!thread) {
25196
- return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25197
- }
25198
- const nodes = await buildAIFileTree(thread.path);
25199
- return c.json({ nodes });
25200
- } catch (error) {
25201
- return errorResponse(
25202
- c,
25203
- ErrorCodes.INTERNAL_ERROR,
25204
- "Failed to list AI files",
25205
- 500,
25206
- error instanceof Error ? error.message : String(error)
25740
+ // src/features/skills/skills.catalog-cache.ts
25741
+ import { cp, mkdtemp, readdir as readdir12, readFile as readFile16, rm as rm7 } from "fs/promises";
25742
+ import { join as join33, relative as relative8, sep } from "path";
25743
+ import { tmpdir as tmpdir3 } from "os";
25744
+
25745
+ // src/features/git/git-clone-folder.ts
25746
+ init_utils();
25747
+ function normalizeGitCloneUrl(repoUrl) {
25748
+ const trimmed = repoUrl.trim().replace(/\/$/, "");
25749
+ return trimmed.endsWith(".git") ? trimmed : `${trimmed}.git`;
25750
+ }
25751
+ function runGit2(cwd, args2) {
25752
+ const result = spawnSyncProcess("git", args2, {
25753
+ cwd,
25754
+ encoding: "utf-8",
25755
+ stdio: ["ignore", "pipe", "pipe"]
25756
+ });
25757
+ return {
25758
+ ok: result.status === 0,
25759
+ stderr: (typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "").trim()
25760
+ };
25761
+ }
25762
+ async function cloneGithubRepoFolder(destPath, repoUrl, folderPath, options = {}) {
25763
+ const ref = options.ref?.trim();
25764
+ const cloneUrl = normalizeGitCloneUrl(repoUrl);
25765
+ const trimmedFolderPath = folderPath.replace(/^\/|\/$/g, "");
25766
+ const useSparseCheckout = trimmedFolderPath.length > 0;
25767
+ const cloneArgs = [
25768
+ "clone",
25769
+ "--depth",
25770
+ "1",
25771
+ ...ref ? ["--branch", ref, "--single-branch"] : [],
25772
+ ...useSparseCheckout ? ["--filter=blob:none", "--sparse"] : [],
25773
+ cloneUrl,
25774
+ destPath
25775
+ ];
25776
+ const cloneResult = runGit2(process.cwd(), cloneArgs);
25777
+ if (!cloneResult.ok) {
25778
+ const refLabel = ref ?? "default branch";
25779
+ throw new Error(`Failed to clone ${repoUrl}@${refLabel}: ${cloneResult.stderr}`);
25780
+ }
25781
+ if (!useSparseCheckout) {
25782
+ return;
25783
+ }
25784
+ const sparseResult = runGit2(destPath, ["sparse-checkout", "set", trimmedFolderPath]);
25785
+ if (!sparseResult.ok) {
25786
+ throw new Error(
25787
+ `Failed to sparse-checkout "${trimmedFolderPath}" from ${repoUrl}: ${sparseResult.stderr}`
25207
25788
  );
25208
25789
  }
25209
25790
  }
25210
- async function handleGetThreadAIFile(c, threadManager) {
25211
- try {
25212
- const threadId = c.req.param("id");
25213
- if (!threadId) {
25214
- return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
25215
- }
25216
- const filePath = c.req.query("path");
25217
- if (!filePath) {
25218
- return c.json(
25219
- { error: { code: "BAD_REQUEST", message: "path query parameter is required" } },
25220
- 400
25221
- );
25222
- }
25223
- const thread = await threadManager.getThread(threadId);
25791
+
25792
+ // src/features/git/github-tree-url.ts
25793
+ function parseGithubTreeUrl(url) {
25794
+ const trimmed = url.trim().replace(/\/$/, "");
25795
+ const treeMatch = trimmed.match(
25796
+ /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/(.*))?$/
25797
+ );
25798
+ if (treeMatch) {
25799
+ const [, owner, repoWithSuffix, ref, folderPath = ""] = treeMatch;
25800
+ const repo = repoWithSuffix.replace(/\.git$/, "");
25801
+ return {
25802
+ owner,
25803
+ repo,
25804
+ repoUrl: `https://github.com/${owner}/${repo}`,
25805
+ ref,
25806
+ folderPath: folderPath.replace(/^\/|\/$/g, "")
25807
+ };
25808
+ }
25809
+ const repoMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
25810
+ if (repoMatch) {
25811
+ const [, owner, repoWithSuffix] = repoMatch;
25812
+ const repo = repoWithSuffix.replace(/\.git$/, "");
25813
+ return {
25814
+ owner,
25815
+ repo,
25816
+ repoUrl: `https://github.com/${owner}/${repo}`,
25817
+ ref: "",
25818
+ folderPath: ""
25819
+ };
25820
+ }
25821
+ throw new Error(`Invalid GitHub tree URL: ${url}`);
25822
+ }
25823
+
25824
+ // src/features/skills/skills.catalog-cache.ts
25825
+ init_dist();
25826
+
25827
+ // src/features/skills/skills.custom-sources.ts
25828
+ init_dist();
25829
+ init_database();
25830
+ init_database_state();
25831
+ var STATE_KEY = "customSkillSources";
25832
+ function isValidSkillSource(value) {
25833
+ return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.title === "string" && typeof value.url === "string";
25834
+ }
25835
+ function parseCustomSkillSources(raw) {
25836
+ if (!Array.isArray(raw)) {
25837
+ return [];
25838
+ }
25839
+ return raw.filter(isValidSkillSource).filter((source) => isCustomSkillSourceId(source.id)).sort((a, b) => a.title.localeCompare(b.title));
25840
+ }
25841
+ async function getCustomSkillSources() {
25842
+ const db = await getDatabase();
25843
+ const raw = await getState(db, STATE_KEY);
25844
+ return parseCustomSkillSources(raw);
25845
+ }
25846
+ async function getCustomSkillSourceById(sourceId) {
25847
+ const sources = await getCustomSkillSources();
25848
+ return sources.find((source) => source.id === sourceId);
25849
+ }
25850
+ async function saveCustomSkillSource(source) {
25851
+ if (!isCustomSkillSourceId(source.id)) {
25852
+ throw new Error("Custom skill sources must use the custom- id prefix");
25853
+ }
25854
+ const db = await getDatabase();
25855
+ const existing = await getCustomSkillSources();
25856
+ const next = [...existing.filter((entry) => entry.id !== source.id), source].sort(
25857
+ (a, b) => a.title.localeCompare(b.title)
25858
+ );
25859
+ await setState(db, STATE_KEY, next);
25860
+ return source;
25861
+ }
25862
+ async function importCustomSkillSources(sources) {
25863
+ const valid = sources.filter(isValidSkillSource).filter((source) => isCustomSkillSourceId(source.id));
25864
+ if (valid.length === 0) {
25865
+ return getCustomSkillSources();
25866
+ }
25867
+ const db = await getDatabase();
25868
+ const existing = await getCustomSkillSources();
25869
+ const byId = new Map(existing.map((source) => [source.id, source]));
25870
+ for (const source of valid) {
25871
+ byId.set(source.id, source);
25872
+ }
25873
+ const next = [...byId.values()].sort((a, b) => a.title.localeCompare(b.title));
25874
+ await setState(db, STATE_KEY, next);
25875
+ return next;
25876
+ }
25877
+ async function deleteCustomSkillSource(sourceId) {
25878
+ if (!isCustomSkillSourceId(sourceId)) {
25879
+ throw new Error("Only custom skill sources can be deleted");
25880
+ }
25881
+ const existing = await getCustomSkillSourceById(sourceId);
25882
+ if (!existing) {
25883
+ throw new Error("Unknown custom skill source");
25884
+ }
25885
+ const db = await getDatabase();
25886
+ const sources = await getCustomSkillSources();
25887
+ const next = sources.filter((entry) => entry.id !== sourceId);
25888
+ await setState(db, STATE_KEY, next);
25889
+ await clearCatalogClone(sourceId);
25890
+ }
25891
+
25892
+ // src/features/skills/skills.sources.json
25893
+ var skills_sources_default = [
25894
+ {
25895
+ id: "openai-curated",
25896
+ title: "OpenAI Curated",
25897
+ url: "https://github.com/openai/skills/tree/main/skills/.curated"
25898
+ },
25899
+ {
25900
+ id: "anthropic",
25901
+ title: "Anthropic",
25902
+ url: "https://github.com/anthropics/skills/tree/main/skills"
25903
+ },
25904
+ {
25905
+ id: "matt-pocock-engineering",
25906
+ title: "Engineering - Matt Pocock",
25907
+ url: "https://github.com/mattpocock/skills/tree/main/skills/engineering"
25908
+ },
25909
+ {
25910
+ id: "matt-pocock-productivity",
25911
+ title: "Productivity -Matt Pocock",
25912
+ url: "https://github.com/mattpocock/skills/tree/main/skills/productivity"
25913
+ },
25914
+ {
25915
+ id: "cloudflare",
25916
+ title: "Cloudflare",
25917
+ url: "https://github.com/cloudflare/skills/tree/main/skills"
25918
+ },
25919
+ {
25920
+ id: "vercel",
25921
+ title: "Vercel",
25922
+ url: "https://github.com/vercel-labs/agent-skills/tree/main/skills"
25923
+ },
25924
+ {
25925
+ id: "microsoft",
25926
+ title: "Microsoft",
25927
+ url: "https://github.com/MicrosoftDocs/Agent-Skills/tree/main/skills"
25928
+ },
25929
+ {
25930
+ id: "sentry",
25931
+ title: "Sentry",
25932
+ url: "https://github.com/getsentry/skills/tree/main/skills"
25933
+ },
25934
+ {
25935
+ id: "firebase",
25936
+ title: "Firebase",
25937
+ url: "https://github.com/firebase/skills/tree/main/skills"
25938
+ },
25939
+ {
25940
+ id: "flutter",
25941
+ title: "Flutter",
25942
+ url: "https://github.com/flutter/skills/tree/main/skills"
25943
+ },
25944
+ {
25945
+ id: "angular",
25946
+ title: "Angular",
25947
+ url: "https://github.com/angular/skills/tree/main"
25948
+ },
25949
+ {
25950
+ id: "firecrawl",
25951
+ title: "Firecrawl",
25952
+ url: "https://github.com/firecrawl/skills/tree/main/skills"
25953
+ },
25954
+ {
25955
+ id: "redis",
25956
+ title: "Redis",
25957
+ url: "https://github.com/redis/agent-skills/tree/main/skills"
25958
+ },
25959
+ {
25960
+ id: "corey-haines-marketing",
25961
+ title: "Marketing - Corey Haines",
25962
+ url: "https://github.com/coreyhaines31/marketingskills/tree/main/skills"
25963
+ },
25964
+ {
25965
+ id: "dean-peters-product-management",
25966
+ title: "Product Management - Dean Peters",
25967
+ url: "https://github.com/deanpeters/Product-Manager-Skills/tree/main/skills"
25968
+ },
25969
+ {
25970
+ id: "venice-ai",
25971
+ title: "AI - Venice",
25972
+ url: "https://github.com/veniceai/skills/tree/main/skills"
25973
+ },
25974
+ {
25975
+ id: "superpowers",
25976
+ title: "Development - Superpowers",
25977
+ url: "https://github.com/obra/superpowers/tree/main/skills"
25978
+ },
25979
+ {
25980
+ id: "cypress",
25981
+ title: "Testing - Cypress",
25982
+ url: "https://github.com/cypress-io/ai-toolkit/tree/main/skills"
25983
+ },
25984
+ {
25985
+ id: "resend",
25986
+ title: "Email - Resend",
25987
+ url: "https://github.com/resend/resend-skills/tree/main/skills"
25988
+ },
25989
+ {
25990
+ id: "n8n",
25991
+ title: "Automation - n8n",
25992
+ url: "https://github.com/czlonkowski/n8n-skills/tree/main/skills"
25993
+ }
25994
+ ];
25995
+
25996
+ // src/features/skills/skills.catalog-cache.ts
25997
+ var catalogCloneCache = /* @__PURE__ */ new Map();
25998
+ var inFlightRefreshes = /* @__PURE__ */ new Map();
25999
+ function getBundledSkillSourceById(sourceId) {
26000
+ return skills_sources_default.find((source) => source.id === sourceId);
26001
+ }
26002
+ async function resolveSkillSource(sourceId) {
26003
+ const bundled = getBundledSkillSourceById(sourceId);
26004
+ if (bundled) {
26005
+ return bundled;
26006
+ }
26007
+ const custom = await getCustomSkillSourceById(sourceId);
26008
+ if (custom) {
26009
+ return custom;
26010
+ }
26011
+ throw new Error(`Unknown skill source: ${sourceId}`);
26012
+ }
26013
+ function normalizeSkillContent(content) {
26014
+ return content.replace(/Claude/gi, "Tarsk");
26015
+ }
26016
+ async function listFilesRecursive(dir, rootDir = dir) {
26017
+ const entries = await readdir12(dir, { withFileTypes: true });
26018
+ const files = [];
26019
+ for (const entry of entries) {
26020
+ const fullPath = join33(dir, entry.name);
26021
+ if (entry.isDirectory()) {
26022
+ files.push(...await listFilesRecursive(fullPath, rootDir));
26023
+ continue;
26024
+ }
26025
+ if (entry.isFile()) {
26026
+ files.push(relative8(rootDir, fullPath));
26027
+ }
26028
+ }
26029
+ return files.sort((a, b) => a.localeCompare(b));
26030
+ }
26031
+ function resolveSkillRelativePath(skillDir, relativePath) {
26032
+ const resolved = join33(skillDir, relativePath);
26033
+ const normalizedSkillDir = skillDir.endsWith(sep) ? skillDir : `${skillDir}${sep}`;
26034
+ if (resolved === skillDir || resolved.startsWith(normalizedSkillDir)) {
26035
+ return resolved;
26036
+ }
26037
+ return null;
26038
+ }
26039
+ async function ensureCatalogClone(sourceId) {
26040
+ const cached = catalogCloneCache.get(sourceId);
26041
+ if (cached) {
26042
+ return cached;
26043
+ }
26044
+ return refreshCatalogClone(sourceId);
26045
+ }
26046
+ async function getSkillDirEnsured(sourceId, skillId) {
26047
+ const entry = await ensureCatalogClone(sourceId);
26048
+ return join33(entry.scanDir, skillId);
26049
+ }
26050
+ async function cloneSourceToCache(sourceId, trustedUrl) {
26051
+ let url;
26052
+ if (trustedUrl !== void 0) {
26053
+ if (!isCustomSkillSourceId(sourceId)) {
26054
+ throw new Error(`Cannot override URL for skill source: ${sourceId}`);
26055
+ }
26056
+ url = trustedUrl;
26057
+ } else {
26058
+ url = (await resolveSkillSource(sourceId)).url;
26059
+ }
26060
+ const parsed = parseGithubTreeUrl(url);
26061
+ const cloneDir = await mkdtemp(join33(tmpdir3(), "tarsk-skills-catalog-clone-"));
26062
+ await cloneGithubRepoFolder(cloneDir, parsed.repoUrl, parsed.folderPath, {
26063
+ ref: parsed.ref
26064
+ });
26065
+ const scanDir = parsed.folderPath ? join33(cloneDir, parsed.folderPath) : cloneDir;
26066
+ const entry = {
26067
+ sourceId,
26068
+ cloneDir,
26069
+ scanDir,
26070
+ parsed,
26071
+ fetchedAt: Date.now()
26072
+ };
26073
+ const previous = catalogCloneCache.get(sourceId);
26074
+ catalogCloneCache.set(sourceId, entry);
26075
+ if (previous) {
26076
+ await rm7(previous.cloneDir, { recursive: true, force: true }).catch(() => {
26077
+ });
26078
+ }
26079
+ return entry;
26080
+ }
26081
+ async function refreshCatalogClone(sourceId, trustedUrl) {
26082
+ const existing = inFlightRefreshes.get(sourceId);
26083
+ if (existing) {
26084
+ return existing;
26085
+ }
26086
+ const refreshPromise = cloneSourceToCache(sourceId, trustedUrl).finally(() => {
26087
+ inFlightRefreshes.delete(sourceId);
26088
+ });
26089
+ inFlightRefreshes.set(sourceId, refreshPromise);
26090
+ return refreshPromise;
26091
+ }
26092
+ function normalizeGithubRepoUrl(repoUrl) {
26093
+ return repoUrl.trim().replace(/\/$/, "").replace(/\.git$/, "");
26094
+ }
26095
+ function resolveCatalogSkillFromGithubFolder(gitHubRepo, gitHubFolder) {
26096
+ const normalizedRepo = normalizeGithubRepoUrl(gitHubRepo);
26097
+ const normalizedFolder = gitHubFolder.replace(/^\/|\/$/g, "");
26098
+ for (const source of skills_sources_default) {
26099
+ const parsed = parseGithubTreeUrl(source.url);
26100
+ if (normalizeGithubRepoUrl(parsed.repoUrl) !== normalizedRepo) {
26101
+ continue;
26102
+ }
26103
+ const basePath = parsed.folderPath.replace(/^\/|\/$/g, "");
26104
+ if (basePath.length > 0) {
26105
+ const prefix = `${basePath}/`;
26106
+ if (!normalizedFolder.startsWith(prefix)) {
26107
+ continue;
26108
+ }
26109
+ const skillId2 = normalizedFolder.slice(prefix.length);
26110
+ if (!skillId2 || skillId2.includes("/")) {
26111
+ continue;
26112
+ }
26113
+ return { sourceId: source.id, skillId: skillId2, ref: parsed.ref };
26114
+ }
26115
+ const skillId = normalizedFolder.split("/")[0];
26116
+ if (!skillId) {
26117
+ continue;
26118
+ }
26119
+ return { sourceId: source.id, skillId, ref: parsed.ref };
26120
+ }
26121
+ return null;
26122
+ }
26123
+ async function resolveCatalogSkillFromGithubFolderEnsured(gitHubRepo, gitHubFolder) {
26124
+ const resolved = resolveCatalogSkillFromGithubFolder(gitHubRepo, gitHubFolder);
26125
+ if (!resolved) {
26126
+ throw new Error(`No catalog source found for ${gitHubRepo}/${gitHubFolder}`);
26127
+ }
26128
+ await ensureCatalogClone(resolved.sourceId);
26129
+ return resolved;
26130
+ }
26131
+ async function listCachedCatalogSkillFilesByGithubFolder(gitHubRepo, gitHubFolder) {
26132
+ const resolved = await resolveCatalogSkillFromGithubFolderEnsured(gitHubRepo, gitHubFolder);
26133
+ const files = await listCachedCatalogSkillFiles(resolved.sourceId, resolved.skillId);
26134
+ return { files, ref: resolved.ref };
26135
+ }
26136
+ async function readCachedCatalogSkillFileByGithubFolder(gitHubRepo, gitHubFolder, relativePath) {
26137
+ const resolved = await resolveCatalogSkillFromGithubFolderEnsured(gitHubRepo, gitHubFolder);
26138
+ return readCachedCatalogSkillFile(resolved.sourceId, resolved.skillId, relativePath);
26139
+ }
26140
+ async function listCachedCatalogSkillFiles(sourceId, skillId) {
26141
+ const skillDir = await getSkillDirEnsured(sourceId, skillId);
26142
+ return listFilesRecursive(skillDir);
26143
+ }
26144
+ async function readCachedCatalogSkillFile(sourceId, skillId, relativePath) {
26145
+ const skillDir = await getSkillDirEnsured(sourceId, skillId);
26146
+ const absPath = resolveSkillRelativePath(skillDir, relativePath);
26147
+ if (!absPath) {
26148
+ throw new Error(`Invalid file path: ${relativePath}`);
26149
+ }
26150
+ const content = await readFile16(absPath, "utf8");
26151
+ return normalizeSkillContent(content);
26152
+ }
26153
+ async function copyCachedCatalogSkillToDirectory(sourceId, skillId, destPath) {
26154
+ const skillDir = await getSkillDirEnsured(sourceId, skillId);
26155
+ await cp(skillDir, destPath, { recursive: true });
26156
+ }
26157
+ async function clearCatalogClone(sourceId) {
26158
+ const entry = catalogCloneCache.get(sourceId);
26159
+ if (!entry) {
26160
+ return;
26161
+ }
26162
+ catalogCloneCache.delete(sourceId);
26163
+ await rm7(entry.cloneDir, { recursive: true, force: true }).catch(() => {
26164
+ });
26165
+ }
26166
+
26167
+ // src/features/threads/threads-ai-files.route.ts
26168
+ async function handleGetThreadAIFiles(c, threadManager) {
26169
+ try {
26170
+ const threadId = c.req.param("id");
26171
+ if (!threadId) {
26172
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
26173
+ }
26174
+ if (!threadId) {
26175
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
26176
+ }
26177
+ const thread = await threadManager.getThread(threadId);
26178
+ if (!thread) {
26179
+ return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
26180
+ }
26181
+ const nodes = await buildAIFileTree(thread.path);
26182
+ return c.json({ nodes });
26183
+ } catch (error) {
26184
+ return errorResponse(
26185
+ c,
26186
+ ErrorCodes.INTERNAL_ERROR,
26187
+ "Failed to list AI files",
26188
+ 500,
26189
+ error instanceof Error ? error.message : String(error)
26190
+ );
26191
+ }
26192
+ }
26193
+ async function handleGetThreadAIFile(c, threadManager) {
26194
+ try {
26195
+ const threadId = c.req.param("id");
26196
+ if (!threadId) {
26197
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
26198
+ }
26199
+ const filePath = c.req.query("path");
26200
+ if (!filePath) {
26201
+ return c.json(
26202
+ { error: { code: "BAD_REQUEST", message: "path query parameter is required" } },
26203
+ 400
26204
+ );
26205
+ }
26206
+ const thread = await threadManager.getThread(threadId);
25224
26207
  if (!thread) {
25225
26208
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25226
26209
  }
@@ -25283,7 +26266,7 @@ Links to important documentation, tools, or references.
25283
26266
  400
25284
26267
  );
25285
26268
  }
25286
- const content = await readFile16(absPath, "utf-8");
26269
+ const content = await readFile17(absPath, "utf-8");
25287
26270
  return c.json({ content, path: filePath });
25288
26271
  } catch (error) {
25289
26272
  return errorResponse(
@@ -25322,7 +26305,7 @@ async function handleSaveThreadAIFile(c, threadManager) {
25322
26305
  if (!absPath) {
25323
26306
  return c.json({ error: { code: "BAD_REQUEST", message: "Invalid file path" } }, 400);
25324
26307
  }
25325
- const parentDir = join33(absPath, "..");
26308
+ const parentDir = join34(absPath, "..");
25326
26309
  await mkdir9(parentDir, { recursive: true });
25327
26310
  await writeFile8(absPath, content, "utf-8");
25328
26311
  return c.json({ success: true, path: filePath });
@@ -25365,7 +26348,7 @@ async function handleDeleteThreadAIFile(c, threadManager) {
25365
26348
  404
25366
26349
  );
25367
26350
  }
25368
- await rm7(absPath, { recursive: true, force: true });
26351
+ await rm8(absPath, { recursive: true, force: true });
25369
26352
  return c.json({ success: true, path: filePath });
25370
26353
  } catch (error) {
25371
26354
  return errorResponse(
@@ -25411,10 +26394,10 @@ async function handleCreateThreadAgent(c, threadManager) {
25411
26394
  if (!thread) {
25412
26395
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25413
26396
  }
25414
- const agentRelPath = join33(".agents", "agents", name);
25415
- const agentAbsPath = join33(thread.path, agentRelPath);
25416
- const agentFileRelPath = join33(agentRelPath, "AGENT.md");
25417
- const agentFileAbsPath = join33(agentAbsPath, "AGENT.md");
26397
+ const agentRelPath = join34(".agents", "agents", name);
26398
+ const agentAbsPath = join34(thread.path, agentRelPath);
26399
+ const agentFileRelPath = join34(agentRelPath, "AGENT.md");
26400
+ const agentFileAbsPath = join34(agentAbsPath, "AGENT.md");
25418
26401
  if (existsSync19(agentAbsPath)) {
25419
26402
  return c.json(
25420
26403
  { error: { code: "CONFLICT", message: `Agent '${name}' already exists` } },
@@ -25481,10 +26464,10 @@ async function handleCreateThreadSkill(c, threadManager) {
25481
26464
  if (!thread) {
25482
26465
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25483
26466
  }
25484
- const skillRelPath = join33(".agents", "skills", name);
25485
- const skillAbsPath = join33(thread.path, skillRelPath);
25486
- const skillFileRelPath = join33(skillRelPath, "SKILL.md");
25487
- const skillFileAbsPath = join33(skillAbsPath, "SKILL.md");
26467
+ const skillRelPath = join34(".agents", "skills", name);
26468
+ const skillAbsPath = join34(thread.path, skillRelPath);
26469
+ const skillFileRelPath = join34(skillRelPath, "SKILL.md");
26470
+ const skillFileAbsPath = join34(skillAbsPath, "SKILL.md");
25488
26471
  if (existsSync19(skillAbsPath)) {
25489
26472
  return c.json(
25490
26473
  { error: { code: "CONFLICT", message: `Skill '${name}' already exists` } },
@@ -25527,7 +26510,7 @@ async function handleInstallGithubFolder(c, threadManager) {
25527
26510
  } catch {
25528
26511
  return c.json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, 400);
25529
26512
  }
25530
- const { gitHubRepo, gitHubFolder, destPath } = body;
26513
+ const { gitHubRepo, gitHubFolder, destPath, gitHubRef } = body;
25531
26514
  if (!gitHubRepo || !gitHubFolder || !destPath) {
25532
26515
  return c.json(
25533
26516
  {
@@ -25547,7 +26530,9 @@ async function handleInstallGithubFolder(c, threadManager) {
25547
26530
  if (!absDestPath) {
25548
26531
  return c.json({ error: { code: "BAD_REQUEST", message: "Invalid destPath" } }, 400);
25549
26532
  }
25550
- await downloadGithubFolder(gitHubRepo, gitHubFolder, absDestPath);
26533
+ await downloadGithubFolder(gitHubRepo, gitHubFolder, absDestPath, {
26534
+ ref: gitHubRef
26535
+ });
25551
26536
  return c.json({ success: true, path: destPath });
25552
26537
  } catch (error) {
25553
26538
  return errorResponse(
@@ -25559,6 +26544,50 @@ async function handleInstallGithubFolder(c, threadManager) {
25559
26544
  );
25560
26545
  }
25561
26546
  }
26547
+ async function handleInstallCatalogSkill(c, threadManager) {
26548
+ try {
26549
+ const threadId = c.req.param("id");
26550
+ if (!threadId) {
26551
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Thread ID is required", 400);
26552
+ }
26553
+ let body;
26554
+ try {
26555
+ body = await c.req.json();
26556
+ } catch {
26557
+ return c.json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, 400);
26558
+ }
26559
+ const { sourceId, skillId, destPath } = body;
26560
+ if (!sourceId || !skillId || !destPath) {
26561
+ return c.json(
26562
+ {
26563
+ error: {
26564
+ code: "BAD_REQUEST",
26565
+ message: "sourceId, skillId, and destPath are required"
26566
+ }
26567
+ },
26568
+ 400
26569
+ );
26570
+ }
26571
+ const thread = await threadManager.getThread(threadId);
26572
+ if (!thread) {
26573
+ return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
26574
+ }
26575
+ const absDestPath = validateFilePath(thread.path, destPath);
26576
+ if (!absDestPath) {
26577
+ return c.json({ error: { code: "BAD_REQUEST", message: "Invalid destPath" } }, 400);
26578
+ }
26579
+ await copyCachedCatalogSkillToDirectory(sourceId, skillId, absDestPath);
26580
+ return c.json({ success: true, path: destPath });
26581
+ } catch (error) {
26582
+ return errorResponse(
26583
+ c,
26584
+ ErrorCodes.INTERNAL_ERROR,
26585
+ "Failed to install catalog skill",
26586
+ 500,
26587
+ error instanceof Error ? error.message : String(error)
26588
+ );
26589
+ }
26590
+ }
25562
26591
 
25563
26592
  // src/features/threads/threads-project-scripts.route.ts
25564
26593
  init_database();
@@ -25568,17 +26597,22 @@ init_utils();
25568
26597
  import { spawnSync as spawnSync2 } from "child_process";
25569
26598
  var ScriptProcessManager = class {
25570
26599
  processes = /* @__PURE__ */ new Map();
26600
+ threadIds = /* @__PURE__ */ new Map();
25571
26601
  /**
25572
26602
  * Run a script and stream its output
25573
26603
  * @param key - Unique key for this script (e.g. "projectId:scriptName")
25574
26604
  * @param command - The shell command to run
25575
26605
  * @param cwd - Working directory for the command
26606
+ * @param threadId - Optional thread ID for dev server URL caching
25576
26607
  * @yields Output chunks from stdout/stderr
25577
26608
  */
25578
- async *runScript(key, command, cwd) {
26609
+ async *runScript(key, command, cwd, threadId) {
25579
26610
  if (this.processes.has(key)) {
25580
26611
  this.stopScript(key);
25581
26612
  }
26613
+ if (threadId) {
26614
+ this.threadIds.set(key, threadId);
26615
+ }
25582
26616
  const { shell, args: shellArgs } = getShellConfig();
25583
26617
  const child = spawnProcess(shell, [...shellArgs, command], {
25584
26618
  cwd,
@@ -25594,16 +26628,39 @@ var ScriptProcessManager = class {
25594
26628
  const decoder = new TextDecoder();
25595
26629
  const outputBuffer = [];
25596
26630
  let isDone = false;
26631
+ let urlDetected = false;
26632
+ let urlBuffer = "";
26633
+ function appendOutput(text, isError) {
26634
+ urlBuffer += text;
26635
+ if (!urlDetected) {
26636
+ const detectedUrl = detectDevServerUrl(urlBuffer);
26637
+ if (detectedUrl) {
26638
+ urlDetected = true;
26639
+ if (threadId) {
26640
+ devServerCache.setUrl(threadId, detectedUrl);
26641
+ }
26642
+ outputBuffer.push({ type: "url", url: detectedUrl });
26643
+ }
26644
+ }
26645
+ if (isError) {
26646
+ outputBuffer.push({ type: "error", message: text });
26647
+ } else {
26648
+ outputBuffer.push(text);
26649
+ }
26650
+ }
25597
26651
  child.stdout?.on("data", (data) => {
25598
- outputBuffer.push(decoder.decode(data));
26652
+ appendOutput(decoder.decode(data), false);
25599
26653
  });
25600
26654
  child.stderr?.on("data", (data) => {
25601
- outputBuffer.push({ type: "error", message: decoder.decode(data) });
26655
+ appendOutput(decoder.decode(data), true);
25602
26656
  });
25603
26657
  child.on("close", (code) => {
25604
26658
  if (code !== 0 && code !== null) {
25605
26659
  outputBuffer.push({ type: "error", message: `Process exited with code ${code}` });
25606
26660
  }
26661
+ if (threadId && urlDetected) {
26662
+ devServerCache.clearUrl(threadId);
26663
+ }
25607
26664
  isDone = true;
25608
26665
  });
25609
26666
  child.on("error", (err) => {
@@ -25620,6 +26677,7 @@ var ScriptProcessManager = class {
25620
26677
  }
25621
26678
  } finally {
25622
26679
  this.processes.delete(key);
26680
+ this.threadIds.delete(key);
25623
26681
  }
25624
26682
  }
25625
26683
  /**
@@ -25630,8 +26688,13 @@ var ScriptProcessManager = class {
25630
26688
  stopScript(key) {
25631
26689
  const entry = this.processes.get(key);
25632
26690
  if (!entry) return false;
26691
+ const threadId = this.threadIds.get(key);
25633
26692
  killProcessTree2(entry.pid, entry.process);
25634
26693
  this.processes.delete(key);
26694
+ this.threadIds.delete(key);
26695
+ if (threadId) {
26696
+ devServerCache.clearUrl(threadId);
26697
+ }
25635
26698
  return true;
25636
26699
  }
25637
26700
  /**
@@ -25706,7 +26769,10 @@ async function handleRunProjectScript(c, threadManager) {
25706
26769
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25707
26770
  }
25708
26771
  const key = `${thread.projectId}:${scriptName}`;
25709
- return streamAsyncGenerator(c, scriptProcessManager.runScript(key, command, thread.path));
26772
+ return streamAsyncGenerator(
26773
+ c,
26774
+ scriptProcessManager.runScript(key, command, thread.path, threadId)
26775
+ );
25710
26776
  } catch (error) {
25711
26777
  return errorResponse(
25712
26778
  c,
@@ -25748,10 +26814,10 @@ async function handleStopProjectScript(c, threadManager) {
25748
26814
  }
25749
26815
 
25750
26816
  // src/features/threads/threads.routes.ts
25751
- function createThreadRoutes(threadManager, gitManager, conversationManager) {
26817
+ function createThreadRoutes(threadManager, gitManager2, conversationManager) {
25752
26818
  const router = new Hono12();
25753
26819
  router.post("/", async (c) => {
25754
- return handleCreateThread(c, threadManager, gitManager);
26820
+ return handleCreateThread(c, threadManager, gitManager2);
25755
26821
  });
25756
26822
  router.get("/", async (c) => {
25757
26823
  const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_database(), database_exports));
@@ -25836,6 +26902,9 @@ function createThreadRoutes(threadManager, gitManager, conversationManager) {
25836
26902
  router.post("/:id/ai-files/github-folder", async (c) => {
25837
26903
  return handleInstallGithubFolder(c, threadManager);
25838
26904
  });
26905
+ router.post("/:id/ai-files/catalog-skill", async (c) => {
26906
+ return handleInstallCatalogSkill(c, threadManager);
26907
+ });
25839
26908
  return router;
25840
26909
  }
25841
26910
 
@@ -26476,9 +27545,63 @@ async function gitGenerateCommitMessageHandler(c, metadataManager) {
26476
27545
  }
26477
27546
 
26478
27547
  // src/features/git/git-commit.route.ts
27548
+ var gitManager = new GitManagerImpl();
27549
+ async function getUniqueBranchName(baseBranchName, gitRoot, existingBranchNames) {
27550
+ async function isBranchNameTaken(branchName) {
27551
+ if (existingBranchNames.has(branchName)) {
27552
+ return true;
27553
+ }
27554
+ return checkBranchExists(gitRoot, branchName);
27555
+ }
27556
+ if (!await isBranchNameTaken(baseBranchName)) {
27557
+ return baseBranchName;
27558
+ }
27559
+ let counter = 2;
27560
+ while (await isBranchNameTaken(`${baseBranchName}-${counter}`)) {
27561
+ counter++;
27562
+ }
27563
+ return `${baseBranchName}-${counter}`;
27564
+ }
27565
+ async function maybeRenameInitialTarskBranch(gitRoot, threadId, commitMessage, metadataManager) {
27566
+ const [commitCountBeforeCommit, currentBranch, threads] = await Promise.all([
27567
+ getCommitCount(gitRoot),
27568
+ getCurrentBranch(gitRoot),
27569
+ metadataManager.loadThreads()
27570
+ ]);
27571
+ if (commitCountBeforeCommit !== 0) {
27572
+ return;
27573
+ }
27574
+ const thread = threads.find((candidate) => candidate.id === threadId);
27575
+ if (!thread) {
27576
+ return;
27577
+ }
27578
+ if (currentBranch !== thread.currentBranch) {
27579
+ return;
27580
+ }
27581
+ const expectedTarskBranchName = gitManager.sanitizeBranchName(thread.title);
27582
+ if (currentBranch !== expectedTarskBranchName) {
27583
+ return;
27584
+ }
27585
+ const requestedBranchName = gitManager.sanitizeBranchName(commitMessage);
27586
+ if (!requestedBranchName || requestedBranchName === currentBranch) {
27587
+ return;
27588
+ }
27589
+ const existingBranchNames = new Set(threads.map((candidate) => candidate.currentBranch));
27590
+ existingBranchNames.delete(currentBranch);
27591
+ const newBranchName = await getUniqueBranchName(
27592
+ requestedBranchName,
27593
+ gitRoot,
27594
+ existingBranchNames
27595
+ );
27596
+ await renameCurrentBranch(gitRoot, newBranchName);
27597
+ await metadataManager.updateThreadFields(threadId, { currentBranch: newBranchName });
27598
+ }
26479
27599
  async function gitCommitHandler(c, metadataManager) {
26480
27600
  try {
26481
27601
  const threadId = c.req.param("threadId");
27602
+ if (!threadId) {
27603
+ return c.json({ error: "Thread ID is required" }, 400);
27604
+ }
26482
27605
  const body = await c.req.json();
26483
27606
  const { message } = body;
26484
27607
  if (!message) {
@@ -26500,6 +27623,7 @@ async function gitCommitHandler(c, metadataManager) {
26500
27623
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
26501
27624
  }
26502
27625
  await stageAllChanges(gitRoot);
27626
+ await maybeRenameInitialTarskBranch(gitRoot, threadId, message, metadataManager);
26503
27627
  await commitChanges(gitRoot, message);
26504
27628
  return c.json({ success: true });
26505
27629
  } catch (error) {
@@ -26803,6 +27927,9 @@ async function gitLogHandler(c, metadataManager) {
26803
27927
  }
26804
27928
 
26805
27929
  // src/features/git/git-create-pr.route.ts
27930
+ function normalizeThreadTitle(title) {
27931
+ return (title ?? "").trim().replace(/\s+/g, " ");
27932
+ }
26806
27933
  async function gitCreatePrHandler(c, metadataManager) {
26807
27934
  try {
26808
27935
  const threadId = c.req.param("threadId");
@@ -26812,6 +27939,9 @@ async function gitCreatePrHandler(c, metadataManager) {
26812
27939
  console.log(`[git-create-pr] PR title: "${title}"`);
26813
27940
  console.log(`[git-create-pr] PR description length: ${description?.length ?? 0} chars`);
26814
27941
  const thread = await metadataManager.loadThreads().then((threads) => threads.find((t) => t.id === threadId));
27942
+ if (!threadId) {
27943
+ return c.json({ error: "Thread ID is required" }, 400);
27944
+ }
26815
27945
  if (!thread) {
26816
27946
  console.log(`[git-create-pr] Thread not found: ${threadId}`);
26817
27947
  return c.json({ error: "Thread not found" }, 404);
@@ -26834,10 +27964,17 @@ async function gitCreatePrHandler(c, metadataManager) {
26834
27964
  }
26835
27965
  const currentBranch = await getCurrentBranch(gitRoot);
26836
27966
  console.log(`[git-create-pr] Current branch: ${currentBranch}`);
27967
+ const prTitle = title ?? currentBranch;
26837
27968
  console.log(`[git-create-pr] Creating PR with GitHub CLI...`);
26838
- const prUrl = await createPullRequest(gitRoot, title ?? currentBranch, description ?? "");
27969
+ const prUrl = await createPullRequest(gitRoot, prTitle, description ?? "");
26839
27970
  console.log(`[git-create-pr] \u2713 PR created successfully: ${prUrl}`);
26840
- return c.json({ success: true, prUrl });
27971
+ const normalizedPrTitle = normalizeThreadTitle(prTitle);
27972
+ if (normalizedPrTitle !== thread.title) {
27973
+ const threadUpdates = { title: normalizedPrTitle };
27974
+ await metadataManager.updateThreadFields(threadId, threadUpdates);
27975
+ console.log(`[git-create-pr] \u2713 Updated thread title to PR title: ${normalizedPrTitle}`);
27976
+ }
27977
+ return c.json({ success: true, prUrl, title: normalizedPrTitle });
26841
27978
  } catch (error) {
26842
27979
  const message = error instanceof Error ? error.message : "Failed to create PR";
26843
27980
  console.log(`[git-create-pr] \u2717 PR creation failed: ${message}`);
@@ -27163,7 +28300,7 @@ function sanitizeBranchName(name) {
27163
28300
  }
27164
28301
  return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9\-_/.]/g, "").replace(/^[/.]+|[/.]+$/g, "").replace(/-+/g, "-") || "branch";
27165
28302
  }
27166
- function checkBranchExists(gitRoot, branchName) {
28303
+ function checkBranchExists2(gitRoot, branchName) {
27167
28304
  return new Promise((resolve8) => {
27168
28305
  const proc = spawnProcess(
27169
28306
  "git",
@@ -27227,12 +28364,12 @@ async function gitCreateRepoHandler(c, metadataManager) {
27227
28364
  }
27228
28365
  const repoUrl = await createGitHubRepo(gitRoot, repoName, description, isPrivate);
27229
28366
  let branchName = sanitizeBranchName(generateRandomThreadName());
27230
- let branchExists = await checkBranchExists(gitRoot, branchName);
28367
+ let branchExists = await checkBranchExists2(gitRoot, branchName);
27231
28368
  let counter = 2;
27232
28369
  const baseBranchName = branchName;
27233
28370
  while (branchExists) {
27234
28371
  branchName = `${baseBranchName}-${counter}`;
27235
- branchExists = await checkBranchExists(gitRoot, branchName);
28372
+ branchExists = await checkBranchExists2(gitRoot, branchName);
27236
28373
  counter++;
27237
28374
  }
27238
28375
  await createAndCheckoutBranch(gitRoot, branchName);
@@ -27285,7 +28422,7 @@ function sanitizeBranchName2(name) {
27285
28422
  }
27286
28423
  return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9\-_/.]/g, "").replace(/^[/.]+|[/.]+$/g, "").replace(/-+/g, "-") || "branch";
27287
28424
  }
27288
- function checkBranchExists2(gitRoot, branchName) {
28425
+ function checkBranchExists3(gitRoot, branchName) {
27289
28426
  return new Promise((resolve8) => {
27290
28427
  const proc = spawnProcess(
27291
28428
  "git",
@@ -27348,7 +28485,7 @@ async function gitCreateBranchHandler(c, metadataManager) {
27348
28485
  if (!branchName) {
27349
28486
  return c.json({ error: "Invalid branch name" }, 400);
27350
28487
  }
27351
- const branchExists = await checkBranchExists2(gitRoot, branchName);
28488
+ const branchExists = await checkBranchExists3(gitRoot, branchName);
27352
28489
  if (body.branchName) {
27353
28490
  if (branchExists) {
27354
28491
  return c.json({ error: `Branch "${branchName}" already exists` }, 409);
@@ -27359,7 +28496,7 @@ async function gitCreateBranchHandler(c, metadataManager) {
27359
28496
  let exists = branchExists;
27360
28497
  while (exists) {
27361
28498
  branchName = `${baseBranchName}-${counter}`;
27362
- exists = await checkBranchExists2(gitRoot, branchName);
28499
+ exists = await checkBranchExists3(gitRoot, branchName);
27363
28500
  counter++;
27364
28501
  }
27365
28502
  }
@@ -27731,7 +28868,7 @@ async function gitCheckoutBranchHandler(c, metadataManager) {
27731
28868
  import { unlinkSync } from "fs";
27732
28869
  import { resolve as resolve6 } from "path";
27733
28870
  init_utils();
27734
- function runGit2(args2, cwd) {
28871
+ function runGit3(args2, cwd) {
27735
28872
  return new Promise((resolve8, reject) => {
27736
28873
  const proc = spawnProcess("git", args2, { cwd });
27737
28874
  let out = "";
@@ -27767,7 +28904,7 @@ async function gitRevertFileHandler(c, metadataManager) {
27767
28904
  } catch {
27768
28905
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
27769
28906
  }
27770
- const statusOutput = await runGit2(["status", "--porcelain", "--", filePath], gitRoot).catch(
28907
+ const statusOutput = await runGit3(["status", "--porcelain", "--", filePath], gitRoot).catch(
27771
28908
  () => ""
27772
28909
  );
27773
28910
  const statusCode = statusOutput.trim().substring(0, 2);
@@ -27777,13 +28914,13 @@ async function gitRevertFileHandler(c, metadataManager) {
27777
28914
  } catch {
27778
28915
  }
27779
28916
  } else if (statusCode === "A ") {
27780
- await runGit2(["rm", "--cached", "--", filePath], gitRoot);
28917
+ await runGit3(["rm", "--cached", "--", filePath], gitRoot);
27781
28918
  try {
27782
28919
  unlinkSync(resolve6(gitRoot, filePath));
27783
28920
  } catch {
27784
28921
  }
27785
28922
  } else {
27786
- await runGit2(["checkout", "HEAD", "--", filePath], gitRoot);
28923
+ await runGit3(["checkout", "HEAD", "--", filePath], gitRoot);
27787
28924
  }
27788
28925
  return c.json({ success: true });
27789
28926
  } catch (error) {
@@ -27974,51 +29111,12 @@ async function gitCheckpointRestoreHandler(c, metadataManager) {
27974
29111
  }
27975
29112
 
27976
29113
  // src/features/git/git-rename-branch.route.ts
27977
- init_utils();
27978
29114
  function sanitizeBranchName3(name) {
27979
29115
  if (!name || typeof name !== "string") {
27980
29116
  return "";
27981
29117
  }
27982
29118
  return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9\-_/.]/g, "").replace(/^[/.]+|[/.]+$/g, "").replace(/-+/g, "-") || "";
27983
29119
  }
27984
- function checkBranchExists3(gitRoot, branchName) {
27985
- return new Promise((resolve8) => {
27986
- const proc = spawnProcess(
27987
- "git",
27988
- ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
27989
- {
27990
- cwd: gitRoot
27991
- }
27992
- );
27993
- proc.on("close", (code) => {
27994
- resolve8(code === 0);
27995
- });
27996
- proc.on("error", () => {
27997
- resolve8(false);
27998
- });
27999
- });
28000
- }
28001
- function renameBranch(gitRoot, newBranchName) {
28002
- return new Promise((resolve8, reject) => {
28003
- const proc = spawnProcess("git", ["branch", "-m", newBranchName], {
28004
- cwd: gitRoot
28005
- });
28006
- let err = "";
28007
- if (proc.stderr) {
28008
- proc.stderr.on("data", (d) => {
28009
- err += d.toString();
28010
- });
28011
- }
28012
- proc.on("close", (code) => {
28013
- if (code === 0) {
28014
- resolve8();
28015
- } else {
28016
- reject(new Error(err || `Failed to rename branch to "${newBranchName}"`));
28017
- }
28018
- });
28019
- proc.on("error", reject);
28020
- });
28021
- }
28022
29120
  async function gitRenameBranchHandler(c, metadataManager) {
28023
29121
  try {
28024
29122
  const threadId = c.req.param("threadId");
@@ -28046,11 +29144,11 @@ async function gitRenameBranchHandler(c, metadataManager) {
28046
29144
  } catch {
28047
29145
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
28048
29146
  }
28049
- const branchExists = await checkBranchExists3(gitRoot, newBranchName);
29147
+ const branchExists = await checkBranchExists(gitRoot, newBranchName);
28050
29148
  if (branchExists) {
28051
29149
  return c.json({ error: `Branch "${newBranchName}" already exists` }, 409);
28052
29150
  }
28053
- await renameBranch(gitRoot, newBranchName);
29151
+ await renameCurrentBranch(gitRoot, newBranchName);
28054
29152
  const threads = await metadataManager.loadThreads();
28055
29153
  const threadIndex = threads.findIndex((t) => t.id === threadId);
28056
29154
  if (threadIndex !== -1) {
@@ -28292,8 +29390,8 @@ function createUpdateRoutes(updater) {
28292
29390
 
28293
29391
  // src/features/mcp/mcp.routes.ts
28294
29392
  import { Hono as Hono15 } from "hono";
28295
- import { readFile as readFile17, writeFile as writeFile9, mkdir as mkdir11, access as access6 } from "fs/promises";
28296
- import { join as join34, dirname as dirname8 } from "path";
29393
+ import { readFile as readFile18, writeFile as writeFile9, mkdir as mkdir11, access as access6 } from "fs/promises";
29394
+ import { join as join35, dirname as dirname8 } from "path";
28297
29395
 
28298
29396
  // src/features/mcp/mcp.popular.json
28299
29397
  var mcp_popular_default = [
@@ -28415,10 +29513,10 @@ var mcp_popular_default = [
28415
29513
  var MCP_CONFIG_PATHS2 = [".agents/mcp.json", "mcp.json"];
28416
29514
  async function readMCPConfig(projectPath) {
28417
29515
  for (const configPath of MCP_CONFIG_PATHS2) {
28418
- const fullPath = join34(projectPath, configPath);
29516
+ const fullPath = join35(projectPath, configPath);
28419
29517
  try {
28420
29518
  await access6(fullPath);
28421
- const content = await readFile17(fullPath, "utf-8");
29519
+ const content = await readFile18(fullPath, "utf-8");
28422
29520
  const config = JSON.parse(content);
28423
29521
  return { config, filePath: fullPath };
28424
29522
  } catch {
@@ -28429,7 +29527,7 @@ async function readMCPConfig(projectPath) {
28429
29527
  }
28430
29528
  async function writeMCPConfig(projectPath, config) {
28431
29529
  const existing = await readMCPConfig(projectPath);
28432
- const targetPath = existing?.filePath ?? join34(projectPath, ".agents/mcp.json");
29530
+ const targetPath = existing?.filePath ?? join35(projectPath, ".agents/mcp.json");
28433
29531
  await mkdir11(dirname8(targetPath), { recursive: true });
28434
29532
  await writeFile9(targetPath, JSON.stringify(config, null, 2), "utf-8");
28435
29533
  }
@@ -28632,11 +29730,328 @@ var skills_popular_default = [
28632
29730
  }
28633
29731
  ];
28634
29732
 
29733
+ // src/features/skills/skills.catalog.ts
29734
+ init_dist();
29735
+ import { readdir as readdir13, readFile as readFile19 } from "fs/promises";
29736
+ import { join as join36 } from "path";
29737
+ function getSkillSources() {
29738
+ const sources = skills_sources_default;
29739
+ return [...sources].sort((a, b) => a.title.localeCompare(b.title));
29740
+ }
29741
+ async function getAllSkillSources() {
29742
+ const bundled = getSkillSources();
29743
+ const custom = await getCustomSkillSources();
29744
+ const bundledIds = new Set(bundled.map((source) => source.id));
29745
+ const uniqueCustom = custom.filter((source) => !bundledIds.has(source.id));
29746
+ return [...bundled, ...uniqueCustom].sort((a, b) => a.title.localeCompare(b.title));
29747
+ }
29748
+ async function scanDownloadedSkillCatalog(tmpDir, parsed, sourceId) {
29749
+ const entries = await readdir13(tmpDir, { withFileTypes: true });
29750
+ const catalogItems = [];
29751
+ for (const entry of entries) {
29752
+ if (!entry.isDirectory()) {
29753
+ continue;
29754
+ }
29755
+ const skillMdPath = join36(tmpDir, entry.name, "SKILL.md");
29756
+ let skillMdContent;
29757
+ try {
29758
+ skillMdContent = await readFile19(skillMdPath, "utf8");
29759
+ } catch {
29760
+ continue;
29761
+ }
29762
+ const { metadata } = parseSkillFrontmatter(skillMdContent);
29763
+ const folderPath = parsed.folderPath ? `${parsed.folderPath}/${entry.name}` : entry.name;
29764
+ catalogItems.push({
29765
+ id: entry.name,
29766
+ name: metadata.name ?? entry.name,
29767
+ description: metadata.description ?? "",
29768
+ sourceId,
29769
+ gitHubRepo: parsed.repoUrl,
29770
+ gitHubFolder: folderPath,
29771
+ gitHubRef: parsed.ref
29772
+ });
29773
+ }
29774
+ catalogItems.sort((a, b) => a.name.localeCompare(b.name));
29775
+ return catalogItems;
29776
+ }
29777
+ async function fetchSkillCatalogFromSource(sourceId) {
29778
+ const entry = await refreshCatalogClone(sourceId);
29779
+ return scanDownloadedSkillCatalog(entry.scanDir, entry.parsed, sourceId);
29780
+ }
29781
+ async function verifySkillSource(title, url) {
29782
+ const trimmedTitle = title.trim();
29783
+ const trimmedUrl = url.trim();
29784
+ if (!trimmedTitle) {
29785
+ throw new Error("Title is required");
29786
+ }
29787
+ if (!trimmedUrl) {
29788
+ throw new Error("URL is required");
29789
+ }
29790
+ parseGithubTreeUrl(trimmedUrl);
29791
+ const bundledIds = skills_sources_default.map((source2) => source2.id);
29792
+ const customIds = (await getCustomSkillSources()).map((source2) => source2.id);
29793
+ const sourceId = createCustomSkillSourceId(trimmedTitle, [...bundledIds, ...customIds]);
29794
+ const entry = await refreshCatalogClone(sourceId, trimmedUrl);
29795
+ const skills = await scanDownloadedSkillCatalog(entry.scanDir, entry.parsed, sourceId);
29796
+ if (skills.length === 0) {
29797
+ await clearCatalogClone(sourceId);
29798
+ throw new Error("No skills found at this URL");
29799
+ }
29800
+ const source = {
29801
+ id: sourceId,
29802
+ title: trimmedTitle,
29803
+ url: trimmedUrl
29804
+ };
29805
+ await saveCustomSkillSource(source);
29806
+ return {
29807
+ source,
29808
+ skillCount: skills.length
29809
+ };
29810
+ }
29811
+ async function updateCustomSkillSource(sourceId, title, url) {
29812
+ if (!isCustomSkillSourceId(sourceId)) {
29813
+ throw new Error("Only custom skill sources can be updated");
29814
+ }
29815
+ const existing = await getCustomSkillSourceById(sourceId);
29816
+ if (!existing) {
29817
+ throw new Error("Unknown custom skill source");
29818
+ }
29819
+ const trimmedTitle = title.trim();
29820
+ const trimmedUrl = url.trim();
29821
+ if (!trimmedTitle) {
29822
+ throw new Error("Title is required");
29823
+ }
29824
+ if (!trimmedUrl) {
29825
+ throw new Error("URL is required");
29826
+ }
29827
+ parseGithubTreeUrl(trimmedUrl);
29828
+ const entry = await refreshCatalogClone(sourceId, trimmedUrl);
29829
+ const skills = await scanDownloadedSkillCatalog(entry.scanDir, entry.parsed, sourceId);
29830
+ if (skills.length === 0) {
29831
+ await clearCatalogClone(sourceId);
29832
+ throw new Error("No skills found at this URL");
29833
+ }
29834
+ const source = {
29835
+ id: sourceId,
29836
+ title: trimmedTitle,
29837
+ url: trimmedUrl
29838
+ };
29839
+ await saveCustomSkillSource(source);
29840
+ return {
29841
+ source,
29842
+ skillCount: skills.length
29843
+ };
29844
+ }
29845
+
28635
29846
  // src/features/skills/skills.routes.ts
28636
29847
  function createSkillRoutes() {
28637
29848
  const router = new Hono16();
28638
29849
  router.get("/popular", (c) => {
28639
- return c.json({ skills: skills_popular_default });
29850
+ const skills = [...skills_popular_default].sort((a, b) => a.name.localeCompare(b.name));
29851
+ return c.json({ skills });
29852
+ });
29853
+ router.get("/sources", async (c) => {
29854
+ return c.json({ sources: await getAllSkillSources() });
29855
+ });
29856
+ router.get("/catalog", async (c) => {
29857
+ const sourceId = c.req.query("sourceId");
29858
+ if (!sourceId) {
29859
+ return c.json({ error: { code: "BAD_REQUEST", message: "sourceId is required" } }, 400);
29860
+ }
29861
+ try {
29862
+ const skills = await fetchSkillCatalogFromSource(sourceId);
29863
+ return c.json({ skills });
29864
+ } catch (error) {
29865
+ const message = error instanceof Error ? error.message : String(error);
29866
+ if (message.startsWith("Unknown skill source:") || message.startsWith("Invalid GitHub tree URL:")) {
29867
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
29868
+ }
29869
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
29870
+ }
29871
+ });
29872
+ router.post("/sources/verify", async (c) => {
29873
+ let body;
29874
+ try {
29875
+ body = await c.req.json();
29876
+ } catch {
29877
+ return c.json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, 400);
29878
+ }
29879
+ const title = body.title?.trim() ?? "";
29880
+ const url = body.url?.trim() ?? "";
29881
+ if (!title || !url) {
29882
+ return c.json({ error: { code: "BAD_REQUEST", message: "title and url are required" } }, 400);
29883
+ }
29884
+ try {
29885
+ const result = await verifySkillSource(title, url);
29886
+ return c.json(result);
29887
+ } catch (error) {
29888
+ const message = error instanceof Error ? error.message : String(error);
29889
+ if (message === "Title is required" || message === "URL is required" || message === "No skills found at this URL" || message.startsWith("Invalid GitHub tree URL:")) {
29890
+ return c.json({ error: { code: "BAD_REQUEST", message } }, 400);
29891
+ }
29892
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
29893
+ }
29894
+ });
29895
+ router.put("/sources/:sourceId", async (c) => {
29896
+ const sourceId = c.req.param("sourceId");
29897
+ let body;
29898
+ try {
29899
+ body = await c.req.json();
29900
+ } catch {
29901
+ return c.json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, 400);
29902
+ }
29903
+ const title = body.title?.trim() ?? "";
29904
+ const url = body.url?.trim() ?? "";
29905
+ if (!title || !url) {
29906
+ return c.json({ error: { code: "BAD_REQUEST", message: "title and url are required" } }, 400);
29907
+ }
29908
+ try {
29909
+ const result = await updateCustomSkillSource(sourceId, title, url);
29910
+ return c.json(result);
29911
+ } catch (error) {
29912
+ const message = error instanceof Error ? error.message : String(error);
29913
+ if (message === "Unknown custom skill source") {
29914
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
29915
+ }
29916
+ if (message === "Only custom skill sources can be updated" || message === "Title is required" || message === "URL is required" || message === "No skills found at this URL" || message.startsWith("Invalid GitHub tree URL:")) {
29917
+ return c.json({ error: { code: "BAD_REQUEST", message } }, 400);
29918
+ }
29919
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
29920
+ }
29921
+ });
29922
+ router.delete("/sources/:sourceId", async (c) => {
29923
+ const sourceId = c.req.param("sourceId");
29924
+ try {
29925
+ await deleteCustomSkillSource(sourceId);
29926
+ return c.json({ success: true });
29927
+ } catch (error) {
29928
+ const message = error instanceof Error ? error.message : String(error);
29929
+ if (message === "Unknown custom skill source") {
29930
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
29931
+ }
29932
+ if (message === "Only custom skill sources can be deleted") {
29933
+ return c.json({ error: { code: "BAD_REQUEST", message } }, 400);
29934
+ }
29935
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
29936
+ }
29937
+ });
29938
+ router.post("/sources/import", async (c) => {
29939
+ let body;
29940
+ try {
29941
+ body = await c.req.json();
29942
+ } catch {
29943
+ return c.json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, 400);
29944
+ }
29945
+ if (!Array.isArray(body.sources)) {
29946
+ return c.json({ error: { code: "BAD_REQUEST", message: "sources array is required" } }, 400);
29947
+ }
29948
+ try {
29949
+ const sources = await importCustomSkillSources(
29950
+ body.sources.filter(
29951
+ (source) => typeof source?.id === "string" && typeof source?.title === "string" && typeof source?.url === "string"
29952
+ )
29953
+ );
29954
+ return c.json({ sources });
29955
+ } catch (error) {
29956
+ const message = error instanceof Error ? error.message : String(error);
29957
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
29958
+ }
29959
+ });
29960
+ router.get("/catalog/files", async (c) => {
29961
+ const sourceId = c.req.query("sourceId");
29962
+ const skillId = c.req.query("skillId");
29963
+ if (!sourceId || !skillId) {
29964
+ return c.json(
29965
+ { error: { code: "BAD_REQUEST", message: "sourceId and skillId are required" } },
29966
+ 400
29967
+ );
29968
+ }
29969
+ try {
29970
+ const files = await listCachedCatalogSkillFiles(sourceId, skillId);
29971
+ return c.json({ files });
29972
+ } catch (error) {
29973
+ const message = error instanceof Error ? error.message : String(error);
29974
+ if (message.startsWith("Unknown skill source:")) {
29975
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
29976
+ }
29977
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
29978
+ }
29979
+ });
29980
+ router.get("/catalog/file", async (c) => {
29981
+ const sourceId = c.req.query("sourceId");
29982
+ const skillId = c.req.query("skillId");
29983
+ const relativePath = c.req.query("relativePath");
29984
+ if (!sourceId || !skillId || !relativePath) {
29985
+ return c.json(
29986
+ {
29987
+ error: {
29988
+ code: "BAD_REQUEST",
29989
+ message: "sourceId, skillId, and relativePath are required"
29990
+ }
29991
+ },
29992
+ 400
29993
+ );
29994
+ }
29995
+ try {
29996
+ const content = await readCachedCatalogSkillFile(sourceId, skillId, relativePath);
29997
+ return c.json({ content, relativePath });
29998
+ } catch (error) {
29999
+ const message = error instanceof Error ? error.message : String(error);
30000
+ if (message.startsWith("Unknown skill source:") || message.startsWith("Invalid file path:")) {
30001
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
30002
+ }
30003
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
30004
+ }
30005
+ });
30006
+ router.get("/github-folder/files", async (c) => {
30007
+ const gitHubRepo = c.req.query("gitHubRepo");
30008
+ const gitHubFolder = c.req.query("gitHubFolder");
30009
+ if (!gitHubRepo || !gitHubFolder) {
30010
+ return c.json(
30011
+ { error: { code: "BAD_REQUEST", message: "gitHubRepo and gitHubFolder are required" } },
30012
+ 400
30013
+ );
30014
+ }
30015
+ try {
30016
+ const result = await listCachedCatalogSkillFilesByGithubFolder(gitHubRepo, gitHubFolder);
30017
+ return c.json(result);
30018
+ } catch (error) {
30019
+ const message = error instanceof Error ? error.message : String(error);
30020
+ if (message.startsWith("No catalog source found")) {
30021
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
30022
+ }
30023
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
30024
+ }
30025
+ });
30026
+ router.get("/github-folder/file", async (c) => {
30027
+ const gitHubRepo = c.req.query("gitHubRepo");
30028
+ const gitHubFolder = c.req.query("gitHubFolder");
30029
+ const relativePath = c.req.query("relativePath");
30030
+ if (!gitHubRepo || !gitHubFolder || !relativePath) {
30031
+ return c.json(
30032
+ {
30033
+ error: {
30034
+ code: "BAD_REQUEST",
30035
+ message: "gitHubRepo, gitHubFolder, and relativePath are required"
30036
+ }
30037
+ },
30038
+ 400
30039
+ );
30040
+ }
30041
+ try {
30042
+ const content = await readCachedCatalogSkillFileByGithubFolder(
30043
+ gitHubRepo,
30044
+ gitHubFolder,
30045
+ relativePath
30046
+ );
30047
+ return c.json({ content, relativePath });
30048
+ } catch (error) {
30049
+ const message = error instanceof Error ? error.message : String(error);
30050
+ if (message.startsWith("No catalog source found") || message.startsWith("Invalid file path:")) {
30051
+ return c.json({ error: { code: "NOT_FOUND", message } }, 404);
30052
+ }
30053
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
30054
+ }
28640
30055
  });
28641
30056
  return router;
28642
30057
  }
@@ -28846,7 +30261,7 @@ function getLocalNetworkAddresses() {
28846
30261
 
28847
30262
  // src/core/server-bind-mode-store.ts
28848
30263
  import { existsSync as existsSync25, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync } from "fs";
28849
- import { join as join35 } from "path";
30264
+ import { join as join37 } from "path";
28850
30265
 
28851
30266
  // src/core/server-bind-mode.ts
28852
30267
  var DEFAULT_SERVER_BIND_MODE = "local";
@@ -28858,7 +30273,7 @@ function getServerBindHostname(mode) {
28858
30273
  }
28859
30274
 
28860
30275
  // src/core/server-bind-mode-store.ts
28861
- var SERVER_BIND_MODE_FILE = join35(DATA_DIR, "server-bind-mode.json");
30276
+ var SERVER_BIND_MODE_FILE = join37(DATA_DIR, "server-bind-mode.json");
28862
30277
  function hasServerBindModeFile() {
28863
30278
  return existsSync25(SERVER_BIND_MODE_FILE);
28864
30279
  }
@@ -29212,7 +30627,7 @@ async function clipboardWriteImage(c) {
29212
30627
 
29213
30628
  // src/features/image/image-save.route.ts
29214
30629
  init_dist();
29215
- import { join as join36 } from "path";
30630
+ import { join as join38 } from "path";
29216
30631
  var Utils5 = null;
29217
30632
  var utilsLoaded5 = false;
29218
30633
  async function loadUtils5() {
@@ -29250,7 +30665,7 @@ async function saveImage(c) {
29250
30665
  return c.json({ error: "imageUrl or imageData is required" }, 400);
29251
30666
  }
29252
30667
  const name = filename ?? `generated-image-${Date.now()}.png`;
29253
- const savePath = join36(Utils5.paths.downloads, name);
30668
+ const savePath = join38(Utils5.paths.downloads, name);
29254
30669
  await Bun.write(savePath, data);
29255
30670
  return c.json({ success: true, path: savePath });
29256
30671
  } catch (error) {
@@ -29431,23 +30846,12 @@ function createBrowserRoutes() {
29431
30846
 
29432
30847
  // src/features/voice-model/voice-model.routes.ts
29433
30848
  import { Hono as Hono24 } from "hono";
29434
- var VOICE_MODEL_URLS = {
29435
- default: "https://install.tarsk.io/voice-models/ggml-tiny.en.bin",
29436
- tiny: "https://install.tarsk.io/voice-models/ggml-tiny.en-q5_1.bin"
29437
- };
29438
- function getVoiceModelUrl(model) {
29439
- if (model === "tiny") {
29440
- return VOICE_MODEL_URLS.tiny;
29441
- }
29442
- return VOICE_MODEL_URLS.default;
29443
- }
30849
+ var VOICE_MODEL_URL = "https://install.tarsk.io/voice-models/vosk-model-small-en-us-0.15.tar.gz";
29444
30850
  function createVoiceModelRoutes() {
29445
30851
  const router = new Hono24();
29446
30852
  router.get("/download", async (c) => {
29447
- const selectedModel = c.req.query("model");
29448
- const modelUrl = getVoiceModelUrl(selectedModel);
29449
30853
  try {
29450
- const response = await fetch(modelUrl);
30854
+ const response = await fetch(VOICE_MODEL_URL);
29451
30855
  if (!response.ok) {
29452
30856
  return errorResponse(
29453
30857
  c,
@@ -29458,8 +30862,7 @@ function createVoiceModelRoutes() {
29458
30862
  }
29459
30863
  const contentLength = response.headers.get("content-length");
29460
30864
  const headers = {
29461
- "Content-Type": "application/octet-stream",
29462
- "X-Tarsk-Voice-Model": selectedModel === "tiny" ? "tiny" : "default"
30865
+ "Content-Type": "application/gzip"
29463
30866
  };
29464
30867
  if (contentLength) {
29465
30868
  headers["Content-Length"] = contentLength;
@@ -29675,7 +31078,7 @@ async function startTarskServer(options) {
29675
31078
  }
29676
31079
  async function startTarskServerInternal(options) {
29677
31080
  const { isDebug: isDebug2, publicDir: publicDirOverride } = options;
29678
- const port = isDebug2 ? 462 : process.env.PORT ? parseInt(process.env.PORT) : 641;
31081
+ const initialPort = isDebug2 ? 462 : process.env.PORT ? parseInt(process.env.PORT) : 641;
29679
31082
  const app = new Hono28();
29680
31083
  app.use("/*", cors());
29681
31084
  app.use("/*", async (c, next) => {
@@ -29687,7 +31090,9 @@ async function startTarskServerInternal(options) {
29687
31090
  if (c.req.path.startsWith("/api/")) {
29688
31091
  const method = c.req.method;
29689
31092
  const reqPath = c.req.path;
29690
- const fullUrl = `http://localhost:${port}${reqPath}`;
31093
+ const hostHeader = c.req.header("host") ?? `localhost:${initialPort}`;
31094
+ const requestUrl = new URL(c.req.url);
31095
+ const fullUrl = `${requestUrl.protocol}//${hostHeader}${reqPath}`;
29691
31096
  const isThreadMessages = reqPath.startsWith("/api/threads/") && reqPath.endsWith("/messages");
29692
31097
  if (!fullUrl.includes("processing") && !reqPath.startsWith("/api/logs") && !isThreadMessages) {
29693
31098
  console.log(`${method} ${fullUrl}`);
@@ -29697,17 +31102,17 @@ async function startTarskServerInternal(options) {
29697
31102
  });
29698
31103
  const dataDir = getDataDir();
29699
31104
  const metadataManager = new MetadataManager(dataDir);
29700
- const gitManager = new GitManagerImpl();
31105
+ const gitManager2 = new GitManagerImpl();
29701
31106
  const processingStateManager = new ProcessingStateManagerImpl();
29702
31107
  const projectManager = new ProjectManagerImpl(
29703
31108
  dataDir,
29704
31109
  metadataManager,
29705
- gitManager,
31110
+ gitManager2,
29706
31111
  processingStateManager
29707
31112
  );
29708
31113
  const threadManager = new ThreadManagerImpl(
29709
31114
  metadataManager,
29710
- gitManager,
31115
+ gitManager2,
29711
31116
  processingStateManager,
29712
31117
  dataDir
29713
31118
  );
@@ -29720,7 +31125,8 @@ async function startTarskServerInternal(options) {
29720
31125
  return c.json({
29721
31126
  status: "ok",
29722
31127
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29723
- service: "project-threads-manager-cli"
31128
+ service: "project-threads-manager-cli",
31129
+ isDevMode: port !== initialPort
29724
31130
  });
29725
31131
  });
29726
31132
  app.get("/api/programs", (c) => {
@@ -29733,10 +31139,10 @@ async function startTarskServerInternal(options) {
29733
31139
  processingThreadIds: processingStateManager.getProcessingThreadIds()
29734
31140
  });
29735
31141
  });
29736
- app.route("/api/projects", createProjectRoutes(projectManager, threadManager));
31142
+ app.route("/api/projects", createProjectRoutes(projectManager, threadManager, metadataManager));
29737
31143
  app.route("/api/projects", createRunRoutes(projectManager));
29738
31144
  app.route("/api/projects", createProjectTodosRoutes(metadataManager));
29739
- app.route("/api/threads", createThreadRoutes(threadManager, gitManager, conversationManager));
31145
+ app.route("/api/threads", createThreadRoutes(threadManager, gitManager2, conversationManager));
29740
31146
  app.route("/api/threads", createUserTaskRoutes());
29741
31147
  app.route(
29742
31148
  "/api/chat",
@@ -29786,16 +31192,37 @@ async function startTarskServerInternal(options) {
29786
31192
  `No static frontend assets found. Expected one of: ${prodPublicDir}, ${devCopiedPublicDir}, ${appDistDir}. Build the app first with \`cd ../app && bun run build\`.`
29787
31193
  );
29788
31194
  }
31195
+ function isStaticAssetPath(requestPath) {
31196
+ if (requestPath.startsWith("/assets/")) {
31197
+ return true;
31198
+ }
31199
+ const lastSegment = requestPath.split("/").pop() ?? "";
31200
+ return lastSegment.includes(".") && !lastSegment.endsWith(".html");
31201
+ }
31202
+ function setNoCacheIndexHeaders(c) {
31203
+ c.header("Cache-Control", "no-cache, no-store, must-revalidate");
31204
+ }
29789
31205
  app.use("/*", async (c, next) => {
29790
31206
  if (c.req.path.startsWith("/api/")) {
29791
31207
  return next();
29792
31208
  }
29793
- return serveStatic({ root: resolvedPublicDir })(c, next);
31209
+ return serveStatic({
31210
+ root: resolvedPublicDir,
31211
+ onFound: (_path, context) => {
31212
+ if (context.req.path === "/" || context.req.path === "/index.html") {
31213
+ setNoCacheIndexHeaders(context);
31214
+ }
31215
+ }
31216
+ })(c, next);
29794
31217
  });
29795
31218
  app.get("*", async (c, next) => {
29796
31219
  if (c.req.path.startsWith("/api/")) {
29797
31220
  return next();
29798
31221
  }
31222
+ if (isStaticAssetPath(c.req.path)) {
31223
+ return next();
31224
+ }
31225
+ setNoCacheIndexHeaders(c);
29799
31226
  return serveStatic({
29800
31227
  path: path5.join(resolvedPublicDir, "index.html")
29801
31228
  })(c, next);
@@ -29812,16 +31239,22 @@ async function startTarskServerInternal(options) {
29812
31239
  });
29813
31240
  const serverBindMode = readServerBindMode();
29814
31241
  const hostname2 = getServerBindHostname(serverBindMode);
29815
- const url = `http://localhost:${port}`;
29816
- const bound = await listenForTarskServer({
31242
+ const port = await listenForTarskServer({
29817
31243
  fetch: app.fetch,
29818
31244
  hostname: hostname2,
29819
- onListening: () => {
31245
+ onPortConflict: (candidatePort) => {
31246
+ process.stdout.write(
31247
+ `Port ${candidatePort} is already in use, trying ${candidatePort + 1}.
31248
+ `
31249
+ );
31250
+ },
31251
+ onListening: (listeningPort) => {
31252
+ const url = `http://localhost:${listeningPort}`;
29820
31253
  process.stdout.write(`Tarsk started on ${url}
29821
31254
  `);
29822
31255
  if (serverBindMode === "network") {
29823
31256
  for (const address of getLocalNetworkAddresses()) {
29824
- process.stdout.write(` Network: http://${address}:${port}
31257
+ process.stdout.write(` Network: http://${address}:${listeningPort}
29825
31258
  `);
29826
31259
  }
29827
31260
  }
@@ -29830,38 +31263,48 @@ async function startTarskServerInternal(options) {
29830
31263
  });
29831
31264
  }
29832
31265
  },
29833
- port
31266
+ port: initialPort
29834
31267
  });
29835
- if (!bound) {
29836
- return { url, port, bound: false };
29837
- }
29838
- return { url, port, bound: true };
31268
+ return {
31269
+ url: `http://localhost:${port}`,
31270
+ port,
31271
+ bound: true,
31272
+ isDevMode: port !== initialPort
31273
+ };
29839
31274
  }
29840
31275
  async function listenForTarskServer(options) {
29841
- const server = createAdaptorServer({
29842
- fetch: options.fetch,
29843
- hostname: options.hostname,
29844
- port: options.port
29845
- });
29846
- return new Promise((resolve8, reject) => {
29847
- server.once("error", (error) => {
29848
- if (error.code === "EADDRINUSE") {
29849
- resolve8(false);
29850
- return;
31276
+ let port = options.port;
31277
+ while (true) {
31278
+ const server = createAdaptorServer({
31279
+ fetch: options.fetch,
31280
+ hostname: options.hostname,
31281
+ port
31282
+ });
31283
+ const result = await new Promise((resolve8, reject) => {
31284
+ server.once("error", (error) => {
31285
+ if (error.code === "EADDRINUSE") {
31286
+ resolve8("in-use");
31287
+ return;
31288
+ }
31289
+ startPromise = null;
31290
+ reject(error);
31291
+ });
31292
+ function onListening() {
31293
+ resolve8("listening");
31294
+ }
31295
+ if (options.hostname) {
31296
+ server.listen(port, options.hostname, onListening);
31297
+ } else {
31298
+ server.listen(port, onListening);
29851
31299
  }
29852
- startPromise = null;
29853
- reject(error);
29854
31300
  });
29855
- function onListening() {
29856
- options.onListening();
29857
- resolve8(true);
31301
+ if (result === "listening") {
31302
+ options.onListening(port);
31303
+ return port;
29858
31304
  }
29859
- if (options.hostname) {
29860
- server.listen(options.port, options.hostname, onListening);
29861
- } else {
29862
- server.listen(options.port, onListening);
29863
- }
29864
- });
31305
+ options.onPortConflict(port);
31306
+ port += 1;
31307
+ }
29865
31308
  }
29866
31309
 
29867
31310
  // src/index.ts