@a9i5k4/dsh-auto-memory 0.1.9 → 0.1.10

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/README.md CHANGED
@@ -204,6 +204,19 @@ Restart **dsh web** to activate (the 「Memory」entry appears in the sidebar).
204
204
 
205
205
  > No pnpm? Use npm: `npm install @a9i5k4/dsh-auto-memory`
206
206
 
207
+ ## Updating (check & upgrade)
208
+
209
+ The plugin is a normal npm package, so updating is one command in the same profile directory:
210
+
211
+ ```bash
212
+ cd ~/.dsh/profiles/web
213
+ pnpm up @a9i5k4/dsh-auto-memory # or: npm install @a9i5k4/dsh-auto-memory@latest
214
+ ```
215
+
216
+ Then restart **dsh web**.
217
+
218
+ The Settings → Auto Memory page has a **Check for updates** button that compares your installed version with the latest one on the npm registry (it shows the update command when a newer version exists).
219
+
207
220
  ---
208
221
 
209
222
  ## AI-Era Installation (copy-paste for your AI)
package/README.zh-CN.md CHANGED
@@ -202,6 +202,19 @@ pnpm add @a9i5k4/dsh-auto-memory
202
202
 
203
203
  > 没有 pnpm?用 npm 也行:`npm install @a9i5k4/dsh-auto-memory`
204
204
 
205
+ ## 更新(检查与升级)
206
+
207
+ 插件就是普通的 npm 包,更新同样是在 profile 目录里一条命令:
208
+
209
+ ```bash
210
+ cd ~/.dsh/profiles/web
211
+ pnpm up @a9i5k4/dsh-auto-memory # 或: npm install @a9i5k4/dsh-auto-memory@latest
212
+ ```
213
+
214
+ 然后**重启 dsh web** 生效。
215
+
216
+ 设置 → 自动记忆 页面有「检查更新」按钮,会拿你当前安装的版本和 npm registry 上的最新版对比(有新版时直接显示更新命令)。
217
+
205
218
  ---
206
219
 
207
220
  ## AI 时代安装(把这句话直接丢给 AI)
