memory-eternal 0.10.0 → 0.10.2

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.
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ // 记忆核心 · 正式版发布(npm + GitHub + Gitee Releases + tag 同步)
3
+ //
4
+ // 为什么要有这个脚本:桌面版的 profile 依赖是 `github:EternalNight996/memory-eternal`,
5
+ // dsh web 的 profile 走 npm 版本号,Gitee 是镜像 + Release 归档。三边任一漏掉,
6
+ // 就会出现「npm 上有新版、桌面版还是旧的」这类不一致(v0.9.16–0.9.23 就漏打了 tag)。
7
+ //
8
+ // 用法:
9
+ // node scripts/release.mjs --dry-run # 只做检查与打印,不写任何远端
10
+ // node scripts/release.mjs # 正式发布(会 npm publish + push + 建 Release)
11
+ // node scripts/release.mjs --skip-npm # 只同步 git/GitHub/Gitee
12
+ // node scripts/release.mjs --only-gitee # 只补建 Gitee Release
13
+ // node scripts/release.mjs --allow-dirty # 允许带未提交改动发布(默认拒绝)
14
+ // node scripts/release.mjs --tag=v0.10.0 --notes-file=notes.md
15
+ //
16
+ // Gitee 需要 token:环境变量 `GITEE_TOKEN`,或文件 `~/.config/memory-eternal/gitee-token`
17
+ // (一行 token,不要提交进仓库)。token 需要在 Gitee → 设置 → 私人令牌 生成,
18
+ // 勾选 `projects`(仓库读写)。**没有 token 就跳过、并明确打印为「未同步」,不要假装成功。**
19
+ //
20
+ // 发布前顺序:先让 `npm test` 全绿;本脚本会再跑一次(`--skip-tests` 可跳过)。
21
+
22
+ import { execFileSync } from 'node:child_process'
23
+ import fs from 'node:fs'
24
+ import os from 'node:os'
25
+ import path from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
29
+ const args = process.argv.slice(2)
30
+ const flag = (name) => args.includes(`--${name}`)
31
+ const opt = (name, def = '') => {
32
+ const hit = args.find((a) => a.startsWith(`--${name}=`))
33
+ return hit ? hit.slice(name.length + 3) : def
34
+ }
35
+ const DRY = flag('dry-run')
36
+ const SKIP_NPM = flag('skip-npm') || flag('only-gitee')
37
+ const ONLY_GITEE = flag('only-gitee')
38
+ const SKIP_TESTS = flag('skip-tests')
39
+
40
+ // 跨平台「怎么调用 npm」(见下方 run() 的说明):Windows 走 node + npm-cli.js。
41
+ const NPM = (() => {
42
+ if (process.platform !== 'win32') return { cmd: 'npm', args: [] }
43
+ const nodeDir = path.dirname(process.execPath)
44
+ const hit = [
45
+ path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
46
+ path.join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
47
+ ].find((p) => fs.existsSync(p))
48
+ return hit ? { cmd: process.execPath, args: [hit] } : { cmd: 'npm.cmd', args: [] }
49
+ })()
50
+
51
+ const run = (cmd, argv, { cwd = ROOT, allowFail = false } = {}) => {
52
+ // Windows 上 npm 只是 .cmd 垫片:execFileSync('npm', …) 直接 ENOENT(Node ≥18 起 spawn
53
+ // .cmd 还要 shell:true,而 shell 拼参数对含空格/中文的 tag 与说明不安全)。所以优先用
54
+ // 「当前 node + npm-cli.js」这两个真实文件,找不到才退回 npm.cmd。git / gh 是真实 exe,不受影响。
55
+ const [realCmd, realArgv] = cmd === 'npm' ? [NPM.cmd, [...NPM.args, ...argv]] : [cmd, argv]
56
+ try {
57
+ return { ok: true, out: execFileSync(realCmd, realArgv, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() }
58
+ } catch (e) {
59
+ const out = `${e.stdout || ''}${e.stderr || ''}`.trim() || String(e.message || e)
60
+ if (!allowFail) throw new Error(`${cmd} ${argv.join(' ')} 失败:\n${out}`)
61
+ return { ok: false, out }
62
+ }
63
+ }
64
+ const say = (msg) => console.log(msg)
65
+ const step = (n, msg) => console.log(`\n=== [${n}] ${msg}`)
66
+
67
+ // ---- 1. 版本与前置检查 ------------------------------------------------------
68
+ step(1, '读取版本与前置检查')
69
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
70
+ const tag = opt('tag') || `v${pkg.version}`
71
+ say(`package.json version = ${pkg.version} → tag = ${tag}`)
72
+ if (tag !== `v${pkg.version}` && !flag('allow-tag-mismatch')) {
73
+ throw new Error(`tag(${tag}) 与 package.json 版本(${pkg.version}) 不一致;确属有意请加 --allow-tag-mismatch`)
74
+ }
75
+
76
+ const dirty = run('git', ['status', '--porcelain']).out
77
+ if (dirty && !flag('allow-dirty')) {
78
+ throw new Error(`工作区有未提交改动,拒绝发布(先 commit,或加 --allow-dirty):\n${dirty}`)
79
+ }
80
+ say('工作区干净 ✓')
81
+
82
+ if (!SKIP_TESTS) {
83
+ step('1b', 'npm test')
84
+ const t = run('npm', ['test'], { allowFail: true })
85
+ if (!t.ok) throw new Error(`npm test 未通过,拒绝发布:\n${t.out.slice(-3000)}`)
86
+ say('npm test 全绿 ✓')
87
+ } else {
88
+ say('(--skip-tests:跳过测试)')
89
+ }
90
+
91
+ // ---- 2. 发布说明 -----------------------------------------------------------
92
+ step(2, '准备发布说明')
93
+ const notesFile = opt('notes-file')
94
+ let notes = ''
95
+ if (notesFile) {
96
+ notes = fs.readFileSync(path.resolve(ROOT, notesFile), 'utf8')
97
+ } else {
98
+ // 从 README 更新日志表里取该版本那一行(| **v0.10.0** | 日期 | 说明 |)
99
+ const readme = fs.readFileSync(path.join(ROOT, 'README.md'), 'utf8')
100
+ const row = readme.split(/\r?\n/).find((l) => l.startsWith(`| **${tag}**`))
101
+ notes = [
102
+ `## ${tag}`,
103
+ '',
104
+ `npm: https://www.npmjs.com/package/memory-eternal/v/${pkg.version}`,
105
+ '',
106
+ row ? row.replace(/^\|\s*/, '').replace(/\s*\|\s*$/, '').split(/\s*\|\s*/).slice(1).join(' — ') : '(README 更新日志里没有这一版,请用 --notes-file 指定)',
107
+ ].join('\n')
108
+ }
109
+ say(notes.split('\n').slice(0, 6).join('\n') + (notes.split('\n').length > 6 ? '\n…' : ''))
110
+ const notesPath = path.join(os.tmpdir(), `memory-eternal-release-${tag}.md`)
111
+ fs.writeFileSync(notesPath, notes, 'utf8')
112
+ say(`发布说明写入 ${notesPath}`)
113
+
114
+ // ---- 3. npm publish -------------------------------------------------------
115
+ step(3, 'npm publish')
116
+ if (SKIP_NPM) {
117
+ say('(已跳过)')
118
+ } else {
119
+ const already = run('npm', ['view', `memory-eternal@${pkg.version}`, 'version'], { allowFail: true })
120
+ if (already.ok && already.out.includes(pkg.version)) {
121
+ say(`npm 上已存在 ${pkg.version},跳过发布`)
122
+ } else if (DRY) {
123
+ say(`[dry-run] 将执行:npm publish --access public(${pkg.version})`)
124
+ } else {
125
+ const r = run('npm', ['publish', '--access public'], { allowFail: true })
126
+ if (!r.ok) throw new Error(`npm publish 失败(2FA / 网络 / 版本冲突?):\n${r.out}`)
127
+ say(`npm publish 完成:memory-eternal@${pkg.version}`)
128
+ }
129
+ }
130
+
131
+ // ---- 4. git tag + push(GitHub + Gitee) ----------------------------------
132
+ step(4, 'git tag + push')
133
+ const hasTag = run('git', ['tag', '-l', tag]).out === tag
134
+ if (!hasTag) {
135
+ if (DRY) say(`[dry-run] 将执行:git tag -a ${tag} -m "<发布说明首行>"`)
136
+ else {
137
+ run('git', ['tag', '-a', tag, '-m', notes.split('\n').filter(Boolean).slice(0, 2).join(' / ')])
138
+ say(`已创建 tag ${tag}`)
139
+ }
140
+ } else {
141
+ say(`tag ${tag} 已存在`)
142
+ }
143
+ const remotes = run('git', ['remote']).out.split(/\s+/).filter(Boolean)
144
+ for (const remote of remotes) {
145
+ if (DRY) { say(`[dry-run] 将执行:git push ${remote} main && git push ${remote} ${tag}`); continue }
146
+ const a = run('git', ['push', remote, 'main'], { allowFail: true })
147
+ const b = run('git', ['push', remote, tag], { allowFail: true })
148
+ say(`${remote}: main ${a.ok ? '✓' : '✗ ' + a.out.slice(0, 200)} | tag ${b.ok ? '✓' : '✗ ' + b.out.slice(0, 200)}`)
149
+ }
150
+
151
+ // ---- 5. GitHub Release ---------------------------------------------------
152
+ step(5, 'GitHub Release')
153
+ if (DRY) say(`[dry-run] 将执行:gh release create ${tag} --title … --notes-file …`)
154
+ else {
155
+ const exists = run('gh', ['release', 'view', tag, '--repo', 'EternalNight996/memory-eternal'], { allowFail: true })
156
+ const title = opt('title') || `${tag} — ${notes.split('\n').find((l) => l.startsWith('## '))?.slice(3) || 'release'}`
157
+ if (exists.ok) {
158
+ const r = run('gh', ['release', 'edit', tag, '--repo', 'EternalNight996/memory-eternal', '--title', title, '--notes-file', notesPath], { allowFail: true })
159
+ say(`GitHub Release 已存在 → ${r.ok ? '已更新说明' : '更新失败:' + r.out.slice(0, 200)}`)
160
+ } else {
161
+ const r = run('gh', ['release', 'create', tag, '--repo', 'EternalNight996/memory-eternal', '--title', title, '--notes-file', notesPath], { allowFail: true })
162
+ say(r.ok ? `GitHub Release 创建成功:${r.out}` : `GitHub Release 失败:${r.out.slice(0, 300)}`)
163
+ }
164
+ }
165
+
166
+ // ---- 6. Gitee Release ---------------------------------------------------
167
+ step(6, 'Gitee Release')
168
+ const giteeToken = (() => {
169
+ if (process.env.GITEE_TOKEN) return process.env.GITEE_TOKEN.trim()
170
+ const f = path.join(os.homedir(), '.config', 'memory-eternal', 'gitee-token')
171
+ try { return fs.readFileSync(f, 'utf8').trim() } catch { return '' }
172
+ })()
173
+ if (!giteeToken) {
174
+ say('⚠ 未找到 Gitee token → **Gitee Release 未同步**(不算成功)')
175
+ say(' 解决:设置环境变量 GITEE_TOKEN,或写入 ~/.config/memory-eternal/gitee-token(一行)')
176
+ say(' token 需在 Gitee → 设置 → 私人令牌 生成,勾选 projects 权限')
177
+ } else if (DRY) {
178
+ say('[dry-run] 将执行:POST https://gitee.com/api/v5/repos/EternalNight996/memory-eternal/releases')
179
+ } else {
180
+ const repo = 'EternalNight996/memory-eternal'
181
+ const list = await fetch(`https://gitee.com/api/v5/repos/${repo}/releases?access_token=${encodeURIComponent(giteeToken)}&per_page=100`)
182
+ .then((r) => r.json()).catch(() => null)
183
+ if (!Array.isArray(list)) {
184
+ say('⚠ Gitee 查询失败(token 无效 / 网络):Gitee Release 未同步')
185
+ } else if (list.some((x) => x.tag_name === tag)) {
186
+ say(`Gitee Release 已存在(${tag}),跳过`)
187
+ } else {
188
+ const body = new URLSearchParams({ access_token: giteeToken, tag_name: tag, name: `${tag} — ${pkg.description.slice(0, 40)}`, body: notes, target_commitish: 'main', prerelease: 'false' })
189
+ const res = await fetch(`https://gitee.com/api/v5/repos/${repo}/releases`, {
190
+ method: 'POST',
191
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
192
+ body,
193
+ })
194
+ const text = await res.text()
195
+ if (res.ok) say(`Gitee Release 创建成功:${tag}`)
196
+ else say(`⚠ Gitee Release 失败(HTTP ${res.status}):${text.slice(0, 300)}`)
197
+ }
198
+ }
199
+
200
+ // ---- 7. 汇总 ------------------------------------------------------------
201
+ step(7, `完成${DRY ? '(dry-run,未写任何远端)' : ''}`)
202
+ say(`版本 ${pkg.version} | tag ${tag}`)
203
+ say('提醒:桌面版用户需 `cd ~/.dsh/profiles/desktop && pnpm update memory-eternal` 后重启 DSH;')
204
+ say(' dsh web 用户需 `cd ~/.dsh/profiles/web && pnpm add memory-eternal@' + pkg.version + '`。')
@@ -1,14 +1,14 @@
1
- ---
2
- name: memory
3
- description: Recall relevant knowledge cards from the unified memory vault and capture durable findings. Use memory_recall before answering when prior context helps; call memory_capture when a session produces a durable insight.
4
- ---
5
-
6
- # Memory Eternal
7
-
8
- This agent has access to `memory_recall` (retrieve cards) and `memory_capture` (store a card) via the `memory` MCP server.
9
-
10
- ## When to recall
11
- Before answering when the user's question may depend on prior project decisions, past bugs, or preferences. Combine with `memory_stats` to gauge coverage.
12
-
13
- ## When to capture
14
- At the end of a session that produced a durable, reusable conclusion (a fix, a decision, an architecture rationale, a constraint). Capture concise findings rather than raw transcripts; the hook also auto-captures at session end into the same vault (pending → audit). Source attribution is automatic.
1
+ ---
2
+ name: memory
3
+ description: Recall relevant knowledge cards from the unified memory vault and capture durable findings. Use memory_recall before answering when prior context helps; call memory_capture when a session produces a durable insight.
4
+ ---
5
+
6
+ # Memory Eternal
7
+
8
+ This agent has access to `memory_recall` (retrieve cards) and `memory_capture` (store a card) via the `memory` MCP server.
9
+
10
+ ## When to recall
11
+ Before answering when the user's question may depend on prior project decisions, past bugs, or preferences. Combine with `memory_stats` to gauge coverage.
12
+
13
+ ## When to capture
14
+ At the end of a session that produced a durable, reusable conclusion (a fix, a decision, an architecture rationale, a constraint). Capture concise findings rather than raw transcripts; the hook also auto-captures at session end into the same vault (pending → audit). Source attribution is automatic.