本章总览
先看用户入口:memdir-commands 讲的是“用户如何显式触达记忆系统”,核心路径是 /memory UI 编辑、remember 技能辅助与 command:memory 快捷键桥接。它不负责后台提取本身,而是把命令层与 memdir 文件系统安全地接起来。
学完本章你应该能
- 描述 /memory 命令 load/call 懒加载模式
- 说明 MemoryFileSelector 与 getMemoryFiles 数据源
- 理解 remember skill 与 isAutoMemoryEnabled 门控
- 知道 editFileInEditor 与 VISUAL/EDITOR 提示
- 区分 /memory UI 编辑与 agent Write 自动保存
- 能在 commands/registry 找到 memory 注册
核心概念(先读懂这些)
/memory 是 LocalJSXCommand
commands/memory/index.ts type local-jsx,load() dynamic import memory.tsx。call 返回 MemoryCommand Dialog,onDone display system 展示相对路径与编辑器 hint。
getMemoryFiles 缓存
call 前 clearMemoryFileCaches + await getMemoryFiles() 避免 Suspense flash。getMemoryFiles 合并 project memory、auto mem、CLAUDE.md 等路径供 selector 展示。
remember skill vs /memory
remember skill 引导模型通过 AskUserQuestion 等收集要记住的内容并 Write;/memory 是人类直接编辑磁盘文件。两者都受 isAutoMemoryEnabled 影响;--bare 时 paths.ts 注释一并关闭。
建议学习步骤
- 阅读 commands/memory/index.ts
- 阅读 memory.tsx handleSelectMemoryFile
- 打开 MemoryFileSelector 数据源
- 阅读 skills/bundled/remember.ts
- 在 utils/claudemd getMemoryFiles 看路径解析
- 搜索 command:memory keybinding 示例
常见误区
注意
无 /memdir 命令——文档 memdir 指模块名
注意
wx flag 创建空文件,EEXIST 保留原内容
注意
memoryPath 含 config home 时才 mkdir getClaudeConfigHomeDir
注意
remember skill 需 EXTRACT_MEMORIES 无直接关系
/memory 命令注册
commands/memory/index.ts:
const memory: Command = {
name: 'memory',
description: 'Edit Claude memory files',
load: () => import('./memory.js'),
}
类型 LocalJSXCommand,supportsNonInteractive 通常 false——需要 Ink Dialog。
源码引用: src/commands/memory/index.ts · 第 1–10 行(共 11 行)
1| import type { Command } from '../../commands.js'
2|
3| const memory: Command = {
4| type: 'local-jsx',
5| name: 'memory',
6| description: 'Edit Claude memory files',
7| load: () => import('./memory.js'),
8| }
9|
10| export default memory
MemoryCommand UI 流程
memory.tsx MemoryCommand 组件:
- MemoryFileSelector onSelect → handleSelectMemoryFile
- 若路径在 config home 下 mkdir recursive
- writeFile wx 创建空文件(EEXIST 忽略)
- editFileInEditor(memoryPath)
- onDone 相对路径 + VISUAL/EDITOR hint,display: system
Dialog color="remember",文档 Link code.claude.com/docs/en/memory。
源码引用: src/commands/memory/memory.tsx · 第 14–82 行(共 103 行)
14|
15| function MemoryCommand({
16| onDone,
17| }: {
18| onDone: (
19| result?: string,
20| options?: { display?: CommandResultDisplay },
21| ) => void
22| }): React.ReactNode {
23| const handleSelectMemoryFile = async (memoryPath: string) => {
24| try {
25| // Create claude directory if it doesn't exist (idempotent with recursive)
26| if (memoryPath.includes(getClaudeConfigHomeDir())) {
27| await mkdir(getClaudeConfigHomeDir(), { recursive: true })
28| }
29|
30| // Create file if it doesn't exist (wx flag fails if file exists,
31| // which we catch to preserve existing content)
32| try {
33| await writeFile(memoryPath, '', { encoding: 'utf8', flag: 'wx' })
34| } catch (e: unknown) {
35| if (getErrnoCode(e) !== 'EEXIST') {
36| throw e
37| }
38| }
39|
40| await editFileInEditor(memoryPath)
41|
42| // Determine which environment variable controls the editor
43| let editorSource = 'default'
44| let editorValue = ''
45| if (process.env.VISUAL) {
46| editorSource = '$VISUAL'
47| editorValue = process.env.VISUAL
48| } else if (process.env.EDITOR) {
49| editorSource = '$EDITOR'
50| editorValue = process.env.EDITOR
51| }
52|
53| const editorInfo =
54| editorSource !== 'default'
55| ? `Using ${editorSource}="${editorValue}".`
56| : ''
57|
58| const editorHint = editorInfo
59| ? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.`
60| : `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`
61|
62| onDone(
63| `Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`,
64| { display: 'system' },
65| )
66| } catch (error) {
67| logError(error)
68| onDone(`Error opening memory file: ${error}`)
69| }
70| }
71|
72| const handleCancel = () => {
73| onDone('Cancelled memory editing', { display: 'system' })
74| }
75|
76| return (
77| <Dialog title="Memory" onCancel={handleCancel} color="remember">
78| <Box flexDirection="column">
79| <React.Suspense fallback={null}>
80| <MemoryFileSelector
81| onSelect={handleSelectMemoryFile}
82| onCancel={handleCancel}
源码引用: src/commands/memory/memory.tsx · 第 83–89 行(共 103 行)
83| />
84| </React.Suspense>
85|
86| <Box marginTop={1}>
87| <Text dimColor>
88| Learn more: <Link url="https://code.claude.com/docs/en/memory" />
89| </Text>
MemoryFileSelector 与 claudemd
components/memory/MemoryFileSelector.tsx 列出可选 memory 文件,Suspense 加载 getMemoryFiles() 结果。
utils/claudemd.ts getMemoryFiles 聚合:
- Project CLAUDE.md、.claude/rules
- Auto memory dir topic files + MEMORY.md
- 缓存供多处 UI 共享
clearMemoryFileCaches 在 /memory 打开前调用保证 fresh 列表。
源码引用: src/components/memory/MemoryFileSelector.tsx · 第 1–60 行(共 325 行)
1| import { feature } from 'bun:bundle'
2| import chalk from 'chalk'
3| import { mkdir } from 'fs/promises'
4| import { join } from 'path'
5| import * as React from 'react'
6| import { use, useEffect, useState } from 'react'
7| import { getOriginalCwd } from '../../bootstrap/state.js'
8| import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
9| import { Box, Text } from '../../ink.js'
10| import { useKeybinding } from '../../keybindings/useKeybinding.js'
11| import { getAutoMemPath, isAutoMemoryEnabled } from '../../memdir/paths.js'
12| import { logEvent } from '../../services/analytics/index.js'
13| import { isAutoDreamEnabled } from '../../services/autoDream/config.js'
14| import { readLastConsolidatedAt } from '../../services/autoDream/consolidationLock.js'
15| import { useAppState } from '../../state/AppState.js'
16| import { getAgentMemoryDir } from '../../tools/AgentTool/agentMemory.js'
17| import { openPath } from '../../utils/browser.js'
18| import { getMemoryFiles, type MemoryFileInfo } from '../../utils/claudemd.js'
19| import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
20| import { getDisplayPath } from '../../utils/file.js'
21| import { formatRelativeTimeAgo } from '../../utils/format.js'
22| import { projectIsInGitRepo } from '../../utils/memory/versions.js'
23| import { updateSettingsForSource } from '../../utils/settings/settings.js'
24| import { Select } from '../CustomSelect/index.js'
25| import { ListItem } from '../design-system/ListItem.js'
26|
27| /* eslint-disable @typescript-eslint/no-require-imports */
28| const teamMemPaths = feature('TEAMMEM')
29| ? (require('../../memdir/teamMemPaths.js') as typeof import('../../memdir/teamMemPaths.js'))
30| : null
31| /* eslint-enable @typescript-eslint/no-require-imports */
32|
33| interface ExtendedMemoryFileInfo extends MemoryFileInfo {
34| isNested?: boolean
35| exists: boolean
36| }
37|
38| // Remember last selected path
39| let lastSelectedPath: string | undefined
40|
41| const OPEN_FOLDER_PREFIX = '__open_folder__'
42|
43| type Props = {
44| onSelect: (path: string) => void
45| onCancel: () => void
46| }
47|
48| export function MemoryFileSelector({
49| onSelect,
50| onCancel,
51| }: Props): React.ReactNode {
52| const existingMemoryFiles = use(getMemoryFiles())
53|
54| // Create entries for User and Project CLAUDE.md even if they don't exist
55| const userMemoryPath = join(getClaudeConfigHomeDir(), 'CLAUDE.md')
56| const projectMemoryPath = join(getOriginalCwd(), 'CLAUDE.md')
57|
58| // Check if these are already in the existing files
59| const hasUserMemory = existingMemoryFiles.some(f => f.path === userMemoryPath)
60| const hasProjectMemory = existingMemoryFiles.some(
源码引用: src/utils/claudemd.ts · 第 1–80 行(共 1480 行)
1| /**
2| * Files are loaded in the following order:
3| *
4| * 1. Managed memory (eg. /etc/claude-code/CLAUDE.md) - Global instructions for all users
5| * 2. User memory (~/.claude/CLAUDE.md) - Private global instructions for all projects
6| * 3. Project memory (CLAUDE.md, .claude/CLAUDE.md, and .claude/rules/*.md in project roots) - Instructions checked into the codebase
7| * 4. Local memory (CLAUDE.local.md in project roots) - Private project-specific instructions
8| *
9| * Files are loaded in reverse order of priority, i.e. the latest files are highest priority
10| * with the model paying more attention to them.
11| *
12| * File discovery:
13| * - User memory is loaded from the user's home directory
14| * - Project and Local files are discovered by traversing from the current directory up to root
15| * - Files closer to the current directory have higher priority (loaded later)
16| * - CLAUDE.md, .claude/CLAUDE.md, and all .md files in .claude/rules/ are checked in each directory for Project memory
17| *
18| * Memory @include directive:
19| * - Memory files can include other files using @ notation
20| * - Syntax: @path, @./relative/path, @~/home/path, or @/absolute/path
21| * - @path (without prefix) is treated as a relative path (same as @./path)
22| * - Works in leaf text nodes only (not inside code blocks or code strings)
23| * - Included files are added as separate entries before the including file
24| * - Circular references are prevented by tracking processed files
25| * - Non-existent files are silently ignored
26| */
27|
28| import { feature } from 'bun:bundle'
29| import ignore from 'ignore'
30| import memoize from 'lodash-es/memoize.js'
31| import { Lexer } from 'marked'
32| import {
33| basename,
34| dirname,
35| extname,
36| isAbsolute,
37| join,
38| parse,
39| relative,
40| sep,
41| } from 'path'
42| import picomatch from 'picomatch'
43| import { logEvent } from 'src/services/analytics/index.js'
44| import {
45| getAdditionalDirectoriesForClaudeMd,
46| getOriginalCwd,
47| } from '../bootstrap/state.js'
48| import { truncateEntrypointContent } from '../memdir/memdir.js'
49| import { getAutoMemEntrypoint, isAutoMemoryEnabled } from '../memdir/paths.js'
50| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
51| import {
52| getCurrentProjectConfig,
53| getManagedClaudeRulesDir,
54| getMemoryPath,
55| getUserClaudeRulesDir,
56| } from './config.js'
57| import { logForDebugging } from './debug.js'
58| import { logForDiagnosticsNoPII } from './diagLogs.js'
59| import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'
60| import { getErrnoCode } from './errors.js'
61| import { normalizePathForComparison } from './file.js'
62| import { cacheKeys, type FileStateCache } from './fileStateCache.js'
63| import {
64| parseFrontmatter,
65| splitPathInFrontmatter,
66| } from './frontmatterParser.js'
67| import { getFsImplementation, safeResolvePath } from './fsOperations.js'
68| import { findCanonicalGitRoot, findGitRoot } from './git.js'
69| import {
70| executeInstructionsLoadedHooks,
71| hasInstructionsLoadedHook,
72| type InstructionsLoadReason,
73| type InstructionsMemoryType,
74| } from './hooks.js'
75| import type { MemoryType } from './memory/types.js'
76| import { expandPath } from './path.js'
77| import { pathInWorkingPath } from './permissions/filesystem.js'
78| import { isSettingSourceEnabled } from './settings/constants.js'
79| import { getInitialSettings } from './settings/settings.js'
80|
源码引用: src/components/memory/MemoryUpdateNotification.tsx · 第 1–40 行(共 43 行)
1| import { homedir } from 'os'
2| import { relative } from 'path'
3| import React from 'react'
4| import { Box, Text } from '../../ink.js'
5| import { getCwd } from '../../utils/cwd.js'
6|
7| export function getRelativeMemoryPath(path: string): string {
8| const homeDir = homedir()
9| const cwd = getCwd()
10|
11| // Calculate relative paths
12| const relativeToHome = path.startsWith(homeDir)
13| ? '~' + path.slice(homeDir.length)
14| : null
15|
16| const relativeToCwd = path.startsWith(cwd) ? './' + relative(cwd, path) : null
17|
18| // Return the shorter path, or absolute if neither is applicable
19| if (relativeToHome && relativeToCwd) {
20| return relativeToHome.length <= relativeToCwd.length
21| ? relativeToHome
22| : relativeToCwd
23| }
24|
25| return relativeToHome || relativeToCwd || path
26| }
27|
28| export function MemoryUpdateNotification({
29| memoryPath,
30| }: {
31| memoryPath: string
32| }): React.ReactNode {
33| const displayPath = getRelativeMemoryPath(memoryPath)
34|
35| return (
36| <Box flexDirection="column" flexGrow={1}>
37| <Text color="text">
38| Memory updated in {displayPath} · /memory to edit
39| </Text>
40| </Box>
remember bundled skill
skills/bundled/remember.ts registerRememberSkill:
- isAutoMemoryEnabled() gate
- name: 'remember'
- 引导用户确认要记住的内容,Write 到 memdir
- AskUserQuestionTool source: 'remember' analytics
与 extractMemories 互补:skill 即时;extract 回合末批量。paths.ts 注释 SIMPLE 模式关闭 remember。
源码引用: src/skills/bundled/remember.ts · 第 1–80 行(共 83 行)
1| import { isAutoMemoryEnabled } from '../../memdir/paths.js'
2| import { registerBundledSkill } from '../bundledSkills.js'
3|
4| export function registerRememberSkill(): void {
5| if (process.env.USER_TYPE !== 'ant') {
6| return
7| }
8|
9| const SKILL_PROMPT = `# Memory Review
10|
11| ## Goal
12| Review the user's memory landscape and produce a clear report of proposed changes, grouped by action type. Do NOT apply changes — present proposals for user approval.
13|
14| ## Steps
15|
16| ### 1. Gather all memory layers
17| Read CLAUDE.md and CLAUDE.local.md from the project root (if they exist). Your auto-memory content is already in your system prompt — review it there. Note which team memory sections exist, if any.
18|
19| **Success criteria**: You have the contents of all memory layers and can compare them.
20|
21| ### 2. Classify each auto-memory entry
22| For each substantive entry in auto-memory, determine the best destination:
23|
24| | Destination | What belongs there | Examples |
25| |---|---|---|
26| | **CLAUDE.md** | Project conventions and instructions for Claude that all contributors should follow | "use bun not npm", "API routes use kebab-case", "test command is bun test", "prefer functional style" |
27| | **CLAUDE.local.md** | Personal instructions for Claude specific to this user, not applicable to other contributors | "I prefer concise responses", "always explain trade-offs", "don't auto-commit", "run tests before committing" |
28| | **Team memory** | Org-wide knowledge that applies across repositories (only if team memory is configured) | "deploy PRs go through #deploy-queue", "staging is at staging.internal", "platform team owns infra" |
29| | **Stay in auto-memory** | Working notes, temporary context, or entries that don't clearly fit elsewhere | Session-specific observations, uncertain patterns |
30|
31| **Important distinctions:**
32| - CLAUDE.md and CLAUDE.local.md contain instructions for Claude, not user preferences for external tools (editor theme, IDE keybindings, etc. don't belong in either)
33| - Workflow practices (PR conventions, merge strategies, branch naming) are ambiguous — ask the user whether they're personal or team-wide
34| - When unsure, ask rather than guess
35|
36| **Success criteria**: Each entry has a proposed destination or is flagged as ambiguous.
37|
38| ### 3. Identify cleanup opportunities
39| Scan across all layers for:
40| - **Duplicates**: Auto-memory entries already captured in CLAUDE.md or CLAUDE.local.md → propose removing from auto-memory
41| - **Outdated**: CLAUDE.md or CLAUDE.local.md entries contradicted by newer auto-memory entries → propose updating the older layer
42| - **Conflicts**: Contradictions between any two layers → propose resolution, noting which is more recent
43|
44| **Success criteria**: All cross-layer issues identified.
45|
46| ### 4. Present the report
47| Output a structured report grouped by action type:
48| 1. **Promotions** — entries to move, with destination and rationale
49| 2. **Cleanup** — duplicates, outdated entries, conflicts to resolve
50| 3. **Ambiguous** — entries where you need the user's input on destination
51| 4. **No action needed** — brief note on entries that should stay put
52|
53| If auto-memory is empty, say so and offer to review CLAUDE.md for cleanup.
54|
55| **Success criteria**: User can review and approve/reject each proposal individually.
56|
57| ## Rules
58| - Present ALL proposals before making any changes
59| - Do NOT modify files without explicit user approval
60| - Do NOT create new files unless the target doesn't exist yet
61| - Ask about ambiguous entries — don't guess
62| `
63|
64| registerBundledSkill({
65| name: 'remember',
66| description:
67| 'Review auto-memory entries and propose promotions to CLAUDE.md, CLAUDE.local.md, or shared memory. Also detects outdated, conflicting, and duplicate entries across memory layers.',
68| whenToUse:
69| 'Use when the user wants to review, organize, or promote their auto-memory entries. Also useful for cleaning up outdated or conflicting entries across CLAUDE.md, CLAUDE.local.md, and auto-memory.',
70| userInvocable: true,
71| isEnabled: () => isAutoMemoryEnabled(),
72| async getPromptForCommand(args) {
73| let prompt = SKILL_PROMPT
74|
75| if (args) {
76| prompt += `\n## Additional context from user\n\n${args}`
77| }
78|
79| return [{ type: 'text', text: prompt }]
80| },
源码引用: src/memdir/paths.ts · 第 38–42 行(共 279 行)
38| // --bare / SIMPLE: prompts.ts already drops the memory section from the
39| // system prompt via its SIMPLE early-return; this gate stops the other half
40| // (extractMemories turn-end fork, autoDream, /remember, /dream, team sync).
41| if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
42| return false
keybinding command:memory
用户可在 keybindings.json 配置:
{ "context": "Chat", "bindings": { "alt+m": "command:memory" } }
CommandKeybindingHandlers 提交 /memory,fromKeybinding: true,走 handlePromptSubmit local-jsx 分支,打开同一 MemoryCommand Dialog。
见 keybindings/command-bindings 子章节。
源码引用: src/hooks/useCommandKeybindings.tsx · 第 77–83 行(共 83 行)
77| context: 'Chat',
78| isActive: isActive && !isModalOverlayActive,
79| })
80|
81| return null
82| }
83|
源码引用: src/types/command.ts · 第 144–155 行(共 217 行)
144| type LocalJSXCommand = {
145| type: 'local-jsx'
146| /**
147| * Lazy-load the command implementation.
148| * Returns a module with a call() function.
149| * This defers loading heavy dependencies until the command is invoked.
150| */
151| load: () => Promise<LocalJSXCommandModule>
152| }
153|
154| /**
155| * Declares which auth/provider environments a command is available in.
compact-memory 与相邻命令
commands/compact-memory-commands(module-content 已有)处理 /compact 与 memory 边界——compact 可能引用 MEMORY_TYPE_VALUES from utils/memory/types。
/dream(若启用)与 autoDream 服务共用 memdir 写入路径,gate 在 isAutoMemoryEnabled。
无 /memdir slash——运维调试 memdir 用 ls ~/.claude/projects/*/memory/ 或 /memory UI。
源码缺失
File not found: c:\Users\123\Desktop\jd\Claude code源码\claude-code-complete\claude-code-complete\src-readable\commands\compact-memory-commands.mjs
源码引用: src/services/compact/compact.ts · 第 59–59 行(共 1706 行)
59| import { MEMORY_TYPE_VALUES } from '../../utils/memory/types.js'
端到端用户旅程
自动:session start loadMemoryPrompt → 模型见 MEMORY.md + 指南
turn end extractMemories → 可能新增 topic.md
手动:/memory → 选人编辑 → editFileInEditor
/remember skill → 对话式保存
Keybinding:command:memory → 同 /memory
memdir/ 提供路径与 prompt;commands/skills 提供入口;extractMemories 提供后台同步。
命令入口与自动写入的分层
/memory、command:memory、remember skill 与自动 extractMemories 都能接触“记忆”,但用户意图不同。/memory 是 local-jsx 命令,commands/memory/index.ts 只注册 name、description、load,真正 UI 在 memory.tsx 中打开 MemoryFileSelector,让用户选文件后用 editFileInEditor 编辑。command:memory 只是 keybindings 的动态入口:CommandKeybindingHandlers 把它转成 /memory,并通过 fromKeybinding 保留当前 prompt 草稿。remember skill 则是模型侧的用户可调用技能,先检查 isAutoMemoryEnabled,再给模型一份“审阅、分类、提出修改建议”的 prompt。
自动写入不从 commands/ 开始,而是 stopHooks 或 print drain 触发 extractMemories。它扫描 memory 目录、构建 manifest、启动 forked agent、受 hasMemoryWritesSince 互斥保护,最后可能追加 memory_saved 系统消息。也就是说,commands/memory 是人类编辑文件的门,skills/bundled/remember 是对话式整理入口,services/extractMemories 是后台维护管线。文档里不要写“/memdir 命令”,因为源码 registry 里没有这个用户入口;如果用户想手动看或改文件,应引导到 /memory 或配置 command:memory;如果想让模型长期记住,则依赖 memdir prompt、Write 工具或自动提取。
/memory 的 local-jsx 形态还意味着它不能被当作普通 prompt 命令看待。它需要 Ink Dialog、MemoryFileSelector、编辑器启动和 onDone 回调,所以非交互环境或 modal 已打开时行为会受限制。command:memory 触发同一路径,但 fromKeybinding 保留草稿;手输 /memory 则会进入正常 slash 命令历史。remember skill 更像“整理建议生成器”,它要求先审阅各层记忆并提出变更,不应擅自写文件。把这三种入口分清,用户文档才不会把人工编辑、快捷键打开和模型自动保存混成一个动作。
维护命令侧内容时,优先从 commands.ts registry、commands/memory/index.ts 和 memory.tsx 的 call/onDone 读起,再去看 utils/claudemd 的文件列表来源。若要新增记忆相关命令,应明确它是人类 UI、模型 prompt skill,还是后台服务开关;不同入口需要的权限、交互模式和 transcript 展示完全不同。
源码引用: src/commands/memory/index.ts · 第 1–10 行(共 11 行)
1| import type { Command } from '../../commands.js'
2|
3| const memory: Command = {
4| type: 'local-jsx',
5| name: 'memory',
6| description: 'Edit Claude memory files',
7| load: () => import('./memory.js'),
8| }
9|
10| export default memory
源码引用: src/hooks/useCommandKeybindings.tsx · 第 77–83 行(共 83 行)
77| context: 'Chat',
78| isActive: isActive && !isModalOverlayActive,
79| })
80|
81| return null
82| }
83|
源码引用: src/skills/bundled/remember.ts · 第 64–81 行(共 83 行)
64| registerBundledSkill({
65| name: 'remember',
66| description:
67| 'Review auto-memory entries and propose promotions to CLAUDE.md, CLAUDE.local.md, or shared memory. Also detects outdated, conflicting, and duplicate entries across memory layers.',
68| whenToUse:
69| 'Use when the user wants to review, organize, or promote their auto-memory entries. Also useful for cleaning up outdated or conflicting entries across CLAUDE.md, CLAUDE.local.md, and auto-memory.',
70| userInvocable: true,
71| isEnabled: () => isAutoMemoryEnabled(),
72| async getPromptForCommand(args) {
73| let prompt = SKILL_PROMPT
74|
75| if (args) {
76| prompt += `\n## Additional context from user\n\n${args}`
77| }
78|
79| return [{ type: 'text', text: prompt }]
80| },
81| })
源码引用: src/services/extractMemories/extractMemories.ts · 第 395–427 行(共 616 行)
395| // Pre-inject the memory directory manifest so the agent doesn't spend
396| // a turn on `ls`. Reuses findRelevantMemories' frontmatter scan.
397| // Placed after the throttle gate so skipped turns don't pay the scan cost.
398| const existingMemories = formatMemoryManifest(
399| await scanMemoryFiles(memoryDir, createAbortController().signal),
400| )
401|
402| const userPrompt =
403| feature('TEAMMEM') && teamMemoryEnabled
404| ? buildExtractCombinedPrompt(
405| newMessageCount,
406| existingMemories,
407| skipIndex,
408| )
409| : buildExtractAutoOnlyPrompt(
410| newMessageCount,
411| existingMemories,
412| skipIndex,
413| )
414|
415| const result = await runForkedAgent({
416| promptMessages: [createUserMessage({ content: userPrompt })],
417| cacheSafeParams,
418| canUseTool,
419| querySource: 'extract_memories',
420| forkLabel: 'extract_memories',
421| // The extractMemories subagent does not need to record to transcript.
422| // Doing so can create race conditions with the main thread.
423| skipTranscript: true,
424| // Well-behaved extractions complete in 2-4 turns (read → write).
425| // A hard cap prevents verification rabbit-holes from burning turns.
426| maxTurns: 5,
427| })
命令层改动时的兼容检查
记忆相关命令改动通常会跨三层:命令注册(commands/registry 与 index.ts)、交互组件(MemoryFileSelector 与 Dialog)、文件系统接入(claudemd 缓存与 memdir 路径)。因此变更评审时应同时检查:命令是否仍可在 Chat context 触发、选择器是否拿到最新文件列表、非交互或 modal 场景是否正确降级、onDone 文案是否与实际写盘路径一致。
如果只改了命令入口而忽略文件列表缓存,用户会看到“命令执行成功但列表没刷新”;如果只改了 UI 而忽略 fromKeybinding 语义,快捷键触发会污染草稿输入。这些都是 memdir-commands 层最常见回归。
源码引用: src/commands/memory/memory.tsx · 第 29–74 行(共 103 行)
29|
30| // Create file if it doesn't exist (wx flag fails if file exists,
31| // which we catch to preserve existing content)
32| try {
33| await writeFile(memoryPath, '', { encoding: 'utf8', flag: 'wx' })
34| } catch (e: unknown) {
35| if (getErrnoCode(e) !== 'EEXIST') {
36| throw e
37| }
38| }
39|
40| await editFileInEditor(memoryPath)
41|
42| // Determine which environment variable controls the editor
43| let editorSource = 'default'
44| let editorValue = ''
45| if (process.env.VISUAL) {
46| editorSource = '$VISUAL'
47| editorValue = process.env.VISUAL
48| } else if (process.env.EDITOR) {
49| editorSource = '$EDITOR'
50| editorValue = process.env.EDITOR
51| }
52|
53| const editorInfo =
54| editorSource !== 'default'
55| ? `Using ${editorSource}="${editorValue}".`
56| : ''
57|
58| const editorHint = editorInfo
59| ? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.`
60| : `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`
61|
62| onDone(
63| `Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`,
64| { display: 'system' },
65| )
66| } catch (error) {
67| logError(error)
68| onDone(`Error opening memory file: ${error}`)
69| }
70| }
71|
72| const handleCancel = () => {
73| onDone('Cancelled memory editing', { display: 'system' })
74| }
源码引用: src/utils/claudemd.ts · 第 541–580 行(共 1480 行)
541| * Only applies to User, Project, and Local memory types.
542| * Managed, AutoMem, and TeamMem types are never excluded.
543| *
544| * Matches both the original path and the realpath-resolved path to handle symlinks
545| * (e.g., /tmp -> /private/tmp on macOS).
546| */
547| function isClaudeMdExcluded(filePath: string, type: MemoryType): boolean {
548| if (type !== 'User' && type !== 'Project' && type !== 'Local') {
549| return false
550| }
551|
552| const patterns = getInitialSettings().claudeMdExcludes
553| if (!patterns || patterns.length === 0) {
554| return false
555| }
556|
557| const matchOpts = { dot: true }
558| const normalizedPath = filePath.replaceAll('\\', '/')
559|
560| // Build an expanded pattern list that includes realpath-resolved versions of
561| // absolute patterns. This handles symlinks like /tmp -> /private/tmp on macOS:
562| // the user writes "/tmp/project/CLAUDE.md" in their exclude, but the system
563| // resolves the CWD to "/private/tmp/project/...", so the file path uses the
564| // real path. By resolving the patterns too, both sides match.
565| const expandedPatterns = resolveExcludePatterns(patterns).filter(
566| p => p.length > 0,
567| )
568| if (expandedPatterns.length === 0) {
569| return false
570| }
571|
572| return picomatch.isMatch(normalizedPath, expandedPatterns, matchOpts)
573| }
574|
575| /**
576| * Expands exclude patterns by resolving symlinks in absolute path prefixes.
577| * For each absolute pattern (starting with /), tries to resolve the longest
578| * existing directory prefix via realpathSync and adds the resolved version.
579| * Glob patterns (containing *) have their static prefix resolved.
580| */
源码引用: src/hooks/useCommandKeybindings.tsx · 第 77–83 行(共 83 行)
77| context: 'Chat',
78| isActive: isActive && !isModalOverlayActive,
79| })
80|
81| return null
82| }
83|
从用户心智看三种入口
给用户解释记忆功能时,建议直接用“三入口心智”避免混淆:/memory 是“我想手动改文件”;remember 是“请你帮我整理该记什么”;自动提取是“回合结束后你自己补记”。三者最终都可能影响同一 memory 目录,但触发者、时机和可见反馈不同。这个区分能明显减少“为什么我刚编辑了还被自动改动”的误解。
命令设计也应围绕这个心智:/memory 偏可控与可审阅,remember 偏交互引导,extract 偏后台兜底。只要文案和 UI 保持这三种角色一致,用户就能预测行为。
在培训或文档里也建议把三入口放在同一页对比说明,并标注“何时触发、是否会改草稿、是否显示系统提示”,这样新用户更容易建立稳定预期。
源码引用: src/commands/memory/memory.tsx · 第 47–74 行(共 103 行)
47| editorValue = process.env.VISUAL
48| } else if (process.env.EDITOR) {
49| editorSource = '$EDITOR'
50| editorValue = process.env.EDITOR
51| }
52|
53| const editorInfo =
54| editorSource !== 'default'
55| ? `Using ${editorSource}="${editorValue}".`
56| : ''
57|
58| const editorHint = editorInfo
59| ? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.`
60| : `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`
61|
62| onDone(
63| `Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`,
64| { display: 'system' },
65| )
66| } catch (error) {
67| logError(error)
68| onDone(`Error opening memory file: ${error}`)
69| }
70| }
71|
72| const handleCancel = () => {
73| onDone('Cancelled memory editing', { display: 'system' })
74| }
源码引用: src/skills/bundled/remember.ts · 第 64–81 行(共 83 行)
64| registerBundledSkill({
65| name: 'remember',
66| description:
67| 'Review auto-memory entries and propose promotions to CLAUDE.md, CLAUDE.local.md, or shared memory. Also detects outdated, conflicting, and duplicate entries across memory layers.',
68| whenToUse:
69| 'Use when the user wants to review, organize, or promote their auto-memory entries. Also useful for cleaning up outdated or conflicting entries across CLAUDE.md, CLAUDE.local.md, and auto-memory.',
70| userInvocable: true,
71| isEnabled: () => isAutoMemoryEnabled(),
72| async getPromptForCommand(args) {
73| let prompt = SKILL_PROMPT
74|
75| if (args) {
76| prompt += `\n## Additional context from user\n\n${args}`
77| }
78|
79| return [{ type: 'text', text: prompt }]
80| },
81| })
源码引用: src/services/extractMemories/extractMemories.ts · 第 395–427 行(共 616 行)
395| // Pre-inject the memory directory manifest so the agent doesn't spend
396| // a turn on `ls`. Reuses findRelevantMemories' frontmatter scan.
397| // Placed after the throttle gate so skipped turns don't pay the scan cost.
398| const existingMemories = formatMemoryManifest(
399| await scanMemoryFiles(memoryDir, createAbortController().signal),
400| )
401|
402| const userPrompt =
403| feature('TEAMMEM') && teamMemoryEnabled
404| ? buildExtractCombinedPrompt(
405| newMessageCount,
406| existingMemories,
407| skipIndex,
408| )
409| : buildExtractAutoOnlyPrompt(
410| newMessageCount,
411| existingMemories,
412| skipIndex,
413| )
414|
415| const result = await runForkedAgent({
416| promptMessages: [createUserMessage({ content: userPrompt })],
417| cacheSafeParams,
418| canUseTool,
419| querySource: 'extract_memories',
420| forkLabel: 'extract_memories',
421| // The extractMemories subagent does not need to record to transcript.
422| // Doing so can create race conditions with the main thread.
423| skipTranscript: true,
424| // Well-behaved extractions complete in 2-4 turns (read → write).
425| // A hard cap prevents verification rabbit-holes from burning turns.
426| maxTurns: 5,
427| })
本章小结与延伸
memdir-commands 连接 REPL UI 与磁盘 memory 文件。回到 memdir 总览串联 9 文件全貌。 继续学习: