tarsk 0.5.44 → 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 (85) hide show
  1. package/dist/index.js +1444 -175
  2. package/dist/public/assets/{account-view-CTizS7Yb.js → account-view-CQetjhCu.js} +1 -1
  3. package/dist/public/assets/api-BiOks4gS.js +1 -0
  4. package/dist/public/assets/{browser-tab-DSpOIu84.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-CxRnlUcQ.js → dialogs-config-Cmwn8AP3.js} +14 -14
  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-BmKKHyrH.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-DAEnT4kC.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-0sXVxXh3.js → settings-system-prompt-view-DNR6lOSK.js} +1 -1
  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-PjkgqbY6.js → terminal-panel-Cp5-H4GS.js} +1 -1
  44. package/dist/public/assets/{ui-components-DaffuSG7.js → ui-components-BMQhWki4.js} +1 -1
  45. package/dist/public/assets/{use-deferred-search-b843Ztu9.js → use-deferred-search-D0WCS0gz.js} +1 -1
  46. package/dist/public/assets/{utils-BIt9KCVx.js → utils-CkwFiI9G.js} +1 -1
  47. package/dist/public/assets/{web-D6dBzWD1.js → web-CdlbRaqq.js} +1 -1
  48. package/dist/public/assets/{web-BBGaE-6B.js → web-zT0ibfrw.js} +1 -1
  49. package/dist/public/index.html +7 -7
  50. package/package.json +1 -1
  51. package/dist/public/assets/api-Cuj9iSVv.js +0 -1
  52. package/dist/public/assets/commit-dialog-kTaKOsNO.js +0 -1
  53. package/dist/public/assets/context-menu-DRXV-Suu.js +0 -1
  54. package/dist/public/assets/create-repo-dialog-gIZLk6LZ.js +0 -1
  55. package/dist/public/assets/diff-view-B2OdJMRe.js +0 -3
  56. package/dist/public/assets/explorer-tab-view-fejcb7N1.js +0 -2
  57. package/dist/public/assets/explorer-tree-C2kLAwI0.js +0 -1
  58. package/dist/public/assets/git-history-dialog-B2ABiCXZ.js +0 -1
  59. package/dist/public/assets/git-ops-button-Bxx_jyqR.js +0 -2
  60. package/dist/public/assets/history-view-DebwP9D7.js +0 -9
  61. package/dist/public/assets/index-C2_gO3dh.css +0 -1
  62. package/dist/public/assets/index-D9aY24hf.js +0 -89
  63. package/dist/public/assets/mcp-server-card-B6iwOybN.js +0 -1
  64. package/dist/public/assets/merged-pr-dialog-CObeSEaB.js +0 -1
  65. package/dist/public/assets/model-star-rating-BgdOyTTO.js +0 -1
  66. package/dist/public/assets/onboarding-BN_K08ma.js +0 -1
  67. package/dist/public/assets/project-settings-view-BDvHOmF8.js +0 -1
  68. package/dist/public/assets/providers-list-view-DydWz8z3.js +0 -1
  69. package/dist/public/assets/pull-request-dialog-LJhZpT8C.js +0 -1
  70. package/dist/public/assets/pull-with-changes-dialog-BPIj9wIk.js +0 -1
  71. package/dist/public/assets/push-before-pr-dialog-DzBp93KT.js +0 -1
  72. package/dist/public/assets/radio-group-B9xMCZ1X.js +0 -1
  73. package/dist/public/assets/react-vendor-CE5Dmjd0.js +0 -16
  74. package/dist/public/assets/settings-general-view-D_44BPVr.js +0 -1
  75. package/dist/public/assets/settings-instructions-view-Y6EK67-G.js +0 -1
  76. package/dist/public/assets/settings-list-BZDwK9bE.js +0 -1
  77. package/dist/public/assets/settings-mcp-servers-view-B8IqOOnN.js +0 -5
  78. package/dist/public/assets/settings-models-view-CluPgRkw.js +0 -1
  79. package/dist/public/assets/settings-rules-view-CxHrCDwn.js +0 -8
  80. package/dist/public/assets/settings-skills-view-BE9u6FvJ.js +0 -2
  81. package/dist/public/assets/settings-slash-commands-view-CVOZ3GH3.js +0 -1
  82. package/dist/public/assets/settings-subagents-view-mguSf6zE.js +0 -2
  83. package/dist/public/assets/settings-view-BE8EW7s7.js +0 -2
  84. package/dist/public/assets/skeleton-CVhaOA8k.js +0 -1
  85. package/dist/public/assets/slug-utils-DGOwJkKI.js +0 -1
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";
@@ -10860,10 +11142,26 @@ async function* loadCodingToolsWithMcpStatus(cwd, options) {
10860
11142
  }