package/lib/client.js CHANGED
@@ -105,6 +105,8 @@ window.__ModuleLoader__.load({
105
105
  fBrowse: '浏览…', browseTitle: '选择记忆根目录', fUp: '上级', fSelectDir: '选择此目录',
106
106
  pickedDir: '已选择目录:', rememberSave: '(点击「保存设置」后生效)', pickerUnavailable: '系统文件夹选择器不可用,改用内嵌浏览。',
107
107
  fDayBoundary: '日界(分钟)', fDayBoundaryHint: '从 0 点起算:凌晨在此之前的活儿归前一天。默认 450=早上 7:30 进入新一天;改 480=8:00;0=按午夜切日。',
108
+ fVersion: '插件版本', checkUpdate: '检查更新', checking: '检查中…', upToDate: '(已是最新)', hasUpdate: '(有新版本)', versionError: '版本检查失败: ', versionCmdHint: '更新命令: cd ~/.dsh/profiles/web && pnpm up @a9i5k4/dsh-auto-memory,然后重启 dsh web 生效。',
109
+ updateNow: '一键更新', updating: '更新中…', updateDone: '更新完成,请重启 dsh web 生效。', updateFailed: '更新失败: ', devLinkHint: '(当前为本地开发链接,请直接同步开发源码)', noProfileHint: '(未找到 dsh profile,无法自动更新)',
108
110
  refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置',
109
111
  generated: '已生成', failed: '失败: ',
110
112
  generatedAt: '生成于 ',
@@ -178,6 +180,8 @@ window.__ModuleLoader__.load({
178
180
  fBrowse: 'Browse…', browseTitle: 'Choose memory root', fUp: 'Up', fSelectDir: 'Select this folder',
179
181
  pickedDir: 'Selected folder:', rememberSave: '(click Save to apply)', pickerUnavailable: 'Native folder picker unavailable, falling back to in-app browser.',
180
182
  fDayBoundary: 'Day boundary (minutes)', fDayBoundaryHint: 'Minutes from midnight: work before it counts as the previous day. Default 450 = 7:30 AM starts the new day; 480 = 8:00; 0 = midnight cut.',
183
+ fVersion: 'Plugin version', checkUpdate: 'Check for updates', checking: 'Checking…', upToDate: '(up to date)', hasUpdate: '(update available)', versionError: 'Version check failed: ', versionCmdHint: 'Update: cd ~/.dsh/profiles/web && pnpm up @a9i5k4/dsh-auto-memory, then restart dsh web.',
184
+ updateNow: 'Update now', updating: 'Updating…', updateDone: 'Update finished, restart dsh web to apply.', updateFailed: 'Update failed: ', devLinkHint: '(local dev link, sync source instead)', noProfileHint: '(no dsh profile found, cannot auto-update)',
181
185
  refresh: 'Refresh', refreshing: 'Refreshing…', close: 'Close', dragMove: 'Drag to move', dragResize: 'Drag to resize', resetPos: 'Reset position',
182
186
  generated: 'Generated', failed: 'Failed: ',
183
187
  generatedAt: 'Generated ',
@@ -272,6 +276,8 @@ window.__ModuleLoader__.load({
272
276
  debug: '/api/dsh-auto-memory/debug',
273
277
  browseDir: '/api/dsh-auto-memory/browse-dir',
274
278
  pickDir: '/api/dsh-auto-memory/pick-dir',
279
+ updateCheck: '/api/dsh-auto-memory/update-check',
280
+ update: '/api/dsh-auto-memory/update',
275
281
  config: '/api/dsh-auto-memory/config',
276
282
  reflect: '/api/dsh-auto-memory/reflect',
277
283
  reflectAuto: '/api/dsh-auto-memory/reflect-auto',
@@ -1244,10 +1250,39 @@ window.__ModuleLoader__.load({
1244
1250
  useEffect(function () {
1245
1251
  var alive = true
1246
1252
  apiGet(API.config).then(function (d) { if (alive) setCfg(d.config) }).catch(function (e) { setErr(e.message) })
1253
+ // 打开设置页自动检查更新(host 有 12h 缓存,不重复查网)
1254
+ apiGet(API.updateCheck).then(function (d) { if (alive) setVerInfo(d) }).catch(function () {})
1247
1255
  return function () { alive = false }
1248
1256
  }, [])
1249
1257
  if (!cfg) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
1250
1258
  function set(key, value) { var next = Object.assign({}, cfg); next[key] = value; setCfg(next) }
1259
+ var verPair = useState(null)
1260
+ var verInfo = verPair[0]
1261
+ var setVerInfo = verPair[1]
1262
+ var checkingPair = useState(false)
1263
+ var checkingUpdate = checkingPair[0]
1264
+ var setCheckingUpdate = checkingPair[1]
1265
+ function checkUpdate() {
1266
+ if (checkingUpdate) return
1267
+ setCheckingUpdate(true)
1268
+ apiGet(API.updateCheck + '?force=1').then(function (d) { setVerInfo(d); setCheckingUpdate(false) })
1269
+ .catch(function (e) { setVerInfo({ error: e.message }); setCheckingUpdate(false) })
1270
+ }
1271
+ var upBusyPair = useState(false)
1272
+ var upBusy = upBusyPair[0]
1273
+ var setUpBusy = upBusyPair[1]
1274
+ var upMsgPair = useState('')
1275
+ var upMsg = upMsgPair[0]
1276
+ var setUpMsg = upMsgPair[1]
1277
+ function doUpdate() {
1278
+ if (upBusy) return
1279
+ setUpBusy(true); setUpMsg('')
1280
+ apiPost(API.update, {}).then(function (d) {
1281
+ setUpBusy(false)
1282
+ if (d && d.ok) { setUpMsg(t('updateDone')); setVerInfo(null); checkUpdate() }
1283
+ else setUpMsg(t('updateFailed') + (d && d.message ? d.message : '(未知)'))
1284
+ }).catch(function (e) { setUpBusy(false); setUpMsg(t('updateFailed') + e.message) })
1285
+ }
1251
1286
  function save() {
1252
1287
  if (busy) return
1253
1288
  setBusy(true); setMsg(''); setErr('')
@@ -1289,6 +1324,12 @@ window.__ModuleLoader__.load({
1289
1324
  field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
1290
1325
  field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
1291
1326
  STYLE_OPTIONS.map(function (o) { return h('option', { key: o[0], value: o[0] }, o[1]) })), t('fStyleHint')),
1327
+ field(t('fVersion'), h('div', { 'data-dam-row': '' },
1328
+ h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
1329
+ h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
1330
+ (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
1331
+ upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
1332
+ t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
1292
1333
  h('div', { 'data-dam-row': '' },
1293
1334
  h('button', { 'data-dam-btn': '', onClick: save, disabled: busy }, busy ? t('saving') : t('saveSettings')),
1294
1335
  msg ? h('span', null, msg) : null),
package/lib/index.js CHANGED
@@ -19,6 +19,9 @@
19
19
 
20
20
  import { readFile, writeFile, mkdir, readdir, stat, rm, copyFile } from 'node:fs/promises'
21
21
  import { createReadStream, existsSync } from 'node:fs'
22
+ import { exec as cpExec } from 'node:child_process'
23
+ import { promisify } from 'node:util'
24
+ const execP = promisify(cpExec)
22
25
  import { homedir } from 'node:os'
23
26
  import path from 'node:path'
24
27
  import { fileURLToPath } from 'node:url'
@@ -46,6 +49,8 @@ export const API = {
46
49
  debug: '/api/dsh-auto-memory/debug',
47
50
  browseDir: '/api/dsh-auto-memory/browse-dir',
48
51
  pickDir: '/api/dsh-auto-memory/pick-dir',
52
+ updateCheck: '/api/dsh-auto-memory/update-check',
53
+ update: '/api/dsh-auto-memory/update',
49
54
  config: '/api/dsh-auto-memory/config',
50
55
  reflect: '/api/dsh-auto-memory/reflect',
51
56
  'reflect-auto': '/api/dsh-auto-memory/reflect-auto',
@@ -275,6 +280,73 @@ class MemoryEngine {
275
280
  }
276
281
  }
277
282
 
283
+ // ---------- 更新检查(registry 自动检查 + profile 探测) ----------
284
+ /** 找 dsh web profile 目录(含 dsh-auto-memory 依赖/bundle 的那个)。 */
285
+ async findProfileDir() {
286
+ try {
287
+ const base = path.join(dshHome(), 'profiles')
288
+ const dirs = await readdir(base, { withFileTypes: true })
289
+ for (const d of dirs) {
290
+ if (!d.isDirectory()) continue
291
+ const pkgFile = path.join(base, d.name, 'package.json')
292
+ const raw = await this.readTextSafe(pkgFile)
293
+ if (!raw) continue
294
+ let pkg
295
+ try { pkg = JSON.parse(raw) } catch (e) { continue }
296
+ const deps = (pkg && pkg.dependencies) || {}
297
+ const bundles = (pkg && pkg.dsh && pkg.dsh.profile && pkg.dsh.profile.bundles) || []
298
+ const key = Object.keys(deps).find((k) => k.includes('dsh-auto-memory'))
299
+ const isLink = !!(key && typeof deps[key] === 'string' && deps[key].startsWith('link:'))
300
+ const isReg = !!deps['@a9i5k4/dsh-auto-memory'] || bundles.indexOf('@a9i5k4/dsh-auto-memory') >= 0
301
+ if (key || bundles.some((b) => b.includes('dsh-auto-memory'))) {
302
+ return {
303
+ dir: path.join(base, d.name),
304
+ installKind: isLink ? 'dev-link' : (isReg ? 'registry' : 'unknown'),
305
+ usesPnpm: existsSync(path.join(base, d.name, 'pnpm-lock.yaml')),
306
+ }
307
+ }
308
+ }
309
+ } catch (e) {}
310
+ return null
311
+ }
312
+
313
+ /** 对比本地版本与 npm registry 最新版;结果缓存 12 小时(启动/设置页打开都走缓存,不重复查网)。 */
314
+ async checkUpdate(force) {
315
+ const cacheFile = path.join(dshHome(), 'memory', 'update-check.json')
316
+ const base = { current: '', installKind: 'unknown' }
317
+ try {
318
+ const indexPath = fileURLToPath(import.meta.url)
319
+ const pkgPath = path.join(path.dirname(indexPath), '..', 'package.json')
320
+ const raw = await this.readTextSafe(pkgPath)
321
+ if (raw) { try { base.current = JSON.parse(raw).version || '' } catch (e) {} }
322
+ } catch (e) {}
323
+ const prof = await this.findProfileDir()
324
+ if (prof) base.installKind = prof.installKind
325
+ if (!force) {
326
+ try {
327
+ const cached = await this.readTextSafe(cacheFile)
328
+ if (cached) {
329
+ const j = JSON.parse(cached)
330
+ if (j && j.checkedAt && Date.now() - j.checkedAt < 12 * 3600 * 1000) return Object.assign({}, j, { fromCache: true })
331
+ }
332
+ } catch (e) {}
333
+ }
334
+ const result = Object.assign({}, base, { checkedAt: Date.now(), fromCache: false, latest: '', upToDate: false, error: '' })
335
+ try {
336
+ const rr = await fetch('https://registry.npmjs.org/@a9i5k4%2Fdsh-auto-memory', { signal: AbortSignal.timeout(8000) })
337
+ if (rr.ok) {
338
+ const j = await rr.json()
339
+ result.latest = (j && j['dist-tags'] && j['dist-tags'].latest) || ''
340
+ result.upToDate = !!(result.current && result.latest && result.current === result.latest)
341
+ } else result.error = 'registry HTTP ' + rr.status
342
+ } catch (e) { result.error = String(e && e.message ? e.message : e) }
343
+ try {
344
+ await mkdir(path.dirname(cacheFile), { recursive: true })
345
+ await writeFile(cacheFile, JSON.stringify(result, null, 2), 'utf8')
346
+ } catch (e) {}
347
+ return result
348
+ }
349
+
278
350
  // ---------- 读取 ----------
279
351
  async readTextSafe(p) {
280
352
  if (!p) return ''
@@ -2052,6 +2124,8 @@ export function apply(ctx, config) {
2052
2124
  // 生命周期刷新
2053
2125
  const refreshAll = (agent) => { void engine.refresh(agent) }
2054
2126
  refreshAll()
2127
+ // 自动检查更新:host 启动时查一次 npm registry(结果缓存 12 小时,设置页打开直接读缓存显示)
2128
+ void engine.checkUpdate(false)
2055
2129
  ctx.on('agent/session-start', (payload) => {
2056
2130
  refreshAll(payload && payload.agent)
2057
2131
  // 缓存最近 agent 引用(subagent parent 需要完整 agent 对象)
@@ -2390,6 +2464,45 @@ export function apply(ctx, config) {
2390
2464
  } catch (e) { writeJson(res, 200, { native: true, dir: null, error: String(e && e.message ? e.message : e) }) }
2391
2465
  },
2392
2466
  },
2467
+ {
2468
+ kind: 'exact',
2469
+ path: API.updateCheck,
2470
+ handler: async (req, res) => {
2471
+ if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
2472
+ if ((req.method || 'GET') !== 'GET') return writeJson(res, 405, { error: 'method not allowed' })
2473
+ try {
2474
+ const url = new URL(req.url || '/', 'http://localhost')
2475
+ const force = url.searchParams.get('force') === '1'
2476
+ writeJson(res, 200, await engine.checkUpdate(force))
2477
+ } catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
2478
+ },
2479
+ },
2480
+ {
2481
+ kind: 'exact',
2482
+ path: API.update,
2483
+ handler: async (req, res) => {
2484
+ if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
2485
+ if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
2486
+ // 一键更新:在 profile 目录执行 pnpm up / npm install,完成后提示重启
2487
+ try {
2488
+ const prof = await engine.findProfileDir()
2489
+ if (!prof) return writeJson(res, 200, { ok: false, message: '未找到 dsh profile 目录(未安装插件或已删除)。' })
2490
+ if (prof.installKind === 'dev-link') return writeJson(res, 200, { ok: false, message: '当前为本地开发链接(link:)安装,不适用 npm 更新,请直接同步开发源码。' })
2491
+ if (prof.installKind !== 'registry') return writeJson(res, 200, { ok: false, message: '未检测到 registry 安装方式,无法自动更新。' })
2492
+ const cmd = prof.usesPnpm ? 'pnpm up @a9i5k4/dsh-auto-memory' : 'npm install @a9i5k4/dsh-auto-memory@latest'
2493
+ const out = await execP(cmd, { cwd: prof.dir, timeout: 120000, windowsHide: true })
2494
+ const tail = ((out.stdout || '') + (out.stderr || '')).trim().slice(-1200)
2495
+ writeJson(res, 200, {
2496
+ ok: true,
2497
+ output: tail || '(无输出)',
2498
+ restartHint: '更新完成,请重启 dsh web 生效。',
2499
+ installKind: prof.installKind,
2500
+ })
2501
+ } catch (e) {
2502
+ writeJson(res, 200, { ok: false, message: String(e && e.message ? e.message : e) })
2503
+ }
2504
+ },
2505
+ },
2393
2506
  {
2394
2507
  kind: 'exact',
2395
2508
  path: API.config,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-auto-memory",
3
3
  "description": "DSH 自动记忆插件:三层记忆(用户级/项目笔记/每日日志)自动注入与检索、每轮对话自动沉淀、每日反思、可视化面板与设置页,支持继承其他 AI 工具的记忆。",
4
- "version": "0.1.9",
4
+ "version": "0.1.10",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {