dsh-config-manager 0.1.78 → 0.1.80
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/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +401 -112
- package/lib/client.js +2899 -1525
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/ancestor.d.ts +22 -0
- package/lib/sync/ancestor.js +66 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +18 -1
- package/lib/sync/gist/gist-transport.js +42 -3
- package/lib/sync/gist/messages.d.ts +2 -0
- package/lib/sync/gist/messages.js +5 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/s3/sigv4.js +11 -2
- package/lib/sync/sync-engine.d.ts +212 -8
- package/lib/sync/sync-engine.js +514 -58
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +9 -0
- package/lib/ui/i18n.js +22 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/api-base.test.ts +36 -0
- package/src/client/common/http.test.ts +12 -1
- package/src/client/common/http.ts +8 -2
- package/src/client/common/routes.ts +46 -3
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/market/market-api.test.ts +3 -2
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +220 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +248 -0
- package/src/client/sync/merge-view.ts +398 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.test.ts +5 -3
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/ancestor.ts +76 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +203 -0
- package/src/sync/gist/gist-transport.ts +53 -5
- package/src/sync/gist/messages.ts +5 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.test.ts +2 -1
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/s3/sigv4.test.ts +4 -1
- package/src/sync/s3/sigv4.ts +11 -2
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +410 -19
- package/src/sync/sync-engine.ts +602 -57
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +22 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
|
@@ -1,40 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code 用户目录的**读盘**层(宿主侧,node:fs)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 三个作用域(2026-10-18 审计后补齐):
|
|
5
|
+
* ① 用户级:`<configDir>/settings.json`、CLAUDE.md、skills、commands、projects/*.jsonl(用户级
|
|
6
|
+
* `agents/` 本层刻意不采集:types 的 ClaudeCodeInput 没有对应字段,读了也无处交付);
|
|
7
|
+
* ② 项目级:`~/.claude.json` 的 `projects` 键(Claude Code 自己写的权威清单)+ 调用方显式给出的
|
|
8
|
+
* `projectPaths`;每个项目读 `.claude/settings(.local).json`、CLAUDE(.local).md、`.claude/rules`、
|
|
9
|
+
* `.claude/skills`、`.claude/commands`、`.claude/agents`、`.mcp.json`,以及全局清单里该项目的
|
|
10
|
+
* `mcpServers`;
|
|
11
|
+
* ③ 插件:委托 `read-claude-plugins.ts`(只读元数据与技能文本,**绝不执行/复制插件代码**)。
|
|
6
12
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
13
|
+
* 配置目录可被 `CLAUDE_CONFIG_DIR` 覆盖(**非空才生效**):命中即报 source-location-overridden,
|
|
14
|
+
* 并且**只读覆盖后的目录** —— 绝不回落到默认位置(回落会把「位置已被覆盖」这件事藏起来,
|
|
15
|
+
* 用户会以为读到的是 `~/.claude`)。
|
|
16
|
+
*
|
|
17
|
+
* 五条硬边界(与同域其它读盘层同口径):
|
|
18
|
+
* - **绝不跟随符号链接**(技能目录是链接时跳过并报 symlink-skipped;项目内直读文件同样先 lstat);
|
|
19
|
+
* - 单文件有字节上限,超限**不读**并如实上报,绝不截断;
|
|
20
|
+
* - 一切触顶(会话数 / 项目数 / 技能文件数)都报**稳定机器码**,绝不静默(audit-foreign F4 同族);
|
|
21
|
+
* - 读盘层只回数据与机器码,**绝不产生用户可见字符串**;本层读到的**值**交给翻译层剥离凭据后才进包;
|
|
22
|
+
* - 项目根**只取真实存在且是目录的**(不猜「项目就是 cwd」),已删除的项目安静跳过。
|
|
23
|
+
*
|
|
24
|
+
* 与 read-claude-plugins.ts 的关系:本文件动态 import 它(避免两个读盘模块的静态 import 环);
|
|
25
|
+
* 它反向静态 import 本文件的 readSkillUnits —— 静态依赖是单向的。
|
|
9
26
|
*/
|
|
10
27
|
import fs from 'node:fs/promises';
|
|
11
28
|
import type { Dirent } from 'node:fs';
|
|
12
29
|
import path from 'node:path';
|
|
13
30
|
|
|
31
|
+
import { isRecord } from '../utils/guards.ts';
|
|
14
32
|
import { dirWalkSkips, resolveLimit, type DirWalkStats } from './session-read.ts';
|
|
15
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
ClaudeCodeInput,
|
|
35
|
+
ClaudeProjectInput,
|
|
36
|
+
ClaudeSessionInput,
|
|
37
|
+
ClaudeSkillInput,
|
|
38
|
+
ForeignLimitOverrides,
|
|
39
|
+
ForeignSkip,
|
|
40
|
+
} from './types.ts';
|
|
16
41
|
|
|
17
42
|
export interface ClaudeCodeReadOptions {
|
|
18
43
|
/** 用户 home(含 .claude 目录 / .claude.json) */
|
|
19
44
|
homeDir: string;
|
|
45
|
+
/** 进程环境快照(只读):只有 CLAUDE_CONFIG_DIR 参与判定,绝不读凭据类变量 */
|
|
46
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
47
|
+
/** 调用方**显式**给出的项目根(优先于 ~/.claude.json 的 projects 键;缺省 = 不猜任何项目) */
|
|
48
|
+
projectPaths?: readonly string[];
|
|
20
49
|
/** 单文件读取上限(默认 8 MiB);超过即不读,并如实计入 unreadable */
|
|
21
50
|
maxFileBytes?: number;
|
|
22
51
|
/** 单个 skill 目录的文件数上限(默认 200) */
|
|
23
52
|
maxSkillFiles?: number;
|
|
24
|
-
/** 会话文件数上限(默认 500
|
|
53
|
+
/** 会话文件数上限(默认 500);触顶报 source-unreadable/max-sessions-reached */
|
|
25
54
|
maxSessionFiles?: number;
|
|
26
55
|
/** 单个会话文件的上限(默认 32 MiB);超限即不读并计入 unreadable,绝不截断 */
|
|
27
56
|
maxSessionFileBytes?: number;
|
|
57
|
+
/** 项目扫描上限(默认 50);触顶报 source-unreadable/max-projects-reached */
|
|
58
|
+
maxProjects?: number;
|
|
59
|
+
/** 插件数上限(默认 50,透传给 read-claude-plugins) */
|
|
60
|
+
maxPlugins?: number;
|
|
61
|
+
/** 单插件文件数上限(默认 200,透传给 read-claude-plugins) */
|
|
62
|
+
maxPluginFiles?: number;
|
|
28
63
|
/** 可选上限覆盖(t36,装配层透传;缺省 = 上面各默认值逐字不变) */
|
|
29
64
|
limits?: ForeignLimitOverrides;
|
|
30
65
|
}
|
|
31
66
|
|
|
32
67
|
export interface ClaudeCodeReadResult {
|
|
33
|
-
/** 是否找到 Claude Code
|
|
68
|
+
/** 是否找到 Claude Code 的痕迹(配置目录或 ~/.claude.json) */
|
|
34
69
|
found: boolean;
|
|
35
70
|
input: ClaudeCodeInput;
|
|
36
71
|
skipped: ForeignSkip[];
|
|
37
|
-
/** 读不到 / 超限 / JSON
|
|
72
|
+
/** 读不到 / 超限 / JSON 解析失败的**相对路径**(不含任何内容、不含机器绝对路径) */
|
|
38
73
|
unreadable: string[];
|
|
39
74
|
}
|
|
40
75
|
|
|
@@ -42,8 +77,17 @@ const DEFAULT_MAX_FILE = 8 * 1024 * 1024;
|
|
|
42
77
|
const DEFAULT_MAX_SKILL_FILES = 200;
|
|
43
78
|
const DEFAULT_MAX_SESSION_FILES = 500;
|
|
44
79
|
const DEFAULT_MAX_SESSION_FILE = 32 * 1024 * 1024;
|
|
80
|
+
const DEFAULT_MAX_PROJECTS = 50;
|
|
45
81
|
/** 全局指令文件(CLAUDE.md)单独给上限:它是纯文本,正常只有几十 KB */
|
|
46
82
|
const MEMORY_MAX_FILE = 1024 * 1024;
|
|
83
|
+
/** 目录走盘的最大下钻深度(与 session-read 的 WalkOptions 缺省一致) */
|
|
84
|
+
const WALK_MAX_DEPTH = 8;
|
|
85
|
+
/** 环境变量名(唯一判据) */
|
|
86
|
+
const ENV_CONFIG_DIR = 'CLAUDE_CONFIG_DIR';
|
|
87
|
+
/** 触顶 / 跳过的稳定机器码(进 ForeignSkip.detail,绝不进用户可见文案) */
|
|
88
|
+
const DETAIL_MAX_SESSIONS = 'max-sessions-reached';
|
|
89
|
+
const DETAIL_MAX_PROJECTS = 'max-projects-reached';
|
|
90
|
+
const DETAIL_SYMLINK = 'symlink-skipped';
|
|
47
91
|
|
|
48
92
|
async function statOrNull(p: string) {
|
|
49
93
|
try {
|
|
@@ -53,6 +97,25 @@ async function statOrNull(p: string) {
|
|
|
53
97
|
}
|
|
54
98
|
}
|
|
55
99
|
|
|
100
|
+
/** lstat:**不跟随**符号链接(项目内直读文件必须先看它) */
|
|
101
|
+
async function lstatOrNull(p: string) {
|
|
102
|
+
try {
|
|
103
|
+
return await fs.lstat(p);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 目录条目(排序;三平台 readdir 顺序不同,排序让产物与测试确定) */
|
|
110
|
+
async function listDirents(dir: string): Promise<Dirent[]> {
|
|
111
|
+
try {
|
|
112
|
+
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
113
|
+
return dirents.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
114
|
+
} catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
56
119
|
interface JsonRead { ok: boolean; value?: unknown }
|
|
57
120
|
|
|
58
121
|
async function readTextSafe(p: string, max: number): Promise<string | null> {
|
|
@@ -75,6 +138,23 @@ async function readJsonSafe(p: string, max: number): Promise<JsonRead> {
|
|
|
75
138
|
}
|
|
76
139
|
}
|
|
77
140
|
|
|
141
|
+
/** CLAUDE_CONFIG_DIR 的非空取值(空串 / 纯空白视为「没设置」——Claude Code 自身也是这个语义) */
|
|
142
|
+
function configDirOverrideOf(env: Readonly<Record<string, string | undefined>> | undefined): string | undefined {
|
|
143
|
+
if (env === undefined) return undefined;
|
|
144
|
+
const raw = env[ENV_CONFIG_DIR];
|
|
145
|
+
if (typeof raw !== 'string') return undefined;
|
|
146
|
+
const trimmed = raw.trim();
|
|
147
|
+
return trimmed === '' ? undefined : trimmed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** ~/.claude.json 的 lastVersionBase(来源侧版本标记;不是字符串就不猜) */
|
|
151
|
+
function lastVersionBaseOf(claudeJson: unknown): string | undefined {
|
|
152
|
+
if (!isRecord(claudeJson)) return undefined;
|
|
153
|
+
const value = claudeJson['lastVersionBase'];
|
|
154
|
+
if (typeof value !== 'string' || value.trim() === '') return undefined;
|
|
155
|
+
return value.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
78
158
|
interface SkillFile { relativePath: string; data: Uint8Array }
|
|
79
159
|
|
|
80
160
|
/** 递归收集一个 skill 目录下的普通文件(不跟随符号链接;条目数与单文件字节都有上限) */
|
|
@@ -124,106 +204,406 @@ async function walkFiles(
|
|
|
124
204
|
return out;
|
|
125
205
|
}
|
|
126
206
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
207
|
+
/** 技能目录读取的可选参数(用户级 / 项目级 / 插件级共用同一份口径) */
|
|
208
|
+
export interface SkillDirReadOptions {
|
|
209
|
+
/** 单个技能目录的文件数上限 */
|
|
210
|
+
maxFiles: number;
|
|
211
|
+
/** 单文件字节上限 */
|
|
212
|
+
maxBytes: number;
|
|
213
|
+
/** 位置标签前缀(**包内相对标签**,绝不含机器路径) */
|
|
214
|
+
origin: string;
|
|
215
|
+
/** 发现写这里(符号链接跳过 / 条数触顶 / 字节超限) */
|
|
216
|
+
skips: ForeignSkip[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 读一个 **skills 目录**(只压平一层:`<skills>/<技能名>/**`)。
|
|
221
|
+
*
|
|
222
|
+
* 三条不静默(缺陷 2 修复):技能目录本身是符号链接 → source-unreadable + symlink-skipped;
|
|
223
|
+
* 条数触顶 → max-skill-files-reached;单文件超字节 → too-large。
|
|
224
|
+
* 结果按技能名排序(readdir 顺序在三个平台不同)。
|
|
225
|
+
*/
|
|
226
|
+
export async function readSkillUnits(dir: string, opts: SkillDirReadOptions): Promise<ClaudeSkillInput[]> {
|
|
139
227
|
const out: ClaudeSkillInput[] = [];
|
|
140
|
-
for (const d of
|
|
228
|
+
for (const d of await listDirents(dir)) {
|
|
229
|
+
if (d.isSymbolicLink()) {
|
|
230
|
+
// 缺陷 2:此前这里直接 continue(整块技能消失且一个字都不报)
|
|
231
|
+
opts.skips.push({ code: 'source-unreadable', origin: opts.origin + '/' + d.name, detail: DETAIL_SYMLINK });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
141
234
|
if (!d.isDirectory()) continue;
|
|
142
|
-
const
|
|
143
|
-
const
|
|
235
|
+
const skillDir = path.join(dir, d.name);
|
|
236
|
+
const stats: DirWalkStats = {};
|
|
237
|
+
const files = await walkFiles(skillDir, skillDir, opts.maxFiles, opts.maxBytes, stats);
|
|
238
|
+
opts.skips.push(...dirWalkSkips(stats, opts.origin, opts.maxFiles));
|
|
144
239
|
if (files.length === 0) continue;
|
|
145
240
|
out.push({ name: d.name, files });
|
|
146
241
|
}
|
|
242
|
+
out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
147
243
|
return out;
|
|
148
244
|
}
|
|
149
245
|
|
|
150
246
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
247
|
+
* 取「记录自带的 sessionId」:只扫前 64 KiB(够看清头部记录,不必为它把整份日志再解析一遍)。
|
|
248
|
+
* 取不到就返回 undefined —— 不猜、不伪造。
|
|
249
|
+
*/
|
|
250
|
+
const RECORD_ID_SCAN_BYTES = 64 * 1024;
|
|
251
|
+
function recordSessionIdOf(text: string): string | undefined {
|
|
252
|
+
const head = text.length > RECORD_ID_SCAN_BYTES ? text.slice(0, RECORD_ID_SCAN_BYTES) : text;
|
|
253
|
+
const m = /"sessionId"\s*:\s*"([^"]+)"/.exec(head);
|
|
254
|
+
const hit = m === null ? undefined : m[1];
|
|
255
|
+
return hit === undefined || hit === '' ? undefined : hit;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 会话文件:只扫 `<projects>/<项目>/<uuid>.jsonl` 这一层(不递归子目录、不跟随符号链接)。
|
|
260
|
+
* 读不到 / 超限的文件**不静默跳过**:进 unreadable 并变成一条 source-unreadable finding;
|
|
261
|
+
* 条数触顶(缺陷 1 修复)回 truncated=true,由调用方推 max-sessions-reached。
|
|
153
262
|
*/
|
|
154
263
|
async function readSessions(
|
|
155
264
|
projectsDir: string,
|
|
156
265
|
maxFiles: number,
|
|
157
266
|
maxBytes: number,
|
|
158
|
-
): Promise<{ files: ClaudeSessionInput[]; unreadable: string[] }> {
|
|
267
|
+
): Promise<{ files: ClaudeSessionInput[]; unreadable: string[]; truncated: boolean }> {
|
|
159
268
|
const files: ClaudeSessionInput[] = [];
|
|
160
269
|
const unreadable: string[] = [];
|
|
161
|
-
let
|
|
162
|
-
|
|
163
|
-
projectDirs = await fs.readdir(projectsDir, { withFileTypes: true });
|
|
164
|
-
} catch {
|
|
165
|
-
return { files, unreadable };
|
|
166
|
-
}
|
|
167
|
-
for (const pd of projectDirs) {
|
|
168
|
-
if (files.length >= maxFiles) break;
|
|
270
|
+
let truncated = false;
|
|
271
|
+
for (const pd of await listDirents(projectsDir)) {
|
|
169
272
|
if (!pd.isDirectory() || pd.isSymbolicLink()) continue;
|
|
170
273
|
const dir = path.join(projectsDir, pd.name);
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
174
|
-
} catch {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
for (const e of entries) {
|
|
178
|
-
if (files.length >= maxFiles) break;
|
|
274
|
+
for (const e of await listDirents(dir)) {
|
|
179
275
|
if (!e.isFile() || e.isSymbolicLink()) continue;
|
|
180
276
|
if (!e.name.endsWith('.jsonl')) continue;
|
|
277
|
+
// 触顶判定放在「真的还有下一个候选文件」处:恰好等于上限但目录里没有下一个 → 不报码
|
|
278
|
+
if (files.length >= maxFiles) { truncated = true; break; }
|
|
181
279
|
const rel = '.claude/projects/' + pd.name + '/' + e.name;
|
|
182
|
-
const
|
|
280
|
+
const abs = path.join(dir, e.name);
|
|
281
|
+
const text = await readTextSafe(abs, maxBytes);
|
|
183
282
|
if (text === null) {
|
|
184
283
|
unreadable.push(rel);
|
|
185
284
|
continue;
|
|
186
285
|
}
|
|
187
|
-
|
|
286
|
+
const id = e.name.slice(0, e.name.length - '.jsonl'.length);
|
|
287
|
+
// 记录自带的 sessionId(≠ 文件名 stem 时由翻译层报 session-id-mismatch,不静默)
|
|
288
|
+
const recordSessionId = recordSessionIdOf(text);
|
|
289
|
+
// 确定性墙钟回落 = 文件 mtime:记录自身有时间戳时它不被使用;没有时它让**同一来源两次
|
|
290
|
+
// 转码字节逐字相同** ⇒ 重复导入才会被判成 Skip 而不是 Conflict(审计缺陷 4 的接线口)。
|
|
291
|
+
const st = await statOrNull(abs);
|
|
292
|
+
files.push({
|
|
293
|
+
id,
|
|
294
|
+
text,
|
|
295
|
+
...(recordSessionId !== undefined ? { recordSessionId } : {}),
|
|
296
|
+
...(st !== null ? { createdAtMs: st.mtimeMs } : {}),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (truncated) break;
|
|
300
|
+
}
|
|
301
|
+
return { files, unreadable, truncated };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 递归统计目录下的普通文件数(不跟随符号链接;深度上限与 walk 同口径) */
|
|
305
|
+
async function countFilesRecursive(dir: string): Promise<number> {
|
|
306
|
+
let count = 0;
|
|
307
|
+
const stack: { dir: string; depth: number }[] = [{ dir, depth: 0 }];
|
|
308
|
+
while (stack.length > 0) {
|
|
309
|
+
const cur = stack.pop();
|
|
310
|
+
if (cur === undefined) continue;
|
|
311
|
+
for (const d of await listDirents(cur.dir)) {
|
|
312
|
+
if (d.isSymbolicLink()) continue;
|
|
313
|
+
if (d.isDirectory()) {
|
|
314
|
+
if (cur.depth < WALK_MAX_DEPTH) stack.push({ dir: path.join(cur.dir, d.name), depth: cur.depth + 1 });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (d.isFile()) count += 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return count;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** `<dir>/*.md` 的「名字 + 文件名」(子代理定义只报告结构,不搬内容) */
|
|
324
|
+
async function readAgentNames(dir: string): Promise<{ name: string; file: string }[]> {
|
|
325
|
+
const out: { name: string; file: string }[] = [];
|
|
326
|
+
for (const d of await listDirents(dir)) {
|
|
327
|
+
if (d.isSymbolicLink() || !d.isFile() || !d.name.endsWith('.md')) continue;
|
|
328
|
+
out.push({ name: d.name.slice(0, 0 - '.md'.length), file: d.name });
|
|
329
|
+
}
|
|
330
|
+
out.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** `<dir>/*.md` 的原文(规则文件带 frontmatter 的作用域由翻译层报 unsupported-rules) */
|
|
335
|
+
async function readRuleFiles(
|
|
336
|
+
dir: string,
|
|
337
|
+
maxBytes: number,
|
|
338
|
+
findings: ForeignSkip[],
|
|
339
|
+
): Promise<{ name: string; text: string }[]> {
|
|
340
|
+
const out: { name: string; text: string }[] = [];
|
|
341
|
+
for (const d of await listDirents(dir)) {
|
|
342
|
+
if (d.isSymbolicLink() || !d.isFile() || !d.name.endsWith('.md')) continue;
|
|
343
|
+
const text = await readTextSafe(path.join(dir, d.name), maxBytes);
|
|
344
|
+
if (text === null) {
|
|
345
|
+
findings.push({ code: 'source-unreadable', origin: '.claude/rules/' + d.name });
|
|
346
|
+
continue;
|
|
188
347
|
}
|
|
348
|
+
out.push({ name: d.name, text });
|
|
189
349
|
}
|
|
190
|
-
|
|
350
|
+
out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
351
|
+
return out;
|
|
191
352
|
}
|
|
192
353
|
|
|
193
|
-
|
|
194
|
-
|
|
354
|
+
/** 直读一个项目内文件(**先 lstat**:符号链接一律不读、报码) */
|
|
355
|
+
async function probeProjectJson(
|
|
356
|
+
p: string,
|
|
357
|
+
label: string,
|
|
358
|
+
maxBytes: number,
|
|
359
|
+
findings: ForeignSkip[],
|
|
360
|
+
): Promise<unknown | undefined> {
|
|
361
|
+
const st = await lstatOrNull(p);
|
|
362
|
+
if (st === null) return undefined;
|
|
363
|
+
if (st.isSymbolicLink()) {
|
|
364
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: DETAIL_SYMLINK });
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
if (!st.isFile()) {
|
|
368
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'not-a-file' });
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
if (st.size === 0) {
|
|
372
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'empty' });
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
if (st.size > maxBytes) {
|
|
376
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'too-large' });
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
let text: string;
|
|
380
|
+
try {
|
|
381
|
+
text = await fs.readFile(p, 'utf8');
|
|
382
|
+
} catch {
|
|
383
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'read-error' });
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
return JSON.parse(text);
|
|
388
|
+
} catch {
|
|
389
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'json-error' });
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 直读一个项目内文本文件(**先 lstat**:符号链接一律不读、报码) */
|
|
395
|
+
async function probeProjectText(
|
|
396
|
+
p: string,
|
|
397
|
+
label: string,
|
|
398
|
+
maxBytes: number,
|
|
399
|
+
findings: ForeignSkip[],
|
|
400
|
+
): Promise<string | undefined> {
|
|
401
|
+
const st = await lstatOrNull(p);
|
|
402
|
+
if (st === null) return undefined;
|
|
403
|
+
if (st.isSymbolicLink()) {
|
|
404
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: DETAIL_SYMLINK });
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
if (!st.isFile()) {
|
|
408
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'not-a-file' });
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
if (st.size > maxBytes) {
|
|
412
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'too-large' });
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
195
415
|
try {
|
|
196
|
-
|
|
416
|
+
return await fs.readFile(p, 'utf8');
|
|
197
417
|
} catch {
|
|
198
|
-
|
|
418
|
+
findings.push({ code: 'source-unreadable', origin: label, detail: 'read-error' });
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 项目候选(root 已 resolve;jsonKey = ~/.claude.json 里的原始键,用于取该项目的 mcpServers) */
|
|
424
|
+
interface ProjectCandidate {
|
|
425
|
+
root: string;
|
|
426
|
+
jsonKey?: string;
|
|
427
|
+
mcp?: unknown;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 收集项目根候选:**调用方显式给出的优先**,然后才是 `~/.claude.json` 的 projects 键(字典序)。
|
|
432
|
+
* 只做去重与排序,**不做存在性判定**(那一步需要 fs,见 scanProjectRoots)。
|
|
433
|
+
*/
|
|
434
|
+
function collectProjectCandidates(
|
|
435
|
+
claudeJson: unknown,
|
|
436
|
+
explicit: readonly string[] | undefined,
|
|
437
|
+
): ProjectCandidate[] {
|
|
438
|
+
const byRoot = new Map<string, ProjectCandidate>();
|
|
439
|
+
const order: string[] = [];
|
|
440
|
+
const add = (raw: string, jsonKey?: string, mcp?: unknown): void => {
|
|
441
|
+
const abs = path.resolve(raw);
|
|
442
|
+
const existing = byRoot.get(abs);
|
|
443
|
+
if (existing !== undefined) {
|
|
444
|
+
// 显式路径与清单键指向同一项目:补上清单里的 mcpServers(显式路径本身不带它)
|
|
445
|
+
if (existing.jsonKey === undefined && jsonKey !== undefined) {
|
|
446
|
+
existing.jsonKey = jsonKey;
|
|
447
|
+
existing.mcp = mcp;
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const candidate: ProjectCandidate = { root: abs };
|
|
452
|
+
if (jsonKey !== undefined) candidate.jsonKey = jsonKey;
|
|
453
|
+
if (mcp !== undefined) candidate.mcp = mcp;
|
|
454
|
+
byRoot.set(abs, candidate);
|
|
455
|
+
order.push(abs);
|
|
456
|
+
};
|
|
457
|
+
for (const p of explicit ?? []) {
|
|
458
|
+
if (typeof p === 'string' && p.trim() !== '') add(p);
|
|
459
|
+
}
|
|
460
|
+
if (isRecord(claudeJson)) {
|
|
461
|
+
const projects = claudeJson['projects'];
|
|
462
|
+
if (isRecord(projects)) {
|
|
463
|
+
for (const key of Object.keys(projects).sort()) {
|
|
464
|
+
const entry = projects[key];
|
|
465
|
+
add(key, key, isRecord(entry) ? entry['mcpServers'] : undefined);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
199
468
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
469
|
+
return order.map((abs) => byRoot.get(abs)).filter((c): c is ProjectCandidate => c !== undefined);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 读一个项目根。
|
|
474
|
+
*
|
|
475
|
+
* 纪律:`.claude` 不存在 → **只留 root 与 hasClaudeDir:false**(绝不产出空 finding:那是
|
|
476
|
+
* 「这个项目没用过 Claude Code」,不是错误);项目根不存在 / 不是目录 → 安静跳过(已删除的项目)。
|
|
477
|
+
*/
|
|
478
|
+
async function scanProjectRoot(
|
|
479
|
+
candidate: ProjectCandidate,
|
|
480
|
+
maxFileBytes: number,
|
|
481
|
+
maxSkillFiles: number,
|
|
482
|
+
): Promise<ClaudeProjectInput> {
|
|
483
|
+
const findings: ForeignSkip[] = [];
|
|
484
|
+
const out: ClaudeProjectInput = { root: candidate.root };
|
|
485
|
+
const claudeDir = path.join(candidate.root, '.claude');
|
|
486
|
+
const dirStat = await statOrNull(claudeDir);
|
|
487
|
+
const hasClaudeDir = dirStat !== null && dirStat.isDirectory();
|
|
488
|
+
out.hasClaudeDir = hasClaudeDir;
|
|
489
|
+
// 没有 .claude 的项目**只留 root + hasClaudeDir:false**(连 claude.json 里的 mcpServers 也不带:
|
|
490
|
+
// 那会把「这个项目没有 Claude 配置目录」与「它有项目级配置」混成同一种形态)
|
|
491
|
+
if (!hasClaudeDir) return out;
|
|
492
|
+
if (candidate.mcp !== undefined) out.projectMcp = candidate.mcp;
|
|
493
|
+
|
|
494
|
+
const settings = await probeProjectJson(path.join(claudeDir, 'settings.json'), '.claude/settings.json', maxFileBytes, findings);
|
|
495
|
+
if (settings !== undefined) out.settings = settings;
|
|
496
|
+
const settingsLocal = await probeProjectJson(path.join(claudeDir, 'settings.local.json'), '.claude/settings.local.json', maxFileBytes, findings);
|
|
497
|
+
if (settingsLocal !== undefined) out.settingsLocal = settingsLocal;
|
|
498
|
+
|
|
499
|
+
const memory = await probeProjectText(path.join(candidate.root, 'CLAUDE.md'), 'CLAUDE.md', MEMORY_MAX_FILE, findings);
|
|
500
|
+
if (memory !== undefined) out.memory = memory;
|
|
501
|
+
const memoryLocal = await probeProjectText(path.join(candidate.root, 'CLAUDE.local.md'), 'CLAUDE.local.md', MEMORY_MAX_FILE, findings);
|
|
502
|
+
if (memoryLocal !== undefined) out.memoryLocal = memoryLocal;
|
|
503
|
+
|
|
504
|
+
const rules = await readRuleFiles(path.join(claudeDir, 'rules'), maxFileBytes, findings);
|
|
505
|
+
if (rules.length > 0) out.rules = rules;
|
|
506
|
+
|
|
507
|
+
const skills = await readSkillUnits(path.join(claudeDir, 'skills'), {
|
|
508
|
+
maxFiles: maxSkillFiles,
|
|
509
|
+
maxBytes: maxFileBytes,
|
|
510
|
+
origin: '.claude/skills',
|
|
511
|
+
skips: findings,
|
|
512
|
+
});
|
|
513
|
+
if (skills.length > 0) out.skills = skills;
|
|
514
|
+
|
|
515
|
+
const commandCount = await countFilesRecursive(path.join(claudeDir, 'commands'));
|
|
516
|
+
if (commandCount > 0) out.commandCount = commandCount;
|
|
517
|
+
|
|
518
|
+
const agents = await readAgentNames(path.join(claudeDir, 'agents'));
|
|
519
|
+
if (agents.length > 0) out.agents = agents;
|
|
520
|
+
|
|
521
|
+
const mcpJson = await probeProjectJson(path.join(candidate.root, '.mcp.json'), '.mcp.json', maxFileBytes, findings);
|
|
522
|
+
if (mcpJson !== undefined) out.mcpJson = mcpJson;
|
|
523
|
+
|
|
524
|
+
if (findings.length > 0) out.readFindings = findings;
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 扫项目根:**只收真实存在且是目录的**(按候选顺序,显式优先 + 清单字典序);
|
|
530
|
+
* 触顶(maxProjects)**必须报码**(origin='projects',count=上限),绝不静默少扫。
|
|
531
|
+
*/
|
|
532
|
+
async function scanProjectRoots(
|
|
533
|
+
candidates: readonly ProjectCandidate[],
|
|
534
|
+
maxProjects: number,
|
|
535
|
+
maxFileBytes: number,
|
|
536
|
+
maxSkillFiles: number,
|
|
537
|
+
): Promise<{ projects: ClaudeProjectInput[]; skipped: ForeignSkip[] }> {
|
|
538
|
+
const roots: ProjectCandidate[] = [];
|
|
539
|
+
let truncated = false;
|
|
540
|
+
for (const candidate of candidates) {
|
|
541
|
+
const st = await statOrNull(candidate.root);
|
|
542
|
+
if (st === null || !st.isDirectory()) continue;
|
|
543
|
+
if (roots.length >= maxProjects) { truncated = true; break; }
|
|
544
|
+
roots.push(candidate);
|
|
545
|
+
}
|
|
546
|
+
const projects: ClaudeProjectInput[] = [];
|
|
547
|
+
for (const root of roots) projects.push(await scanProjectRoot(root, maxFileBytes, maxSkillFiles));
|
|
548
|
+
const skipped: ForeignSkip[] = [];
|
|
549
|
+
if (truncated) {
|
|
550
|
+
skipped.push({ code: 'source-unreadable', origin: 'projects', detail: DETAIL_MAX_PROJECTS, count: maxProjects });
|
|
551
|
+
}
|
|
552
|
+
return { projects, skipped };
|
|
203
553
|
}
|
|
204
554
|
|
|
205
555
|
export async function readClaudeCode(opts: ClaudeCodeReadOptions): Promise<ClaudeCodeReadResult> {
|
|
206
556
|
const home = opts.homeDir;
|
|
207
557
|
const maxFileBytes = resolveLimit(opts.limits?.maxFileBytes, opts.maxFileBytes, DEFAULT_MAX_FILE);
|
|
208
558
|
const maxSkillFiles = resolveLimit(opts.limits?.maxSkillFiles, opts.maxSkillFiles, DEFAULT_MAX_SKILL_FILES);
|
|
559
|
+
const maxSessionFiles = resolveLimit(opts.limits?.maxSessionFiles, opts.maxSessionFiles, DEFAULT_MAX_SESSION_FILES);
|
|
560
|
+
const maxSessionFileBytes = resolveLimit(undefined, opts.maxSessionFileBytes, DEFAULT_MAX_SESSION_FILE);
|
|
561
|
+
const maxProjects = resolveLimit(undefined, opts.maxProjects, DEFAULT_MAX_PROJECTS);
|
|
209
562
|
const skipped: ForeignSkip[] = [];
|
|
210
563
|
const unreadable: string[] = [];
|
|
211
564
|
const input: ClaudeCodeInput = {};
|
|
212
565
|
|
|
213
|
-
|
|
566
|
+
/* ---------------- A. 配置目录(CLAUDE_CONFIG_DIR 覆盖) ---------------- */
|
|
567
|
+
const override = configDirOverrideOf(opts.env);
|
|
568
|
+
const claudeDir = override !== undefined ? path.resolve(override) : path.join(home, '.claude');
|
|
569
|
+
input.configDir = claudeDir;
|
|
570
|
+
if (override !== undefined) {
|
|
571
|
+
input.configDirOverridden = true;
|
|
572
|
+
skipped.push({ code: 'source-location-overridden', origin: 'settings' });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* ~/.claude.json 的位置:未覆盖时只有 home 一处;覆盖时先看配置目录内(Claude Code 把全局
|
|
577
|
+
* 配置文件也放在配置目录里),再看 home —— 两处都只是**存在性**判定,绝不猜内容。
|
|
578
|
+
*/
|
|
579
|
+
const legacyCandidates = override !== undefined
|
|
580
|
+
? [path.join(claudeDir, '.claude.json'), path.join(home, '.claude.json')]
|
|
581
|
+
: [path.join(home, '.claude.json')];
|
|
582
|
+
let legacyPath: string | undefined;
|
|
583
|
+
for (const candidate of legacyCandidates) {
|
|
584
|
+
if ((await statOrNull(candidate)) !== null) { legacyPath = candidate; break; }
|
|
585
|
+
}
|
|
214
586
|
const dirStat = await statOrNull(claudeDir);
|
|
215
587
|
const hasDir = dirStat !== null && dirStat.isDirectory();
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
if (!hasDir &&
|
|
588
|
+
/** 显式给出的项目根也算「可能的痕迹」:用户级什么都没装、只有项目级 .claude 的情况真实存在 */
|
|
589
|
+
const hasExplicitProjects = (opts.projectPaths ?? []).some((p) => typeof p === 'string' && p.trim() !== '');
|
|
590
|
+
if (!hasDir && legacyPath === undefined && !hasExplicitProjects) {
|
|
591
|
+
return { found: false, input, skipped, unreadable };
|
|
592
|
+
}
|
|
219
593
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
594
|
+
if (legacyPath !== undefined) {
|
|
595
|
+
const jr = await readJsonSafe(legacyPath, maxFileBytes);
|
|
596
|
+
if (jr.ok) {
|
|
597
|
+
input.claudeJson = jr.value;
|
|
598
|
+
const version = lastVersionBaseOf(jr.value);
|
|
599
|
+
if (version !== undefined) input.sourceVersion = version;
|
|
600
|
+
} else {
|
|
601
|
+
unreadable.push('.claude.json');
|
|
602
|
+
skipped.push({ code: 'source-unreadable', origin: '.claude.json' });
|
|
603
|
+
}
|
|
225
604
|
}
|
|
226
605
|
|
|
606
|
+
/* ---------------- 用户级 ---------------- */
|
|
227
607
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
228
608
|
const sr = await readJsonSafe(settingsPath, maxFileBytes);
|
|
229
609
|
if (sr.ok) input.settings = sr.value;
|
|
@@ -240,25 +620,64 @@ export async function readClaudeCode(opts: ClaudeCodeReadOptions): Promise<Claud
|
|
|
240
620
|
skipped.push({ code: 'source-unreadable', origin: '.claude/CLAUDE.md' });
|
|
241
621
|
}
|
|
242
622
|
|
|
243
|
-
const
|
|
244
|
-
|
|
623
|
+
const skills = await readSkillUnits(path.join(claudeDir, 'skills'), {
|
|
624
|
+
maxFiles: maxSkillFiles,
|
|
625
|
+
maxBytes: maxFileBytes,
|
|
626
|
+
origin: 'skills',
|
|
627
|
+
skips: skipped,
|
|
628
|
+
});
|
|
245
629
|
if (skills.length > 0) input.skills = skills;
|
|
246
|
-
// t36:技能文件遍历的条数/字节触顶必须可见(origin 只用包内相对标签)
|
|
247
|
-
skipped.push(...dirWalkSkips(skillStats, 'skills', maxSkillFiles));
|
|
248
630
|
|
|
249
|
-
const commandCount = await
|
|
631
|
+
const commandCount = await countFilesRecursive(path.join(claudeDir, 'commands'));
|
|
250
632
|
if (commandCount > 0) input.commandCount = commandCount;
|
|
251
633
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
634
|
+
/**
|
|
635
|
+
* 用户级 `agents/` 刻意**不采集**:types 的 ClaudeCodeInput 没有对应字段(契约冻结),
|
|
636
|
+
* 读出来也无处交付 —— 采集了却只能丢掉,等于制造「读到了但包外看不见」的假象。
|
|
637
|
+
* 项目级 agents 有字段(ClaudeProjectInput.agents)且任务要求,故只做项目级。
|
|
638
|
+
*/
|
|
639
|
+
|
|
640
|
+
const sessions = await readSessions(path.join(claudeDir, 'projects'), maxSessionFiles, maxSessionFileBytes);
|
|
257
641
|
if (sessions.files.length > 0) input.sessions = sessions.files;
|
|
258
642
|
for (const rel of sessions.unreadable) {
|
|
259
643
|
unreadable.push(rel);
|
|
260
644
|
skipped.push({ code: 'source-unreadable', origin: rel });
|
|
261
645
|
}
|
|
646
|
+
if (sessions.truncated) {
|
|
647
|
+
// 缺陷 1:此前直接 break,条数触顶在包外完全不可见
|
|
648
|
+
skipped.push({ code: 'source-unreadable', origin: 'sessions', detail: DETAIL_MAX_SESSIONS, count: maxSessionFiles });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* ---------------- B. 项目级 ---------------- */
|
|
652
|
+
const candidates = collectProjectCandidates(input.claudeJson, opts.projectPaths);
|
|
653
|
+
if (candidates.length > 0) {
|
|
654
|
+
const scanned = await scanProjectRoots(candidates, maxProjects, maxFileBytes, maxSkillFiles);
|
|
655
|
+
if (scanned.projects.length > 0) input.projects = scanned.projects;
|
|
656
|
+
skipped.push(...scanned.skipped);
|
|
657
|
+
}
|
|
262
658
|
|
|
659
|
+
/* ---------------- C. 插件(委托独立读盘模块,避免静态 import 环) ---------------- */
|
|
660
|
+
if (hasDir) {
|
|
661
|
+
const { readClaudePlugins } = await import('./read-claude-plugins.ts');
|
|
662
|
+
const pluginRead = await readClaudePlugins({
|
|
663
|
+
claudeDir,
|
|
664
|
+
settings: input.settings,
|
|
665
|
+
maxFileBytes,
|
|
666
|
+
limits: {
|
|
667
|
+
...(opts.maxPlugins !== undefined ? { maxPlugins: opts.maxPlugins } : {}),
|
|
668
|
+
...(opts.maxPluginFiles !== undefined ? { maxPluginFiles: opts.maxPluginFiles } : {}),
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
if (pluginRead.plugins.length > 0) input.plugins = pluginRead.plugins;
|
|
672
|
+
if (pluginRead.marketplaces.length > 0) input.marketplaces = pluginRead.marketplaces;
|
|
673
|
+
// 只有**没有归属单元**的发现走顶层 skipped(插件自身的发现挂在 plugin.readFindings 上)
|
|
674
|
+
skipped.push(...pluginRead.skipped);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* 只有「项目级 .claude 存在」时才把 found 判成 true;否则如实说「没有痕迹」(绝不假装找到了) */
|
|
678
|
+
const projectsFound = (input.projects ?? []).some((project) => project.hasClaudeDir === true);
|
|
679
|
+
if (!hasDir && legacyPath === undefined && !projectsFound) {
|
|
680
|
+
return { found: false, input, skipped, unreadable };
|
|
681
|
+
}
|
|
263
682
|
return { found: true, input, skipped, unreadable };
|
|
264
683
|
}
|