สมองที่สองข้าม session: จดสิ่งที่ Claude ควรจำเกี่ยวกับโปรเจกต์ (แบบส่วนตัว ไม่ลง repo) แล้วแนบให้ Claude อ่านทุกครั้งอัตโนมัติ

สมองที่สองของ Claude ข้าม session: จดเรื่องที่อยากให้จำ เช่น "ใช้ pnpm ไม่ใช้ npm" แยกตามโปรเจกต์หรือใช้ทุกโปรเจกต์ แล้ว Mod แนบให้ Claude อ่านทุกบทสนทนาอัตโนมัติ พิมพ์ "จำไว้ว่า ..." ในแชตก็จดให้เอง และให้ Claude อ่านบทสนทนาแล้วเสนอเรื่องที่ควรจำให้ติ๊กเลือกได้
| คำสั่ง | ทำอะไร | |
|---|---|---|
/jam <ข้อความ> | จดให้ Claude จำ (เฉพาะโปรเจกต์นี้) | |
/jam global <ข้อความ> | จดให้จำทุกโปรเจกต์ | |
/jam list | ดูทั้งหมด | |
/jam forget <เลข> | ลบข้อที่ระบุ | |
/jam suggest | ให้ Claude เสนอเรื่องที่ควรจำ แล้วเลือกในหน้าต่าง | |
| `/jam on\ | off` | แนบ/ไม่แนบให้ Claude |
/jam clear confirm | ล้างของโปรเจกต์นี้ |
/plugin install project-memory-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ต่างจาก CLAUDE.md ตรงที่เก็บในเครื่องคุณ ไม่ลง repo ไม่กระทบเพื่อนร่วมทีม ใช้ชื่อ /jam (จำ) เพราะ /memory เป็นคำสั่งของ Claude Code เอง ถ้าพิมพ์ "จำไว้ว่า" Claude Code อาจบันทึกลง auto-memory ของตัวเองด้วย
hooks/register.tsx 191 lines1/**
2 * ============================================================================
3 * MOD 52: Project Memory TH - สมองที่สองของ Claude ข้าม session (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: เทคนิค "Memory Palace" และไฟล์ CLAUDE.md
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /jam <ข้อความ> - จดเรื่องที่อยากให้ Claude จำ เช่น "ใช้ pnpm ไม่ใช้ npm"
9 * แยกตามโปรเจกต์ (หรือ global ใช้ทุกโปรเจกต์)
10 * 2. แนบสิ่งที่จดไว้ให้ Claude อ่านอัตโนมัติ ผ่าน event prompt.context (บริบทของบทสนทนา)
11 * 3. พิมพ์ "จำไว้ว่า ..." หรือ "remember that ..." ในแชต จะถูกจดให้เอง
12 * 4. /jam suggest - ให้ Haiku อ่านบทสนทนาแล้วเสนอเรื่องที่ควรจำ ให้คุณติ๊กเลือก
13 *
14 * ต่างจาก CLAUDE.md ยังไง: เก็บใน $.store ของเครื่องคุณ ไม่ลง repo ไม่กระทบเพื่อนร่วมทีม
15 * เหมาะกับความชอบส่วนตัว หรือเรื่องที่ยังไม่อยากประกาศให้ทั้งทีม
16 */
17
18import type { EngineInterface, Register } from 'claude-code'
19
20interface Fact {
21 text: string
22 at: number
23}
24
25const MAX_FACTS = 50
26const MAX_LEN = 200
27const AUTO = /^\s*(?:จำไว้(?:ด้วย)?ว่า|ช่วยจำว่า|remember that)\s*[::]?\s*(.+)$/is
28
29let root = ''
30let project: Fact[] = []
31let global: Fact[] = []
32let enabled = true
33
34// ==========================================================================
35// บทที่ 1: เก็บแยกตามโปรเจกต์ - ใช้ path ของโปรเจกต์เป็นกุญแจ
36// ==========================================================================
37function key(): string {
38 return `facts:${root.toLowerCase().replace(/\\/g, '/')}`
39}
40
41async function load($: EngineInterface): Promise<void> {
42 root = await $.session.root()
43 project = ((await $.store.get(key())) as Fact[] | undefined) ?? []
44 global = ((await $.store.get('facts:global')) as Fact[] | undefined) ?? []
45}
46
47async function save($: EngineInterface): Promise<void> {
48 await $.store.set(key(), project)
49 await $.store.set('facts:global', global)
50 // บอก engine ว่าบริบทเปลี่ยน ให้คำนวณใหม่ (ส่งฉบับใหม่ให้โมเดลในรอบถัดไป)
51 try {
52 $.ui.invalidate('prompt.context')
53 } catch {}
54}
55
56async function add($: EngineInterface, text: string, isGlobal: boolean): Promise<string> {
57 const clean = text.replace(/\s+/g, ' ').trim().slice(0, MAX_LEN)
58 if (!clean) return 'วิธีใช้: /jam ใช้ pnpm ไม่ใช้ npm'
59 const list = isGlobal ? global : project
60 if (list.some(f => f.text.toLowerCase() === clean.toLowerCase())) return `ℹ️ จำไว้อยู่แล้ว: ${clean}`
61 const next = [...list, { text: clean, at: await $.clock.now() }].slice(-MAX_FACTS)
62 if (isGlobal) global = next
63 else project = next
64 await save($)
65 return `🧠 จำแล้ว${isGlobal ? ' (ทุกโปรเจกต์)' : ''}: ${clean}`
66}
67
68// ==========================================================================
69// บทที่ 2: ข้อความที่แนบเข้า system prompt
70// ==========================================================================
71function section(): string {
72 const lines = [...global.map(f => `- ${f.text}`), ...project.map(f => `- ${f.text}`)]
73 return `# สิ่งที่ผู้ใช้ขอให้จำ (Project Memory)\nผู้ใช้จดเรื่องเหล่านี้ไว้ให้คุณจำข้าม session ทำตามเสมอ เว้นแต่ผู้ใช้สั่งเป็นอย่างอื่นในแชต:\n${lines.join('\n')}`
74}
75
76function listText(): string {
77 const fmt = (list: Fact[], offset: number) => list.map((f, i) => ` ${String(i + 1 + offset).padStart(2)}. ${f.text}`)
78 const out: string[] = [`🧠 Project Memory ${enabled ? '' : '(ปิดอยู่ ไม่ได้แนบให้ Claude)'}`]
79 out.push(`\n📁 ${root.split(/[\\/]/).pop() ?? root} (${project.length})`, ...(project.length ? fmt(project, 0) : [' (ว่าง)']))
80 out.push(`\n🌍 ทุกโปรเจกต์ (${global.length})`, ...(global.length ? fmt(global, project.length) : [' (ว่าง)']))
81 return out.join('\n')
82}
83
84const HELP = `🧠 Project Memory TH (Mod 52)
85========================================
86/jam <ข้อความ> จดให้ Claude จำ (เฉพาะโปรเจกต์นี้)
87/jam global <ข้อความ> จดให้จำทุกโปรเจกต์
88/jam list ดูทั้งหมด
89/jam forget <เลข> ลบข้อที่ระบุ
90/jam suggest ให้ Claude เสนอเรื่องที่ควรจำจากบทสนทนา
91/jam on|off แนบ/ไม่แนบให้ Claude
92/jam clear confirm ล้างของโปรเจกต์นี้
93หรือพิมพ์ในแชตว่า "จำไว้ว่า ..." ก็จดให้เอง`
94
95export const register: Register = on => {
96 on('session.start', async ($, e, next) => {
97 await load($)
98 enabled = ((await $.store.get('enabled')) as boolean | undefined) ?? true
99 await $.command.register({ name: 'jam', description: 'จดสิ่งที่ Claude ควรจำข้าม session (/jam help)', argumentHint: '<ข้อความ>|global|list|forget <เลข>|suggest' })
100 return next(e)
101 })
102
103 // ==========================================================================
104 // บทที่ 3: prompt.context - เติม block ของเราลงในบริบทที่แนบไปกับข้อความแรก
105 // ==========================================================================
106 on('prompt.context', async ($, e, next) => {
107 const r = await next(e)
108 const blocks = r.blocks.filter(b => b.name !== 'projectMemoryTh')
109 if (!enabled || project.length + global.length === 0) return { blocks }
110 return { blocks: [...blocks, { name: 'projectMemoryTh', text: section() }] }
111 })
112
113 // บทที่ 4: "จำไว้ว่า ..." ในแชต - จดให้เอง แล้วส่งข้อความต่อให้ Claude ตามปกติ
114 on('prompt.submit', async ($, e, next) => {
115 const m = e.origin.kind === 'composer' || e.origin.kind === 'sdk' ? AUTO.exec(e.text) : null
116 if (m?.[1]) $.ui.toast(await add($, m[1], false))
117 return next(e)
118 })
119
120 on('command.run', { command: 'jam' }, async ($, e) => {
121 const args = e.args.trim()
122 const [sub = '', ...rest] = args.split(/\s+/).filter(Boolean)
123 switch (sub) {
124 case '':
125 case 'list':
126 return { text: listText() }
127 case 'help':
128 return { text: HELP }
129 case 'global':
130 return { text: await add($, rest.join(' '), true) }
131 case 'forget': {
132 const n = Number(rest[0])
133 if (!Number.isInteger(n) || n < 1 || n > project.length + global.length) return { text: 'วิธีใช้: /jam forget 2 (ดูเลขจาก /jam list)' }
134 let removed: Fact | undefined
135 if (n <= project.length) [removed] = project.splice(n - 1, 1)
136 else [removed] = global.splice(n - 1 - project.length, 1)
137 project = [...project]
138 global = [...global]
139 await save($)
140 return { text: `🗑️ ลืมแล้ว: ${removed?.text ?? ''}` }
141 }
142 case 'on':
143 case 'off':
144 enabled = sub === 'on'
145 await $.store.set('enabled', enabled)
146 await save($)
147 return { text: enabled ? '🧠 แนบความจำให้ Claude แล้ว' : '😶 หยุดแนบความจำ (ยังเก็บไว้)' }
148 case 'clear':
149 if (rest[0] !== 'confirm') return { text: `ล้างความจำของโปรเจกต์นี้ ${project.length} ข้อ? พิมพ์ /jam clear confirm` }
150 project = []
151 await save($)
152 return { text: '🧹 ล้างแล้ว' }
153 case 'suggest': {
154 // ==================================================================
155 // บทที่ 5: ให้ Haiku เสนอ แล้วถามด้วยหน้าต่างเลือกของ Claude Code ($.ui.ask)
156 // ==================================================================
157 const messages = await $.session.messages()
158 const convo = messages.filter(m => m.role === 'user').slice(-30).map(m => `- ${m.text.replace(/\s+/g, ' ').slice(0, 300)}`).join('\n')
159 if (!convo) return { text: 'ยังไม่มีบทสนทนาให้อ่าน' }
160 const system = 'อ่านข้อความของผู้ใช้ แล้วดึง "ความชอบหรือกฎที่คงทน" ที่ผู้ใช้อยากให้ทำตามในอนาคต เช่น เครื่องมือที่ใช้ สไตล์โค้ด ภาษาที่ตอบ ห้ามเอางานชั่วคราวหรือคำสั่งครั้งเดียว ตอบเป็นรายการ ภาษาไทย ไม่เกิน 4 ข้อ ข้อละบรรทัด ขึ้นต้นด้วย "- " สั้นไม่เกิน 80 ตัวอักษร ถ้าไม่มีให้ตอบ NONE'
161 let suggestions: string[] = []
162 for (const model of ['haiku', await $.session.model()]) {
163 const r = await $.model.complete({ model, system, prompt: convo, maxTokens: 300 }).catch(() => undefined)
164 if (r?.isAnswered) {
165 suggestions = r.text.split('\n').map(l => l.replace(/^\s*[-*•]\s*/, '').trim()).filter(l => l && l !== 'NONE').slice(0, 4)
166 break
167 }
168 }
169 const fresh = suggestions.filter(s => !project.some(f => f.text === s))
170 if (fresh.length === 0) return { text: '🤔 ยังไม่เห็นเรื่องที่ควรจำเพิ่ม' }
171 const picked = await $.ui.ask('อยากให้ Claude จำข้อไหนบ้าง?', fresh.length >= 2 ? { options: fresh, header: 'จำอะไรดี', multiSelect: true } : [fresh[0] ?? '', 'ไม่ต้องจำ']).catch(() => '')
172 const chosen = fresh.filter(s => picked.split(', ').includes(s) || picked === s)
173 if (chosen.length === 0) return { text: `💡 ข้อเสนอ:\n${fresh.map(s => ` - ${s}`).join('\n')}\nจดเองได้ด้วย /jam <ข้อความ>` }
174 for (const s of chosen) await add($, s, false)
175 return { text: `🧠 จำเพิ่ม ${chosen.length} ข้อ:\n${chosen.map(s => ` - ${s}`).join('\n')}` }
176 }
177 default:
178 return { text: await add($, args, false) }
179 }
180 })
181}
182
183/**
184 * ========================================================================
185 * การบ้าน / ต่อยอด
186 * ========================================================================
187 * 1. ส่งออกความจำของโปรเจกต์เป็นไฟล์ CLAUDE.local.md
188 * 2. ให้ความจำแต่ละข้อมีวันหมดอายุ (เช่น "สัปดาห์นี้ห้าม deploy")
189 * 3. แยกความจำตาม branch ของ git
190 */
191