cc-viewer 1.9.14 → 1.9.16
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/cli.js +1 -0
- package/concepts/ar/UltraPlan.md +9 -9
- package/concepts/da/UltraPlan.md +9 -9
- package/concepts/de/UltraPlan.md +9 -9
- package/concepts/en/UltraPlan.md +9 -9
- package/concepts/es/UltraPlan.md +9 -9
- package/concepts/fr/UltraPlan.md +9 -9
- package/concepts/it/UltraPlan.md +9 -9
- package/concepts/ja/UltraPlan.md +9 -9
- package/concepts/ko/UltraPlan.md +9 -9
- package/concepts/no/UltraPlan.md +9 -9
- package/concepts/pl/UltraPlan.md +9 -9
- package/concepts/pt-BR/UltraPlan.md +9 -9
- package/concepts/ru/UltraPlan.md +9 -9
- package/concepts/th/UltraPlan.md +9 -9
- package/concepts/tr/UltraPlan.md +9 -9
- package/concepts/uk/UltraPlan.md +9 -9
- package/concepts/zh/UltraPlan.md +9 -9
- package/concepts/zh-TW/UltraPlan.md +9 -9
- package/dist/assets/App-BvVdj099.css +1 -0
- package/dist/assets/App-CYdOioAz.js +2 -0
- package/dist/assets/{MdxEditorPanel-DI3B4gsM.js → MdxEditorPanel-CuCAe1OG.js} +1 -1
- package/dist/assets/Mobile-CfNYOgRa.js +1 -0
- package/dist/assets/{ProxyStatsModal-B8K0AtAq.js → ProxyStatsModal-Dhj4EVnh.js} +1 -1
- package/dist/assets/index-sgW8MlD3.js +2 -0
- package/dist/assets/seqResourceLoaders-B-5xGs9q.css +41 -0
- package/dist/assets/seqResourceLoaders-Cj0pdz-l.js +2 -0
- package/dist/index.html +1 -1
- package/node_modules/@ccv/core/src/contentFilter.js +3 -0
- package/node_modules/@ccv/core/src/queued-prompt.js +66 -0
- package/node_modules/@ccv/core/src/v2-transcript-normalizer.js +78 -14
- package/package.json +1 -1
- package/server/lib/ask/perm-bridge.js +4 -1
- package/server/lib/auto-approve.js +160 -0
- package/server/lib/chat-queue.js +350 -120
- package/server/lib/file-access-policy.js +24 -2
- package/server/lib/scheduled-task-executor.js +3 -1
- package/server/lib/sdk-manager.js +377 -76
- package/server/lib/v2/conversation-store.js +15 -11
- package/server/lib/v2/v2-writer.js +1 -1
- package/server/lib/view-root.js +4 -4
- package/server/pty-manager.js +8 -8
- package/server/routes/ask-perm.js +54 -3
- package/server/routes/files-content.js +352 -16
- package/server/routes/files-fs.js +122 -36
- package/server/routes/resume.js +1 -1
- package/server/routes/sdk-context.js +13 -2
- package/server/routes/skills.js +19 -8
- package/server/server.js +205 -34
- package/server/system-prompt-templates/presets/kimi-k3.md +1 -0
- package/ultraAgents/code-expert.json +1 -1
- package/dist/assets/App-BzV1ieDH.js +0 -2
- package/dist/assets/App-DU5jmGrx.css +0 -1
- package/dist/assets/Mobile-qBkAnXy3.js +0 -1
- package/dist/assets/index-CU0gt6m1.js +0 -2
- package/dist/assets/seqResourceLoaders-CxUFE36z.css +0 -41
- package/dist/assets/seqResourceLoaders-nGCs11zu.js +0 -2
|
@@ -7,16 +7,17 @@ import { join, dirname, basename, resolve, sep } from 'node:path';
|
|
|
7
7
|
import { homedir, tmpdir } from 'node:os';
|
|
8
8
|
import { execFile, spawn } from 'node:child_process';
|
|
9
9
|
import { bumpWorkspacesVersion, isReadAllowed } from '../lib/file-access-policy.js';
|
|
10
|
-
import { validateImportDir } from '../lib/file-api.js';
|
|
10
|
+
import { isPathContained, validateImportDir, renameSyncWithRetry } from '../lib/file-api.js';
|
|
11
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
11
12
|
import { viewRootOrReply } from '../lib/view-root.js';
|
|
12
13
|
import { loadWorkspaces } from '../workspace-registry.js';
|
|
13
14
|
import { listLivePtys } from '../pty-manager.js';
|
|
14
15
|
import { PROFILE_PATH, _projectName, _logDir } from '../interceptor.js';
|
|
15
16
|
import { LOG_DIR, getClaudeConfigDir } from '../../findcc.js';
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// Project-scoped file operations resolve the viewed project via the shared
|
|
19
|
+
// viewRootOrReply (?project=, or POST body {project}/{instance}). Mutations
|
|
20
|
+
// still enforce realpath containment in that root before touching the disk.
|
|
20
21
|
// SYNCHRONOUS: the affected handlers were sync before multi-project and their
|
|
21
22
|
// tests assert on res immediately after the call.
|
|
22
23
|
function _viewRootOrReply(req, res, parsedUrl, bodyProject, bodyInstance) {
|
|
@@ -161,11 +162,31 @@ function importFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
161
162
|
req.on('end', () => {
|
|
162
163
|
if (aborted) return;
|
|
163
164
|
try {
|
|
164
|
-
const cwd =
|
|
165
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl);
|
|
166
|
+
if (!cwd) return;
|
|
165
167
|
const targetDir = join(cwd, dir);
|
|
166
|
-
mkdirSync(targetDir, { recursive: true });
|
|
167
|
-
const realDir = realpathSync(targetDir);
|
|
168
168
|
const realCwd = realpathSync(cwd);
|
|
169
|
+
let cursor = targetDir;
|
|
170
|
+
const missing = [];
|
|
171
|
+
while (!existsSync(cursor)) {
|
|
172
|
+
missing.unshift(basename(cursor));
|
|
173
|
+
const parent = dirname(cursor);
|
|
174
|
+
if (parent === cursor) break;
|
|
175
|
+
cursor = parent;
|
|
176
|
+
}
|
|
177
|
+
const realAncestor = realpathSync(cursor);
|
|
178
|
+
if (realAncestor !== realCwd && !realAncestor.startsWith(realCwd + sep)) {
|
|
179
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
180
|
+
res.end(JSON.stringify({ error: 'Path traversal not allowed' }));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Create only after the nearest existing ancestor is known to be inside
|
|
184
|
+
// the view; mkdir first would let an escaping symlink create outside dirs.
|
|
185
|
+
for (const segment of missing) {
|
|
186
|
+
cursor = join(cursor, segment);
|
|
187
|
+
mkdirSync(cursor);
|
|
188
|
+
}
|
|
189
|
+
const realDir = realpathSync(targetDir);
|
|
169
190
|
if (realDir !== realCwd && !realDir.startsWith(realCwd + sep)) {
|
|
170
191
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
171
192
|
res.end(JSON.stringify({ error: 'Path traversal not allowed' }));
|
|
@@ -339,7 +360,8 @@ function renameFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
339
360
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
340
361
|
return;
|
|
341
362
|
}
|
|
342
|
-
const cwd =
|
|
363
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
364
|
+
if (!cwd) return;
|
|
343
365
|
const oldFullPath = join(cwd, oldPath);
|
|
344
366
|
const parentDir = dirname(oldFullPath);
|
|
345
367
|
const newFullPath = join(parentDir, newName);
|
|
@@ -349,13 +371,48 @@ function renameFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
349
371
|
res.end(JSON.stringify({ error: 'File not found' }));
|
|
350
372
|
return;
|
|
351
373
|
}
|
|
374
|
+
if (!isPathContained(oldFullPath, cwd)) {
|
|
375
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
376
|
+
res.end(JSON.stringify({ error: 'Path traversal not allowed' }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// On a case-insensitive filesystem the differently-cased destination is
|
|
380
|
+
// the source itself, not a collision. A temporary name makes the case
|
|
381
|
+
// change work on NTFS and APFS even when realpath keeps the old casing.
|
|
382
|
+
const oldName = basename(oldPath);
|
|
383
|
+
if (oldName !== newName && oldName.toLowerCase() === newName.toLowerCase()) {
|
|
384
|
+
try {
|
|
385
|
+
if (realpathSync.native(newFullPath) === realpathSync.native(oldFullPath)) {
|
|
386
|
+
const tempPath = join(parentDir, `.ccv-rename-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
387
|
+
renameSyncWithRetry(oldFullPath, tempPath);
|
|
388
|
+
try { renameSyncWithRetry(tempPath, newFullPath); }
|
|
389
|
+
catch (err) {
|
|
390
|
+
try { renameSyncWithRetry(tempPath, oldFullPath); }
|
|
391
|
+
catch (rollbackErr) { reportSwallowed('filesFs.renameRollback', rollbackErr, { tempPath }); }
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
const newRelPath = oldPath.includes('/') ? oldPath.substring(0, oldPath.lastIndexOf('/') + 1) + newName : newName;
|
|
395
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
396
|
+
res.end(JSON.stringify({ ok: true, newPath: newRelPath }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// Case-sensitive filesystems have no destination to realpath yet;
|
|
401
|
+
// that is the normal rename case, not a server error.
|
|
402
|
+
if (err.code !== 'ENOENT') {
|
|
403
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
404
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
352
409
|
// 检查目标是否已存在
|
|
353
410
|
if (existsSync(newFullPath)) {
|
|
354
411
|
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
355
412
|
res.end(JSON.stringify({ error: 'Target already exists' }));
|
|
356
413
|
return;
|
|
357
414
|
}
|
|
358
|
-
|
|
415
|
+
renameSyncWithRetry(oldFullPath, newFullPath);
|
|
359
416
|
const newRelPath = oldPath.includes('/') ? oldPath.substring(0, oldPath.lastIndexOf('/') + 1) + newName : newName;
|
|
360
417
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
361
418
|
res.end(JSON.stringify({ ok: true, newPath: newRelPath }));
|
|
@@ -391,7 +448,8 @@ function moveFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
391
448
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
392
449
|
return;
|
|
393
450
|
}
|
|
394
|
-
const cwd =
|
|
451
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
452
|
+
if (!cwd) return;
|
|
395
453
|
const oldFullPath = join(cwd, fromPath);
|
|
396
454
|
const toDirFull = join(cwd, toDir);
|
|
397
455
|
// 检查源文件/目录存在
|
|
@@ -417,6 +475,11 @@ function moveFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
417
475
|
}
|
|
418
476
|
}
|
|
419
477
|
const name = basename(fromPath);
|
|
478
|
+
if (!isPathContained(oldFullPath, cwd) || !isPathContained(toDirFull, cwd)) {
|
|
479
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
480
|
+
res.end(JSON.stringify({ error: 'Path traversal not allowed' }));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
420
483
|
const newFullPath = join(toDirFull, name);
|
|
421
484
|
// 检查目标位置不存在同名文件
|
|
422
485
|
if (existsSync(newFullPath)) {
|
|
@@ -487,7 +550,8 @@ function deleteFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
487
550
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
488
551
|
return;
|
|
489
552
|
}
|
|
490
|
-
const cwd =
|
|
553
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
554
|
+
if (!cwd) return;
|
|
491
555
|
const fullPath = join(cwd, filePath);
|
|
492
556
|
if (!existsSync(fullPath)) {
|
|
493
557
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -565,7 +629,8 @@ function revealFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
565
629
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
566
630
|
return;
|
|
567
631
|
}
|
|
568
|
-
const cwd =
|
|
632
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
633
|
+
if (!cwd) return;
|
|
569
634
|
const fullPath = join(cwd, filePath);
|
|
570
635
|
if (!existsSync(fullPath)) {
|
|
571
636
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -623,7 +688,8 @@ function openFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
623
688
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
624
689
|
return;
|
|
625
690
|
}
|
|
626
|
-
const cwd =
|
|
691
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
692
|
+
if (!cwd) return;
|
|
627
693
|
const fullPath = join(cwd, filePath);
|
|
628
694
|
if (!existsSync(fullPath)) {
|
|
629
695
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
@@ -671,12 +737,11 @@ function resolvePath(req, res, parsedUrl, isLocal, deps) {
|
|
|
671
737
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
672
738
|
return;
|
|
673
739
|
}
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const cwd = process.env.CCV_PROJECT_DIR || process.cwd();
|
|
740
|
+
// Resolve a project-relative path against the same viewed root used by
|
|
741
|
+
// reveal/open. Registered projects and live instances use the shared
|
|
742
|
+
// resolver, so same-name projects cannot silently fall back to the bound cwd.
|
|
743
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
744
|
+
if (!cwd) return;
|
|
680
745
|
const fullPath = relPath ? join(cwd, relPath) : cwd;
|
|
681
746
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
682
747
|
res.end(JSON.stringify({ ok: true, fullPath }));
|
|
@@ -790,7 +855,8 @@ function createFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
790
855
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
791
856
|
return;
|
|
792
857
|
}
|
|
793
|
-
const cwd =
|
|
858
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
859
|
+
if (!cwd) return;
|
|
794
860
|
const fullDirPath = relDir ? join(cwd, relDir) : cwd;
|
|
795
861
|
if (!existsSync(fullDirPath) || !statSync(fullDirPath).isDirectory()) {
|
|
796
862
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -838,7 +904,8 @@ function openTerminal(req, res, parsedUrl, isLocal, deps) {
|
|
|
838
904
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
839
905
|
return;
|
|
840
906
|
}
|
|
841
|
-
const cwd =
|
|
907
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
908
|
+
if (!cwd) return;
|
|
842
909
|
const fullDir = relDir ? join(cwd, relDir) : cwd;
|
|
843
910
|
if (!existsSync(fullDir) || !statSync(fullDir).isDirectory()) {
|
|
844
911
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -860,23 +927,40 @@ function openTerminal(req, res, parsedUrl, isLocal, deps) {
|
|
|
860
927
|
} else {
|
|
861
928
|
// Linux: try common terminal emulators
|
|
862
929
|
const terminals = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm'];
|
|
863
|
-
|
|
864
|
-
|
|
930
|
+
// spawn reports a missing executable asynchronously, outside try/catch.
|
|
931
|
+
// Probe each candidate with piped stdio so the next one can be attempted
|
|
932
|
+
// after ENOENT; detach only after a successful spawn.
|
|
933
|
+
const tryTerminal = (index) => {
|
|
934
|
+
if (index >= terminals.length) {
|
|
935
|
+
const child = spawn('xdg-open', [fullDir], { stdio: 'ignore', detached: true });
|
|
936
|
+
child.on('error', (err) => { reportSwallowed('filesFs.openTerminal', err, { terminal: 'xdg-open' }); });
|
|
937
|
+
child.unref();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const term = terminals[index];
|
|
941
|
+
let child;
|
|
865
942
|
try {
|
|
866
943
|
if (term === 'gnome-terminal') {
|
|
867
|
-
spawn(term, ['--working-directory=' + fullDir], { stdio: '
|
|
944
|
+
child = spawn(term, ['--working-directory=' + fullDir], { stdio: 'pipe' });
|
|
868
945
|
} else if (term === 'konsole') {
|
|
869
|
-
spawn(term, ['--workdir', fullDir], { stdio: '
|
|
946
|
+
child = spawn(term, ['--workdir', fullDir], { stdio: 'pipe' });
|
|
870
947
|
} else {
|
|
871
|
-
spawn(term, [], { cwd: fullDir, stdio: '
|
|
948
|
+
child = spawn(term, [], { cwd: fullDir, stdio: 'pipe' });
|
|
872
949
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
950
|
+
} catch {
|
|
951
|
+
tryTerminal(index + 1);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
child.once('error', (err) => {
|
|
955
|
+
reportSwallowed('filesFs.openTerminal', err, { terminal: term });
|
|
956
|
+
tryTerminal(index + 1);
|
|
957
|
+
});
|
|
958
|
+
child.once('spawn', () => {
|
|
959
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) stream?.destroy();
|
|
960
|
+
child.unref();
|
|
961
|
+
});
|
|
962
|
+
};
|
|
963
|
+
tryTerminal(0);
|
|
880
964
|
}
|
|
881
965
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
882
966
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -915,7 +999,8 @@ function createDir(req, res, parsedUrl, isLocal, deps) {
|
|
|
915
999
|
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
916
1000
|
return;
|
|
917
1001
|
}
|
|
918
|
-
const cwd =
|
|
1002
|
+
const cwd = _viewRootOrReply(req, res, parsedUrl, parsed.project, parsed.instance);
|
|
1003
|
+
if (!cwd) return;
|
|
919
1004
|
const fullDirPath = relDir ? join(cwd, relDir) : cwd;
|
|
920
1005
|
if (!existsSync(fullDirPath) || !statSync(fullDirPath).isDirectory()) {
|
|
921
1006
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -965,8 +1050,9 @@ function openProfileDir(req, res) {
|
|
|
965
1050
|
res.end(JSON.stringify({ ok: true, dir }));
|
|
966
1051
|
}
|
|
967
1052
|
|
|
968
|
-
function openProjectDir(req, res) {
|
|
969
|
-
const dir =
|
|
1053
|
+
function openProjectDir(req, res, parsedUrl) {
|
|
1054
|
+
const dir = _viewRootOrReply(req, res, parsedUrl);
|
|
1055
|
+
if (!dir) return;
|
|
970
1056
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
|
|
971
1057
|
execFile(cmd, [dir], { windowsHide: true }, () => {});
|
|
972
1058
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
package/server/routes/resume.js
CHANGED
|
@@ -560,7 +560,7 @@ function postResumeSessionHandler(req, res, parsedUrl, isLocal, deps) {
|
|
|
560
560
|
if (r.ok) {
|
|
561
561
|
// Session identity just changed — drop any still-queued composer messages so a
|
|
562
562
|
// message typed for the OLD conversation never drains into the resumed one.
|
|
563
|
-
try { chatQueue.clear(); } catch (err) { reportSwallowed('resume-route.clear-queue', err); }
|
|
563
|
+
try { chatQueue.clear({ project: targetProject || undefined, instanceKey: instanceKey || undefined }); } catch (err) { reportSwallowed('resume-route.clear-queue', err); }
|
|
564
564
|
sendJson(res, 200, { ok: true });
|
|
565
565
|
return;
|
|
566
566
|
}
|
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
// the automatic turn-end/compact refreshes flow over the WS broadcast instead. A
|
|
3
3
|
// `session-inactive` outcome is a NORMAL state (no turn run yet / query dead), so it
|
|
4
4
|
// returns 200 with { ok:false, reason } rather than an HTTP error.
|
|
5
|
+
//
|
|
6
|
+
// Multi-session: `?project=` and `?instance=` pin the pull to the VIEWED workspace (same
|
|
7
|
+
// query-param names as the other view-scoped routes). No params → active session. Unknown
|
|
8
|
+
// or ambiguous anchors stay `session-inactive` (never fall back to the active session).
|
|
5
9
|
import { reportSwallowed } from '../lib/error-report.js';
|
|
6
10
|
|
|
7
|
-
async function sdkContextUsage(req, res) {
|
|
11
|
+
async function sdkContextUsage(req, res, parsedUrl) {
|
|
8
12
|
try {
|
|
9
13
|
// Dynamic import: sdk-manager is only loaded in SDK mode (resume.js parity). The
|
|
10
14
|
// literal specifier satisfies the boundary verifier's R7 rule.
|
|
11
15
|
const sdk = await import('../lib/sdk-manager.js');
|
|
12
|
-
const
|
|
16
|
+
const params = parsedUrl && parsedUrl.searchParams;
|
|
17
|
+
const project = (params && params.get('project')) || '';
|
|
18
|
+
const instance = (params && params.get('instance')) || '';
|
|
19
|
+
// No params must stay `undefined` so fetchContextUsage keeps the active-session fallback.
|
|
20
|
+
const anchor = (project || instance)
|
|
21
|
+
? { project: project || undefined, instanceKey: instance || undefined }
|
|
22
|
+
: undefined;
|
|
23
|
+
const r = await sdk.fetchContextUsage(anchor);
|
|
13
24
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
14
25
|
res.end(JSON.stringify(r.ok ? { ok: true, snapshot: r.snapshot } : { ok: false, reason: r.reason }));
|
|
15
26
|
} catch (err) {
|
package/server/routes/skills.js
CHANGED
|
@@ -9,13 +9,20 @@ import { loadWorkspaces } from '../workspace-registry.js';
|
|
|
9
9
|
import { listLivePtys } from '../pty-manager.js';
|
|
10
10
|
|
|
11
11
|
// Multi-project (2026-10): the skills LIST resolves the viewed project via
|
|
12
|
-
// the shared viewRootOrReply (?project=). toggle/delete
|
|
13
|
-
// bound root
|
|
14
|
-
|
|
12
|
+
// the shared viewRootOrReply (?project=). toggle/delete used to keep the
|
|
13
|
+
// bound root — that was a deliberate safety choice, but it breaks multi-view
|
|
14
|
+
// semantics: the list shows skills for project B while a click on toggle or
|
|
15
|
+
// delete silently rewrote project A's same-named skill. Resolve mutations
|
|
16
|
+
// through the same viewRootOrReply (with body-level project/instance) so the
|
|
17
|
+
// action targets the project the user is actually looking at; the helper
|
|
18
|
+
// still constrains the resolved root to bound/registered/live projects.
|
|
19
|
+
function _viewRootOrReply(req, res, parsedUrl, bodyProject, bodyInstance) {
|
|
15
20
|
return viewRootOrReply(req, res, parsedUrl, {
|
|
16
21
|
boundCwd: process.env.CCV_PROJECT_DIR || process.cwd(),
|
|
17
22
|
loadWorkspaces,
|
|
18
23
|
listLivePtys,
|
|
24
|
+
bodyProject,
|
|
25
|
+
bodyInstance,
|
|
19
26
|
});
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -34,18 +41,20 @@ async function skillsList(req, res, parsedUrl) {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
// Skill 动态装卸 —— 切换单个 skill(在 skills/ 和 skills-skip/ 之间 move)
|
|
37
|
-
function skillsToggle(req, res) {
|
|
44
|
+
function skillsToggle(req, res, parsedUrl) {
|
|
38
45
|
let body = '';
|
|
39
46
|
req.on('data', chunk => { body += chunk; if (body.length > 4096) req.destroy(); });
|
|
40
47
|
req.on('end', async () => {
|
|
41
48
|
try {
|
|
42
|
-
const { source, name, enable } = JSON.parse(body);
|
|
49
|
+
const { source, name, enable, project, instance } = JSON.parse(body);
|
|
50
|
+
const projectDir = _viewRootOrReply(req, res, parsedUrl, project, instance);
|
|
51
|
+
if (!projectDir) return;
|
|
43
52
|
// 不加进程锁:moveSkill 里 existsSync 前置 + renameSync 原子性已能让并发
|
|
44
53
|
// toggle 落到合理分支(一个成功、另一个拿 SOURCE_MISSING 或 DUPLICATE);
|
|
45
54
|
// 前端 toggling:Set 也已防同 tab 连点,两者叠加足够安全
|
|
46
55
|
moveSkill({
|
|
47
56
|
source, name, enable: !!enable,
|
|
48
|
-
projectDir
|
|
57
|
+
projectDir,
|
|
49
58
|
});
|
|
50
59
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
51
60
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -75,10 +84,12 @@ function skillsDelete(req, res, parsedUrl, isLocal) {
|
|
|
75
84
|
req.on('data', chunk => { body += chunk; if (body.length > 4096) req.destroy(); });
|
|
76
85
|
req.on('end', async () => {
|
|
77
86
|
try {
|
|
78
|
-
const { source, name, enabled } = JSON.parse(body);
|
|
87
|
+
const { source, name, enabled, project, instance } = JSON.parse(body);
|
|
88
|
+
const projectDir = _viewRootOrReply(req, res, parsedUrl, project, instance);
|
|
89
|
+
if (!projectDir) return;
|
|
79
90
|
deleteSkill({
|
|
80
91
|
source, name, enabled: !!enabled,
|
|
81
|
-
projectDir
|
|
92
|
+
projectDir,
|
|
82
93
|
});
|
|
83
94
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
84
95
|
res.end(JSON.stringify({ ok: true }));
|