SLOPSHOPPER

project-memory-th

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

newcommandtoastpromptmodel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · project-memory-th
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /jam ⎿ project-memory-th: 🧠 Project Memory ⎿ project-memory-th: ⎿ project-memory-th: 📁 app (0) ⎿ project-memory-th: (ว่าง) ⎿ project-memory-th: ⎿ project-memory-th: 🌍 ทุกโปรเจกต์ (0) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Mod 52: Project Memory TH

สมองที่สองของ 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. เก็บแยกตามโปรเจกต์ใน $.store
  2. ข้อความที่แนบให้ Claude
  3. แนบด้วย event prompt.context และ invalidate เมื่อเปลี่ยน
  4. จับ "จำไว้ว่า" ใน prompt.submit
  5. เสนอด้วย Haiku แล้วให้เลือกด้วย $.ui.ask

หมายเหตุ

ต่างจาก CLAUDE.md ตรงที่เก็บในเครื่องคุณ ไม่ลง repo ไม่กระทบเพื่อนร่วมทีม ใช้ชื่อ /jam (จำ) เพราะ /memory เป็นคำสั่งของ Claude Code เอง ถ้าพิมพ์ "จำไว้ว่า" Claude Code อาจบันทึกลง auto-memory ของตัวเองด้วย

Source 1 files
hooks/register.tsx 191 lines
1/**
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