10861
11143
 
10862
11144
  // src/tools/agent-tool.ts
11145
+ init_dist();
10863
11146
  import { Type as Type21 } from "@sinclair/typebox";
10864
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
+
10865
11162
  // src/agent/agent.subagent-executor.ts
10866
11163
  import { Agent } from "@earendil-works/pi-agent-core";
11164
+ init_dist();
10867
11165
 
10868
11166
  // src/agent/agent.event-transformer.ts
10869
11167
  init_dist();
@@ -11124,7 +11422,14 @@ async function executeSubagent(options) {
11124
11422
  `No API key found for subagent provider: ${providerName}. Set ${providerConfig.apiKeyEnv ?? "an API key"} in environment variables or configuration.`
11125
11423
  );
11126
11424
  }
11127
- 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
+ );
11128
11433
  console.log(
11129
11434
  `[subagent] Starting with model=${resolvedModel.id}, tools=${tools.map((t) => t.name).join(",")}`
11130
11435
  );
@@ -11132,7 +11437,8 @@ async function executeSubagent(options) {
11132
11437
  initialState: {
11133
11438
  systemPrompt,
11134
11439
  model: resolvedModel,
11135
- tools
11440
+ tools,
11441
+ thinkingLevel
11136
11442
  },
11137
11443
  streamFn: cachedStreamFn,
11138
11444
  getApiKey: async () => apiKey
@@ -11231,6 +11537,31 @@ function filterTools(baseTools, allowedToolNames) {
11231
11537
  );
11232
11538
  return baseTools.filter((t) => allowed.has(t.name));
11233
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
+ }
11234
11565
  function createAgentTool(options) {
11235
11566
  const {
11236
11567
  agents,
@@ -11240,7 +11571,8 @@ function createAgentTool(options) {
11240
11571
  parentProvider,
11241
11572
  defaultSystemPrompt,
11242
11573
  onEvent,
11243
- signal
11574
+ signal,
11575
+ modelAliases
11244
11576
  } = options;
11245
11577
  return {
11246
11578
  name: "agent",
@@ -11263,8 +11595,13 @@ function createAgentTool(options) {
11263
11595
  }
11264
11596
  const label = description ?? agentName ?? "subagent";
11265
11597
  const systemPrompt = agentDef?.systemPrompt ?? defaultSystemPrompt;
11266
- const model = agentDef?.model ?? parentModel;
11267
- const provider = agentDef?.provider ?? parentProvider;
11598
+ const { model, provider } = await resolveSubagentModelSelectionWithAliases(
11599
+ agentDef,
11600
+ parentModel,
11601
+ parentProvider,
11602
+ modelAliases,
11603
+ metadataManager
11604
+ );
11268
11605
  const subTools = filterTools(baseTools, agentDef?.tools);
11269
11606
  console.log(
11270
11607
  `[agent-tool] Spawning subagent '${label}' with model=${model}, tools=${subTools.map((t) => t.name).join(",")}`
@@ -12550,7 +12887,14 @@ var PiExecutorImpl = class {
12550
12887
  );
12551
12888
  }
12552
12889
  console.log(`[ai] API key source for ${providerName}: ${apiKeySource}`);
12553
- 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
+ );
12554
12898
  console.log("[ai] Resolved model:", {
12555
12899
  id: resolvedModel.id,
12556
12900
  api: resolvedModel.api,
@@ -12629,7 +12973,8 @@ var PiExecutorImpl = class {
12629
12973
  parentProvider: providerName,
12630
12974
  defaultSystemPrompt: systemPrompt,
12631
12975
  onEvent: (event) => eventQueue.push(event),
12632
- signal
12976
+ signal,
12977
+ modelAliases: context.modelAliases
12633
12978
  });
12634
12979
  tools.push(agentToolInstance);
12635
12980
  }
@@ -12639,7 +12984,8 @@ var PiExecutorImpl = class {
12639
12984
  systemPrompt,
12640
12985
  model: resolvedModel,
12641
12986
  tools,
12642
- messages: historyMessages
12987
+ messages: historyMessages,
12988
+ thinkingLevel
12643
12989
  },
12644
12990
  streamFn: cachedStreamFn,
12645
12991
  getApiKey: async () => apiKey,
@@ -14476,8 +14822,8 @@ async function insertProject(db, project) {
14476
14822
  try {
14477
14823
  await db.execute(
14478
14824
  `
14479
- INSERT INTO projects (id, name, gitUrl, path, createdAt, openWith, commands, setupScript, validationScript, runCommand, commitMethod, planPrompt, testPrompt, reviewPrompt, customSystemPrompt, useCustomSystemPrompt, allowAllCommands, allowedCommandPatterns, allowedFetchDomains)
14480
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
14481
14827
  `,
14482
14828
  [
14483
14829
  project.id,
@@ -14498,7 +14844,8 @@ async function insertProject(db, project) {
14498
14844
  project.useCustomSystemPrompt ? 1 : 0,
14499
14845
  project.allowAllCommands === false ? 0 : 1,
14500
14846
  project.allowedCommandPatterns ? JSON.stringify(project.allowedCommandPatterns) : null,
14501
- project.allowedFetchDomains ? JSON.stringify(project.allowedFetchDomains) : null
14847
+ project.allowedFetchDomains ? JSON.stringify(project.allowedFetchDomains) : null,
14848
+ project.modelAliases ? JSON.stringify(project.modelAliases) : null
14502
14849
  ]
14503
14850
  );
14504
14851
  } catch (error) {
@@ -14582,6 +14929,10 @@ async function updateProject(db, id, updates) {
14582
14929
  updates.allowedFetchDomains.length > 0 ? JSON.stringify(updates.allowedFetchDomains) : null
14583
14930
  );
14584
14931
  }
14932
+ if (updates.modelAliases !== void 0) {
14933
+ fields.push("modelAliases = ?");
14934
+ values.push(updates.modelAliases ? JSON.stringify(updates.modelAliases) : null);
14935
+ }
14585
14936
  if (fields.length === 0) {
14586
14937
  return;
14587
14938
  }
@@ -14689,7 +15040,8 @@ function deserializeProjectWithThreads(row, threadIds) {
14689
15040
  useCustomSystemPrompt: Boolean(row.useCustomSystemPrompt),
14690
15041
  allowAllCommands: row.allowAllCommands === null ? true : Boolean(row.allowAllCommands),
14691
15042
  allowedCommandPatterns: row.allowedCommandPatterns ? JSON.parse(row.allowedCommandPatterns) : void 0,
14692
- 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
14693
15045
  };
14694
15046
  }
14695
15047
  async function deserializeProject(db, row) {
@@ -14885,7 +15237,8 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
14885
15237
  await updateProject(dbInstance2, thread.projectId, {
14886
15238
  allowedFetchDomains: domains
14887
15239
  });
14888
- }
15240
+ },
15241
+ modelAliases: project?.modelAliases
14889
15242
  };
14890
15243
  console.log("[ChatRoute] Execution context:", {
14891
15244
  threadId,
@@ -16992,7 +17345,8 @@ async function computeContextBreakdown(threadId, conversationId, conversationMan
16992
17345
  parentProvider: thread.modelProvider ?? "",
16993
17346
  defaultSystemPrompt: promptSections.systemPrompt,
16994
17347
  onEvent: () => {
16995
- }
17348
+ },
17349
+ modelAliases: project?.modelAliases
16996
17350
  });
16997
17351
  tools.push(agentToolInstance);
16998
17352
  }
