diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b398497de..27b330aa9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,8 +20,8 @@ jobs: close-pr-message: 'This PR has been automatically closed due to inactivity.' days-before-stale: 30 # Days of inactivity before marking as stale days-before-close: 7 # Days of inactivity before closing stale issues/PRs - stale-issue-label: 'stale' - stale-pr-label: 'stale' - exempt-issue-labels: 'do not close' - exempt-pr-labels: 'do not close' + stale-issue-label: 'status:stale' + stale-pr-label: 'status:stale' + exempt-issue-labels: 'status:do not close' + exempt-pr-labels: 'status:do not close' remove-stale-when-updated: true diff --git a/apps/memos-local-plugin/adapters/hermes/install.hermes.sh b/apps/memos-local-plugin/adapters/hermes/install.hermes.sh index cfec47682..b1c6d8789 100755 --- a/apps/memos-local-plugin/adapters/hermes/install.hermes.sh +++ b/apps/memos-local-plugin/adapters/hermes/install.hermes.sh @@ -48,16 +48,38 @@ else fi # ── 3. wire up Python provider ──────────────────────────────────────────────── -# Hermes discovers providers from $PREFIX. Symlinking the Python package -# to a discoverable path (if HERMES_PLUGINS_DIR is set) avoids asking the -# user to edit PYTHONPATH manually. +# Hermes source-tree upgrades can replace the checkout-local +# plugins/memory directory, so always maintain a user-level link too. +: "${HOME:?HOME must be set for user-level plugin directory creation}" +USER_HERMES_PLUGINS_DIR="${HOME}/.hermes/plugins/memory" +mkdir -p "$USER_HERMES_PLUGINS_DIR" +USER_TARGET="$USER_HERMES_PLUGINS_DIR/memtensor" +# Clean up a pre-existing non-symlink entry (file or dir) left over from +# a previous manual install so `ln -sfn` doesn't refuse or misbehave. +if [[ -L "$USER_TARGET" ]]; then rm "$USER_TARGET" +elif [[ -e "$USER_TARGET" ]]; then rm -rf "$USER_TARGET" +fi +ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$USER_TARGET" +log "Linked Python provider → $USER_TARGET" + if [[ -n "${HERMES_PLUGINS_DIR:-}" ]]; then mkdir -p "$HERMES_PLUGINS_DIR" - ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$HERMES_PLUGINS_DIR/memos_provider" - log "Linked Python provider → $HERMES_PLUGINS_DIR/memos_provider" + HERMES_TARGET="$HERMES_PLUGINS_DIR/memtensor" + # Remove the legacy 'memos_provider' symlink name if it lingers from an + # earlier installer version, so re-running the script doesn't leave a + # dangling reference behind. + LEGACY_TARGET="$HERMES_PLUGINS_DIR/memos_provider" + if [[ -L "$LEGACY_TARGET" ]]; then + rm "$LEGACY_TARGET" + log "Removed legacy symlink $LEGACY_TARGET" + fi + if [[ -L "$HERMES_TARGET" ]]; then rm "$HERMES_TARGET" + elif [[ -e "$HERMES_TARGET" ]]; then rm -rf "$HERMES_TARGET" + fi + ln -sfn "$PREFIX/adapters/hermes/memos_provider" "$HERMES_TARGET" + log "Linked Python provider → $HERMES_TARGET" else - log "HERMES_PLUGINS_DIR not set; skipping Python provider symlink. You can manually link:" - log " ln -s \"$PREFIX/adapters/hermes/memos_provider\" /memos_provider" + log "HERMES_PLUGINS_DIR not set; user-level provider link was created." fi log "Hermes adapter install complete." diff --git a/apps/memos-local-plugin/install.ps1 b/apps/memos-local-plugin/install.ps1 index 0290ee47b..098b7b4e7 100644 --- a/apps/memos-local-plugin/install.ps1 +++ b/apps/memos-local-plugin/install.ps1 @@ -436,13 +436,27 @@ function Install-Hermes { } if (-not $PluginDir -or -not (Test-Path $PluginDir)) { Stop-Die "plugins\memory not found" } - - $Target = Join-Path $PluginDir "memtensor" - if (Test-Path $Target) { Remove-Item -Recurse -Force $Target } - - New-Item -ItemType Junction -Path $Target -Value (Join-Path $AdapterDir "memos_provider") | Out-Null + + $UserPluginDir = Join-Path $env:LOCALAPPDATA "hermes\plugins\memory" + New-Item -ItemType Directory -Path $UserPluginDir -Force | Out-Null + Write-Host "Ensuring user plugin dir: $UserPluginDir" + $ProviderTargets = @( + (Join-Path $PluginDir "memtensor"), + (Join-Path $UserPluginDir "memtensor") + ) + foreach ($Target in $ProviderTargets) { + if (Test-Path $Target) { Remove-Item -Recurse -Force $Target } + try { + New-Item -ItemType Junction -Path $Target -Value (Join-Path $AdapterDir "memos_provider") -ErrorAction Stop | Out-Null + Write-Success "Linked -> $Target" + } catch { + Write-Warning "Failed to create junction at $Target : $_" + } + } Copy-Item -Path (Join-Path $AdapterDir "plugin.yaml") -Destination (Join-Path $AdapterDir "memos_provider\plugin.yaml") -ErrorAction SilentlyContinue - Write-Success "Linked -> $Target" + if (-not (Test-Path (Join-Path $AdapterDir "memos_provider\plugin.yaml"))) { + Write-Warning "plugin.yaml copy may have failed; verify $AdapterDir\memos_provider\plugin.yaml exists." + } if (Test-Path $ConfigFile) { $PyScript = @" diff --git a/apps/memos-local-plugin/install.sh b/apps/memos-local-plugin/install.sh index 989f0ea24..20135e3f7 100755 --- a/apps/memos-local-plugin/install.sh +++ b/apps/memos-local-plugin/install.sh @@ -732,13 +732,22 @@ except Exception: success "plugins/memory: ${plugin_dir}" step "Linking memtensor provider" - local target="${plugin_dir}/memtensor" - if [[ -L "${target}" ]]; then rm "${target}" - elif [[ -e "${target}" ]]; then rm -rf "${target}" - fi - ln -s "${adapter_dir}/memos_provider" "${target}" + local user_plugin_dir="${HOME}/.hermes/plugins/memory" + mkdir -p "${user_plugin_dir}" + # Ensure the provider directory is fully populated before symlinking so + # the second symlink (user-level) already points at a complete tree. cp "${adapter_dir}/plugin.yaml" "${adapter_dir}/memos_provider/plugin.yaml" 2>/dev/null || true - success "Symlinked → ${target}" + local provider_targets=( + "${plugin_dir}/memtensor" + "${user_plugin_dir}/memtensor" + ) + local target + for target in "${provider_targets[@]}"; do + # Use `ln -sfn` for atomic, idempotent replace; matches install.hermes.sh. + if [[ -e "${target}" && ! -L "${target}" ]]; then rm -rf "${target}"; fi + ln -sfn "${adapter_dir}/memos_provider" "${target}" + success "Symlinked → ${target}" + done step "Verifying provider & patching config" local verify diff --git a/apps/memos-local-plugin/tests/unit/install/hermes-provider-link.test.ts b/apps/memos-local-plugin/tests/unit/install/hermes-provider-link.test.ts new file mode 100644 index 000000000..581304d85 --- /dev/null +++ b/apps/memos-local-plugin/tests/unit/install/hermes-provider-link.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const repoRoot = path.resolve(__dirname, "../../.."); + +describe("Hermes provider install links", () => { + it("main Unix installer links both checkout-local and user-level provider paths", () => { + const source = readFileSync(path.join(repoRoot, "install.sh"), "utf8"); + + expect(source).toContain('${HOME}/.hermes/plugins/memory'); + expect(source).toContain('"${plugin_dir}/memtensor"'); + expect(source).toContain('"${user_plugin_dir}/memtensor"'); + }); + + it("adapter Unix installer keeps a user-level provider link", () => { + const source = readFileSync( + path.join(repoRoot, "adapters/hermes/install.hermes.sh"), + "utf8", + ); + + expect(source).toContain('USER_HERMES_PLUGINS_DIR="${HOME}/.hermes/plugins/memory"'); + expect(source).toContain('$USER_HERMES_PLUGINS_DIR/memtensor'); + }); + + it("PowerShell installer links both checkout-local and user-level provider paths", () => { + const source = readFileSync(path.join(repoRoot, "install.ps1"), "utf8"); + + expect(source).toContain('hermes\\plugins\\memory'); + expect(source).toContain('(Join-Path $PluginDir "memtensor")'); + expect(source).toContain('(Join-Path $UserPluginDir "memtensor")'); + }); + + it("PowerShell installer surfaces junction failures instead of swallowing them", () => { + const source = readFileSync(path.join(repoRoot, "install.ps1"), "utf8"); + + // New-Item -ItemType Junction now runs inside try/catch with -ErrorAction Stop + expect(source).toContain("-ErrorAction Stop"); + expect(source).toContain("Failed to create junction at $Target"); + }); + + it("Unix adapter installer guards HOME and cleans stale symlink targets", () => { + const source = readFileSync( + path.join(repoRoot, "adapters/hermes/install.hermes.sh"), + "utf8", + ); + + expect(source).toContain('${HOME:?HOME must be set'); + expect(source).toContain('if [[ -L "$USER_TARGET" ]]; then rm "$USER_TARGET"'); + expect(source).toContain('LEGACY_TARGET="$HERMES_PLUGINS_DIR/memos_provider"'); + }); + + it("main Unix installer uses atomic ln -sfn and prepares provider dir first", () => { + const source = readFileSync(path.join(repoRoot, "install.sh"), "utf8"); + + // cp runs BEFORE the loop now so the provider dir is populated before + // the second symlink is created. + const cpPos = source.indexOf('cp "${adapter_dir}/plugin.yaml"'); + const loopPos = source.indexOf("provider_targets=("); + expect(cpPos).toBeGreaterThan(0); + expect(loopPos).toBeGreaterThan(cpPos); + expect(source).toContain('ln -sfn "${adapter_dir}/memos_provider" "${target}"'); + }); +}); diff --git a/docs/cn/open_source/modules/dream.md b/docs/cn/open_source/modules/dream.md index a83beab42..55cd9e2cf 100644 --- a/docs/cn/open_source/modules/dream.md +++ b/docs/cn/open_source/modules/dream.md @@ -79,7 +79,7 @@ Dream 模拟这一点:从*未完成的内在动机*出发,而不是从原始 ```json { "motive_id": "motive:dream_memory_strategy_alignment", - "description": "Several conversations failed for the same hidden reason: weekly reporting, future planning, and filter design were treated as separate tasks, while the user needed a shared strategic narrative.", + "description": "几次对话失败,背后是同一个隐藏原因:周报、未来规划和 filter 设计被当成三个独立任务,而用户需要的是一条统一的战略叙事。", "memory_ids": ["weekly_report_thread", "future_planning_thread", "filter_design_thread"] } ```