@openchamber/web 1.11.1 → 1.11.2
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/{JsonTreeView-bLRkNPS9.js → JsonTreeView-9F0tH9yA.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-FSTq-eVA.js → MarkdownRendererImpl-C3QofAGm.js} +1 -1
- package/dist/assets/{MultiRunWindow-DezP_Pyy.js → MultiRunWindow-wnUGv0Dl.js} +1 -1
- package/dist/assets/{OnboardingScreen-CeZMVgH8.js → OnboardingScreen-17dAs0NH.js} +2 -2
- package/dist/assets/{SettingsWindow-Dlfk0yzA.js → SettingsWindow-BgyVY5gz.js} +1 -1
- package/dist/assets/TerminalView-D11XIZuz.js +1 -0
- package/dist/assets/{ToolOutputDialog-CePopKV7.js → ToolOutputDialog-DFG7ANVw.js} +6 -6
- package/dist/assets/es-Chl2Hu6K.js +15 -0
- package/dist/assets/index-0bVkxg-Z.css +1 -0
- package/dist/assets/{index-CKlDk4Io.js → index-BV2XTsJJ.js} +1 -1
- package/dist/assets/ko-BSrH3F9n.js +15 -0
- package/dist/assets/{main-BvaFBcXN.js → main-BHkNwOz1.js} +2 -2
- package/dist/assets/main-Bqf4fXgq.js +225 -0
- package/dist/assets/miniChat-BmB-E5xo.js +2 -0
- package/dist/assets/{modelPrefsAutoSave-HUPrH1r2.js → modelPrefsAutoSave-2uwW8uD9.js} +98 -96
- package/dist/assets/pl-YlGvPmFg.js +15 -0
- package/dist/assets/pt-BR-BonIMDN_.js +15 -0
- package/dist/assets/{renderElectronMiniChatApp-EXGwfTjq.js → renderElectronMiniChatApp-B_qrXCU2.js} +2 -2
- package/dist/assets/uk-lPqA3MHn.js +15 -0
- package/dist/assets/{vendor-.bun-BFTPeDgG.js → vendor-.bun-Boz6Tqcq.js} +20 -20
- package/dist/assets/zh-CN-C5nQQsUL.js +15 -0
- package/dist/index.html +4 -4
- package/dist/mini-chat.html +4 -4
- package/package.json +1 -1
- package/server/lib/git/DOCUMENTATION.md +1 -0
- package/server/lib/git/routes.js +26 -0
- package/server/lib/git/service.js +96 -10
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/opencode/settings-helpers.js +10 -1
- package/server/lib/opencode/settings-helpers.test.js +35 -0
- package/server/lib/opencode/skill-routes.js +43 -49
- package/server/lib/opencode/skills.js +78 -10
- package/dist/assets/TerminalView-CdCfCaFq.js +0 -1
- package/dist/assets/es-CyjenLd8.js +0 -15
- package/dist/assets/index-NnYXwoao.css +0 -1
- package/dist/assets/ko-Cs7yF9Jn.js +0 -15
- package/dist/assets/main-DcpUnRRo.js +0 -225
- package/dist/assets/miniChat-Dmzb8Mwv.js +0 -2
- package/dist/assets/pl-XjTt7Hsk.js +0 -15
- package/dist/assets/pt-BR-knvpJ94d.js +0 -15
- package/dist/assets/uk-DUPvcQAj.js +0 -15
- package/dist/assets/zh-CN-DWiTG93s.js +0 -15
package/dist/index.html
CHANGED
|
@@ -532,10 +532,10 @@
|
|
|
532
532
|
pointer-events: none;
|
|
533
533
|
}
|
|
534
534
|
</style>
|
|
535
|
-
<script type="module" crossorigin src="/assets/main-
|
|
536
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
537
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-
|
|
538
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
535
|
+
<script type="module" crossorigin src="/assets/main-BHkNwOz1.js"></script>
|
|
536
|
+
<link rel="modulepreload" crossorigin href="/assets/index-BV2XTsJJ.js">
|
|
537
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-Boz6Tqcq.js">
|
|
538
|
+
<link rel="stylesheet" crossorigin href="/assets/index-0bVkxg-Z.css">
|
|
539
539
|
<link rel="stylesheet" crossorigin href="/assets/vendor--V65Sl9C2.css">
|
|
540
540
|
</head>
|
|
541
541
|
<body class="h-full bg-background text-foreground">
|
package/dist/mini-chat.html
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
6
|
<title>OpenChamber Mini Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/assets/index-
|
|
9
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/miniChat-BmB-E5xo.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/assets/index-BV2XTsJJ.js">
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-.bun-Boz6Tqcq.js">
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-0bVkxg-Z.css">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/vendor--V65Sl9C2.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body class="h-full bg-background text-foreground">
|
package/package.json
CHANGED
|
@@ -58,6 +58,7 @@ The following functions are exported and used by the web server:
|
|
|
58
58
|
### Log Operations
|
|
59
59
|
- `getLog(directory, options)`: Get commit history with stats (supports maxCount, from, to, file filters).
|
|
60
60
|
- `getCommitFiles(directory, commitHash)`: Get file changes for a specific commit.
|
|
61
|
+
- `getCommitFileDiff(directory, hash, filePath, isBinary)`: Get before/after content for a specific file in a commit. Returns `{ original, modified, isBinary }`. Runs `git show <hash>^:<path>` and `git show <hash>:<path>` in parallel; returns empty strings on failure (added/deleted/root-commit edge cases).
|
|
61
62
|
|
|
62
63
|
### Merge and Rebase Operations
|
|
63
64
|
- `rebase(directory, options)`: Start a rebase onto a target branch.
|
package/server/lib/git/routes.js
CHANGED
|
@@ -943,4 +943,30 @@ export function registerGitRoutes(app) {
|
|
|
943
943
|
}
|
|
944
944
|
});
|
|
945
945
|
|
|
946
|
+
app.get('/api/git/commit-file-diff', async (req, res) => {
|
|
947
|
+
const { getCommitFileDiff } = await getGitLibraries();
|
|
948
|
+
try {
|
|
949
|
+
const { directory, hash, path: filePath } = req.query;
|
|
950
|
+
if (!directory || typeof directory !== 'string') {
|
|
951
|
+
return res.status(400).json({ error: 'directory parameter is required' });
|
|
952
|
+
}
|
|
953
|
+
if (!hash || typeof hash !== 'string') {
|
|
954
|
+
return res.status(400).json({ error: 'hash parameter is required' });
|
|
955
|
+
}
|
|
956
|
+
if (!/^[0-9a-fA-F]{7,40}$/.test(hash)) {
|
|
957
|
+
return res.status(400).json({ error: 'hash must be a valid commit SHA' });
|
|
958
|
+
}
|
|
959
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
960
|
+
return res.status(400).json({ error: 'path parameter is required' });
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const isBinary = req.query.binary === 'true';
|
|
964
|
+
const result = await getCommitFileDiff(directory, hash, filePath, isBinary);
|
|
965
|
+
res.json(result);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
console.error('Failed to get commit file diff:', error);
|
|
968
|
+
res.status(500).json({ error: error.message || 'Failed to get commit file diff' });
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
946
972
|
}
|
|
@@ -1611,6 +1611,27 @@ const parseIsBinaryFromNumstat = (raw) => {
|
|
|
1611
1611
|
return added === '-' || deleted === '-';
|
|
1612
1612
|
};
|
|
1613
1613
|
|
|
1614
|
+
const extractGitStatusPath = (status, pathPart) => {
|
|
1615
|
+
if ((status === 'R' || status === 'C') && pathPart.includes('\t')) {
|
|
1616
|
+
return pathPart.split('\t').pop() || pathPart;
|
|
1617
|
+
}
|
|
1618
|
+
return pathPart;
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
const extractGitNumstatDestinationPath = (filePath) => {
|
|
1622
|
+
if (!filePath.includes(' => ')) {
|
|
1623
|
+
return filePath;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const braceMatch = filePath.match(/^(.*)\{([^{}]*)\s=>\s([^{}]*)\}(.*)$/);
|
|
1627
|
+
if (braceMatch) {
|
|
1628
|
+
const [, prefix, , destination, suffix] = braceMatch;
|
|
1629
|
+
return `${prefix}${destination}${suffix}`.replace(/\/+/g, '/');
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
return filePath.split(' => ').pop()?.trim() || filePath;
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1614
1635
|
const looksBinaryBySniff = async (absolutePath) => {
|
|
1615
1636
|
try {
|
|
1616
1637
|
const handle = await fsp.open(absolutePath, 'r');
|
|
@@ -2683,14 +2704,52 @@ export async function deleteBranch(directory, branch, options = {}) {
|
|
|
2683
2704
|
}
|
|
2684
2705
|
}
|
|
2685
2706
|
|
|
2707
|
+
/**
|
|
2708
|
+
* Resolve a log base ref using local-first semantics.
|
|
2709
|
+
*
|
|
2710
|
+
* - If `from` is falsy / whitespace → return undefined.
|
|
2711
|
+
* - If the local ref resolves → return it unchanged (caller's intent preserved).
|
|
2712
|
+
* - If the local ref is absent but `origin/<from>` exists → return `origin/<from>`
|
|
2713
|
+
* (common when the user has never checked out the base branch locally).
|
|
2714
|
+
* - If neither resolves → return `from` unchanged so git surfaces a meaningful error.
|
|
2715
|
+
*
|
|
2716
|
+
* @param {string | undefined} from - The raw `from` option value.
|
|
2717
|
+
* @param {(ref: string) => Promise<boolean>} checkRef - Returns true when the ref resolves.
|
|
2718
|
+
* @returns {Promise<string | undefined>}
|
|
2719
|
+
*/
|
|
2720
|
+
export async function resolveBaseRefForLog(from, checkRef) {
|
|
2721
|
+
const normalized = typeof from === 'string' ? from.trim() : undefined;
|
|
2722
|
+
if (!normalized) return undefined;
|
|
2723
|
+
|
|
2724
|
+
if (await checkRef(normalized)) return normalized;
|
|
2725
|
+
|
|
2726
|
+
const originRef = `refs/remotes/origin/${normalized}`;
|
|
2727
|
+
if (await checkRef(originRef)) return `origin/${normalized}`;
|
|
2728
|
+
|
|
2729
|
+
return normalized;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2686
2732
|
export async function getLog(directory, options = {}) {
|
|
2687
2733
|
const git = await createGit(directory);
|
|
2688
2734
|
|
|
2689
2735
|
try {
|
|
2690
2736
|
const maxCount = options.maxCount || 50;
|
|
2737
|
+
|
|
2738
|
+
// Prefer the local ref; fall back to origin/<from> only when the local ref
|
|
2739
|
+
// cannot be resolved (e.g. user has never checked out the base branch).
|
|
2740
|
+
const checkRef = async (ref) => {
|
|
2741
|
+
try {
|
|
2742
|
+
const out = await git.raw(['rev-parse', '--verify', ref]);
|
|
2743
|
+
return Boolean(out && out.trim());
|
|
2744
|
+
} catch {
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
const resolvedFrom = await resolveBaseRefForLog(options.from, checkRef);
|
|
2749
|
+
|
|
2691
2750
|
const baseLog = await git.log({
|
|
2692
2751
|
maxCount,
|
|
2693
|
-
from:
|
|
2752
|
+
from: resolvedFrom,
|
|
2694
2753
|
to: options.to,
|
|
2695
2754
|
file: options.file
|
|
2696
2755
|
});
|
|
@@ -2703,10 +2762,10 @@ export async function getLog(directory, options = {}) {
|
|
|
2703
2762
|
'--shortstat'
|
|
2704
2763
|
];
|
|
2705
2764
|
|
|
2706
|
-
if (
|
|
2707
|
-
logArgs.push(`${
|
|
2708
|
-
} else if (
|
|
2709
|
-
logArgs.push(`${
|
|
2765
|
+
if (resolvedFrom && options.to) {
|
|
2766
|
+
logArgs.push(`${resolvedFrom}..${options.to}`);
|
|
2767
|
+
} else if (resolvedFrom) {
|
|
2768
|
+
logArgs.push(`${resolvedFrom}..HEAD`);
|
|
2710
2769
|
} else if (options.to) {
|
|
2711
2770
|
logArgs.push(options.to);
|
|
2712
2771
|
}
|
|
@@ -2990,15 +3049,13 @@ export async function getCommitFiles(directory, commitHash) {
|
|
|
2990
3049
|
for (const line of statusLines) {
|
|
2991
3050
|
const match = line.match(/^([AMDRC])\d*\t(.+)$/);
|
|
2992
3051
|
if (match) {
|
|
2993
|
-
const [, status,
|
|
2994
|
-
statusMap.set(
|
|
3052
|
+
const [, status, pathPart] = match;
|
|
3053
|
+
statusMap.set(extractGitStatusPath(status, pathPart), status);
|
|
2995
3054
|
}
|
|
2996
3055
|
}
|
|
2997
3056
|
|
|
2998
3057
|
for (const file of files) {
|
|
2999
|
-
const basePath = file.path
|
|
3000
|
-
? file.path.split(' => ').pop()?.replace(/[{}]/g, '') || file.path
|
|
3001
|
-
: file.path;
|
|
3058
|
+
const basePath = extractGitNumstatDestinationPath(file.path);
|
|
3002
3059
|
|
|
3003
3060
|
const status = statusMap.get(basePath) || statusMap.get(file.path);
|
|
3004
3061
|
if (status) {
|
|
@@ -3072,6 +3129,9 @@ export async function getRemotes(directory) {
|
|
|
3072
3129
|
pushUrl: remote.refs.push
|
|
3073
3130
|
}));
|
|
3074
3131
|
} catch (error) {
|
|
3132
|
+
if (isNotGitRepositoryError(error)) {
|
|
3133
|
+
return [];
|
|
3134
|
+
}
|
|
3075
3135
|
console.error('Failed to get remotes:', error);
|
|
3076
3136
|
throw error;
|
|
3077
3137
|
}
|
|
@@ -3341,3 +3401,29 @@ export async function getConflictDetails(directory) {
|
|
|
3341
3401
|
throw error;
|
|
3342
3402
|
}
|
|
3343
3403
|
}
|
|
3404
|
+
|
|
3405
|
+
export async function getCommitFileDiff(directory, hash, filePath, isBinary) {
|
|
3406
|
+
if (!directory || !hash || !filePath) {
|
|
3407
|
+
throw new Error('directory, hash, and path are required for getCommitFileDiff');
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
if (isBinary) {
|
|
3411
|
+
return { original: '', modified: '', isBinary: true };
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
const directoryPath = normalizeDirectoryPath(directory);
|
|
3415
|
+
|
|
3416
|
+
const [originalResult, modifiedResult] = await Promise.all([
|
|
3417
|
+
runGitCommand(directoryPath, ['show', `${hash}^:${filePath}`]),
|
|
3418
|
+
runGitCommand(directoryPath, ['show', `${hash}:${filePath}`]),
|
|
3419
|
+
]);
|
|
3420
|
+
|
|
3421
|
+
const original = originalResult.success ? originalResult.stdout : '';
|
|
3422
|
+
const modified = modifiedResult.success ? modifiedResult.stdout : '';
|
|
3423
|
+
|
|
3424
|
+
if (!originalResult.success && !modifiedResult.success) {
|
|
3425
|
+
throw new Error(`Failed to read file content at commit ${hash}: ${originalResult.stderr || modifiedResult.stderr}`);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
return { original, modified, isBinary: false };
|
|
3429
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { resolveBaseRefForLog } from './service.js';
|
|
4
|
+
|
|
5
|
+
describe('resolveBaseRefForLog', () => {
|
|
6
|
+
it('returns the local ref unchanged when it exists, even if origin also exists', async () => {
|
|
7
|
+
// Both local 'main' and 'refs/remotes/origin/main' are present.
|
|
8
|
+
// The local ref takes precedence — callers that ask for 'main' get 'main'.
|
|
9
|
+
const checkRef = async (ref) => ref === 'main' || ref === 'refs/remotes/origin/main';
|
|
10
|
+
expect(await resolveBaseRefForLog('main', checkRef)).toBe('main');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('falls back to origin/<from> when local ref cannot be resolved but origin can', async () => {
|
|
14
|
+
// Local 'main' is absent (e.g. user never checked it out), but origin/main exists.
|
|
15
|
+
const checkRef = async (ref) => ref === 'refs/remotes/origin/main';
|
|
16
|
+
expect(await resolveBaseRefForLog('main', checkRef)).toBe('origin/main');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns the original ref when neither local nor origin ref can be resolved', async () => {
|
|
20
|
+
// Neither ref exists; return as-is so git surfaces a meaningful error.
|
|
21
|
+
const checkRef = async () => false;
|
|
22
|
+
expect(await resolveBaseRefForLog('nonexistent-branch', checkRef)).toBe('nonexistent-branch');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns undefined when from is undefined', async () => {
|
|
26
|
+
const checkRef = async () => true;
|
|
27
|
+
expect(await resolveBaseRefForLog(undefined, checkRef)).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns undefined when from is an empty string', async () => {
|
|
31
|
+
const checkRef = async () => true;
|
|
32
|
+
expect(await resolveBaseRefForLog('', checkRef)).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns undefined when from is a whitespace-only string', async () => {
|
|
36
|
+
const checkRef = async () => true;
|
|
37
|
+
expect(await resolveBaseRefForLog(' ', checkRef)).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -168,6 +168,9 @@ export const createSettingsHelpers = (dependencies) => {
|
|
|
168
168
|
if (typeof candidate.showReasoningTraces === 'boolean') {
|
|
169
169
|
result.showReasoningTraces = candidate.showReasoningTraces;
|
|
170
170
|
}
|
|
171
|
+
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
|
|
172
|
+
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
|
|
173
|
+
}
|
|
171
174
|
if (typeof candidate.showTextJustificationActivity === 'boolean') {
|
|
172
175
|
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
|
|
173
176
|
}
|
|
@@ -728,7 +731,13 @@ export const createSettingsHelpers = (dependencies) => {
|
|
|
728
731
|
? settings.showReasoningTraces
|
|
729
732
|
: typeof sanitized.showReasoningTraces === 'boolean'
|
|
730
733
|
? sanitized.showReasoningTraces
|
|
731
|
-
: false
|
|
734
|
+
: false,
|
|
735
|
+
collapsibleThinkingBlocks:
|
|
736
|
+
typeof settings.collapsibleThinkingBlocks === 'boolean'
|
|
737
|
+
? settings.collapsibleThinkingBlocks
|
|
738
|
+
: typeof sanitized.collapsibleThinkingBlocks === 'boolean'
|
|
739
|
+
? sanitized.collapsibleThinkingBlocks
|
|
740
|
+
: true,
|
|
732
741
|
};
|
|
733
742
|
};
|
|
734
743
|
|
|
@@ -71,4 +71,39 @@ describe('settings helpers', () => {
|
|
|
71
71
|
|
|
72
72
|
expect(helpers.sanitizeSettingsUpdate({ mobileKeyboardMode: 'fixed-layout' })).toEqual({});
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
it('accepts collapsibleThinkingBlocks as a persisted shared setting', () => {
|
|
76
|
+
const helpers = createTestHelpers();
|
|
77
|
+
|
|
78
|
+
expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: true })).toEqual({
|
|
79
|
+
collapsibleThinkingBlocks: true,
|
|
80
|
+
});
|
|
81
|
+
expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: false })).toEqual({
|
|
82
|
+
collapsibleThinkingBlocks: false,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('rejects non-boolean collapsibleThinkingBlocks values', () => {
|
|
87
|
+
const helpers = createTestHelpers();
|
|
88
|
+
|
|
89
|
+
expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: 'true' })).toEqual({});
|
|
90
|
+
expect(helpers.sanitizeSettingsUpdate({ collapsibleThinkingBlocks: 1 })).toEqual({});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('includes collapsibleThinkingBlocks in formatSettingsResponse', () => {
|
|
94
|
+
const helpers = createTestHelpers();
|
|
95
|
+
|
|
96
|
+
const response = helpers.formatSettingsResponse({ collapsibleThinkingBlocks: false });
|
|
97
|
+
expect(response.collapsibleThinkingBlocks).toBe(false);
|
|
98
|
+
|
|
99
|
+
const responseTrue = helpers.formatSettingsResponse({ collapsibleThinkingBlocks: true });
|
|
100
|
+
expect(responseTrue.collapsibleThinkingBlocks).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('defaults collapsibleThinkingBlocks to true in formatSettingsResponse when absent', () => {
|
|
104
|
+
const helpers = createTestHelpers();
|
|
105
|
+
|
|
106
|
+
const response = helpers.formatSettingsResponse({});
|
|
107
|
+
expect(response.collapsibleThinkingBlocks).toBe(true);
|
|
108
|
+
});
|
|
74
109
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
2
|
+
|
|
1
3
|
export const registerSkillRoutes = (app, dependencies) => {
|
|
2
4
|
const {
|
|
3
5
|
fs,
|
|
@@ -14,7 +16,6 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
14
16
|
getOpenCodeAuthHeaders,
|
|
15
17
|
getOpenCodePort,
|
|
16
18
|
getSkillSources,
|
|
17
|
-
discoverSkills,
|
|
18
19
|
createSkill,
|
|
19
20
|
updateSkill,
|
|
20
21
|
deleteSkill,
|
|
@@ -114,31 +115,23 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
114
115
|
|
|
115
116
|
const fetchOpenCodeDiscoveredSkills = async (workingDirectory) => {
|
|
116
117
|
if (!getOpenCodePort()) {
|
|
117
|
-
return
|
|
118
|
+
return [];
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const response = await fetch(url.toString(), {
|
|
127
|
-
method: 'GET',
|
|
128
|
-
headers: {
|
|
129
|
-
Accept: 'application/json',
|
|
130
|
-
...getOpenCodeAuthHeaders(),
|
|
131
|
-
},
|
|
132
|
-
signal: AbortSignal.timeout(8_000),
|
|
122
|
+
const client = createOpencodeClient({
|
|
123
|
+
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
124
|
+
directory: workingDirectory || undefined,
|
|
125
|
+
headers: getOpenCodeAuthHeaders(),
|
|
126
|
+
fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
|
|
133
127
|
});
|
|
134
128
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const payload = await response.json();
|
|
129
|
+
const response = await client.app.skills(
|
|
130
|
+
workingDirectory ? { directory: workingDirectory } : undefined,
|
|
131
|
+
);
|
|
132
|
+
const payload = response?.data;
|
|
140
133
|
if (!Array.isArray(payload)) {
|
|
141
|
-
return
|
|
134
|
+
return [];
|
|
142
135
|
}
|
|
143
136
|
|
|
144
137
|
return payload
|
|
@@ -146,7 +139,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
146
139
|
const name = typeof item?.name === 'string' ? item.name.trim() : '';
|
|
147
140
|
const location = typeof item?.location === 'string' ? item.location : '';
|
|
148
141
|
const description = typeof item?.description === 'string' ? item.description : '';
|
|
149
|
-
if (!name || !location) {
|
|
142
|
+
if (!name || !location || location === '<built-in>') {
|
|
150
143
|
return null;
|
|
151
144
|
}
|
|
152
145
|
const inferred = inferSkillScopeAndSourceFromPath(location, workingDirectory);
|
|
@@ -159,8 +152,9 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
159
152
|
};
|
|
160
153
|
})
|
|
161
154
|
.filter(Boolean);
|
|
162
|
-
} catch {
|
|
163
|
-
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('Failed to list OpenCode skills:', error);
|
|
157
|
+
return [];
|
|
164
158
|
}
|
|
165
159
|
};
|
|
166
160
|
|
|
@@ -191,11 +185,11 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
191
185
|
|
|
192
186
|
app.get('/api/config/skills', async (req, res) => {
|
|
193
187
|
try {
|
|
194
|
-
const { directory, error } = await
|
|
195
|
-
if (
|
|
188
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
189
|
+
if (error) {
|
|
196
190
|
return res.status(400).json({ error });
|
|
197
191
|
}
|
|
198
|
-
const skills =
|
|
192
|
+
const skills = await fetchOpenCodeDiscoveredSkills(directory);
|
|
199
193
|
|
|
200
194
|
const enrichedSkills = skills.map((skill) => {
|
|
201
195
|
const sources = getSkillSources(skill.name, directory, skill);
|
|
@@ -277,9 +271,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
277
271
|
return res.status(404).json({ ok: false, error: { kind: 'invalidSource', message: 'Unknown source' } });
|
|
278
272
|
}
|
|
279
273
|
|
|
280
|
-
const discovered = directory
|
|
281
|
-
? ((await fetchOpenCodeDiscoveredSkills(directory)) || discoverSkills(directory))
|
|
282
|
-
: [];
|
|
274
|
+
const discovered = await fetchOpenCodeDiscoveredSkills(directory);
|
|
283
275
|
const installedByName = new Map(discovered.map((s) => [s.name, s]));
|
|
284
276
|
|
|
285
277
|
if (src.sourceType === 'clawdhub' || isClawdHubSource(src.source)) {
|
|
@@ -504,11 +496,11 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
504
496
|
app.get('/api/config/skills/:name', async (req, res) => {
|
|
505
497
|
try {
|
|
506
498
|
const skillName = req.params.name;
|
|
507
|
-
const { directory, error } = await
|
|
508
|
-
if (
|
|
499
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
500
|
+
if (error) {
|
|
509
501
|
return res.status(400).json({ error });
|
|
510
502
|
}
|
|
511
|
-
const discoveredSkill = (
|
|
503
|
+
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
512
504
|
.find((skill) => skill.name === skillName) || null;
|
|
513
505
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
514
506
|
|
|
@@ -532,12 +524,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
532
524
|
if (isUnsafeSkillRelativePath(filePath)) {
|
|
533
525
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
534
526
|
}
|
|
535
|
-
const { directory, error } = await
|
|
536
|
-
if (
|
|
527
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
528
|
+
if (error) {
|
|
537
529
|
return res.status(400).json({ error });
|
|
538
530
|
}
|
|
539
531
|
|
|
540
|
-
const discoveredSkill = (
|
|
532
|
+
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
541
533
|
.find((skill) => skill.name === skillName) || null;
|
|
542
534
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
543
535
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -563,9 +555,11 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
563
555
|
try {
|
|
564
556
|
const skillName = req.params.name;
|
|
565
557
|
const { scope, source: skillSource, ...config } = req.body;
|
|
566
|
-
const { directory, error } =
|
|
567
|
-
|
|
568
|
-
|
|
558
|
+
const { directory, error } = scope === SKILL_SCOPE.PROJECT
|
|
559
|
+
? await resolveProjectDirectory(req)
|
|
560
|
+
: await resolveOptionalProjectDirectory(req);
|
|
561
|
+
if (error || (scope === SKILL_SCOPE.PROJECT && !directory)) {
|
|
562
|
+
return res.status(400).json({ error: error || 'Project skill creation requires a directory' });
|
|
569
563
|
}
|
|
570
564
|
|
|
571
565
|
console.log('[Server] Creating skill:', skillName);
|
|
@@ -590,15 +584,15 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
590
584
|
try {
|
|
591
585
|
const skillName = req.params.name;
|
|
592
586
|
const updates = req.body;
|
|
593
|
-
const { directory, error } = await
|
|
594
|
-
if (
|
|
587
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
588
|
+
if (error) {
|
|
595
589
|
return res.status(400).json({ error });
|
|
596
590
|
}
|
|
597
591
|
|
|
598
592
|
console.log(`[Server] Updating skill: ${skillName}`);
|
|
599
593
|
console.log('[Server] Working directory:', directory);
|
|
600
594
|
|
|
601
|
-
updateSkill(skillName, updates, directory);
|
|
595
|
+
updateSkill(skillName, updates, directory, updates?.targetPath);
|
|
602
596
|
await refreshOpenCodeAfterConfigChange('skill update');
|
|
603
597
|
|
|
604
598
|
res.json({
|
|
@@ -621,12 +615,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
621
615
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
622
616
|
}
|
|
623
617
|
const { content } = req.body;
|
|
624
|
-
const { directory, error } = await
|
|
625
|
-
if (
|
|
618
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
619
|
+
if (error) {
|
|
626
620
|
return res.status(400).json({ error });
|
|
627
621
|
}
|
|
628
622
|
|
|
629
|
-
const discoveredSkill = (
|
|
623
|
+
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
630
624
|
.find((skill) => skill.name === skillName) || null;
|
|
631
625
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
632
626
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -655,12 +649,12 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
655
649
|
if (isUnsafeSkillRelativePath(filePath)) {
|
|
656
650
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
657
651
|
}
|
|
658
|
-
const { directory, error } = await
|
|
659
|
-
if (
|
|
652
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
653
|
+
if (error) {
|
|
660
654
|
return res.status(400).json({ error });
|
|
661
655
|
}
|
|
662
656
|
|
|
663
|
-
const discoveredSkill = (
|
|
657
|
+
const discoveredSkill = (await fetchOpenCodeDiscoveredSkills(directory))
|
|
664
658
|
.find((skill) => skill.name === skillName) || null;
|
|
665
659
|
const sources = getSkillSources(skillName, directory, discoveredSkill);
|
|
666
660
|
if (!sources.md.exists || !sources.md.dir) {
|
|
@@ -685,8 +679,8 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
685
679
|
app.delete('/api/config/skills/:name', async (req, res) => {
|
|
686
680
|
try {
|
|
687
681
|
const skillName = req.params.name;
|
|
688
|
-
const { directory, error } = await
|
|
689
|
-
if (
|
|
682
|
+
const { directory, error } = await resolveOptionalProjectDirectory(req);
|
|
683
|
+
if (error) {
|
|
690
684
|
return res.status(400).json({ error });
|
|
691
685
|
}
|
|
692
686
|
|