@@ -17404,6 +17758,7 @@ var MetadataManager = class {
17404
17758
  await setState(this.db, "selectedThreadId", state.selectedThreadId);
17405
17759
  await setState(this.db, "enabledModels", state.enabledModels);
17406
17760
  await setState(this.db, "enabledImageModels", state.enabledImageModels);
17761
+ await setState(this.db, "modelSettings", state.modelSettings);
17407
17762
  await setState(this.db, "providerEnvValues", state.providerEnvValues);
17408
17763
  await setState(this.db, "onboardingCompleted", state.onboardingCompleted);
17409
17764
  const encryptedKeys = {};
@@ -17441,6 +17796,7 @@ var MetadataManager = class {
17441
17796
  const enabledImageModels = mergeEnabledModelsByProviderSlug(rawEnabledImageModels);
17442
17797
  const needsEnabledModelsMigration = enabledModelsNeedMigration(rawEnabledModels, enabledModels) || enabledModelsNeedMigration(rawEnabledImageModels, enabledImageModels);
17443
17798
  const providerEnvValues = allState.providerEnvValues || {};
17799
+ const modelSettings = allState.modelSettings || {};
17444
17800
  const encryptedKeys = allState.providerKeys || {};
17445
17801
  const providerKeys = {};
17446
17802
  let needsMigration = false;
@@ -17482,7 +17838,8 @@ var MetadataManager = class {
17482
17838
  providerKeys,
17483
17839
  providerEnvValues,
17484
17840
  enabledModels,
17485
- enabledImageModels
17841
+ enabledImageModels,
17842
+ modelSettings
17486
17843
  };
17487
17844
  } catch (error) {
17488
17845
  throw new Error(`Failed to load state: ${String(error)}`);
@@ -17673,6 +18030,34 @@ var MetadataManager = class {
17673
18030
  }
17674
18031
  await this.saveState(state);
17675
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
+ }
17676
18061
  // ── Image model methods ─────────────────────────────────────────────────────
17677
18062
  async enableImageModel(provider, modelId) {
17678
18063
  const providerSlug = normalizeProviderSlug(provider);
@@ -17993,11 +18378,43 @@ async function handleGetModelInfo(c, modelManager) {
17993
18378
  }
17994
18379
  }
17995
18380
 
17996
- // src/features/models/models.routes.ts
17997
- function createModelRoutes(metadataManager) {
17998
- const router = new Hono6();
17999
- const modelManager = new ModelManager(metadataManager);
18000
- router.get("/", async (c) => {
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
+
18413
+ // src/features/models/models.routes.ts
18414
+ function createModelRoutes(metadataManager) {
18415
+ const router = new Hono6();
18416
+ const modelManager = new ModelManager(metadataManager);
18417
+ router.get("/", async (c) => {
18001
18418
  return handleGetAvailableModels(c, modelManager);
18002
18419
  });
18003
18420
  router.get("/info", async (c) => {
@@ -18015,6 +18432,9 @@ function createModelRoutes(metadataManager) {
18015
18432
  router.post("/:provider/:modelId/disable", async (c) => {
18016
18433
  return handleModelDisable(c, modelManager);
18017
18434
  });
18435
+ router.post("/:provider/:modelId/settings", async (c) => {
18436
+ return handleModelSettings(c, metadataManager, modelManager);
18437
+ });
18018
18438
  router.post("/:provider/refresh", async (c) => {
18019
18439
  return handleProviderRefresh(c, modelManager);
18020
18440
  });
@@ -18192,12 +18612,7 @@ async function handleCreateFolderProject(c, projectManager) {
18192
18612
  }
18193
18613
 
18194
18614
  // src/features/projects/projects-list.route.ts
18195
- function computeGitStatusDot(status) {
18196
- if (status.hasChanges) return true;
18197
- if (status.commitsAheadOfDefault && !status.prExists) return true;
18198
- if (status.status === "Behind" || status.status === "Diverged") return true;
18199
- return false;
18200
- }
18615
+ init_dist();
18201
18616
  async function handleListProjects(c, projectManager, threadManager, db) {
18202
18617
  try {
18203
18618
  const projects = await projectManager.listProjects();
@@ -18234,6 +18649,7 @@ async function handleListProjects(c, projectManager, threadManager, db) {
18234
18649
  useCustomSystemPrompt: project.useCustomSystemPrompt ?? false,
18235
18650
  allowAllCommands: project.allowAllCommands !== false,
18236
18651
  allowedCommandPatterns: project.allowedCommandPatterns ?? [],
18652
+ modelAliases: project.modelAliases,
18237
18653
  threads: threads.map((thread) => {
18238
18654
  const cached = statusCache.get(thread.id);
18239
18655
  const gitStatusDot = cached ? computeGitStatusDot(cached) : false;
@@ -18499,7 +18915,7 @@ async function handleOpenProject(c, projectManager) {
18499
18915
 
18500
18916
  // src/features/projects/projects-update.route.ts
18501
18917
  init_dist();
18502
- async function handleUpdateProject(c, projectManager) {
18918
+ async function handleUpdateProject(c, projectManager, metadataManager) {
18503
18919
  try {
18504
18920
  const projectId = c.req.param("id");
18505
18921
  const body = await c.req.json();
@@ -18517,7 +18933,8 @@ async function handleUpdateProject(c, projectManager) {
18517
18933
  customSystemPrompt,
18518
18934
  useCustomSystemPrompt,
18519
18935
  allowAllCommands,
18520
- allowedCommandPatterns
18936
+ allowedCommandPatterns,
18937
+ modelAliases
18521
18938
  } = body;
18522
18939
  if (!projectId) {
18523
18940
  return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Project ID is required", 400);
@@ -18581,6 +18998,17 @@ async function handleUpdateProject(c, projectManager) {
18581
18998
  allowedCommandPatterns
18582
18999
  });
18583
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
+ }
18584
19012
  if (name !== void 0) {
18585
19013
  if (!name?.trim()) {
18586
19014
  return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Project name is required", 400);
@@ -19436,7 +19864,7 @@ async function handleSyncProjectEnvVars(c, projectManager) {
19436
19864
  }
19437
19865
 
19438
19866
  // src/features/projects/projects.routes.ts
19439
- function createProjectRoutes(projectManager, threadManager) {
19867
+ function createProjectRoutes(projectManager, threadManager, metadataManager) {
19440
19868
  const router = new Hono8();
19441
19869
  router.post("/", async (c) => {
19442
19870
  return handleCreateProject(c, projectManager);
@@ -19462,7 +19890,7 @@ function createProjectRoutes(projectManager, threadManager) {
19462
19890
  return handleOpenProject(c, projectManager);
19463
19891
  });
19464
19892
  router.put("/:id", async (c) => {
19465
- return handleUpdateProject(c, projectManager);
19893
+ return handleUpdateProject(c, projectManager, metadataManager);
19466
19894
  });
19467
19895
  router.post("/:id/commands", async (c) => {
19468
19896
  return handleSaveCommand(c, projectManager);
@@ -21993,6 +22421,15 @@ ___CWD___%s
21993
22421
  }
21994
22422
  await this.metadataManager.saveProjects(projects);
21995
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
+ }
21996
22433
  /**
21997
22434
  * Starts running a dev server process for a project
21998
22435
  * @param projectId - The project ID
@@ -23934,12 +24371,7 @@ async function handleCreateThread(c, threadManager, gitManager2) {
23934
24371
  }
23935
24372
 
23936
24373
  // src/features/threads/threads-list.route.ts
23937
- function computeGitStatusDot2(status) {
23938
- if (status.hasChanges) return true;
23939
- if (status.commitsAheadOfDefault && !status.prExists) return true;
23940
- if (status.status === "Behind" || status.status === "Diverged") return true;
23941
- return false;
23942
- }
24374
+ init_dist();
23943
24375
  async function handleListThreads(c, threadManager, db) {
23944
24376
  try {
23945
24377
  const projectId = c.req.query("projectId");
@@ -23961,7 +24393,7 @@ async function handleListThreads(c, threadManager, db) {
23961
24393
  );
23962
24394
  const threadsWithStatus = threads.map((thread) => {
23963
24395
  const cached = statusCache.get(thread.id);
23964
- const gitStatusDot = cached ? computeGitStatusDot2(cached) : false;
24396
+ const gitStatusDot = cached ? computeGitStatusDot(cached) : false;
23965
24397
  return { ...thread, gitStatusDot };
23966
24398
  });
23967
24399
  return c.json(threadsWithStatus);
@@ -25197,20 +25629,43 @@ async function handleFixComments(c, threadManager) {
25197
25629
 
25198
25630
  // src/features/threads/threads-ai-files.route.ts
25199
25631
  init_dist();
25200
- import { readFile as readFile16, writeFile as writeFile8, mkdir as mkdir9, access as access5, rm as rm7, stat as stat6 } from "fs/promises";
25201
- 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";
25202
25634
  import { existsSync as existsSync19 } from "fs";
25203
25635
 
25204
25636
  // src/features/git/git-download-folder.ts
25205
25637
  import { mkdir as mkdir8, writeFile as writeFile7 } from "fs/promises";
25206
25638
  import { join as join32, dirname as dirname6 } from "path";
25207
- async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25208
- const { token, ref, excludeRelativePrefixes = [] } = options;
25639
+
25640
+ // src/features/git/github-folder.ts
25641
+ function parseGithubRepoUrl(repoUrl) {
25209
25642
  const match = repoUrl.replace(/\.git$/, "").match(/github\.com\/([^/]+)\/([^/]+)/);
25210
25643
  if (!match) {
25211
25644
  throw new Error(`Invalid GitHub URL: ${repoUrl}`);
25212
25645
  }
25213
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) {
25214
25669
  const headers = {
25215
25670
  Accept: "application/vnd.github+json",
25216
25671
  "X-GitHub-Api-Version": "2022-11-28"
@@ -25218,42 +25673,63 @@ async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25218
25673
  if (token) {
25219
25674
  headers["Authorization"] = `Bearer ${token}`;
25220
25675
  }
25221
- const refParam = ref ?? "HEAD";
25676
+ return headers;
25677
+ }
25678
+ async function fetchGithubTree(owner, repo, refParam, token) {
25222
25679
  const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${refParam}?recursive=1`;
25223
- const treeRes = await fetch(treeUrl, { headers });
25680
+ const treeRes = await fetch(treeUrl, { headers: buildGithubHeaders(token) });
25224
25681
  if (!treeRes.ok) {
25225
25682
  throw new Error(`GitHub API error ${treeRes.status}: ${await treeRes.text()}`);
25226
25683
  }
25227
- 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);
25228
25691
  if (tree.truncated) {
25229
25692
  console.warn(
25230
25693
  "Warning: the repository tree was truncated by GitHub. Large repos may be incomplete."
25231
25694
  );
25232
25695
  }
25233
- const prefix = srcPath.replace(/^\/|\/$/g, "") + "/";
25234
- const files = tree.tree.filter((entry) => {
25235
- if (entry.type !== "blob" || !entry.path.startsWith(prefix)) {
25236
- return false;
25237
- }
25238
- const relativePath = entry.path.slice(prefix.length);
25239
- return !excludeRelativePrefixes.some((skipPrefix) => relativePath.startsWith(skipPrefix));
25240
- });
25696
+ const files = filterFolderBlobPaths(tree.tree, srcPath, excludeRelativePrefixes);
25241
25697
  if (files.length === 0) {
25242
25698
  throw new Error(`No files found at path "${srcPath}" in ${owner}/${repo}@${refParam}`);
25243
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);
25244
25723
  console.log(`Downloading ${files.length} file(s) from ${owner}/${repo}/${srcPath} \u2192 ${destPath}`);
25245
25724
  await Promise.all(
25246
- files.map(async (file) => {
25247
- const relativePath = file.path.slice(prefix.length);
25725
+ files.map(async (relativePath) => {
25248
25726
  const localPath = join32(destPath, relativePath);
25249
25727
  await mkdir8(dirname6(localPath), { recursive: true });
25250
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${refParam}/${file.path}`;
25251
- const fileRes = await fetch(rawUrl, { headers });
25252
- if (!fileRes.ok) {
25253
- throw new Error(`Failed to download ${file.path}: HTTP ${fileRes.status}`);
25254
- }
25255
- let content = Buffer.from(await fileRes.arrayBuffer()).toString("utf8");
25256
- content = content.replace(/Claude/gi, "Tarsk");
25728
+ const content = await fetchGithubFolderFile(repoUrl, srcPath, relativePath, {
25729
+ ...options,
25730
+ ref: ref ?? refParam,
25731
+ excludeRelativePrefixes
25732
+ });
25257
25733
  await writeFile7(localPath, content, "utf8");
25258
25734
  console.log(` \u2713 ${relativePath}`);
25259
25735
  })
@@ -25261,6 +25737,433 @@ async function downloadGithubFolder(repoUrl, srcPath, destPath, options = {}) {
25261
25737
  console.log("Done.");
25262
25738
  }
25263
25739
 
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}`
25788
+ );
25789
+ }
25790
+ }
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
+
25264
26167
  // src/features/threads/threads-ai-files.route.ts
25265
26168
  async function handleGetThreadAIFiles(c, threadManager) {
25266
26169
  try {
@@ -25363,7 +26266,7 @@ Links to important documentation, tools, or references.
25363
26266
  400
25364
26267
  );
25365
26268
  }
25366
- const content = await readFile16(absPath, "utf-8");
26269
+ const content = await readFile17(absPath, "utf-8");
25367
26270
  return c.json({ content, path: filePath });
25368
26271
  } catch (error) {
25369
26272
  return errorResponse(
@@ -25402,7 +26305,7 @@ async function handleSaveThreadAIFile(c, threadManager) {
25402
26305
  if (!absPath) {
25403
26306
  return c.json({ error: { code: "BAD_REQUEST", message: "Invalid file path" } }, 400);
25404
26307
  }
25405
- const parentDir = join33(absPath, "..");
26308
+ const parentDir = join34(absPath, "..");
25406
26309
  await mkdir9(parentDir, { recursive: true });
25407
26310
  await writeFile8(absPath, content, "utf-8");
25408
26311
  return c.json({ success: true, path: filePath });
@@ -25445,7 +26348,7 @@ async function handleDeleteThreadAIFile(c, threadManager) {
25445
26348
  404
25446
26349
  );
25447
26350
  }
25448
- await rm7(absPath, { recursive: true, force: true });
26351
+ await rm8(absPath, { recursive: true, force: true });
25449
26352
  return c.json({ success: true, path: filePath });
25450
26353
  } catch (error) {
25451
26354
  return errorResponse(
@@ -25491,10 +26394,10 @@ async function handleCreateThreadAgent(c, threadManager) {
25491
26394
  if (!thread) {
25492
26395
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25493
26396
  }
25494
- const agentRelPath = join33(".agents", "agents", name);
25495
- const agentAbsPath = join33(thread.path, agentRelPath);
25496
- const agentFileRelPath = join33(agentRelPath, "AGENT.md");
25497
- 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");
25498
26401
  if (existsSync19(agentAbsPath)) {
25499
26402
  return c.json(
25500
26403
  { error: { code: "CONFLICT", message: `Agent '${name}' already exists` } },
@@ -25561,10 +26464,10 @@ async function handleCreateThreadSkill(c, threadManager) {
25561
26464
  if (!thread) {
25562
26465
  return errorResponse(c, ErrorCodes.THREAD_NOT_FOUND, `Thread not found: ${threadId}`, 404);
25563
26466
  }
25564
- const skillRelPath = join33(".agents", "skills", name);
25565
- const skillAbsPath = join33(thread.path, skillRelPath);
25566
- const skillFileRelPath = join33(skillRelPath, "SKILL.md");
25567
- 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");
25568
26471
  if (existsSync19(skillAbsPath)) {
25569
26472
  return c.json(
25570
26473
  { error: { code: "CONFLICT", message: `Skill '${name}' already exists` } },
@@ -25607,7 +26510,7 @@ async function handleInstallGithubFolder(c, threadManager) {
25607
26510
  } catch {
25608
26511
  return c.json({ error: { code: "BAD_REQUEST", message: "Invalid JSON body" } }, 400);
25609
26512
  }
25610
- const { gitHubRepo, gitHubFolder, destPath } = body;
26513
+ const { gitHubRepo, gitHubFolder, destPath, gitHubRef } = body;
25611
26514
  if (!gitHubRepo || !gitHubFolder || !destPath) {
25612
26515
  return c.json(
25613
26516
  {
@@ -25627,7 +26530,9 @@ async function handleInstallGithubFolder(c, threadManager) {
25627
26530
  if (!absDestPath) {
25628
26531
  return c.json({ error: { code: "BAD_REQUEST", message: "Invalid destPath" } }, 400);
25629
26532
  }
25630
- await downloadGithubFolder(gitHubRepo, gitHubFolder, absDestPath);
26533
+ await downloadGithubFolder(gitHubRepo, gitHubFolder, absDestPath, {
26534
+ ref: gitHubRef
26535
+ });
25631
26536
  return c.json({ success: true, path: destPath });
25632
26537
  } catch (error) {
25633
26538
  return errorResponse(
@@ -25639,6 +26544,50 @@ async function handleInstallGithubFolder(c, threadManager) {
25639
26544
  );
25640
26545
  }
25641
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
+ }
25642
26591
 
25643
26592
  // src/features/threads/threads-project-scripts.route.ts
25644
26593
  init_database();
@@ -25953,6 +26902,9 @@ function createThreadRoutes(threadManager, gitManager2, conversationManager) {
25953
26902
  router.post("/:id/ai-files/github-folder", async (c) => {
25954
26903
  return handleInstallGithubFolder(c, threadManager);
25955
26904
  });
26905
+ router.post("/:id/ai-files/catalog-skill", async (c) => {
26906
+ return handleInstallCatalogSkill(c, threadManager);
26907
+ });
25956
26908
  return router;
25957
26909
  }
25958
26910
 
@@ -27916,7 +28868,7 @@ async function gitCheckoutBranchHandler(c, metadataManager) {
27916
28868
  import { unlinkSync } from "fs";
27917
28869
  import { resolve as resolve6 } from "path";
27918
28870
  init_utils();
27919
- function runGit2(args2, cwd) {
28871
+ function runGit3(args2, cwd) {
27920
28872
  return new Promise((resolve8, reject) => {
27921
28873
  const proc = spawnProcess("git", args2, { cwd });
27922
28874
  let out = "";
@@ -27952,7 +28904,7 @@ async function gitRevertFileHandler(c, metadataManager) {
27952
28904
  } catch {
27953
28905
  return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
27954
28906
  }
27955
- const statusOutput = await runGit2(["status", "--porcelain", "--", filePath], gitRoot).catch(
28907
+ const statusOutput = await runGit3(["status", "--porcelain", "--", filePath], gitRoot).catch(
27956
28908
  () => ""
27957
28909
  );
27958
28910
  const statusCode = statusOutput.trim().substring(0, 2);
@@ -27962,13 +28914,13 @@ async function gitRevertFileHandler(c, metadataManager) {
27962
28914
  } catch {
27963
28915
  }
27964
28916
  } else if (statusCode === "A ") {
27965
- await runGit2(["rm", "--cached", "--", filePath], gitRoot);
28917
+ await runGit3(["rm", "--cached", "--", filePath], gitRoot);
27966
28918
  try {
27967
28919
  unlinkSync(resolve6(gitRoot, filePath));
27968
28920
  } catch {
27969
28921
  }
27970
28922
  } else {
27971
- await runGit2(["checkout", "HEAD", "--", filePath], gitRoot);
28923
+ await runGit3(["checkout", "HEAD", "--", filePath], gitRoot);
27972
28924
  }
27973
28925
  return c.json({ success: true });
27974
28926
  } catch (error) {
@@ -28438,8 +29390,8 @@ function createUpdateRoutes(updater) {
28438
29390
 
28439
29391
  // src/features/mcp/mcp.routes.ts
28440
29392
  import { Hono as Hono15 } from "hono";
28441
- import { readFile as readFile17, writeFile as writeFile9, mkdir as mkdir11, access as access6 } from "fs/promises";
28442
- 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";
28443
29395
 
28444
29396
  // src/features/mcp/mcp.popular.json
28445
29397
  var mcp_popular_default = [
@@ -28561,10 +29513,10 @@ var mcp_popular_default = [
28561
29513
  var MCP_CONFIG_PATHS2 = [".agents/mcp.json", "mcp.json"];
28562
29514
  async function readMCPConfig(projectPath) {
28563
29515
  for (const configPath of MCP_CONFIG_PATHS2) {
28564
- const fullPath = join34(projectPath, configPath);
29516
+ const fullPath = join35(projectPath, configPath);
28565
29517
  try {
28566
29518
  await access6(fullPath);
28567
- const content = await readFile17(fullPath, "utf-8");
29519
+ const content = await readFile18(fullPath, "utf-8");
28568
29520
  const config = JSON.parse(content);
28569
29521
  return { config, filePath: fullPath };
28570
29522
  } catch {
@@ -28575,7 +29527,7 @@ async function readMCPConfig(projectPath) {
28575
29527
  }
28576
29528
  async function writeMCPConfig(projectPath, config) {
28577
29529
  const existing = await readMCPConfig(projectPath);
28578
- const targetPath = existing?.filePath ?? join34(projectPath, ".agents/mcp.json");
29530
+ const targetPath = existing?.filePath ?? join35(projectPath, ".agents/mcp.json");
28579
29531
  await mkdir11(dirname8(targetPath), { recursive: true });
28580
29532
  await writeFile9(targetPath, JSON.stringify(config, null, 2), "utf-8");
28581
29533
  }
@@ -28778,11 +29730,328 @@ var skills_popular_default = [
28778
29730
  }
28779
29731
  ];
28780
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
+
28781
29846
  // src/features/skills/skills.routes.ts
28782
29847
  function createSkillRoutes() {
28783
29848
  const router = new Hono16();
28784
29849
  router.get("/popular", (c) => {
28785
- 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
+ }
28786
30055
  });
28787
30056
  return router;
28788
30057
  }
@@ -28992,7 +30261,7 @@ function getLocalNetworkAddresses() {
28992
30261
 
28993
30262
  // src/core/server-bind-mode-store.ts
28994
30263
  import { existsSync as existsSync25, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync } from "fs";
28995
- import { join as join35 } from "path";
30264
+ import { join as join37 } from "path";
28996
30265
 
28997
30266
  // src/core/server-bind-mode.ts
28998
30267
  var DEFAULT_SERVER_BIND_MODE = "local";
@@ -29004,7 +30273,7 @@ function getServerBindHostname(mode) {
29004
30273
  }
29005
30274
 
29006
30275
  // src/core/server-bind-mode-store.ts
29007
- 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");
29008
30277
  function hasServerBindModeFile() {
29009
30278
  return existsSync25(SERVER_BIND_MODE_FILE);
29010
30279
  }
@@ -29358,7 +30627,7 @@ async function clipboardWriteImage(c) {
29358
30627
 
29359
30628
  // src/features/image/image-save.route.ts
29360
30629
  init_dist();
29361
- import { join as join36 } from "path";
30630
+ import { join as join38 } from "path";
29362
30631
  var Utils5 = null;
29363
30632
  var utilsLoaded5 = false;
29364
30633
  async function loadUtils5() {
@@ -29396,7 +30665,7 @@ async function saveImage(c) {
29396
30665
  return c.json({ error: "imageUrl or imageData is required" }, 400);
29397
30666
  }
29398
30667
  const name = filename ?? `generated-image-${Date.now()}.png`;
29399
- const savePath = join36(Utils5.paths.downloads, name);
30668
+ const savePath = join38(Utils5.paths.downloads, name);
29400
30669
  await Bun.write(savePath, data);
29401
30670
  return c.json({ success: true, path: savePath });
29402
30671
  } catch (error) {
@@ -29870,7 +31139,7 @@ async function startTarskServerInternal(options) {
29870
31139
  processingThreadIds: processingStateManager.getProcessingThreadIds()
29871
31140
  });
29872
31141
  });
29873
- app.route("/api/projects", createProjectRoutes(projectManager, threadManager));
31142
+ app.route("/api/projects", createProjectRoutes(projectManager, threadManager, metadataManager));
29874
31143
  app.route("/api/projects", createRunRoutes(projectManager));
29875
31144
  app.route("/api/projects", createProjectTodosRoutes(metadataManager));
29876
31145
  app.route("/api/threads", createThreadRoutes(threadManager, gitManager2, conversationManager));