@openchamber/web 1.4.6 → 1.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{ToolOutputDialog-Bk6Uzhck.js → ToolOutputDialog-Dn1Yyik_.js} +1 -1
- package/dist/assets/{index-_QJSNcFo.js → index-CyM2ZMJa.js} +2 -2
- package/dist/assets/{index-Cxzt1pIT.css → index-RdQawb7R.css} +1 -1
- package/dist/assets/main-hUJJZcBf.js +128 -0
- package/dist/assets/{vendor-.bun-C07YQe9X.js → vendor-.bun-94fMDU1C.js} +16 -16
- package/dist/index.html +63 -4
- package/package.json +6 -4
- package/server/index.js +243 -51
- package/server/lib/git-credentials.js +74 -0
- package/server/lib/git-identity-storage.js +2 -0
- package/server/lib/git-service.js +23 -1
- package/server/lib/skills-catalog/clawdhub/api.js +129 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +200 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +73 -0
- package/server/lib/skills-catalog/curated-sources.js +9 -1
- package/dist/assets/main-COr_R8wu.js +0 -128
package/dist/index.html
CHANGED
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
<link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png" />
|
|
16
16
|
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png" />
|
|
17
17
|
|
|
18
|
+
<!-- Preload Nerd Fonts for terminal icon display -->
|
|
19
|
+
<link rel="preload" href="https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2"
|
|
20
|
+
as="font" type="font/woff2" crossorigin="anonymous">
|
|
21
|
+
<link rel="preload" href="https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2"
|
|
22
|
+
as="font" type="font/woff2" crossorigin="anonymous">
|
|
23
|
+
|
|
18
24
|
<!-- Web app manifest (data URL to avoid nginx auth issues) -->
|
|
19
25
|
<script>
|
|
20
26
|
const baseUrl = location.origin;
|
|
@@ -119,8 +125,33 @@
|
|
|
119
125
|
<meta name="application-name" content="OpenChamber" />
|
|
120
126
|
<meta name="apple-mobile-web-app-title" content="OpenChamber" />
|
|
121
127
|
|
|
122
|
-
<!-- Inline CSS for loading screen (before Tailwind loads) -->
|
|
128
|
+
<!-- Inline CSS for loading screen and Nerd Fonts (before Tailwind loads) -->
|
|
123
129
|
<style>
|
|
130
|
+
/* Nerd Font @font-face declarations for terminal icon support */
|
|
131
|
+
@font-face {
|
|
132
|
+
font-family: 'JetBrainsMono Nerd Font';
|
|
133
|
+
src:
|
|
134
|
+
local('JetBrainsMono Nerd Font'),
|
|
135
|
+
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2') format('woff2'),
|
|
136
|
+
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff') format('woff');
|
|
137
|
+
font-weight: normal;
|
|
138
|
+
font-style: normal;
|
|
139
|
+
font-display: swap;
|
|
140
|
+
unicode-range: U+E000-F8FF, U+F0000-FFFFF;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@font-face {
|
|
144
|
+
font-family: 'FiraCode Nerd Font';
|
|
145
|
+
src:
|
|
146
|
+
local('FiraCode Nerd Font'),
|
|
147
|
+
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2') format('woff2'),
|
|
148
|
+
url('https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff') format('woff');
|
|
149
|
+
font-weight: normal;
|
|
150
|
+
font-style: normal;
|
|
151
|
+
font-display: swap;
|
|
152
|
+
unicode-range: U+E000-F8FF, U+F0000-FFFFF;
|
|
153
|
+
}
|
|
154
|
+
|
|
124
155
|
:root {
|
|
125
156
|
--splash-background: #151313;
|
|
126
157
|
--splash-stroke: white;
|
|
@@ -160,10 +191,10 @@
|
|
|
160
191
|
pointer-events: none;
|
|
161
192
|
}
|
|
162
193
|
</style>
|
|
163
|
-
<script type="module" crossorigin src="/assets/index-
|
|
164
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-
|
|
194
|
+
<script type="module" crossorigin src="/assets/index-CyM2ZMJa.js"></script>
|
|
195
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-94fMDU1C.js">
|
|
165
196
|
<link rel="stylesheet" crossorigin href="/assets/vendor--Jn2c0Clh.css">
|
|
166
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
197
|
+
<link rel="stylesheet" crossorigin href="/assets/index-RdQawb7R.css">
|
|
167
198
|
</head>
|
|
168
199
|
<body class="h-full bg-background text-foreground">
|
|
169
200
|
<div id="root" class="h-full">
|
|
@@ -243,6 +274,34 @@
|
|
|
243
274
|
}, 10000);
|
|
244
275
|
</script>
|
|
245
276
|
|
|
277
|
+
<!-- CSS Font Loading API for reliable Nerd Font loading -->
|
|
278
|
+
<script>
|
|
279
|
+
(function() {
|
|
280
|
+
const fonts = [
|
|
281
|
+
{
|
|
282
|
+
name: 'JetBrainsMono Nerd Font',
|
|
283
|
+
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/JetBrainsMonoNerdFont-Regular.woff2'
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'FiraCode Nerd Font',
|
|
287
|
+
url: 'https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/FiraCodeNerdFont-Regular.woff2'
|
|
288
|
+
}
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const fontPromises = fonts.map(font => {
|
|
292
|
+
const fontFace = new FontFace(font.name, `url(${font.url}) format('woff2')`);
|
|
293
|
+
document.fonts.add(fontFace);
|
|
294
|
+
return fontFace.load().catch(err => {
|
|
295
|
+
console.warn(`Failed to load font: ${font.name}`, err);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
Promise.allSettled(fontPromises).then(() => {
|
|
300
|
+
document.documentElement.classList.add('fonts-loaded');
|
|
301
|
+
});
|
|
302
|
+
})();
|
|
303
|
+
</script>
|
|
304
|
+
|
|
246
305
|
<!-- Polyfill for process before loading React -->
|
|
247
306
|
<script>
|
|
248
307
|
if (typeof process === 'undefined') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openchamber/web",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./server/index.js",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"start": "node bin/cli.js serve"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"qrcode-terminal": "^0.12.0",
|
|
26
25
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
|
27
26
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
|
28
27
|
"@ibm/plex": "^6.4.1",
|
|
@@ -38,16 +37,18 @@
|
|
|
38
37
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
39
38
|
"@remixicon/react": "^4.7.0",
|
|
40
39
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
41
|
-
"
|
|
40
|
+
"adm-zip": "^0.5.16",
|
|
41
|
+
"bun-pty": "^0.4.5",
|
|
42
42
|
"class-variance-authority": "^0.7.1",
|
|
43
43
|
"clsx": "^2.1.1",
|
|
44
44
|
"cmdk": "^1.1.1",
|
|
45
45
|
"express": "^5.1.0",
|
|
46
|
+
"ghostty-web": "0.3.0",
|
|
46
47
|
"http-proxy-middleware": "^3.0.5",
|
|
47
48
|
"jsonc-parser": "^3.3.1",
|
|
48
49
|
"next-themes": "^0.4.6",
|
|
49
|
-
"bun-pty": "^0.4.5",
|
|
50
50
|
"node-pty": "^1.1.0",
|
|
51
|
+
"qrcode-terminal": "^0.12.0",
|
|
51
52
|
"react": "^19.1.1",
|
|
52
53
|
"react-dom": "^19.1.1",
|
|
53
54
|
"react-markdown": "^10.1.0",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@eslint/js": "^9.33.0",
|
|
65
66
|
"@tailwindcss/postcss": "^4.0.0",
|
|
67
|
+
"@types/adm-zip": "^0.5.7",
|
|
66
68
|
"@types/node": "^24.3.1",
|
|
67
69
|
"@types/react": "^19.1.10",
|
|
68
70
|
"@types/react-dom": "^19.1.7",
|
package/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
4
|
+
import { spawn, spawnSync } from 'child_process';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import http from 'http';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
@@ -39,6 +39,9 @@ const FILE_SEARCH_EXCLUDED_DIRS = new Set([
|
|
|
39
39
|
'logs'
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
|
+
// Lock to prevent race conditions in persistSettings
|
|
43
|
+
let persistSettingsLock = Promise.resolve();
|
|
44
|
+
|
|
42
45
|
const normalizeDirectoryPath = (value) => {
|
|
43
46
|
if (typeof value !== 'string') {
|
|
44
47
|
return value;
|
|
@@ -608,6 +611,10 @@ const sanitizeSettingsUpdate = (payload) => {
|
|
|
608
611
|
if (typeof candidate.autoCreateWorktree === 'boolean') {
|
|
609
612
|
result.autoCreateWorktree = candidate.autoCreateWorktree;
|
|
610
613
|
}
|
|
614
|
+
if (typeof candidate.commitMessageModel === 'string') {
|
|
615
|
+
const trimmed = candidate.commitMessageModel.trim();
|
|
616
|
+
result.commitMessageModel = trimmed.length > 0 ? trimmed : undefined;
|
|
617
|
+
}
|
|
611
618
|
|
|
612
619
|
const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
|
|
613
620
|
if (skillCatalogs) {
|
|
@@ -696,30 +703,39 @@ const formatSettingsResponse = (settings) => {
|
|
|
696
703
|
};
|
|
697
704
|
|
|
698
705
|
const validateProjectEntries = async (projects) => {
|
|
706
|
+
console.log(`[validateProjectEntries] Starting validation for ${projects.length} projects`);
|
|
707
|
+
|
|
699
708
|
if (!Array.isArray(projects)) {
|
|
709
|
+
console.warn(`[validateProjectEntries] Input is not an array, returning empty`);
|
|
700
710
|
return [];
|
|
701
711
|
}
|
|
702
712
|
|
|
703
713
|
const results = [];
|
|
704
714
|
for (const project of projects) {
|
|
705
715
|
if (!project || typeof project.path !== 'string' || project.path.length === 0) {
|
|
716
|
+
console.error(`[validateProjectEntries] Invalid project entry: missing or empty path`, project);
|
|
706
717
|
continue;
|
|
707
718
|
}
|
|
708
719
|
try {
|
|
709
720
|
const stats = await fsPromises.stat(project.path);
|
|
710
721
|
if (!stats.isDirectory()) {
|
|
722
|
+
console.error(`[validateProjectEntries] Project path is not a directory: ${project.path}`);
|
|
711
723
|
continue;
|
|
712
724
|
}
|
|
713
725
|
results.push(project);
|
|
714
726
|
} catch (error) {
|
|
715
727
|
const err = error;
|
|
728
|
+
console.error(`[validateProjectEntries] Failed to validate project "${project.path}": ${err.code || err.message || err}`);
|
|
716
729
|
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
730
|
+
console.log(`[validateProjectEntries] Removing project with ENOENT: ${project.path}`);
|
|
717
731
|
continue;
|
|
718
732
|
}
|
|
719
|
-
|
|
733
|
+
console.log(`[validateProjectEntries] Keeping project despite non-ENOENT error: ${project.path}`);
|
|
734
|
+
results.push(project);
|
|
720
735
|
}
|
|
721
736
|
}
|
|
722
737
|
|
|
738
|
+
console.log(`[validateProjectEntries] Validation complete: ${results.length}/${projects.length} projects valid`);
|
|
723
739
|
return results;
|
|
724
740
|
};
|
|
725
741
|
|
|
@@ -794,27 +810,39 @@ const readSettingsFromDiskMigrated = async () => {
|
|
|
794
810
|
};
|
|
795
811
|
|
|
796
812
|
const persistSettings = async (changes) => {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
803
|
-
next =
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
813
|
+
// Serialize concurrent calls using lock
|
|
814
|
+
persistSettingsLock = persistSettingsLock.then(async () => {
|
|
815
|
+
console.log(`[persistSettings] Called with changes:`, JSON.stringify(changes, null, 2));
|
|
816
|
+
const current = await readSettingsFromDisk();
|
|
817
|
+
console.log(`[persistSettings] Current projects count:`, Array.isArray(current.projects) ? current.projects.length : 'N/A');
|
|
818
|
+
const sanitized = sanitizeSettingsUpdate(changes);
|
|
819
|
+
let next = mergePersistedSettings(current, sanitized);
|
|
820
|
+
|
|
821
|
+
if (Array.isArray(next.projects)) {
|
|
822
|
+
console.log(`[persistSettings] Validating ${next.projects.length} projects...`);
|
|
823
|
+
const validated = await validateProjectEntries(next.projects);
|
|
824
|
+
console.log(`[persistSettings] After validation: ${validated.length} projects remain`);
|
|
825
|
+
next = { ...next, projects: validated };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (Array.isArray(next.projects) && next.projects.length > 0) {
|
|
829
|
+
const activeId = typeof next.activeProjectId === 'string' ? next.activeProjectId : '';
|
|
830
|
+
const active = next.projects.find((project) => project.id === activeId) || null;
|
|
831
|
+
if (!active) {
|
|
832
|
+
console.log(`[persistSettings] Active project ID ${activeId} not found, switching to ${next.projects[0].id}`);
|
|
833
|
+
next = { ...next, activeProjectId: next.projects[0].id };
|
|
834
|
+
}
|
|
835
|
+
} else if (next.activeProjectId) {
|
|
836
|
+
console.log(`[persistSettings] No projects found, clearing activeProjectId ${next.activeProjectId}`);
|
|
837
|
+
next = { ...next, activeProjectId: undefined };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
await writeSettingsToDisk(next);
|
|
841
|
+
console.log(`[persistSettings] Successfully saved ${next.projects?.length || 0} projects to disk`);
|
|
842
|
+
return formatSettingsResponse(next);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
return persistSettingsLock;
|
|
818
846
|
};
|
|
819
847
|
|
|
820
848
|
// HMR-persistent state via globalThis
|
|
@@ -945,6 +973,70 @@ function setOpenCodePort(port) {
|
|
|
945
973
|
lastOpenCodeError = null;
|
|
946
974
|
}
|
|
947
975
|
|
|
976
|
+
async function waitForOpenCodePort(timeoutMs = 15000) {
|
|
977
|
+
if (openCodePort !== null) {
|
|
978
|
+
return openCodePort;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const deadline = Date.now() + timeoutMs;
|
|
982
|
+
while (Date.now() < deadline) {
|
|
983
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
984
|
+
if (openCodePort !== null) {
|
|
985
|
+
return openCodePort;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
throw new Error('Timed out waiting for OpenCode port');
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function getLoginShellPath() {
|
|
993
|
+
if (process.platform === 'win32') {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const shell = process.env.SHELL || '/bin/zsh';
|
|
998
|
+
const shellName = path.basename(shell);
|
|
999
|
+
|
|
1000
|
+
// Nushell requires different flag syntax and PATH access
|
|
1001
|
+
const isNushell = shellName === 'nu' || shellName === 'nushell';
|
|
1002
|
+
const args = isNushell
|
|
1003
|
+
? ['-l', '-i', '-c', '$env.PATH | str join (char esep)']
|
|
1004
|
+
: ['-lic', 'echo -n "$PATH"'];
|
|
1005
|
+
|
|
1006
|
+
try {
|
|
1007
|
+
const result = spawnSync(shell, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1008
|
+
if (result.status === 0 && typeof result.stdout === 'string') {
|
|
1009
|
+
const value = result.stdout.trim();
|
|
1010
|
+
if (value) {
|
|
1011
|
+
return value;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
// ignore
|
|
1016
|
+
}
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function buildAugmentedPath() {
|
|
1021
|
+
const augmented = new Set();
|
|
1022
|
+
|
|
1023
|
+
const loginShellPath = getLoginShellPath();
|
|
1024
|
+
if (loginShellPath) {
|
|
1025
|
+
for (const segment of loginShellPath.split(path.delimiter)) {
|
|
1026
|
+
if (segment) {
|
|
1027
|
+
augmented.add(segment);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const current = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
1033
|
+
for (const segment of current) {
|
|
1034
|
+
augmented.add(segment);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return Array.from(augmented).join(path.delimiter);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
948
1040
|
const API_PREFIX_CANDIDATES = ['', '/api']; // Simplified - only check root and /api
|
|
949
1041
|
|
|
950
1042
|
async function waitForReady(url, timeoutMs = 10000) {
|
|
@@ -1794,11 +1886,11 @@ function startHealthMonitoring() {
|
|
|
1794
1886
|
}
|
|
1795
1887
|
|
|
1796
1888
|
healthCheckInterval = setInterval(async () => {
|
|
1797
|
-
if (!openCodeProcess || isShuttingDown) return;
|
|
1889
|
+
if (!openCodeProcess || isShuttingDown || isRestartingOpenCode) return;
|
|
1798
1890
|
|
|
1799
1891
|
try {
|
|
1800
|
-
|
|
1801
|
-
if (
|
|
1892
|
+
const healthy = await isOpenCodeProcessHealthy();
|
|
1893
|
+
if (!healthy) {
|
|
1802
1894
|
console.log('OpenCode process not running, restarting...');
|
|
1803
1895
|
await restartOpenCode();
|
|
1804
1896
|
}
|
|
@@ -1822,28 +1914,29 @@ async function gracefulShutdown(options = {}) {
|
|
|
1822
1914
|
|
|
1823
1915
|
if (openCodeProcess) {
|
|
1824
1916
|
console.log('Stopping OpenCode process...');
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
}, SHUTDOWN_TIMEOUT);
|
|
1832
|
-
|
|
1833
|
-
openCodeProcess.on('exit', () => {
|
|
1834
|
-
clearTimeout(timeout);
|
|
1835
|
-
resolve();
|
|
1836
|
-
});
|
|
1837
|
-
});
|
|
1917
|
+
try {
|
|
1918
|
+
openCodeProcess.close();
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
console.warn('Error closing OpenCode process:', error);
|
|
1921
|
+
}
|
|
1922
|
+
openCodeProcess = null;
|
|
1838
1923
|
}
|
|
1839
1924
|
|
|
1840
1925
|
if (server) {
|
|
1841
|
-
await
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1926
|
+
await Promise.race([
|
|
1927
|
+
new Promise((resolve) => {
|
|
1928
|
+
server.close(() => {
|
|
1929
|
+
console.log('HTTP server closed');
|
|
1930
|
+
resolve();
|
|
1931
|
+
});
|
|
1932
|
+
}),
|
|
1933
|
+
new Promise((resolve) => {
|
|
1934
|
+
setTimeout(() => {
|
|
1935
|
+
console.warn('Server close timeout reached, forcing shutdown');
|
|
1936
|
+
resolve();
|
|
1937
|
+
}, SHUTDOWN_TIMEOUT);
|
|
1938
|
+
})
|
|
1939
|
+
]);
|
|
1847
1940
|
}
|
|
1848
1941
|
|
|
1849
1942
|
if (uiAuthController) {
|
|
@@ -1883,7 +1976,7 @@ async function main(options = {}) {
|
|
|
1883
1976
|
status: 'ok',
|
|
1884
1977
|
timestamp: new Date().toISOString(),
|
|
1885
1978
|
openCodePort: openCodePort,
|
|
1886
|
-
openCodeRunning: Boolean(
|
|
1979
|
+
openCodeRunning: Boolean(openCodePort && isOpenCodeReady && !isRestartingOpenCode),
|
|
1887
1980
|
openCodeApiPrefix,
|
|
1888
1981
|
openCodeApiPrefixDetected,
|
|
1889
1982
|
isOpenCodeReady,
|
|
@@ -2337,11 +2430,15 @@ async function main(options = {}) {
|
|
|
2337
2430
|
});
|
|
2338
2431
|
|
|
2339
2432
|
app.put('/api/config/settings', async (req, res) => {
|
|
2433
|
+
console.log(`[API:PUT /api/config/settings] Received request`);
|
|
2434
|
+
console.log(`[API:PUT /api/config/settings] Request body:`, JSON.stringify(req.body, null, 2));
|
|
2340
2435
|
try {
|
|
2341
2436
|
const updated = await persistSettings(req.body ?? {});
|
|
2437
|
+
console.log(`[API:PUT /api/config/settings] Success, returning ${updated.projects?.length || 0} projects`);
|
|
2342
2438
|
res.json(updated);
|
|
2343
2439
|
} catch (error) {
|
|
2344
|
-
console.error(
|
|
2440
|
+
console.error(`[API:PUT /api/config/settings] Failed to save settings:`, error);
|
|
2441
|
+
console.error(`[API:PUT /api/config/settings] Error stack:`, error.stack);
|
|
2345
2442
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to save settings' });
|
|
2346
2443
|
}
|
|
2347
2444
|
});
|
|
@@ -2643,6 +2740,7 @@ async function main(options = {}) {
|
|
|
2643
2740
|
const { parseSkillRepoSource } = await import('./lib/skills-catalog/source.js');
|
|
2644
2741
|
const { scanSkillsRepository } = await import('./lib/skills-catalog/scan.js');
|
|
2645
2742
|
const { installSkillsFromRepository } = await import('./lib/skills-catalog/install.js');
|
|
2743
|
+
const { scanClawdHub, installSkillsFromClawdHub, isClawdHubSource } = await import('./lib/skills-catalog/clawdhub/index.js');
|
|
2646
2744
|
const { getProfiles, getProfile } = await import('./lib/git-identity-storage.js');
|
|
2647
2745
|
|
|
2648
2746
|
const listGitIdentitiesForResponse = () => {
|
|
@@ -2699,6 +2797,37 @@ async function main(options = {}) {
|
|
|
2699
2797
|
const itemsBySource = {};
|
|
2700
2798
|
|
|
2701
2799
|
for (const src of sources) {
|
|
2800
|
+
// Handle ClawdHub sources separately (API-based, not git-based)
|
|
2801
|
+
if (src.sourceType === 'clawdhub' || isClawdHubSource(src.source)) {
|
|
2802
|
+
const cacheKey = 'clawdhub:registry';
|
|
2803
|
+
let scanResult = !refresh ? getCachedScan(cacheKey) : null;
|
|
2804
|
+
|
|
2805
|
+
if (!scanResult) {
|
|
2806
|
+
const scanned = await scanClawdHub();
|
|
2807
|
+
if (!scanned.ok) {
|
|
2808
|
+
itemsBySource[src.id] = [];
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
scanResult = scanned;
|
|
2812
|
+
setCachedScan(cacheKey, scanResult);
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
const items = (scanResult.items || []).map((item) => {
|
|
2816
|
+
const installed = installedByName.get(item.skillName);
|
|
2817
|
+
return {
|
|
2818
|
+
...item,
|
|
2819
|
+
sourceId: src.id,
|
|
2820
|
+
installed: installed
|
|
2821
|
+
? { isInstalled: true, scope: installed.scope }
|
|
2822
|
+
: { isInstalled: false },
|
|
2823
|
+
};
|
|
2824
|
+
});
|
|
2825
|
+
|
|
2826
|
+
itemsBySource[src.id] = items;
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// Handle GitHub sources (git clone based)
|
|
2702
2831
|
const parsed = parseSkillRepoSource(src.source);
|
|
2703
2832
|
if (!parsed.ok) {
|
|
2704
2833
|
itemsBySource[src.id] = [];
|
|
@@ -2808,6 +2937,29 @@ async function main(options = {}) {
|
|
|
2808
2937
|
}
|
|
2809
2938
|
workingDirectory = resolved.directory;
|
|
2810
2939
|
}
|
|
2940
|
+
|
|
2941
|
+
// Handle ClawdHub sources (ZIP download based)
|
|
2942
|
+
if (isClawdHubSource(source)) {
|
|
2943
|
+
const result = await installSkillsFromClawdHub({
|
|
2944
|
+
scope,
|
|
2945
|
+
workingDirectory,
|
|
2946
|
+
userSkillDir: SKILL_DIR,
|
|
2947
|
+
selections,
|
|
2948
|
+
conflictPolicy,
|
|
2949
|
+
conflictDecisions,
|
|
2950
|
+
});
|
|
2951
|
+
|
|
2952
|
+
if (!result.ok) {
|
|
2953
|
+
if (result.error?.kind === 'conflicts') {
|
|
2954
|
+
return res.status(409).json({ ok: false, error: result.error });
|
|
2955
|
+
}
|
|
2956
|
+
return res.status(400).json({ ok: false, error: result.error });
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
return res.json({ ok: true, installed: result.installed || [], skipped: result.skipped || [] });
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// Handle GitHub sources (git clone based)
|
|
2811
2963
|
const identity = resolveGitIdentity(gitIdentityId);
|
|
2812
2964
|
|
|
2813
2965
|
const result = await installSkillsFromRepository({
|
|
@@ -3153,6 +3305,17 @@ async function main(options = {}) {
|
|
|
3153
3305
|
}
|
|
3154
3306
|
});
|
|
3155
3307
|
|
|
3308
|
+
app.get('/api/git/discover-credentials', async (req, res) => {
|
|
3309
|
+
try {
|
|
3310
|
+
const { discoverGitCredentials } = await import('./lib/git-credentials.js');
|
|
3311
|
+
const credentials = discoverGitCredentials();
|
|
3312
|
+
res.json(credentials);
|
|
3313
|
+
} catch (error) {
|
|
3314
|
+
console.error('Failed to discover git credentials:', error);
|
|
3315
|
+
res.status(500).json({ error: 'Failed to discover git credentials' });
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
|
|
3156
3319
|
app.get('/api/git/check', async (req, res) => {
|
|
3157
3320
|
const { isGitRepository } = await getGitLibraries();
|
|
3158
3321
|
try {
|
|
@@ -3169,6 +3332,23 @@ async function main(options = {}) {
|
|
|
3169
3332
|
}
|
|
3170
3333
|
});
|
|
3171
3334
|
|
|
3335
|
+
app.get('/api/git/remote-url', async (req, res) => {
|
|
3336
|
+
const { getRemoteUrl } = await getGitLibraries();
|
|
3337
|
+
try {
|
|
3338
|
+
const directory = req.query.directory;
|
|
3339
|
+
if (!directory) {
|
|
3340
|
+
return res.status(400).json({ error: 'directory parameter is required' });
|
|
3341
|
+
}
|
|
3342
|
+
const remote = req.query.remote || 'origin';
|
|
3343
|
+
|
|
3344
|
+
const url = await getRemoteUrl(directory, remote);
|
|
3345
|
+
res.json({ url });
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
console.error('Failed to get remote url:', error);
|
|
3348
|
+
res.status(500).json({ error: 'Failed to get remote url' });
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
|
|
3172
3352
|
app.get('/api/git/current-identity', async (req, res) => {
|
|
3173
3353
|
const { getCurrentIdentity } = await getGitLibraries();
|
|
3174
3354
|
try {
|
|
@@ -3353,6 +3533,15 @@ async function main(options = {}) {
|
|
|
3353
3533
|
.join('\n\n');
|
|
3354
3534
|
|
|
3355
3535
|
const prompt = `You are drafting git commit notes for this codebase. Respond in JSON of the shape {"subject": string, "highlights": string[]} (ONLY the JSON in response, no markdown wrappers or anything except JSON) with these rules:\n- subject follows our convention: type[optional-scope]: summary (examples: "feat: add diff virtualization", "fix(chat): restore enter key handling")\n- allowed types: feat, fix, chore, style, refactor, perf, docs, test, build, ci (choose the best match or fallback to chore)\n- summary must be imperative, concise, <= 70 characters, no trailing punctuation\n- scope is optional; include only when obvious from filenames/folders; do not invent scopes\n- focus on the most impactful user-facing change; if multiple capabilities ship together, align the subject with the dominant theme and use highlights to cover the other major outcomes\n- highlights array should contain 2-3 plain sentences (<= 90 chars each) that describe distinct features or UI changes users will notice (e.g. "Add per-file revert action in Changes list"). Avoid subjective benefit statements, marketing tone, repeating the subject, or referencing helper function names. Highlight additions such as new controls/buttons, new actions (e.g. revert), or stored state changes explicitly. Skip highlights if fewer than two meaningful points exist.\n- text must be plain (no markdown bullets); each highlight should start with an uppercase verb\n\nDiff summary:\n${diffSummaries}`;
|
|
3536
|
+
|
|
3537
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
3538
|
+
const rawModel = typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel.trim() : '';
|
|
3539
|
+
const model = (() => {
|
|
3540
|
+
if (!rawModel) return 'big-pickle';
|
|
3541
|
+
const parts = rawModel.split('/').filter(Boolean);
|
|
3542
|
+
const candidate = parts.length > 1 ? parts[parts.length - 1] : parts[0];
|
|
3543
|
+
return candidate || 'big-pickle';
|
|
3544
|
+
})();
|
|
3356
3545
|
|
|
3357
3546
|
const completionTimeout = createTimeoutSignal(LONG_REQUEST_TIMEOUT_MS);
|
|
3358
3547
|
let response;
|
|
@@ -3361,7 +3550,7 @@ async function main(options = {}) {
|
|
|
3361
3550
|
method: 'POST',
|
|
3362
3551
|
headers: { 'Content-Type': 'application/json' },
|
|
3363
3552
|
body: JSON.stringify({
|
|
3364
|
-
model
|
|
3553
|
+
model,
|
|
3365
3554
|
messages: [{ role: 'user', content: prompt }],
|
|
3366
3555
|
max_tokens: 3000,
|
|
3367
3556
|
stream: false,
|
|
@@ -4678,9 +4867,12 @@ async function main(options = {}) {
|
|
|
4678
4867
|
});
|
|
4679
4868
|
|
|
4680
4869
|
if (attachSignals && !signalsAttached) {
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4870
|
+
const handleSignal = async () => {
|
|
4871
|
+
await gracefulShutdown();
|
|
4872
|
+
};
|
|
4873
|
+
process.on('SIGTERM', handleSignal);
|
|
4874
|
+
process.on('SIGINT', handleSignal);
|
|
4875
|
+
process.on('SIGQUIT', handleSignal);
|
|
4684
4876
|
signalsAttached = true;
|
|
4685
4877
|
syncToHmrState();
|
|
4686
4878
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const GIT_CREDENTIALS_PATH = path.join(os.homedir(), '.git-credentials');
|
|
6
|
+
|
|
7
|
+
export function discoverGitCredentials() {
|
|
8
|
+
const credentials = [];
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(GIT_CREDENTIALS_PATH)) {
|
|
11
|
+
return credentials;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(GIT_CREDENTIALS_PATH, 'utf8');
|
|
16
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
17
|
+
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(line.trim());
|
|
21
|
+
const hostname = url.hostname;
|
|
22
|
+
const pathname = url.pathname && url.pathname !== '/' ? url.pathname : '';
|
|
23
|
+
const host = hostname + pathname;
|
|
24
|
+
const username = url.username || '';
|
|
25
|
+
|
|
26
|
+
if (host && username) {
|
|
27
|
+
const exists = credentials.some(c => c.host === host && c.username === username);
|
|
28
|
+
if (!exists) {
|
|
29
|
+
credentials.push({ host, username });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to read .git-credentials:', error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return credentials;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getCredentialForHost(host) {
|
|
44
|
+
if (!fs.existsSync(GIT_CREDENTIALS_PATH)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(GIT_CREDENTIALS_PATH, 'utf8');
|
|
50
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(line.trim());
|
|
55
|
+
const hostname = url.hostname;
|
|
56
|
+
const pathname = url.pathname && url.pathname !== '/' ? url.pathname : '';
|
|
57
|
+
const credHost = hostname + pathname;
|
|
58
|
+
|
|
59
|
+
if (credHost === host) {
|
|
60
|
+
return {
|
|
61
|
+
username: url.username || '',
|
|
62
|
+
token: url.password || ''
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to read .git-credentials for host lookup:', error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
@@ -66,7 +66,9 @@ export function createProfile(profileData) {
|
|
|
66
66
|
name: profileData.name || profileData.userName,
|
|
67
67
|
userName: profileData.userName,
|
|
68
68
|
userEmail: profileData.userEmail,
|
|
69
|
+
authType: profileData.authType || 'ssh',
|
|
69
70
|
sshKey: profileData.sshKey || null,
|
|
71
|
+
host: profileData.host || null,
|
|
70
72
|
color: profileData.color || 'keyword',
|
|
71
73
|
icon: profileData.icon || 'branch'
|
|
72
74
|
};
|