cc-viewer 1.6.348 → 1.7.1
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/README.md +13 -1
- package/cli.js +116 -37
- package/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/Mobile-qfRsRth0.js +1 -0
- package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
- package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
- package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
- package/dist/assets/clone-BQtvy9Ae.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
- package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
- package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
- package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
- package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
- package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
- package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
- package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
- package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
- package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
- package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
- package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
- package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
- package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
- package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
- package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
- package/dist/index.html +4 -4
- package/findcc.js +4 -3
- package/package.json +3 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-log-watcher.js +68 -8
- package/server/lib/interceptor-core.js +19 -174
- package/server/lib/log-management.js +214 -247
- package/server/lib/log-stream.js +59 -108
- package/server/lib/log-watcher.js +62 -274
- package/server/lib/log-zip.js +148 -0
- package/server/lib/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/stats-worker.js +138 -267
- package/server/lib/user-prompt-extract.js +250 -0
- package/server/lib/v2/adapter.js +1124 -0
- package/server/lib/v2/blob-store.js +44 -0
- package/server/lib/v2/conversation-store.js +217 -0
- package/server/lib/v2/convert-manager.js +79 -0
- package/server/lib/v2/convert-worker.js +42 -0
- package/server/lib/v2/convert.js +409 -0
- package/server/lib/v2/identity.js +204 -0
- package/server/lib/v2/journal.js +62 -0
- package/server/lib/v2/layout.js +167 -0
- package/server/lib/v2/live-feed.js +558 -0
- package/server/lib/v2/meta-rows.js +360 -0
- package/server/lib/v2/migrate-prompt.js +54 -0
- package/server/lib/v2/replay.js +191 -0
- package/server/lib/v2/session-select.js +222 -0
- package/server/lib/v2/singleflight.js +104 -0
- package/server/lib/v2/v2-writer.js +528 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +120 -150
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- package/server/routes/logs.js +209 -164
- package/server/routes/preferences.js +0 -3
- package/server/routes/project-meta.js +2 -3
- package/server/routes/session-pin.js +7 -8
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +50 -14
- package/server/server.js +70 -66
- package/server/workspace-registry.js +24 -4
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +27 -0
- package/src/utils/avatarAnimationPostPass.js +49 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +7 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +620 -0
- package/src/utils/contextRaw.js +18 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/contextTurns.js +117 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/effectiveModel.js +13 -0
- package/src/utils/entry-slim.js +563 -0
- package/src/utils/entryCache.js +144 -0
- package/src/utils/errorReport.js +6 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +846 -0
- package/src/utils/identityHeal.js +99 -0
- package/src/utils/imConnState.js +59 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +112 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +119 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +30 -0
- package/src/utils/requestType.js +208 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/searchApi.js +47 -0
- package/src/utils/searchReplace.js +51 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +471 -0
- package/src/utils/sessionMerge.js +180 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tClaude.js +20 -0
- package/src/utils/teamModalBuilder.js +351 -0
- package/src/utils/teamSessionParser.js +190 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +121 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/toolCatalog.js +29 -0
- package/src/utils/toolResultBuilder.js +393 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +167 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +154 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/v3Assembler.js +112 -0
- package/src/utils/v3Rows.js +48 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webSearchGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- package/dist/assets/App-VTo4NVdo.js +0 -2
- package/dist/assets/Mobile-CcmYvMm_.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
- package/dist/assets/clone-md-Mc1-T.js +0 -1
- package/dist/assets/index-DCo650PG.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
- package/server/lib/instance-registry.js +0 -57
- package/server/lib/jsonl-archive.js +0 -180
- package/server/lib/teammate-detect.js +0 -32
- package/server/lib/zip-safety.js +0 -68
package/server/routes/logs.js
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
// Local log management routes (moved verbatim from server.js handleRequest).
|
|
2
|
-
import { existsSync, realpathSync, statSync, createReadStream,
|
|
2
|
+
import { existsSync, realpathSync, statSync, createReadStream, mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import AdmZip from 'adm-zip';
|
|
4
6
|
import { LOG_DIR } from '../../findcc.js';
|
|
5
|
-
import { _projectName,
|
|
6
|
-
import {
|
|
7
|
-
import { countLogEntries, streamRawEntriesAsync, readTailEntries
|
|
8
|
-
import {
|
|
7
|
+
import { _projectName, _v2Writer } from '../interceptor.js';
|
|
8
|
+
import { listV2Logs, listLocalLogs, countListedV1Files, deleteLogFiles, validateLogPath } from '../lib/log-management.js';
|
|
9
|
+
import { countLogEntries, streamRawEntriesAsync, readTailEntries } from '../lib/log-stream.js';
|
|
10
|
+
import { sseHead, sseWrite, wireEnd } from '../lib/wire-compress.js';
|
|
11
|
+
import { dirSizeSync } from '../lib/v2/layout.js';
|
|
12
|
+
import { extractV2Zip } from '../lib/log-zip.js';
|
|
13
|
+
import { startConvert, stopConvert, convertStatus } from '../lib/v2/convert-manager.js';
|
|
14
|
+
import { migrationStatus } from '../lib/v2/migrate-prompt.js';
|
|
15
|
+
|
|
16
|
+
// Cap for the in-memory zip build (adm-zip toBuffer holds the whole archive in
|
|
17
|
+
// heap) and the upload accumulation — the repo has an OOM history, so both the
|
|
18
|
+
// zipped source folder and the uploaded archive are bounded before buffering.
|
|
19
|
+
const MAX_UPLOAD = 300 * 1024 * 1024;
|
|
20
|
+
// Download-zip source cap; read per-request so CCV_MAX_ZIP_SOURCE is an ops
|
|
21
|
+
// tunable (and a test seam) without a restart.
|
|
22
|
+
const maxZipSource = () => Number(process.env.CCV_MAX_ZIP_SOURCE) || 300 * 1024 * 1024;
|
|
9
23
|
|
|
10
24
|
async function localLogs(req, res, parsedUrl, isLocal, deps) {
|
|
11
25
|
try {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
// 1.7.0: the DEFAULT list is v2 sessions, unconditionally. `?view=v1`
|
|
27
|
+
// serves the legacy-file list for the modal's v1 view (list + migrate +
|
|
28
|
+
// open + delete for leftovers the converter never removes).
|
|
29
|
+
if (parsedUrl?.searchParams?.get('view') === 'v1') {
|
|
30
|
+
const v1 = await listLocalLogs(LOG_DIR, _projectName);
|
|
31
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify(v1));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const result = listV2Logs(LOG_DIR, _projectName);
|
|
36
|
+
// Legacy v1 files are not in this list — surface two distinct signals:
|
|
37
|
+
// - _v1FileCount: v1 files ON DISK (gates the v1-view entry link; the
|
|
38
|
+
// converter never deletes sources, so this outlives a finished migration)
|
|
39
|
+
// - _unmigratedV1Count/Bytes: files still AWAITING migration (gates the
|
|
40
|
+
// migrate button + hint inside the v1 view and the startup prompt)
|
|
41
|
+
const mig = migrationStatus(LOG_DIR, _projectName || '');
|
|
42
|
+
result._unmigratedV1Count = mig.files;
|
|
43
|
+
result._unmigratedV1Bytes = mig.totalBytes;
|
|
44
|
+
result._v1FileCount = _projectName ? countListedV1Files(join(LOG_DIR, _projectName)) : 0;
|
|
16
45
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
17
46
|
res.end(JSON.stringify(result));
|
|
18
47
|
} catch (err) {
|
|
@@ -28,7 +57,69 @@ async function downloadLog(req, res, parsedUrl) {
|
|
|
28
57
|
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
29
58
|
return;
|
|
30
59
|
}
|
|
31
|
-
|
|
60
|
+
// wire-v2 S5: rebuilt download for v2 sessions — the adapter stream as a v1
|
|
61
|
+
// `.jsonl`. `format=raw` (session-dir zip) is the S6a half of the S0 contract:
|
|
62
|
+
// a lossless zip of the whole session folder, the transport for round-tripping
|
|
63
|
+
// a v2 session (folder) back through the upload-and-parse flow.
|
|
64
|
+
if (file.startsWith('v2:')) {
|
|
65
|
+
if (parsedUrl.searchParams.get('format') === 'raw') {
|
|
66
|
+
try {
|
|
67
|
+
const sessionDir = validateLogPath(LOG_DIR, file);
|
|
68
|
+
if (dirSizeSync(sessionDir) > maxZipSource()) {
|
|
69
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
70
|
+
res.end(JSON.stringify({ error: 'Session too large to zip', code: 'TOO_LARGE' }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const dirName = basename(sessionDir);
|
|
74
|
+
const zip = new AdmZip();
|
|
75
|
+
// Wrap the folder under `<dirName>/` and skip dotfiles (.DS_Store etc.).
|
|
76
|
+
// adm-zip passes the zip-internal path (incl. the `<dirName>/` root) to
|
|
77
|
+
// the filter, so match on any path segment starting with a dot.
|
|
78
|
+
zip.addLocalFolder(sessionDir, dirName, (n) => !/(^|\/)\./.test(n.replace(/\\/g, '/')));
|
|
79
|
+
const buf = zip.toBuffer();
|
|
80
|
+
res.writeHead(200, {
|
|
81
|
+
'Content-Type': 'application/zip',
|
|
82
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(`${dirName}.zip`)}"`,
|
|
83
|
+
'Content-Length': buf.length,
|
|
84
|
+
});
|
|
85
|
+
res.end(buf);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (!res.headersSent) {
|
|
88
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
89
|
+
// Don't leak internal messages (may contain absolute paths) on 500 —
|
|
90
|
+
// genericize like the upload handler does.
|
|
91
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
92
|
+
res.end(JSON.stringify({ error: status >= 500 ? 'server_error' : err.message }));
|
|
93
|
+
} else {
|
|
94
|
+
res.end();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const sessionDir = validateLogPath(LOG_DIR, file);
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
'Content-Type': 'application/octet-stream',
|
|
103
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(`${basename(sessionDir)}.v2-rebuilt.jsonl`)}"`,
|
|
104
|
+
'Transfer-Encoding': 'chunked',
|
|
105
|
+
});
|
|
106
|
+
await streamRawEntriesAsync(sessionDir, (raw) => {
|
|
107
|
+
res.write(raw);
|
|
108
|
+
res.write('\n---\n');
|
|
109
|
+
});
|
|
110
|
+
res.end();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (!res.headersSent) {
|
|
113
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
114
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
115
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
116
|
+
} else {
|
|
117
|
+
res.end();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!file.endsWith('.jsonl')) {
|
|
32
123
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
33
124
|
res.end(JSON.stringify({ error: 'Invalid file type' }));
|
|
34
125
|
return;
|
|
@@ -48,10 +139,7 @@ async function downloadLog(req, res, parsedUrl) {
|
|
|
48
139
|
return;
|
|
49
140
|
}
|
|
50
141
|
const format = parsedUrl.searchParams.get('format');
|
|
51
|
-
|
|
52
|
-
const fileName = (format !== 'raw' && file.endsWith('.jsonl.zip'))
|
|
53
|
-
? basename(file).slice(0, -4)
|
|
54
|
-
: basename(file);
|
|
142
|
+
const fileName = basename(file);
|
|
55
143
|
// Delta storage: format=raw 下载原始文件;默认下载重建后的全量格式
|
|
56
144
|
if (format === 'raw') {
|
|
57
145
|
const stat = statSync(realPath);
|
|
@@ -89,20 +177,24 @@ async function localLog(req, res, parsedUrl) {
|
|
|
89
177
|
return;
|
|
90
178
|
}
|
|
91
179
|
|
|
92
|
-
//
|
|
93
|
-
|
|
180
|
+
// 验证文件类型:v2 寻址(spec §12);直连 .jsonl 保留为未迁移日志的逃生舱
|
|
181
|
+
// (代码与转换器共享,UI 无入口——1.7.0 决策)。
|
|
182
|
+
const isV2Ref = file.startsWith('v2:');
|
|
183
|
+
if (!isV2Ref && !file.endsWith('.jsonl')) {
|
|
94
184
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
95
|
-
res.end(JSON.stringify({ error: 'Invalid file type. Only .jsonl
|
|
185
|
+
res.end(JSON.stringify({ error: 'Invalid file type. Only .jsonl files are allowed.' }));
|
|
96
186
|
return;
|
|
97
187
|
}
|
|
98
188
|
|
|
99
189
|
try {
|
|
100
190
|
// 独立 SSE 流:直接向请求方返回 event-stream,不走 /events 广播
|
|
101
|
-
validateLogPath
|
|
102
|
-
const filePath =
|
|
191
|
+
// v2 寻址时 validateLogPath 返回 session 目录绝对路径,log-stream 据此分派适配器
|
|
192
|
+
const filePath = validateLogPath(LOG_DIR, file);
|
|
103
193
|
const limitVal = Math.min(parseInt(parsedUrl.searchParams.get('limit'), 10) || 0, 500);
|
|
104
194
|
|
|
105
|
-
|
|
195
|
+
// Negotiated Content-Encoding (br|identity) — every subsequent byte MUST
|
|
196
|
+
// go through sseWrite/wireEnd (see server/lib/wire-compress.js).
|
|
197
|
+
sseHead(req, res, 200, {
|
|
106
198
|
'Content-Type': 'text/event-stream',
|
|
107
199
|
'Cache-Control': 'no-cache',
|
|
108
200
|
'Connection': 'keep-alive',
|
|
@@ -111,27 +203,27 @@ async function localLog(req, res, parsedUrl) {
|
|
|
111
203
|
if (limitVal > 0) {
|
|
112
204
|
// 尾部加载模式:跳过 countLogEntries,只读文件末尾
|
|
113
205
|
const { entries, hasMore, oldestTimestamp, estimatedTotal } = await readTailEntries(filePath, { limit: limitVal });
|
|
114
|
-
res
|
|
206
|
+
sseWrite(res, `event: load_start\ndata: ${JSON.stringify({ total: estimatedTotal, incremental: false, hasMore, oldestTs: oldestTimestamp })}\n\n`);
|
|
115
207
|
for (const raw of entries) {
|
|
116
|
-
res
|
|
117
|
-
res
|
|
118
|
-
res
|
|
208
|
+
sseWrite(res, 'event: load_chunk\ndata: [');
|
|
209
|
+
sseWrite(res, raw.includes('\n') ? raw.replace(/\n/g, '') : raw);
|
|
210
|
+
sseWrite(res, ']\n\n');
|
|
119
211
|
}
|
|
120
212
|
} else {
|
|
121
213
|
// 全量加载模式(向后兼容)
|
|
122
214
|
const total = await countLogEntries(filePath);
|
|
123
|
-
res
|
|
215
|
+
sseWrite(res, `event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
|
|
124
216
|
await streamRawEntriesAsync(filePath, (raw) => {
|
|
125
|
-
res
|
|
126
|
-
res
|
|
127
|
-
res
|
|
217
|
+
sseWrite(res, 'event: load_chunk\ndata: [');
|
|
218
|
+
sseWrite(res, raw.includes('\n') ? raw.replace(/\n/g, '') : raw);
|
|
219
|
+
sseWrite(res, ']\n\n');
|
|
128
220
|
});
|
|
129
221
|
}
|
|
130
|
-
res
|
|
131
|
-
res
|
|
222
|
+
sseWrite(res, `event: load_end\ndata: {}\n\n`);
|
|
223
|
+
wireEnd(res);
|
|
132
224
|
} catch (err) {
|
|
133
225
|
// 如果 headers 未发送,返回 JSON 错误;否则关闭连接
|
|
134
|
-
// 落 stderr 让用户在 ccv
|
|
226
|
+
// 落 stderr 让用户在 ccv 终端能看到具体原因(SSE onerror 在客户端
|
|
135
227
|
// 不携带错误明细,只能从服务端日志反查)
|
|
136
228
|
console.error('[local-log]', file, err && err.stack || err);
|
|
137
229
|
if (!res.headersSent) {
|
|
@@ -139,7 +231,7 @@ async function localLog(req, res, parsedUrl) {
|
|
|
139
231
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
140
232
|
res.end(JSON.stringify({ error: err.message }));
|
|
141
233
|
} else {
|
|
142
|
-
res
|
|
234
|
+
wireEnd(res); // flush the encoder trailer if the stream was compressed
|
|
143
235
|
}
|
|
144
236
|
}
|
|
145
237
|
}
|
|
@@ -155,7 +247,7 @@ function deleteLogs(req, res, parsedUrl, isLocal, deps) {
|
|
|
155
247
|
res.end(JSON.stringify({ error: 'No files specified' }));
|
|
156
248
|
return;
|
|
157
249
|
}
|
|
158
|
-
const results = deleteLogFiles(LOG_DIR, files);
|
|
250
|
+
const results = deleteLogFiles(LOG_DIR, files, { liveSessionDir: _v2Writer.currentSessionDir() });
|
|
159
251
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
160
252
|
res.end(JSON.stringify({ results }));
|
|
161
253
|
} catch {
|
|
@@ -165,162 +257,115 @@ function deleteLogs(req, res, parsedUrl, isLocal, deps) {
|
|
|
165
257
|
});
|
|
166
258
|
}
|
|
167
259
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
176
|
-
res.end(JSON.stringify({ ok: true, merged }));
|
|
177
|
-
} catch (err) {
|
|
178
|
-
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'INVALID_INPUT' ? 400 : 500;
|
|
179
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
180
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
181
|
-
}
|
|
182
|
-
});
|
|
260
|
+
// wire-v2 S8: one-click v1→v2 migration of the CURRENT project, run by the
|
|
261
|
+
// resident convert-manager worker. GET = status snapshot for the modal's
|
|
262
|
+
// polling; POST {action:'start'|'stop'} controls the task. Local/LAN exposure
|
|
263
|
+
// via the existing auth middleware (same posture as delete-logs).
|
|
264
|
+
function getWireV2Convert(req, res) {
|
|
265
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(JSON.stringify(convertStatus(LOG_DIR, _projectName || '')));
|
|
183
267
|
}
|
|
184
268
|
|
|
185
|
-
function
|
|
269
|
+
function postWireV2Convert(req, res, parsedUrl, isLocal, deps) {
|
|
186
270
|
let body = '';
|
|
187
271
|
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
188
272
|
req.on('end', () => {
|
|
189
273
|
try {
|
|
190
|
-
const {
|
|
191
|
-
if (
|
|
274
|
+
const { action } = JSON.parse(body || '{}');
|
|
275
|
+
if (action !== 'start' && action !== 'stop') {
|
|
192
276
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
193
|
-
res.end(JSON.stringify({ error: '
|
|
277
|
+
res.end(JSON.stringify({ error: 'action must be "start" or "stop"' }));
|
|
194
278
|
return;
|
|
195
279
|
}
|
|
196
|
-
if (
|
|
280
|
+
if (action === 'start' && !_projectName) {
|
|
197
281
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
198
|
-
res.end(JSON.stringify({ error: '
|
|
282
|
+
res.end(JSON.stringify({ error: 'no active project' }));
|
|
199
283
|
return;
|
|
200
284
|
}
|
|
201
|
-
const result =
|
|
202
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
203
|
-
res.end(JSON.stringify(result));
|
|
204
|
-
} catch {
|
|
205
|
-
|
|
206
|
-
res.
|
|
285
|
+
const result = action === 'start' ? startConvert(LOG_DIR, _projectName) : stopConvert();
|
|
286
|
+
res.writeHead(result.ok ? 200 : 409, { 'Content-Type': 'application/json' });
|
|
287
|
+
res.end(JSON.stringify(result.ok ? { ok: true } : { error: result.error }));
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const status = err instanceof SyntaxError ? 400 : 500;
|
|
290
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
291
|
+
res.end(JSON.stringify({ error: err?.message || 'internal error' }));
|
|
207
292
|
}
|
|
208
293
|
});
|
|
209
294
|
}
|
|
210
295
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
closeSync(fd);
|
|
224
|
-
}
|
|
225
|
-
const frameEnd = head.indexOf('\n---\n');
|
|
226
|
-
if (frameEnd <= 0) return null;
|
|
227
|
-
const entry = JSON.parse(head.slice(0, frameEnd));
|
|
228
|
-
return entry && entry.ccvRotationContext ? entry : null;
|
|
229
|
-
} catch {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Cheap substring gate evaluated before JSON.parse: every teammate shape
|
|
235
|
-
// carries at least one of these markers in its raw JSON (external tag,
|
|
236
|
-
// proxy-mode system marker, native SDK prompt). Giant MainAgent checkpoint
|
|
237
|
-
// frames are skipped without parsing.
|
|
238
|
-
function teammateRawPrefilter(raw) {
|
|
239
|
-
return raw.includes('"teammate"')
|
|
240
|
-
|| raw.includes('agent in a team')
|
|
241
|
-
|| raw.includes('Agent Teammate Communication')
|
|
242
|
-
|| raw.includes('You are a Claude agent');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Single-flight + short-TTL cache for the previous-segment scan. A real
|
|
246
|
-
// predecessor is ~300MB and the parse pass transiently costs hundreds of MB of
|
|
247
|
-
// RSS; every non-incremental cold load triggers the route, so concurrent
|
|
248
|
-
// viewers / rapid refreshes must share one scan. Keyed by path+size+mtime —
|
|
249
|
-
// the old segment can still GROW (external teammates keep appending to it),
|
|
250
|
-
// which changes the key and forces a rescan.
|
|
251
|
-
const PREV_SCAN_TTL_MS = 60 * 1000;
|
|
252
|
-
let _prevScanCache = null; // { key, promise, expiresAt }
|
|
253
|
-
|
|
254
|
-
function collectPrevSegmentCached(prev, predicate, opts) {
|
|
255
|
-
let st = null;
|
|
256
|
-
try { st = statSync(prev); } catch { }
|
|
257
|
-
const key = `${prev}|${st ? st.size : 0}|${st ? st.mtimeMs : 0}`;
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
if (_prevScanCache && _prevScanCache.key === key && _prevScanCache.expiresAt > now) {
|
|
260
|
-
return _prevScanCache.promise;
|
|
296
|
+
// wire-v2 S6a: upload a v2-session zip (produced by `?format=raw` download),
|
|
297
|
+
// extract it to a temp dir, synthesize + stream it back as v1-shaped `\n---\n`
|
|
298
|
+
// raw entries for ephemeral viewing, then delete the temp dir. Nothing is
|
|
299
|
+
// written into LOG_DIR — view-only, parity with the legacy client-side `.jsonl`
|
|
300
|
+
// upload. Raw `application/zip` body (no multipart); uses its own MAX_UPLOAD
|
|
301
|
+
// cap, NOT deps.MAX_POST_BODY (which is ~10MB and would reject any real zip).
|
|
302
|
+
function uploadLogZip(req, res) {
|
|
303
|
+
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
304
|
+
if (contentLength > MAX_UPLOAD) {
|
|
305
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
306
|
+
res.end(JSON.stringify({ error: 'File too large', code: 'TOO_LARGE' }));
|
|
307
|
+
return;
|
|
261
308
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* registry — the sentinel may sit outside the client's load window)
|
|
277
|
-
* lines: one backfill entry per line (teammate-like, renderable only)
|
|
278
|
-
* last: { done: true, truncated, prevSegment }
|
|
279
|
-
*/
|
|
280
|
-
async function prevSegmentTeammates(req, res, parsedUrl, isLocal, deps) {
|
|
281
|
-
res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });
|
|
282
|
-
try {
|
|
283
|
-
const current = LOG_FILE;
|
|
284
|
-
const sentinel = readHeadRotationContext(current);
|
|
285
|
-
const names = new Map(sentinel?.teammateNames?.filter((p) => Array.isArray(p) && p[0]) || []);
|
|
286
|
-
for (const [prefix, name] of getAgentSpawnRegistrySnapshot()) names.set(prefix, name);
|
|
287
|
-
res.write(JSON.stringify({
|
|
288
|
-
rotationContext: sentinel ? { from: sentinel.from || null } : null,
|
|
289
|
-
teammateNames: [...names.entries()],
|
|
290
|
-
}) + '\n');
|
|
291
|
-
// No live file (workspace pre-select) or resume placeholder → context only.
|
|
292
|
-
if (!current || current.endsWith('_temp.jsonl')) {
|
|
293
|
-
res.end(JSON.stringify({ done: true, truncated: false, prevSegment: null }) + '\n');
|
|
309
|
+
let chunks = [];
|
|
310
|
+
let totalSize = 0;
|
|
311
|
+
let aborted = false;
|
|
312
|
+
// A large remote upload can reset mid-flight; without an 'error' listener the
|
|
313
|
+
// socket error would crash the process (unhandled 'error' event).
|
|
314
|
+
req.on('error', () => { aborted = true; chunks = []; });
|
|
315
|
+
req.on('data', (chunk) => {
|
|
316
|
+
if (aborted) return;
|
|
317
|
+
totalSize += chunk.length;
|
|
318
|
+
if (totalSize > MAX_UPLOAD) {
|
|
319
|
+
aborted = true;
|
|
320
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
321
|
+
res.end(JSON.stringify({ error: 'File too large', code: 'TOO_LARGE' }));
|
|
322
|
+
req.destroy();
|
|
294
323
|
return;
|
|
295
324
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
325
|
+
chunks.push(chunk);
|
|
326
|
+
});
|
|
327
|
+
req.on('end', async () => {
|
|
328
|
+
if (aborted) return;
|
|
329
|
+
let tmpDir = null;
|
|
330
|
+
try {
|
|
331
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'ccv-upload-'));
|
|
332
|
+
const buf = Buffer.concat(chunks);
|
|
333
|
+
chunks = []; // free the per-chunk copies; adm-zip holds `buf` from here
|
|
334
|
+
const sessionDir = extractV2Zip(buf, tmpDir);
|
|
335
|
+
res.writeHead(200, {
|
|
336
|
+
'Content-Type': 'application/octet-stream',
|
|
337
|
+
'Transfer-Encoding': 'chunked',
|
|
338
|
+
});
|
|
339
|
+
await streamRawEntriesAsync(sessionDir, (raw) => {
|
|
340
|
+
res.write(raw);
|
|
341
|
+
res.write('\n---\n');
|
|
342
|
+
});
|
|
343
|
+
res.end();
|
|
344
|
+
} catch (err) {
|
|
345
|
+
const status = err?.status || 500;
|
|
346
|
+
if (status >= 500) console.error('[api/upload-log-zip]', err);
|
|
347
|
+
if (!res.headersSent) {
|
|
348
|
+
const safeError = status >= 500 ? 'server_error' : (err?.message || 'error');
|
|
349
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
350
|
+
res.end(JSON.stringify({ error: safeError, code: err?.code || 'unknown' }));
|
|
351
|
+
} else {
|
|
352
|
+
res.end();
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
// Always reclaim the temp dir. The adapter has finished all reads by the
|
|
356
|
+
// time the await settles (cold v2 reads are bounded), so this is race-free
|
|
357
|
+
// even when the client disconnected mid-stream.
|
|
358
|
+
if (tmpDir) { try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} }
|
|
301
359
|
}
|
|
302
|
-
|
|
303
|
-
!entry.inProgress && !entry.isHeartbeat && !entry.isCountTokens
|
|
304
|
-
&& !entry.ccvRotationContext
|
|
305
|
-
&& !(entry._deltaFormat && entry.mainAgent && !entry.teammate)
|
|
306
|
-
&& !!entry.timestamp
|
|
307
|
-
&& Array.isArray(entry.response?.body?.content) && entry.response.body.content.length > 0
|
|
308
|
-
&& isTeammateLikeEntry(entry)
|
|
309
|
-
), { rawPrefilter: teammateRawPrefilter });
|
|
310
|
-
for (const entry of entries) res.write(JSON.stringify(entry) + '\n');
|
|
311
|
-
res.end(JSON.stringify({ done: true, truncated, prevSegment: basename(prev) }) + '\n');
|
|
312
|
-
} catch (err) {
|
|
313
|
-
// Headers already sent — terminate the NDJSON stream with an error line.
|
|
314
|
-
try { res.end(JSON.stringify({ done: true, error: err?.message || 'internal error' }) + '\n'); } catch { }
|
|
315
|
-
}
|
|
360
|
+
});
|
|
316
361
|
}
|
|
317
362
|
|
|
318
363
|
export const logsRoutes = [
|
|
319
364
|
{ method: 'GET', match: 'exact', path: '/api/local-logs', handler: localLogs },
|
|
365
|
+
{ method: 'GET', match: 'exact', path: '/api/wire-v2-convert', handler: getWireV2Convert },
|
|
366
|
+
{ method: 'POST', match: 'exact', path: '/api/wire-v2-convert', handler: postWireV2Convert },
|
|
320
367
|
{ method: 'GET', match: 'exact', path: '/api/download-log', handler: downloadLog },
|
|
321
368
|
{ method: 'GET', match: 'exact', path: '/api/local-log', handler: localLog },
|
|
322
369
|
{ method: 'POST', match: 'exact', path: '/api/delete-logs', handler: deleteLogs },
|
|
323
|
-
{ method: 'POST', match: 'exact', path: '/api/
|
|
324
|
-
{ method: 'POST', match: 'exact', path: '/api/archive-logs', handler: archiveLogs },
|
|
325
|
-
{ method: 'GET', match: 'exact', path: '/api/prev-segment-teammates', handler: prevSegmentTeammates },
|
|
370
|
+
{ method: 'POST', match: 'exact', path: '/api/upload-log-zip', handler: uploadLogZip },
|
|
326
371
|
];
|
|
@@ -58,7 +58,6 @@ function preferencesGet(req, res, parsedUrl, isLocal, deps) {
|
|
|
58
58
|
prefs.logDir = LOG_DIR; // 始终返回当前运行时的日志目录
|
|
59
59
|
// 日志设置出厂默认"继承":键缺失(从未设置过)才注入;显式关闭持久化的是 null(键存在),不覆盖。
|
|
60
60
|
// 虚拟默认 —— 仅注入回包不落盘(GET 不写文件),直接读 preferences.json 的代码看不到该默认。
|
|
61
|
-
if (!('resumeAutoChoice' in prefs)) prefs.resumeAutoChoice = 'continue';
|
|
62
61
|
// home-friendly 展示形态:设了 CLAUDE_CONFIG_DIR 的用户看到真实路径,默认用户看到 "~/.claude"
|
|
63
62
|
// join() 而非字符串拼接,避免 Windows 分隔符不匹配导致比较失败
|
|
64
63
|
const _cDir = getClaudeConfigDir();
|
|
@@ -168,8 +167,6 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
168
167
|
delete prefs.prefsByProject; // fork blob 绝不回显(与 GET 一致)
|
|
169
168
|
stripImConfigs(prefs);
|
|
170
169
|
prefs.logDir = LOG_DIR;
|
|
171
|
-
// 与 GET 一致:回显里补齐 resumeAutoChoice 虚拟默认(已在上方落盘,文件不含该默认值)
|
|
172
|
-
if (!('resumeAutoChoice' in prefs)) prefs.resumeAutoChoice = 'continue';
|
|
173
170
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
174
171
|
res.end(JSON.stringify(prefs));
|
|
175
172
|
} catch (err) {
|
|
@@ -22,10 +22,9 @@ export function getInstallMethod({
|
|
|
22
22
|
return 'npm';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function projectName(req, res
|
|
25
|
+
function projectName(req, res) {
|
|
26
26
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
27
|
-
|
|
28
|
-
res.end(JSON.stringify({ projectName: _projectName || '', instanceId: (deps && deps.instanceId) || null }));
|
|
27
|
+
res.end(JSON.stringify({ projectName: _projectName || '' }));
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function projectDir(req, res) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Server-side "current session" pin — replaces the old per-browser localStorage pin so a
|
|
2
|
-
// project's
|
|
3
|
-
//
|
|
4
|
-
// - GET /api/session-pin → { pinnedSessionId
|
|
2
|
+
// project's current-session pointer is shared across every device hitting THIS ccv process.
|
|
3
|
+
// View state only → no loopback gating.
|
|
4
|
+
// - GET /api/session-pin → { pinnedSessionId }
|
|
5
5
|
// - POST /api/session-pin {pinnedSessionId: string|null} → persist + SSE-broadcast
|
|
6
6
|
// `_logDir` is the live per-project dir ('' = no active project → GET null / POST no-op).
|
|
7
7
|
import { _logDir } from '../interceptor.js';
|
|
@@ -13,9 +13,8 @@ function sendJson(res, code, obj) {
|
|
|
13
13
|
res.end(JSON.stringify(obj));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function getHandler(req, res
|
|
17
|
-
|
|
18
|
-
sendJson(res, 200, { pinnedSessionId: readPin(_logDir, instanceId), instanceId });
|
|
16
|
+
function getHandler(req, res) {
|
|
17
|
+
sendJson(res, 200, { pinnedSessionId: readPin(_logDir) });
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
function postHandler(req, res, parsedUrl, isLocal, deps) {
|
|
@@ -27,8 +26,8 @@ function postHandler(req, res, parsedUrl, isLocal, deps) {
|
|
|
27
26
|
catch { return sendJson(res, 400, { error: 'Invalid JSON' }); }
|
|
28
27
|
const raw = incoming.pinnedSessionId;
|
|
29
28
|
const pinnedSessionId = (typeof raw === 'string' && raw) ? raw : null;
|
|
30
|
-
writePin(_logDir,
|
|
31
|
-
// Broadcast to THIS process's SSE clients so every device
|
|
29
|
+
writePin(_logDir, pinnedSessionId);
|
|
30
|
+
// Broadcast to THIS process's SSE clients so every device stays in sync.
|
|
32
31
|
sendEventToClients(deps.clients, 'session_pin', { pinnedSessionId });
|
|
33
32
|
sendJson(res, 200, { ok: true, pinnedSessionId });
|
|
34
33
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Wire v3 routes (V3.S1+): on-demand single-entry detail for the metadata-row
|
|
2
|
+
// request list. Registered unconditionally — harmless while CCV_WIRE_V3 is off
|
|
3
|
+
// (nothing calls it), so the route itself needs no flag gate.
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
6
|
+
import { validateLogPath } from '../lib/log-management.js';
|
|
7
|
+
import { resolveSessionDirName } from '../lib/v2/session-select.js';
|
|
8
|
+
import { readV2SingleEntry } from '../lib/v2/adapter.js';
|
|
9
|
+
import { sseHead, sseWrite, wireEnd } from '../lib/wire-compress.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* GET /api/v2-entry?file=v2:<project>/<dirToken>&seq=N[&sid=<uuid>]
|
|
13
|
+
* dirToken is normally the session dir basename (`<ts>_<uuid>`); a bare UUID
|
|
14
|
+
* (the client only holds `_seqEpoch="v2:<uuid>"`) is resolved to the basename
|
|
15
|
+
* BEFORE validateLogPath — validation expects an existing directory path.
|
|
16
|
+
* `sid` disambiguates teammate rows: the fold runs over the leader dir, the
|
|
17
|
+
* target is keyed (sid, seq); omitted = match seq in any folded session.
|
|
18
|
+
*/
|
|
19
|
+
async function v2Entry(req, res, parsedUrl) {
|
|
20
|
+
const file = parsedUrl.searchParams.get('file') || '';
|
|
21
|
+
const seq = parseInt(parsedUrl.searchParams.get('seq'), 10);
|
|
22
|
+
const sid = parsedUrl.searchParams.get('sid') || null;
|
|
23
|
+
const m = /^v2:([^/]+)\/([^/]+)$/.exec(file);
|
|
24
|
+
if (!m || !Number.isInteger(seq) || seq < 0) {
|
|
25
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
26
|
+
res.end(JSON.stringify({ error: 'expected file=v2:<project>/<session>&seq=<n>' }));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// UUID → dir-basename resolution first (rename/-c adoption safe), then the
|
|
31
|
+
// same path validation every other v2 ref goes through.
|
|
32
|
+
const [, project, dirToken] = m;
|
|
33
|
+
const resolved = resolveSessionDirName(join(LOG_DIR, project), dirToken) || dirToken;
|
|
34
|
+
const sessionDir = validateLogPath(LOG_DIR, `v2:${project}/${resolved}`);
|
|
35
|
+
const result = await readV2SingleEntry(sessionDir, { seq, sessionId: sid });
|
|
36
|
+
if (!result) {
|
|
37
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
38
|
+
res.end(JSON.stringify({ error: 'entry not found' }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Whole-stream JSON (like /api/requests): compress without per-event flush.
|
|
42
|
+
sseHead(req, res, 200, { 'Content-Type': 'application/json' }, { flush: false });
|
|
43
|
+
// slots are raw JSON strings — compose without re-parse/re-stringify.
|
|
44
|
+
sseWrite(res, `{"entry":${result.entry},"prevMain":${result.prevMain ?? 'null'}}`);
|
|
45
|
+
wireEnd(res);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('[v2-entry]', file, err && err.stack || err);
|
|
48
|
+
if (!res.headersSent) {
|
|
49
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
50
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
51
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
52
|
+
} else {
|
|
53
|
+
wireEnd(res);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const v2Routes = [
|
|
59
|
+
{ method: 'GET', match: 'exact', path: '/api/v2-entry', handler: v2Entry },
|
|
60
|
+
];
|