Skip to content

Commit 7006871

Browse files
王超claude
authored andcommitted
Merge origin/main into feat/cc-switch-import
Resolve history.md conflict: the cc-switch import entry moves into a new Unreleased section (main's 1.7.3 stamp landed independently); main's 1.7.3 content kept verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 parents 3c6d6c5 + c94882c commit 7006871

214 files changed

Lines changed: 23395 additions & 11867 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,23 @@ Once started in programming mode, the web page opens automatically.
5252

5353
cc-viewer also ships as a native desktop app: [download page](https://github.com/weiesky/cc-viewer/releases)
5454

55+
### Upgrading to 1.7.0 (log format v2)
56+
57+
Since 1.7.0, logs are stored in a per-session directory format (wire-format v2) instead of single `.jsonl` files — roughly 90% smaller on disk. Existing v1 `.jsonl` files are never modified or deleted; the log dialog lists v2 sessions by default, and a small “View legacy (v1) logs” entry (shown while old files exist) opens a v1 view where they can be viewed, migrated, or deleted. On startup, cc-viewer offers one-click migration when legacy logs are found (strongly recommended when continuing an old conversation with `claude -c`, whose first half lives in the old files). You can also migrate from the terminal:
58+
59+
```bash
60+
ccv convert <project> # migrate one project
61+
ccv convert --all # migrate every project
62+
ccv verify <v1-file> # check a v1 file against its converted sessions
63+
```
64+
65+
A session that fails golden verification is held in `sessions-quarantine/` for inspection instead of failing the whole migration — the other sessions still migrate.
66+
5567
### Logger mode
5668

5769
If you still prefer the native claude tool or the VS Code extension, use this mode.
5870

59-
In this mode, launching `claude` will automatically start a logging process that records request logs to \~/.claude/cc-viewer/*yourproject*/date.jsonl
71+
In this mode, launching `claude` will automatically start a logging process that records request logs to per-session directories under \~/.claude/cc-viewer/*yourproject*/sessions/ (wire-format v2)
6072

6173
Enable logger mode:
6274

@@ -149,7 +161,7 @@ The **Edit System Prompt** modal (hamburger menu → Edit System Prompt) is tabb
149161

150162
* The **Default** tab keeps the classic behavior: it writes `CC_SYSTEM.md` (override) or `CC_APPEND_SYSTEM.md` (append) into the current workspace, injected as `--system-prompt-file` / `--append-system-prompt-file` on the next ccv launch.
151163
* **Model tabs**: click **+ Add model**, type a name such as `opus` or `Gemini3`, and pick a scope — **Global** (`~/.claude/cc-viewer/system_prompt/`, applies to every workspace) or **Workspace** (`<project>/system_prompt/`). Each tab has its own Append/Override switch and Markdown preview.
152-
* Entries are stored as uppercase files: `OPUS_SYSTEM.md` (override) or `OPUS_APPEND_SYSTEM.md` (append). Matching is fuzzy — a case-insensitive substring of the model ID used at the last launch, so `opus` matches `claude-opus-4-8[1m]` regardless of version. A workspace match beats a global one; within a scope the longest name wins; a matched entry fully replaces the Default files for that launch.
164+
* Entries are stored as uppercase files: `OPUS_SYSTEM.md` (override) or `OPUS_APPEND_SYSTEM.md` (append). Matching is fuzzy — a case-insensitive substring of the model ID resolved from the ACTIVE configuration (an active third-party proxy profile's model mapping > the launch environment's `ANTHROPIC_MODEL`/`CLAUDE_MODEL` > the `model` configured in `settings.json`; with no configuration signal, no entry is injected), so `opus` matches `claude-opus-4-8[1m]` regardless of version. Switching the proxy profile mid-session re-matches only after the claude session is restarted, and a `--model` flag passed through extra args is not consulted (known limitations). A workspace match beats a global one; within a scope the longest name wins; a matched entry fully replaces the Default files for that launch.
153165
* Saving a tab empty deletes the entry. Model switches made mid-session apply at the next relaunch. Set `CCV_DISABLE_AUTO_SYSTEM_PROMPT=1` to disable all automatic injection. You may commit `<project>/system_prompt/` to share prompts with your team, or add it to `.gitignore` to keep them private.
154166

155167
### Logger mode (view the complete Claude Code session)

cli.js

Lines changed: 137 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { INJECT_IMPORT, LEGACY_INJECT_IMPORTS, resolveCliPath, resolveNativePath
1515
import { ensureHooks, removeAllManagedHooks } from './server/lib/ensure-hooks.js';
1616
import { injectCliJsAt, removeCliJsInjectionAt, INJECT_START as _INJECT_START, INJECT_END as _INJECT_END, buildInjectBlock as _buildInjectBlock } from './server/lib/cli-inject.js';
1717
import { normalizeBasePath } from './server/lib/base-path.js';
18+
import { mergeSettingsIntoArgs } from './server/lib/settings-merge.js';
1819
import { createHardenedCleanup, installWinKeypressFallback } from './server/lib/term-signals.js';
1920

2021
const __dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -222,6 +223,9 @@ function removeCliJsInjection() {
222223

223224
async function runProxyCommand(args) {
224225
try {
226+
// P2 detection channel ①(shell-hook 形态):`claude -c` 经 hook 变成
227+
// `ccv run -- claude --ccv-internal -c …`,在 proxy/server 模块加载前打标。
228+
markContinueEnv(args);
225229
// Dynamic import to avoid side effects when just installing
226230
const { startProxy } = await import('./server/proxy.js');
227231
const proxyPort = await startProxy();
@@ -269,17 +273,34 @@ async function runProxyCommand(args) {
269273
// 剥离 cc-viewer 的内部短路开关,避免泄漏给 claude 子进程
270274
delete env.CCV_SKIP_THINKING_DISPLAY;
271275

272-
const settingsJson = JSON.stringify({
276+
const settingsObj = {
273277
env: {
274278
ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL
275279
}
276-
});
280+
};
281+
let settingsJson = JSON.stringify(settingsObj);
282+
283+
const isClaudeCmd = cmd === 'claude' || /[\\/]claude(\.exe)?$/.test(cmd);
284+
285+
// Fold a user-supplied --settings into the injected settings and emit a single flag
286+
// (claude is last-wins for duplicate --settings; two parallel flags let the user's
287+
// silently clobber the injected ANTHROPIC_BASE_URL and break capture). claude only:
288+
// another tool's --settings belongs to that tool and must not be touched.
289+
// Runs on the RAW cmdArgs BEFORE the --thinking-display append below: otherwise a
290+
// trailing valueless user --settings would consume our appended token as its value.
291+
if (isClaudeCmd) {
292+
const settingsMerge = mergeSettingsIntoArgs(cmdArgs, settingsObj, { cwd: process.cwd() });
293+
if (settingsMerge.warningDetail) {
294+
console.warn(t('cli.settingsMergeFailed', settingsMerge.warningDetail));
295+
}
296+
settingsJson = settingsMerge.settingsJson;
297+
cmdArgs = settingsMerge.args;
298+
}
277299

278300
// 注入默认 --thinking-display summarized,仅对 claude 二进制(其他命令如 `ccv run -- sometool` 跳过)。
279301
// 若 claude 不识别该 flag(老版本/fork)会 unknown option 崩溃——由 pty-manager.js::spawnClaude 的
280302
// onExit reactive retry 兜底;cli.js 这条路径是一次性子进程,没有 respawn 机会,用户需手动重试。
281303
// 可通过环境变量 CCV_SKIP_THINKING_DISPLAY=1 强制跳过。
282-
const isClaudeCmd = cmd === 'claude' || /[\\/]claude(\.exe)?$/.test(cmd);
283304
if (isClaudeCmd && process.env.CCV_SKIP_THINKING_DISPLAY !== '1') {
284305
const { withDefaultThinkingDisplay } = await import('./server/pty-manager.js');
285306
cmdArgs = withDefaultThinkingDisplay(cmdArgs);
@@ -306,16 +327,41 @@ async function runProxyCommand(args) {
306327

307328
// ensureHooks() extracted to server/lib/ensure-hooks.js (shared with electron/tab-worker.js)
308329

309-
// Print the `--pid` instance id + this project's previously-used ids in the startup banner,
310-
// so the user can recall which id to reuse (with `-c`) next time. No-op without `--pid`.
311-
function printInstanceBanner(serverMod) {
330+
// P2 (-c migration guidance, detection channel ①): the interceptor resolves
331+
// CCV_CLAUDE_CONTINUE at prompt time, so setting it before the server module
332+
// loads is sufficient — claude's own -c semantics are untouched.
333+
function markContinueEnv(claudeArgs) {
334+
if (!Array.isArray(claudeArgs)) return;
335+
if (claudeArgs.some((a) => a === '-c' || a === '--continue' || a === '-r' || a === '--resume')) {
336+
process.env.CCV_CLAUDE_CONTINUE = '1';
337+
}
338+
// Explicit `-r`/`--resume` targets a session of the user's choosing, while
339+
// folder adoption always targets the LATEST main session — so a resume must
340+
// keep its pre-adoption behavior (own folder). The CONTINUE env above still
341+
// marks the launch for the migrate prompt.
342+
if (claudeArgs.some((a) => a === '-r' || a === '--resume')) {
343+
process.env.CCV_CLAUDE_RESUME = '1';
344+
}
345+
// `--fork-session` continues/resumes but mints a NEW session id on purpose —
346+
// the interceptor reads this before the server module loads so `-c` folder
347+
// adoption is suppressed (claude's own semantics are untouched).
348+
if (claudeArgs.includes('--fork-session')) {
349+
process.env.CCV_CLAUDE_FORK_SESSION = '1';
350+
}
351+
}
352+
353+
// 1.7.0: legacy v1 logs still present → one localized banner line pointing at
354+
// `ccv convert` (headless users never see the web migrate prompt).
355+
async function printMigrationBanner() {
312356
try {
313-
const id = serverMod.getInstanceId && serverMod.getInstanceId();
314-
if (!id) return;
315-
console.log(` ${t('cli.instanceId', { id })}`);
316-
const known = (serverMod.getKnownInstances && serverMod.getKnownInstances()) || [];
317-
const others = known.filter((x) => x !== id);
318-
if (others.length) console.log(` ${t('cli.instanceHistory', { ids: others.join(', ') })}`);
357+
const { LOG_DIR } = await import('./findcc.js');
358+
const { listConvertibleProjects } = await import('./server/lib/v2/convert.js');
359+
const { migrationStatus } = await import('./server/lib/v2/migrate-prompt.js');
360+
const pending = listConvertibleProjects(LOG_DIR)
361+
.filter((p) => migrationStatus(LOG_DIR, p).pending);
362+
if (pending.length > 0) {
363+
console.log(` ${t('cli.v1LogsFound', { count: pending.length })}`);
364+
}
319365
} catch { /* banner is best-effort */ }
320366
}
321367

@@ -440,7 +486,7 @@ async function runCliMode(extraClaudeArgs = [], cwd, noOpen = false) {
440486
if (_auth.password === '') console.error(` ${t('server.passwordEmptyWarn')}`);
441487
else console.log(` ${t('server.passwordActive', { password: _auth.password })}`);
442488
}
443-
printInstanceBanner(serverMod);
489+
await printMigrationBanner();
444490

445491
// 5. 注册退出处理(hardened:watchdog 5s 强退 + 连按 Ctrl+C 立退,
446492
// 防 Windows 上 ConPTY kill / IM teardown 挂住导致"Ctrl+C 完全无反应")
@@ -623,7 +669,7 @@ async function runSdkMode(extraClaudeArgs = [], cwd, noOpen = false) {
623669
if (_auth.password === '') console.error(` ${t('server.passwordEmptyWarn')}`);
624670
else console.log(` ${t('server.passwordActive', { password: _auth.password })}`);
625671
}
626-
printInstanceBanner(serverMod);
672+
await printMigrationBanner();
627673

628674
// 注册退出处理(hardened,与 PTY 模式同款三层防御)
629675
const cleanup = createHardenedCleanup({
@@ -649,9 +695,9 @@ const logDirIdx = args.indexOf('--log-dir');
649695
if (logDirIdx !== -1) {
650696
const logDirVal = args[logDirIdx + 1];
651697
if (logDirVal && !logDirVal.startsWith('-')) {
652-
const prevDir = LOG_DIR;
653-
setLogDir(logDirVal);
654-
if (LOG_DIR === prevDir) {
698+
// setLogDir returns false only on a rejected path; comparing LOG_DIR
699+
// before/after mis-rejected a --log-dir equal to the current default.
700+
if (!setLogDir(logDirVal)) {
655701
console.error(`Error: --log-dir path rejected (must be under home directory or /tmp/): ${logDirVal}`);
656702
process.exit(1);
657703
}
@@ -721,29 +767,6 @@ if (usePwdIdx !== -1) {
721767
args.splice(usePwdIdx, 1);
722768
}
723769

724-
// Extract --pid[=<name>] / --pid <name> — instance id for per-instance session-pin isolation.
725-
// NB: "pid" here is an instance *id* LABEL (user-chosen, e.g. alpha/beta), NOT an OS process id —
726-
// it only keys the session-pin file `.session-pin.<id>.json`; unrelated to process.pid.
727-
// ccv-owned (NEVER forwarded to claude — an unknown --pid would otherwise crash claude): sets
728-
// CCV_INSTANCE_ID and splices out. Sanitized to a filesystem-safe token (it becomes part of a
729-
// `.session-pin.<id>.json` filename → guard against path traversal / invalid names).
730-
const pidIdx = args.findIndex((a) => a === '--pid' || a.startsWith('--pid='));
731-
if (pidIdx !== -1) {
732-
const arg = args[pidIdx];
733-
let rawVal;
734-
if (arg.startsWith('--pid=')) {
735-
rawVal = arg.slice('--pid='.length);
736-
args.splice(pidIdx, 1);
737-
} else {
738-
rawVal = args[pidIdx + 1];
739-
if (rawVal && !rawVal.startsWith('-')) args.splice(pidIdx, 2);
740-
else { console.error(t('cli.pidInvalid')); process.exit(1); }
741-
}
742-
const sanitized = (rawVal || '').replace(/[^a-zA-Z0-9_\-.]/g, '_');
743-
if (!sanitized) { console.error(t('cli.pidInvalid')); process.exit(1); }
744-
process.env.CCV_INSTANCE_ID = sanitized;
745-
}
746-
747770
// Extract --im <platformId> — 启动一个独立常驻 IM worker:工作目录 IM_<id>/、绑 127.0.0.1、
748771
// skip-permissions、全局唯一锁。必须在动态 import 之前提取(runImMode 会在 import server.js 前设 env)。
749772
let imPlatform = null;
@@ -917,19 +940,93 @@ if (imPlatform) {
917940
console.error('IM mode error:', err);
918941
process.exit(1);
919942
});
943+
} else if (args[0] === 'verify') {
944+
// wire-v2 S4: dual-write consistency check (docs/refactor/WIRE_FORMAT_V2_PLAN.md S4)
945+
// Usage: ccv verify <v1-log.jsonl> [more.jsonl ...] — compares each v1 file
946+
// against its project's v2 session dirs; exit 0 = zero diffs.
947+
(async () => {
948+
const files = args.slice(1).filter(a => a.endsWith('.jsonl'));
949+
if (files.length === 0) {
950+
console.error('usage: ccv verify <v1-log.jsonl> [more.jsonl ...]');
951+
process.exit(2);
952+
}
953+
const { verifyV1File, renderReport } = await import('./server/lib/v2/verify.js');
954+
let allOk = true;
955+
for (const f of files) {
956+
const report = await verifyV1File(resolve(f));
957+
console.log(renderReport(report));
958+
if (!report.ok) allOk = false;
959+
}
960+
process.exit(allOk ? 0 : 1);
961+
})().catch(err => {
962+
console.error('verify error:', err);
963+
process.exit(1);
964+
});
965+
} else if (args[0] === 'convert') {
966+
// wire-v2 S8: offline v1→v2 migration (docs/refactor/WIRE_FORMAT_V2_PLAN.md S8)
967+
// Usage: ccv convert <project> [more-projects ...] | ccv convert --all
968+
// [--log-dir <dir>] — chronological per project, file-level resume,
969+
// staging + golden verify + promote. Strictly additive: v1 files untouched.
970+
(async () => {
971+
const { convertProject, listConvertibleProjects, QUARANTINE_DIR_NAME } = await import('./server/lib/v2/convert.js');
972+
const rest = args.slice(1);
973+
const dirIdx = rest.indexOf('--log-dir');
974+
let logDir = LOG_DIR;
975+
if (dirIdx !== -1) {
976+
logDir = resolve(rest[dirIdx + 1] || '');
977+
rest.splice(dirIdx, 2);
978+
}
979+
const all = rest.includes('--all');
980+
const projects = all ? listConvertibleProjects(logDir) : rest.filter(a => !a.startsWith('-'));
981+
if (projects.length === 0) {
982+
console.error('usage: ccv convert <project> [more-projects ...] | ccv convert --all [--log-dir <dir>]');
983+
process.exit(2);
984+
}
985+
let allOk = true;
986+
for (const project of projects) {
987+
console.error(`converting ${project} (${logDir}) ...`);
988+
try {
989+
const state = await convertProject(logDir, project, {
990+
onProgress: (p) => {
991+
if (p.phase === 'done') return;
992+
console.error(` [${p.phase}] ${p.file} (${p.fileIndex}/${p.filesTotal}) entries=${p.entries} sessions=${p.sessionsConverted} skipped=${p.sessionsSkipped}`);
993+
},
994+
});
995+
const quarantined = state.sessionsQuarantined || 0;
996+
console.error(` ${project}: ${state.status}${state.sessionsConverted} sessions converted, ${state.sessionsSkipped} skipped${quarantined ? `, ${quarantined} quarantined` : ''}, ${state.entries} entries`);
997+
if (quarantined > 0) {
998+
// Non-blocking: the good sessions promoted; only the flagged ones are held.
999+
console.error(` WARNING: ${quarantined} session(s) failed golden verify and were held in ${QUARANTINE_DIR_NAME}/ (not promoted). Suspect sessions:`);
1000+
for (const q of state.quarantined || []) {
1001+
console.error(` - ${q.sessionId}${q.reasons && q.reasons.length ? ` (${q.reasons.slice(0, 5).join(', ')})` : ''}`);
1002+
}
1003+
}
1004+
if (state.status !== 'done') allOk = false;
1005+
} catch (err) {
1006+
console.error(` ${project}: FAILED — ${err.message}`);
1007+
allOk = false;
1008+
}
1009+
}
1010+
process.exit(allOk ? 0 : 1);
1011+
})().catch(err => {
1012+
console.error('convert error:', err);
1013+
process.exit(1);
1014+
});
9201015
} else if (args[0] === 'run') {
9211016
runProxyCommand(args);
9221017
} else if (args.includes('-SDK') || args.includes('--sdk')) {
9231018
// SDK 模式(显式 -SDK 切换)
9241019
const claudeArgs = args.filter(a => a !== '-SDK' && a !== '--sdk')
9251020
.map(a => a === '--d' ? '--dangerously-skip-permissions' : a === '--ad' ? '--allow-dangerously-skip-permissions' : a);
1021+
markContinueEnv(claudeArgs);
9261022
runSdkMode(claudeArgs, process.cwd(), noOpen).catch(err => {
9271023
console.error('SDK mode error:', err);
9281024
process.exit(1);
9291025
});
9301026
} else {
9311027
// PTY 模式(默认)
9321028
const claudeArgs = args.map(a => a === '--d' ? '--dangerously-skip-permissions' : a === '--ad' ? '--allow-dangerously-skip-permissions' : a);
1029+
markContinueEnv(claudeArgs);
9331030
runCliMode(claudeArgs, process.cwd(), noOpen).catch(err => {
9341031
console.error('CLI mode error:', err);
9351032
process.exit(1);

0 commit comments

Comments
 (0)