@modusensus/dsh-mneme 0.13.0 → 0.14.0-beta.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.
- package/LICENSE +21 -21
- package/README.md +830 -828
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -254
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +407 -402
- package/docs/CONTINUITY.md +139 -139
- package/docs/DAEMON.md +57 -57
- package/docs/DIRECTORIES.md +114 -114
- package/docs/DISTILL.md +397 -371
- package/docs/DSH-GO.md +97 -97
- package/docs/ENTITIES.md +247 -247
- package/docs/FORGET.md +77 -77
- package/docs/KB.md +110 -110
- package/docs/LOCAL_MODEL.md +222 -222
- package/docs/SEMANTIC.md +256 -256
- package/docs/SHARE.md +552 -552
- package/docs/SLEEP.md +202 -202
- package/docs/STORAGE.md +122 -122
- package/docs/WORKFLOWS.md +366 -366
- package/lib/api-standalone.js +1007 -1007
- package/lib/api.js +3600 -3394
- package/lib/attachment-text.js +122 -122
- package/lib/bootstrap-http.js +105 -105
- package/lib/bootstrap.js +215 -215
- package/lib/candidates.js +454 -454
- package/lib/capabilities.js +819 -819
- package/lib/choro-command.js +111 -111
- package/lib/client.js +14420 -13436
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1317
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +258 -258
- package/lib/date-hint.js +32 -0
- package/lib/diary/auto.js +190 -190
- package/lib/diary/compose.js +440 -440
- package/lib/diary/dir.js +59 -59
- package/lib/diary/export.js +97 -97
- package/lib/diary/index.js +26 -26
- package/lib/diary/library.js +189 -189
- package/lib/diary/qq-inbox.js +142 -142
- package/lib/diary/rollover.js +61 -61
- package/lib/diary/session-provider.js +122 -122
- package/lib/document-index.js +94 -94
- package/lib/document.js +322 -322
- package/lib/dream/clustering.js +123 -123
- package/lib/dream/decisions.js +945 -907
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -1887
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +1879 -1764
- package/lib/dsh-go/host.js +162 -162
- package/lib/dsh-go/panel.js +2091 -2091
- package/lib/dsh-go/text.js +342 -342
- package/lib/dsh-go/views/common.js +68 -68
- package/lib/dsh-go/views/entity.js +48 -48
- package/lib/dsh-go/views/memory.js +198 -198
- package/lib/dsh-go/views/root.js +30 -30
- package/lib/dsh-go/views/settings.js +398 -398
- package/lib/dsh-go/views/status.js +51 -51
- package/lib/embedding.js +342 -342
- package/lib/entities/extractor.js +285 -285
- package/lib/fence.js +24 -24
- package/lib/graph/anchoring.js +184 -184
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +119 -107
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -822
- package/lib/inject.js +656 -639
- package/lib/kb/index.js +309 -309
- package/lib/kb/routes.js +219 -219
- package/lib/kb/tools.js +148 -148
- package/lib/lang.js +607 -584
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -162
- package/lib/memory-source.js +9 -9
- package/lib/mirror.js +330 -330
- package/lib/organize.js +456 -456
- package/lib/peak-hours.js +172 -172
- package/lib/pipeline-activities.js +178 -178
- package/lib/pipeline-attribution.js +165 -165
- package/lib/pipeline-command.js +279 -279
- package/lib/pipeline-confirm.js +142 -142
- package/lib/pipeline-exec.js +174 -174
- package/lib/pipeline-nudge-auto.js +129 -129
- package/lib/pipeline-nudge.js +1623 -1623
- package/lib/pipeline-observations.js +181 -181
- package/lib/pipeline-observe.js +133 -133
- package/lib/pipeline-runner.js +361 -361
- package/lib/pipeline-runs.js +442 -442
- package/lib/pipeline.js +716 -716
- package/lib/placement.js +13 -13
- package/lib/pre-inject-gate.js +354 -354
- package/lib/profile-extract.js +164 -164
- package/lib/profile-facts.js +356 -356
- package/lib/provenance-http.js +251 -251
- package/lib/quality-filter.js +162 -162
- package/lib/recall-stats.js +190 -190
- package/lib/reclaim-http.js +150 -150
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -223
- package/lib/safe-slice.js +18 -18
- package/lib/scope-state.js +277 -277
- package/lib/scope.js +187 -187
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +572 -572
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +228 -228
- package/lib/service.js +3268 -3196
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -802
- package/lib/share-http.js +281 -281
- package/lib/share-import.js +234 -234
- package/lib/share.js +335 -335
- package/lib/store.js +5895 -5662
- package/lib/summarize.js +1388 -1343
- package/lib/tools.js +1370 -1356
- package/lib/user-settings.js +230 -230
- package/lib/vector-index.js +180 -180
- package/lib/version-check.js +122 -122
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/cli-hint.js +90 -90
- package/lib/wiki/inject.js +164 -164
- package/lib/wiki/partition.js +200 -200
- package/lib/wiki/plan.js +214 -214
- package/lib/wiki/queries.js +640 -474
- package/lib/wiki/render.js +261 -187
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -463
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -502
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +15 -1
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +91 -91
- package/scripts/check-sync.js +44 -44
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/publish-gate.mjs +197 -197
- package/scripts/release-docs.mjs +210 -204
- package/scripts/release-notes.mjs +164 -161
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +48 -48
- package/src/api-standalone.js +1007 -1007
- package/src/api.js +3600 -3394
- package/src/attachment-text.js +122 -122
- package/src/bootstrap-http.js +105 -105
- package/src/bootstrap.js +215 -215
- package/src/candidates.js +454 -454
- package/src/capabilities.js +819 -819
- package/src/choro-command.js +111 -111
- package/src/commands.js +95 -95
- package/src/config-keys.js +144 -144
- package/src/config.js +1361 -1317
- package/src/content-hash.js +54 -54
- package/src/continuity.js +258 -258
- package/src/date-hint.js +32 -0
- package/src/diary/auto.js +190 -190
- package/src/diary/compose.js +440 -440
- package/src/diary/dir.js +59 -59
- package/src/diary/export.js +97 -97
- package/src/diary/index.js +26 -26
- package/src/diary/library.js +189 -189
- package/src/diary/qq-inbox.js +142 -142
- package/src/diary/rollover.js +61 -61
- package/src/diary/session-provider.js +122 -122
- package/src/document-index.js +94 -94
- package/src/document.js +322 -322
- package/src/dream/clustering.js +123 -123
- package/src/dream/decisions.js +945 -907
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -1887
- package/src/dream/triage.js +139 -0
- package/src/dream.js +1879 -1764
- package/src/dsh-go/host.js +162 -162
- package/src/dsh-go/panel.js +2091 -2091
- package/src/dsh-go/text.js +342 -342
- package/src/dsh-go/views/common.js +68 -68
- package/src/dsh-go/views/entity.js +48 -48
- package/src/dsh-go/views/memory.js +198 -198
- package/src/dsh-go/views/root.js +30 -30
- package/src/dsh-go/views/settings.js +398 -398
- package/src/dsh-go/views/status.js +51 -51
- package/src/embedding.js +342 -342
- package/src/entities/extractor.js +285 -285
- package/src/fence.js +24 -24
- package/src/graph/anchoring.js +184 -184
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +119 -107
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -822
- package/src/inject.js +656 -639
- package/src/kb/index.js +309 -309
- package/src/kb/routes.js +219 -219
- package/src/kb/tools.js +148 -148
- package/src/lang.js +607 -584
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -162
- package/src/memory-source.js +9 -9
- package/src/mirror.js +330 -330
- package/src/organize.js +456 -456
- package/src/peak-hours.js +172 -172
- package/src/pipeline-activities.js +178 -178
- package/src/pipeline-attribution.js +165 -165
- package/src/pipeline-command.js +279 -279
- package/src/pipeline-confirm.js +142 -142
- package/src/pipeline-exec.js +174 -174
- package/src/pipeline-nudge-auto.js +129 -129
- package/src/pipeline-nudge.js +1623 -1623
- package/src/pipeline-observations.js +181 -181
- package/src/pipeline-observe.js +133 -133
- package/src/pipeline-runner.js +361 -361
- package/src/pipeline-runs.js +442 -442
- package/src/pipeline.js +716 -716
- package/src/placement.js +13 -13
- package/src/pre-inject-gate.js +354 -354
- package/src/profile-extract.js +164 -164
- package/src/profile-facts.js +356 -356
- package/src/provenance-http.js +251 -251
- package/src/quality-filter.js +162 -162
- package/src/recall-stats.js +190 -190
- package/src/reclaim-http.js +150 -150
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -223
- package/src/safe-slice.js +18 -18
- package/src/scope-state.js +277 -277
- package/src/scope.js +187 -187
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +572 -572
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +228 -228
- package/src/service.js +3268 -3196
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -802
- package/src/share-http.js +281 -281
- package/src/share-import.js +234 -234
- package/src/share.js +335 -335
- package/src/store.js +5895 -5662
- package/src/summarize.js +1388 -1343
- package/src/tools.js +1370 -1356
- package/src/user-settings.js +230 -230
- package/src/vector-index.js +180 -180
- package/src/version-check.js +122 -122
- package/src/wiki/claims.js +267 -0
- package/src/wiki/cli-hint.js +90 -90
- package/src/wiki/inject.js +164 -164
- package/src/wiki/partition.js +200 -200
- package/src/wiki/plan.js +214 -214
- package/src/wiki/queries.js +640 -474
- package/src/wiki/render.js +261 -187
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -463
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -502
- package/src/write-admission.js +404 -404
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-hardening.test.js +173 -173
- package/test/api-nudge-route.test.js +259 -259
- package/test/api-routes-gaps.test.js +1092 -1092
- package/test/api.test.js +3007 -2988
- package/test/attachment-text.test.js +139 -139
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bm25-corpus-cache.test.js +145 -145
- package/test/bootstrap.test.js +228 -228
- package/test/candidates.test.js +345 -345
- package/test/capabilities.test.js +577 -577
- package/test/capacity-warning.test.js +262 -262
- package/test/changelog-prep.test.js +125 -125
- package/test/choro-command.test.js +218 -218
- package/test/cli-enums.test.js +37 -37
- package/test/cli-flag-value.test.js +86 -86
- package/test/cli-receive.test.js +390 -390
- package/test/cli-share.test.js +437 -437
- package/test/client-share.test.js +225 -225
- package/test/client.test.js +4595 -4184
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config-keys-routes.test.js +233 -233
- package/test/config.test.js +153 -153
- package/test/conflict-freeze.test.js +344 -344
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/conflict-reviewed.test.js +79 -79
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +452 -452
- package/test/contrast.test.js +432 -320
- package/test/date-hint.test.js +156 -0
- package/test/dedupe-candidate-window.test.js +180 -180
- package/test/dependency-overrides.test.js +78 -78
- package/test/diary-library.test.js +187 -187
- package/test/diary.test.js +576 -576
- package/test/differentiate-feedback.test.js +248 -248
- package/test/distill-tx-embed-gap.test.js +110 -110
- package/test/docs-config-keys.test.js +65 -65
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +703 -703
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-decision-evidence.test.js +314 -314
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-fence.test.js +207 -207
- package/test/dream-maxtokens-fallback.test.js +132 -132
- package/test/dream-merge-guardrail.test.js +219 -219
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1528 -1528
- package/test/dsh-go-host.test.js +134 -134
- package/test/dsh-go-panel-actions.test.js +854 -854
- package/test/dsh-go-panel.test.js +251 -251
- package/test/embed-ready-poll.test.js +94 -94
- package/test/embed-text-reasons.test.js +112 -112
- package/test/embedding-cache-f32.test.js +276 -276
- package/test/embedding-ready.test.js +226 -226
- package/test/entities.test.js +711 -711
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/failure-query.test.js +147 -147
- package/test/failure-retention.test.js +105 -105
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/forget-corrections.test.js +70 -70
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +333 -209
- package/test/graph-boosted-relations.test.js +189 -189
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-multipath.test.js +269 -269
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +86 -86
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +312 -312
- package/test/hot-memory.test.js +259 -211
- package/test/image-description.test.js +271 -0
- package/test/image-memory.test.js +170 -170
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +134 -106
- package/test/inject-pin-explicit.test.js +300 -300
- package/test/inject-pin-vector-crowd.test.js +225 -225
- package/test/inject-pools.test.js +195 -195
- package/test/inject-recall.test.js +172 -172
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +553 -494
- package/test/issue-617-step1-query-and-fusion.test.js +361 -361
- package/test/kb-api-wiring.test.js +128 -128
- package/test/kb-collections.test.js +272 -272
- package/test/kb-routes.test.js +379 -379
- package/test/lang.test.js +98 -98
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +116 -116
- package/test/llm-audit.test.js +815 -815
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -501
- package/test/mcp-stdio.test.js +385 -385
- package/test/mint-source.test.js +311 -311
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize-tool.test.js +232 -232
- package/test/organize.test.js +418 -418
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/peak-hours-observability.test.js +70 -70
- package/test/peer-blockers.test.js +193 -193
- package/test/persona-addressing.test.js +83 -83
- package/test/persona-api.test.js +172 -172
- package/test/persona-by-session.test.js +568 -568
- package/test/persona-flag.test.js +36 -36
- package/test/persona-inject.test.js +145 -145
- package/test/persona-settings.test.js +198 -198
- package/test/pipeline-activities.test.js +155 -155
- package/test/pipeline-attribution.test.js +175 -175
- package/test/pipeline-command.test.js +243 -243
- package/test/pipeline-confirm.test.js +148 -148
- package/test/pipeline-exec.test.js +172 -172
- package/test/pipeline-nudge-auto.test.js +248 -248
- package/test/pipeline-nudge.test.js +1278 -1278
- package/test/pipeline-observations.test.js +157 -157
- package/test/pipeline-observe.test.js +127 -127
- package/test/pipeline-r1r2.test.js +278 -278
- package/test/pipeline-runner.test.js +468 -468
- package/test/pipeline-runs.test.js +767 -767
- package/test/pipeline.test.js +636 -636
- package/test/placement.test.js +36 -36
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -444
- package/test/profile-extract.test.js +269 -269
- package/test/profile-facts.test.js +492 -492
- package/test/prompt-stance.test.js +128 -128
- package/test/provenance-http.test.js +367 -367
- package/test/publish-gate.test.js +272 -272
- package/test/quality-backfill.test.js +290 -290
- package/test/quality-filter.test.js +295 -295
- package/test/readme-claims.test.js +113 -113
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-retention.test.js +128 -128
- package/test/recall-stats.test.js +274 -274
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -209
- package/test/reflection-update-gate.test.js +129 -129
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +475 -475
- package/test/reindex-scheduler.test.js +369 -369
- package/test/release-docs.test.js +156 -136
- package/test/release-notes.test.js +265 -240
- package/test/release-workflow.test.js +93 -93
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/route-policy.test.js +113 -113
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -319
- package/test/scope-candidates.test.js +491 -491
- package/test/scope-distill-stamp.test.js +134 -134
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-optional-service.test.js +110 -110
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-state.test.js +610 -610
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/security-doc.test.js +188 -178
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +251 -251
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +258 -258
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -410
- package/test/session-gate.test.js +264 -264
- package/test/session-usage.test.js +317 -317
- package/test/settings.test.js +273 -273
- package/test/share-foreign-rows.test.js +144 -144
- package/test/share-http.test.js +144 -144
- package/test/share-import.test.js +577 -577
- package/test/share-imported-guard.test.js +311 -311
- package/test/share-imported-identity.test.js +111 -111
- package/test/share-pack.test.js +511 -511
- package/test/share-receive-http.test.js +756 -756
- package/test/share-routes.test.js +337 -337
- package/test/share-store.test.js +133 -133
- package/test/share-validate.test.js +101 -101
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +441 -441
- package/test/sleep-catchup.test.js +315 -315
- package/test/sleep-failure-backoff.test.js +156 -156
- package/test/sleep-heat.test.js +174 -174
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +1455 -1418
- package/test/standalone-api.test.js +1344 -1344
- package/test/store-vector-cache.test.js +98 -98
- package/test/store-write-path-columns.test.js +191 -191
- package/test/store.test.js +744 -744
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -127
- package/test/summarize-parse-diagnostics.test.js +75 -75
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +2251 -2251
- package/test/supersede-state-tracking.test.js +394 -394
- package/test/test-count-sync.test.js +71 -71
- package/test/tool-state-ignore.test.js +65 -65
- package/test/tools-dto-schema-matrix.test.js +398 -398
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +585 -585
- package/test/updated-at-semantics.test.js +113 -113
- package/test/user-settings-preview.test.js +127 -127
- package/test/user-settings-segments.test.js +527 -527
- package/test/vector-index.test.js +334 -334
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/wiki-api.test.js +734 -627
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-cli-hint.test.js +118 -118
- package/test/wiki-inject.test.js +265 -265
- package/test/wiki-partition.test.js +112 -112
- package/test/wiki-plan.test.js +80 -80
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +398 -277
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -380
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -235
- package/test/wiki-tools.test.js +176 -176
- package/test/workflow-guards.test.js +155 -155
- package/test/write-admission.test.js +668 -668
package/bin/cli.mjs
CHANGED
|
@@ -1,964 +1,964 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* dsh-mneme CLI —— @modusensus/dsh-mneme 外部 API 命令行客户端
|
|
4
|
-
*
|
|
5
|
-
* 零依赖 ESM(Node >= 20,使用全局 fetch)。
|
|
6
|
-
* 配置优先级:命令行 --url/--token > 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN
|
|
7
|
-
* > 配置文件 ~/.dsh-mneme/cli.json({"url","token"})
|
|
8
|
-
* 默认服务地址:http://127.0.0.1:8790
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import fsp from 'node:fs/promises';
|
|
13
|
-
import os from 'node:os';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import process from 'node:process';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
|
|
18
|
-
const CLI_NAME = 'dsh-mneme';
|
|
19
|
-
const DEFAULT_URL = 'http://127.0.0.1:8790';
|
|
20
|
-
const CONFIG_DIR = path.join(os.homedir(), '.dsh-mneme');
|
|
21
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, 'cli.json');
|
|
22
|
-
// 与 src/store.js 的 TYPES 对齐(CLI 是零依赖 HTTP 客户端,不 import 服务端
|
|
23
|
-
// 模块,所以这里是手写字面量)。document 是唯一例外:它只能经
|
|
24
|
-
// memory_register_document 铸造,saveWithDedupe 会拒绝旁路写入,所以不作为
|
|
25
|
-
// --type 的选项——test/cli-enums.test.js 锁住「差值恰好是 document」。
|
|
26
|
-
const MEMORY_TYPES = ['preference', 'project', 'decision', 'history', 'summary', 'pattern', 'rejected_solution', 'pitfall', 'constraint'];
|
|
27
|
-
// hybrid:API 的 searchMemories 支持(向量先、关键词补位、双路命中做分数混合,
|
|
28
|
-
// 见 src/api.js 的 mode 注释)。此前漏列,search --mode hybrid 会被客户端自己拒掉。
|
|
29
|
-
const SEARCH_MODES = ['keyword', 'vector', 'auto', 'hybrid'];
|
|
30
|
-
const REQUEST_TIMEOUT_MS = 10_000;
|
|
31
|
-
|
|
32
|
-
function readPkgVersion() {
|
|
33
|
-
try {
|
|
34
|
-
const pkgPath = path.resolve(
|
|
35
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
36
|
-
'..',
|
|
37
|
-
'package.json'
|
|
38
|
-
);
|
|
39
|
-
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? '?';
|
|
40
|
-
} catch {
|
|
41
|
-
return '?';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const PKG_VERSION = readPkgVersion();
|
|
46
|
-
|
|
47
|
-
const USAGE = `${CLI_NAME} —— DSH 记忆插件(@modusensus/dsh-mneme)外部 API 命令行客户端
|
|
48
|
-
|
|
49
|
-
用法:
|
|
50
|
-
${CLI_NAME} <命令> [参数] [选项]
|
|
51
|
-
|
|
52
|
-
配置(优先级从高到低):
|
|
53
|
-
1. 命令行参数 --url <url> / --token <token>
|
|
54
|
-
2. 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN
|
|
55
|
-
3. 配置文件 ~/.dsh-mneme/cli.json(由 \`config set\` 写入)
|
|
56
|
-
未配置时使用默认地址:${DEFAULT_URL}
|
|
57
|
-
|
|
58
|
-
命令:
|
|
59
|
-
config set <url> <token> 写入配置文件(token 省略时保留原值)
|
|
60
|
-
config show 查看当前配置(token 打码显示)
|
|
61
|
-
config path 打印配置文件路径
|
|
62
|
-
status 服务状态(版本 / 记忆统计 / 实体 / 运行时长)
|
|
63
|
-
list [--type t] [--min-importance n] [--source s]
|
|
64
|
-
[--limit n] [--offset n] [--json]
|
|
65
|
-
列出记忆(type: preference | project | decision | summary | history)
|
|
66
|
-
search <query> [--mode m] [--topk n] [--json]
|
|
67
|
-
搜索记忆(mode: keyword | vector | auto)
|
|
68
|
-
get <id> [--json] 查看单条记忆
|
|
69
|
-
add --type <t> --title <t> --content <c>
|
|
70
|
-
[--importance n] [--tags a,b] [--source s] [--json]
|
|
71
|
-
新增记忆(type: preference | project | decision | summary | history)
|
|
72
|
-
delete <id> 删除记忆
|
|
73
|
-
reclaim [--older-than n] [--apply] [--vacuum] [--json]
|
|
74
|
-
存储无损回收(默认只报数字,不动数据)
|
|
75
|
-
--older-than n 输入快照保留窗口(天,默认 7;0 = 全部)
|
|
76
|
-
--apply 真的执行(不带就是 dry-run)
|
|
77
|
-
--vacuum 执行后整库 VACUUM(需配 --apply)
|
|
78
|
-
只清「可由记忆库重建的输入快照」与「归档行的向量」,
|
|
79
|
-
一条记忆都不删;收益按 VACUUM 前后体积量
|
|
80
|
-
share [--alias 名字] [--type a,b] [--tag 标签] --out <文件>
|
|
81
|
-
打一个可外发的脱敏分享包(不改库,纯读)
|
|
82
|
-
--alias 名字 在包里署名,对方看到的是这个而不是你的路径
|
|
83
|
-
--type/--tag 收窄到某几类/某个标签(超限分批用这两个)
|
|
84
|
-
--out <文件> 写出包文件;--dry-run 只报数不落盘
|
|
85
|
-
跳过归档/已遗忘/只读指针行/summary,密钥 PII 整条不进包
|
|
86
|
-
receive <包文件> [--apply]
|
|
87
|
-
把对方 share 出来的包吃进自己的库(默认干跑,一个字都不写)
|
|
88
|
-
外来行标 source=imported,不参与本机的巩固/降级,也不并入你的条目
|
|
89
|
-
--apply 才真的写;重复吃同一个包不会多出第二份
|
|
90
|
-
undo-share <包文件> [--apply]
|
|
91
|
-
按 pack_id 收起整包(物删:包还能重新导入,归档位会占住 UNIQUE)
|
|
92
|
-
|
|
93
|
-
选项:
|
|
94
|
-
--url <url> 覆盖服务地址
|
|
95
|
-
--token <token> 覆盖访问 token
|
|
96
|
-
--json 输出原始 JSON
|
|
97
|
-
-h, --help 显示本帮助
|
|
98
|
-
-V, --version 显示版本号
|
|
99
|
-
|
|
100
|
-
说明:
|
|
101
|
-
token 在 DSH 面板「设置 → 外部访问」或插件设置中查看;
|
|
102
|
-
除 GET /health 外,所有外部 API 路由均需 Bearer token。
|
|
103
|
-
|
|
104
|
-
示例:
|
|
105
|
-
${CLI_NAME} config set ${DEFAULT_URL} my-token
|
|
106
|
-
${CLI_NAME} status
|
|
107
|
-
${CLI_NAME} list --type project --limit 10
|
|
108
|
-
${CLI_NAME} search "部署流程" --mode vector --topk 5
|
|
109
|
-
${CLI_NAME} add --type decision --title "采用 SQLite" --content "存储层使用 node:sqlite" --importance 4 --tags 存储,决策
|
|
110
|
-
${CLI_NAME} get 42
|
|
111
|
-
${CLI_NAME} delete 42
|
|
112
|
-
${CLI_NAME} reclaim (先看:会清几行、库多大)
|
|
113
|
-
${CLI_NAME} reclaim --apply --vacuum (确认后执行,并把空间真的收回来)
|
|
114
|
-
${CLI_NAME} share --dry-run (先看:会进几条、为什么跳过那些,不落盘)
|
|
115
|
-
${CLI_NAME} share --out pack.json (写包;~/… 由 CLI 自己展开,PowerShell/cmd 不会)
|
|
116
|
-
${CLI_NAME} receive pack.json (吃包:默认干跑,只报账,一个字都不写)
|
|
117
|
-
${CLI_NAME} receive pack.json --apply (确认数字后真的吃进自己的库;重跑同一个包 created=0)
|
|
118
|
-
${CLI_NAME} undo-share pack.json --apply (按包里那个 pack_id 整包收回:物删,之后还能重新导入)`;
|
|
119
|
-
|
|
120
|
-
/* ---------------------------------- 参数解析 ---------------------------------- */
|
|
121
|
-
|
|
122
|
-
// `--flag value` 形态下,「下一个词以 -- 开头就别当值」这条规则有一个致命反例:
|
|
123
|
-
// 值本身就可能是 `--` 开头的字符串。外部 API 的 token 由
|
|
124
|
-
// `crypto.randomBytes(24).toString("base64url")` 生成(src/api-standalone.js),
|
|
125
|
-
// base64url 的字母表含 `-`,所以约 1/4096 的 token 头两位恰好都是 `-`:
|
|
126
|
-
//
|
|
127
|
-
// dsh-mneme reclaim --url http://127.0.0.1:55423 --token --UYoOHxr6PtE2-bfqW...
|
|
128
|
-
//
|
|
129
|
-
// 旧规则把 `--UYoOHxr6PtE2-bfqW...` 当成一个新旗标(键名 `uyoohxr6pte2-bfqw...`),
|
|
130
|
-
// 于是 token 解析成 `true`,flagValue 视作「没给」→ 退回环境变量/配置文件 → 全空
|
|
131
|
-
// → 报「未配置访问 token」。而 CLI 生成 token 的另一半同样会撞:`--token=--abc`
|
|
132
|
-
// 走等号分支没事,`--token --abc` 就废。这不是测试的随机性,是真实用户的随机性。
|
|
133
|
-
//
|
|
134
|
-
// 修法:区分「已知旗标」与「未知词」。下一个词以 `--` 开头时,只有它确实是一个
|
|
135
|
-
// 已注册的旗标才当作旗标,否则当作当前 flag 的值。于是 `--limit --json` 仍按
|
|
136
|
-
// 旧语义把 --json 当旗标,而 `--token --UYo...` 正确取到 token。
|
|
137
|
-
// 已知旗标表必须与下面 flagValue/flagBool 的实际用法保持同步;漏登记的代价是
|
|
138
|
-
// 该旗标被当成值吞掉,所以 test/cli-flag-value.test.js 用文本锁对齐两者。
|
|
139
|
-
const KNOWN_FLAGS = new Set([
|
|
140
|
-
// 取值的旗标
|
|
141
|
-
'url', 'token', 'type', 'title', 'content', 'importance', 'tags', 'source',
|
|
142
|
-
'limit', 'offset', 'mode', 'topk', 'top-k', 'min-importance', 'minimportance',
|
|
143
|
-
'older-than', 'olderthandays',
|
|
144
|
-
'alias', 'tag', 'out',
|
|
145
|
-
// 布尔旗标
|
|
146
|
-
'apply', 'vacuum', 'json', 'help', 'version',
|
|
147
|
-
'dry-run', 'dryrun',
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
/** `--foo=bar` / `--foo` 取出小写键名;非旗标词返回 null。 */
|
|
151
|
-
function flagKeyOf(word) {
|
|
152
|
-
if (typeof word !== 'string' || !word.startsWith('--') || word === '--') return null;
|
|
153
|
-
const key = word.slice(2);
|
|
154
|
-
const eq = key.indexOf('=');
|
|
155
|
-
return (eq >= 0 ? key.slice(0, eq) : key).toLowerCase();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseArgv(argv) {
|
|
159
|
-
const positionals = [];
|
|
160
|
-
const flags = {};
|
|
161
|
-
for (let i = 0; i < argv.length; i++) {
|
|
162
|
-
const arg = argv[i];
|
|
163
|
-
if (arg === '--') {
|
|
164
|
-
positionals.push(...argv.slice(i + 1));
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
if (arg.startsWith('--')) {
|
|
168
|
-
let key = arg.slice(2);
|
|
169
|
-
let value;
|
|
170
|
-
const eq = key.indexOf('=');
|
|
171
|
-
if (eq >= 0) {
|
|
172
|
-
value = key.slice(eq + 1);
|
|
173
|
-
key = key.slice(0, eq);
|
|
174
|
-
} else {
|
|
175
|
-
const next = argv[i + 1];
|
|
176
|
-
const nextIsFlag = next !== undefined && flagKeyOf(next) !== null && KNOWN_FLAGS.has(flagKeyOf(next));
|
|
177
|
-
if (next !== undefined && next !== '--' && !nextIsFlag) {
|
|
178
|
-
value = argv[++i];
|
|
179
|
-
} else {
|
|
180
|
-
value = true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
flags[key.toLowerCase()] = value;
|
|
184
|
-
} else {
|
|
185
|
-
positionals.push(arg);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return { positionals, flags };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function flagValue(flags, ...names) {
|
|
192
|
-
for (const name of names) {
|
|
193
|
-
const value = flags[name.toLowerCase()];
|
|
194
|
-
if (value !== undefined && value !== true && value !== '') return value;
|
|
195
|
-
}
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function flagBool(flags, ...names) {
|
|
200
|
-
for (const name of names) {
|
|
201
|
-
const value = flags[name.toLowerCase()];
|
|
202
|
-
if (value !== undefined) return value === true || value === 'true' || value === '';
|
|
203
|
-
}
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/* ---------------------------------- 配置解析 ---------------------------------- */
|
|
208
|
-
|
|
209
|
-
function readConfigFile() {
|
|
210
|
-
try {
|
|
211
|
-
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
212
|
-
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
213
|
-
} catch {
|
|
214
|
-
return {};
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function firstNonEmpty(...values) {
|
|
219
|
-
for (const value of values) {
|
|
220
|
-
if (typeof value === 'string' && value.trim() !== '') return value.trim();
|
|
221
|
-
}
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function resolveConfig(flags) {
|
|
226
|
-
const file = readConfigFile();
|
|
227
|
-
const url =
|
|
228
|
-
firstNonEmpty(flagValue(flags, 'url'), process.env.DSH_MNEME_URL, file.url) ?? DEFAULT_URL;
|
|
229
|
-
const token =
|
|
230
|
-
firstNonEmpty(flagValue(flags, 'token'), process.env.DSH_MNEME_TOKEN, file.token) ?? '';
|
|
231
|
-
return { url: url.replace(/\/+$/, ''), token, jsonMode: false };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function maskToken(token) {
|
|
235
|
-
if (!token) return '(未设置)';
|
|
236
|
-
if (token.length <= 8) return '****';
|
|
237
|
-
return `${token.slice(0, 4)}****${token.slice(-4)}`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/* ---------------------------------- 输出与错误 ---------------------------------- */
|
|
241
|
-
|
|
242
|
-
function stringify(value) {
|
|
243
|
-
return JSON.stringify(value, null, 2);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function fail(cfg, message, { rawJson } = {}) {
|
|
247
|
-
if (cfg.jsonMode) {
|
|
248
|
-
console.error(rawJson !== undefined ? rawJson : stringify({ error: message }));
|
|
249
|
-
} else {
|
|
250
|
-
console.error(message);
|
|
251
|
-
}
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function failHttp(cfg, status, pathname, data, text) {
|
|
256
|
-
const serverError =
|
|
257
|
-
data && typeof data === 'object' && data.error
|
|
258
|
-
? String(data.error)
|
|
259
|
-
: (text || '').slice(0, 200);
|
|
260
|
-
let hint = '';
|
|
261
|
-
if (status === 401) {
|
|
262
|
-
hint =
|
|
263
|
-
`\n访问 token 缺失或无效。token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看,\n` +
|
|
264
|
-
`然后用 \`${CLI_NAME} config set <url> <token>\`、环境变量 DSH_MNEME_TOKEN 或 --token 配置。`;
|
|
265
|
-
} else if (status === 404) {
|
|
266
|
-
hint = `\n资源不存在:${pathname}`;
|
|
267
|
-
}
|
|
268
|
-
// 拒答体自己带的下一步必须转出去,不然命令侧只能再写一份错误分支。撞上的这一刀是
|
|
269
|
-
// share 的 413:响应体是 {error:"too-large", bytes, limitBytes, hint, report},
|
|
270
|
-
// 旧实现只取 data.error,于是现场只剩「请求失败(HTTP 413):too-large」——「大了
|
|
271
|
-
// 多少」与「用 --type/--tag 分批」那两句留在体里没人看得见(措辞只许有一份,就在
|
|
272
|
-
// 拒发的那侧,同 RECLAIM_CONFIRM_HINT 的口径)。完整 report(含 skip_list)走
|
|
273
|
-
// --json:下面 rawJson 把整个响应体原样交给 stderr,这里不复制第二份打印器。
|
|
274
|
-
//
|
|
275
|
-
// 已知取舍(C10 记档,见 docs/SHARE.md「已知还没做」):这里只转 hint / bytes /
|
|
276
|
-
// limitBytes 三样,所以 receive 那侧 400 的 `detail`(闸门指到哪一格)与数据面 413 的
|
|
277
|
-
// `limit` 到不了人类可读的输出。刻意不补:补一格就得在这里多一条按路由分支的判断,
|
|
278
|
-
// 而「错误文案只许有一份」是 #411 那条纪律的另一半。要逐格看就加 --json——rawJson
|
|
279
|
-
// 那行给的正是原样响应体,一格都不丢。
|
|
280
|
-
if (data && typeof data === 'object') {
|
|
281
|
-
if (typeof data.hint === 'string' && data.hint.trim()) hint += `\n${data.hint.trim()}`;
|
|
282
|
-
if (Number.isFinite(data.bytes) && Number.isFinite(data.limitBytes)) {
|
|
283
|
-
hint += `\n量到 ${formatBytes(data.bytes)},水位 ${formatBytes(data.limitBytes)}`;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
fail(cfg, `请求失败(HTTP ${status})${serverError ? `:${serverError}` : ''} [${pathname}]${hint}`, {
|
|
287
|
-
rawJson: cfg.jsonMode && text ? text : undefined,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function failNetwork(cfg, url, err) {
|
|
292
|
-
const timedOut = err && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
293
|
-
const reason = timedOut
|
|
294
|
-
? `请求超时(${REQUEST_TIMEOUT_MS / 1000} 秒)`
|
|
295
|
-
: `无法连接到服务(${err?.code ?? err?.cause?.code ?? err?.message ?? '未知错误'})`;
|
|
296
|
-
fail(
|
|
297
|
-
cfg,
|
|
298
|
-
`${reason}:${url}\n` +
|
|
299
|
-
`请确认 DSH 已启动、插件外部 API 已开启,且地址正确(默认 ${DEFAULT_URL})。`
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function requireToken(cfg) {
|
|
304
|
-
if (cfg.token) return;
|
|
305
|
-
fail(
|
|
306
|
-
cfg,
|
|
307
|
-
[
|
|
308
|
-
'未配置访问 token,无法调用该命令(外部 API 需要 Bearer 鉴权)。',
|
|
309
|
-
'',
|
|
310
|
-
'token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看,然后任选其一配置:',
|
|
311
|
-
` 1. ${CLI_NAME} config set <url> <token>`,
|
|
312
|
-
' 2. 环境变量 DSH_MNEME_TOKEN=<token>',
|
|
313
|
-
' 3. 命令行参数 --token <token>',
|
|
314
|
-
].join('\n')
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function parseIntArg(cfg, value, label) {
|
|
319
|
-
if (value === undefined || value === true || value === '') return undefined;
|
|
320
|
-
const n = Number(value);
|
|
321
|
-
if (!Number.isInteger(n) || n < 0) {
|
|
322
|
-
fail(cfg, `无效的 ${label}: ${value}(应为非负整数)`);
|
|
323
|
-
}
|
|
324
|
-
return n;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/* ---------------------------------- HTTP 请求 ---------------------------------- */
|
|
328
|
-
|
|
329
|
-
async function apiRequest(cfg, method, pathname, { query, body } = {}) {
|
|
330
|
-
let url = cfg.url + pathname;
|
|
331
|
-
if (query) {
|
|
332
|
-
const params = new URLSearchParams();
|
|
333
|
-
for (const [key, value] of Object.entries(query)) {
|
|
334
|
-
if (value !== undefined && value !== null && value !== '') {
|
|
335
|
-
params.set(key, String(value));
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const qs = params.toString();
|
|
339
|
-
if (qs) url += `?${qs}`;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const headers = { Accept: 'application/json' };
|
|
343
|
-
if (cfg.token) headers.Authorization = `Bearer ${cfg.token}`;
|
|
344
|
-
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
345
|
-
|
|
346
|
-
let res;
|
|
347
|
-
try {
|
|
348
|
-
res = await fetch(url, {
|
|
349
|
-
method,
|
|
350
|
-
headers,
|
|
351
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
352
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
353
|
-
});
|
|
354
|
-
} catch (err) {
|
|
355
|
-
failNetwork(cfg, url, err);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const text = await res.text();
|
|
359
|
-
let data = null;
|
|
360
|
-
try {
|
|
361
|
-
data = text ? JSON.parse(text) : null;
|
|
362
|
-
} catch {
|
|
363
|
-
// 非 JSON 响应体,保留原始文本用于报错展示
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (!res.ok) {
|
|
367
|
-
failHttp(cfg, res.status, pathname, data, text);
|
|
368
|
-
}
|
|
369
|
-
return data;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/* ---------------------------------- 结果格式化 ---------------------------------- */
|
|
373
|
-
|
|
374
|
-
function formatTimestamp(value) {
|
|
375
|
-
if (!value) return '';
|
|
376
|
-
const s = String(value);
|
|
377
|
-
return s.length >= 10 ? s.slice(0, 10) : s;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function formatMemoryLine(item) {
|
|
381
|
-
const id = item?.id ?? '?';
|
|
382
|
-
const type = item?.type ?? '?';
|
|
383
|
-
const importance = item?.importance != null ? ` ★${item.importance}` : '';
|
|
384
|
-
const title = item?.title ? ` ${item.title}` : '';
|
|
385
|
-
const tags =
|
|
386
|
-
Array.isArray(item?.tags) && item.tags.length ? ` [${item.tags.join(',')}]` : '';
|
|
387
|
-
const date = formatTimestamp(item?.created_at ?? item?.createdAt);
|
|
388
|
-
return ` #${id} [${type}]${importance}${title}${tags}${date ? ` (${date})` : ''}`;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function printMemoryItems(payload, { withMode = false } = {}) {
|
|
392
|
-
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
393
|
-
const total = payload?.total ?? items.length;
|
|
394
|
-
if (withMode && payload?.mode) console.log(`搜索模式: ${payload.mode}`);
|
|
395
|
-
console.log(`共 ${total} 条,本次显示 ${items.length} 条`);
|
|
396
|
-
if (!items.length) return;
|
|
397
|
-
console.log('');
|
|
398
|
-
for (const item of items) {
|
|
399
|
-
const score = item?.score ?? item?.similarity;
|
|
400
|
-
const scoreText =
|
|
401
|
-
typeof score === 'number' && Number.isFinite(score) ? ` (score ${score.toFixed(3)})` : '';
|
|
402
|
-
console.log(formatMemoryLine(item) + scoreText);
|
|
403
|
-
const content =
|
|
404
|
-
typeof item?.content === 'string' ? item.content.replace(/\s+/g, ' ').trim() : '';
|
|
405
|
-
if (content) {
|
|
406
|
-
console.log(` ${content.length > 80 ? `${content.slice(0, 80)}…` : content}`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function printMemoryDetail(item) {
|
|
412
|
-
if (!item || typeof item !== 'object') {
|
|
413
|
-
console.log(stringify(item ?? null));
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
const fields = [];
|
|
417
|
-
if (item.id !== undefined) fields.push(`id: ${item.id}`);
|
|
418
|
-
if (item.type !== undefined) fields.push(`类型: ${item.type}`);
|
|
419
|
-
if (item.importance !== undefined) fields.push(`重要性: ${item.importance}`);
|
|
420
|
-
if (item.title !== undefined) fields.push(`标题: ${item.title}`);
|
|
421
|
-
if (item.tags !== undefined) {
|
|
422
|
-
fields.push(`标签: ${Array.isArray(item.tags) ? item.tags.join(', ') : item.tags}`);
|
|
423
|
-
}
|
|
424
|
-
if (item.source !== undefined) fields.push(`来源: ${item.source}`);
|
|
425
|
-
const created = item.created_at ?? item.createdAt;
|
|
426
|
-
if (created) fields.push(`创建时间: ${created}`);
|
|
427
|
-
for (const line of fields) console.log(line);
|
|
428
|
-
if (item.content !== undefined) console.log(`\n${item.content}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function printStatus(data) {
|
|
432
|
-
console.log(`服务版本: ${data?.version ?? '?'}`);
|
|
433
|
-
console.log(`运行时长: ${data?.uptime_s ?? data?.uptimeS ?? '?'} 秒`);
|
|
434
|
-
const memories = data?.memories ?? {};
|
|
435
|
-
console.log(`记忆总数: ${memories.total ?? '?'}`);
|
|
436
|
-
const byType = memories.byType;
|
|
437
|
-
if (Array.isArray(byType)) {
|
|
438
|
-
for (const row of byType) {
|
|
439
|
-
console.log(` - ${row?.type ?? row?.name ?? '?'}: ${row?.count ?? row?.total ?? '?'}`);
|
|
440
|
-
}
|
|
441
|
-
} else if (byType && typeof byType === 'object') {
|
|
442
|
-
for (const [type, count] of Object.entries(byType)) {
|
|
443
|
-
console.log(` - ${type}: ${count}`);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
const entities = data?.entities;
|
|
447
|
-
if (entities != null) {
|
|
448
|
-
const total =
|
|
449
|
-
typeof entities === 'object' && !Array.isArray(entities)
|
|
450
|
-
? entities.total
|
|
451
|
-
: entities;
|
|
452
|
-
console.log(`实体数量: ${total ?? '?'}`);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/* ---------------------------------- 子命令 ---------------------------------- */
|
|
457
|
-
|
|
458
|
-
async function cmdConfig(cfg, sub, rest) {
|
|
459
|
-
const subCmd = sub || 'show';
|
|
460
|
-
|
|
461
|
-
if (subCmd === 'set') {
|
|
462
|
-
const [url, tokenArg] = rest;
|
|
463
|
-
if (!url) {
|
|
464
|
-
fail(
|
|
465
|
-
cfg,
|
|
466
|
-
`用法: ${CLI_NAME} config set <url> <token>\n示例: ${CLI_NAME} config set ${DEFAULT_URL} my-token`
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
if (!/^https?:\/\//i.test(url)) {
|
|
470
|
-
fail(cfg, `无效的服务地址: ${url}(应以 http:// 或 https:// 开头)`);
|
|
471
|
-
}
|
|
472
|
-
const existing = readConfigFile();
|
|
473
|
-
const token = tokenArg ?? existing.token ?? '';
|
|
474
|
-
await fsp.mkdir(CONFIG_DIR, { recursive: true });
|
|
475
|
-
await fsp.writeFile(CONFIG_PATH, `${JSON.stringify({ url, token }, null, 2)}\n`, 'utf8');
|
|
476
|
-
if (cfg.jsonMode) {
|
|
477
|
-
console.log(stringify({ configPath: CONFIG_PATH, url, tokenConfigured: Boolean(token) }));
|
|
478
|
-
} else {
|
|
479
|
-
console.log(`已写入配置文件: ${CONFIG_PATH}`);
|
|
480
|
-
console.log(` url: ${url}`);
|
|
481
|
-
console.log(` token: ${maskToken(token)}`);
|
|
482
|
-
}
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (subCmd === 'show') {
|
|
487
|
-
const hasFile = fs.existsSync(CONFIG_PATH);
|
|
488
|
-
const file = readConfigFile();
|
|
489
|
-
if (cfg.jsonMode) {
|
|
490
|
-
console.log(
|
|
491
|
-
stringify({
|
|
492
|
-
configPath: CONFIG_PATH,
|
|
493
|
-
configExists: hasFile,
|
|
494
|
-
url: file.url ?? DEFAULT_URL,
|
|
495
|
-
token: maskToken(file.token),
|
|
496
|
-
})
|
|
497
|
-
);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
if (!hasFile) {
|
|
501
|
-
console.log(`尚未创建配置文件(未配置): ${CONFIG_PATH}`);
|
|
502
|
-
console.log('');
|
|
503
|
-
console.log(`可运行 \`${CLI_NAME} config set <url> <token>\` 创建配置;`);
|
|
504
|
-
console.log('token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看。');
|
|
505
|
-
console.log(
|
|
506
|
-
`未配置时使用默认地址 ${DEFAULT_URL},也可用环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN。`
|
|
507
|
-
);
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
console.log(`配置文件: ${CONFIG_PATH}`);
|
|
511
|
-
console.log(` url: ${file.url ?? `(未设置,默认 ${DEFAULT_URL})`}`);
|
|
512
|
-
console.log(` token: ${maskToken(file.token)}`);
|
|
513
|
-
console.log('');
|
|
514
|
-
console.log('配置优先级: --url/--token > 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN > 配置文件');
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (subCmd === 'path') {
|
|
519
|
-
console.log(CONFIG_PATH);
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
fail(cfg, `未知的 config 子命令: ${subCmd}\n可用子命令: set / show / path`);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
async function cmdStatus(cfg) {
|
|
527
|
-
requireToken(cfg);
|
|
528
|
-
const data = await apiRequest(cfg, 'GET', '/status');
|
|
529
|
-
if (cfg.jsonMode) {
|
|
530
|
-
console.log(stringify(data));
|
|
531
|
-
} else {
|
|
532
|
-
printStatus(data);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
async function cmdList(cfg, flags) {
|
|
537
|
-
requireToken(cfg);
|
|
538
|
-
const query = {
|
|
539
|
-
type: flagValue(flags, 'type'),
|
|
540
|
-
minImportance: parseIntArg(cfg, flagValue(flags, 'min-importance', 'minImportance'), '--min-importance'),
|
|
541
|
-
source: flagValue(flags, 'source'),
|
|
542
|
-
limit: parseIntArg(cfg, flagValue(flags, 'limit'), '--limit'),
|
|
543
|
-
offset: parseIntArg(cfg, flagValue(flags, 'offset'), '--offset'),
|
|
544
|
-
};
|
|
545
|
-
const data = await apiRequest(cfg, 'GET', '/memories', { query });
|
|
546
|
-
if (cfg.jsonMode) {
|
|
547
|
-
console.log(stringify(data));
|
|
548
|
-
} else {
|
|
549
|
-
printMemoryItems(data);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
async function cmdSearch(cfg, rest, flags) {
|
|
554
|
-
requireToken(cfg);
|
|
555
|
-
const q = rest[0];
|
|
556
|
-
if (!q) {
|
|
557
|
-
fail(
|
|
558
|
-
cfg,
|
|
559
|
-
`用法: ${CLI_NAME} search <query> [--mode m] [--topk n]\n` +
|
|
560
|
-
`示例: ${CLI_NAME} search "部署流程" --mode vector --topk 5`
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
const mode = flagValue(flags, 'mode');
|
|
564
|
-
if (mode !== undefined && !SEARCH_MODES.includes(mode)) {
|
|
565
|
-
fail(cfg, `无效的 --mode: ${mode}(可选: ${SEARCH_MODES.join(' | ')})`);
|
|
566
|
-
}
|
|
567
|
-
const topK = parseIntArg(cfg, flagValue(flags, 'topk', 'top-k', 'topK'), '--topk');
|
|
568
|
-
const data = await apiRequest(cfg, 'GET', '/search', { query: { q, mode, topK } });
|
|
569
|
-
if (cfg.jsonMode) {
|
|
570
|
-
console.log(stringify(data));
|
|
571
|
-
} else {
|
|
572
|
-
printMemoryItems(data, { withMode: true });
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
async function cmdGet(cfg, rest) {
|
|
577
|
-
requireToken(cfg);
|
|
578
|
-
const id = rest[0];
|
|
579
|
-
if (!id) {
|
|
580
|
-
fail(cfg, `用法: ${CLI_NAME} get <id> [--json]`);
|
|
581
|
-
}
|
|
582
|
-
const data = await apiRequest(cfg, 'GET', `/memories/${encodeURIComponent(id)}`);
|
|
583
|
-
if (cfg.jsonMode) {
|
|
584
|
-
console.log(stringify(data));
|
|
585
|
-
} else {
|
|
586
|
-
printMemoryDetail(data);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function cmdAdd(cfg, flags) {
|
|
591
|
-
requireToken(cfg);
|
|
592
|
-
const type = flagValue(flags, 'type');
|
|
593
|
-
const title = flagValue(flags, 'title');
|
|
594
|
-
const content = flagValue(flags, 'content');
|
|
595
|
-
if (!type || !title || !content) {
|
|
596
|
-
fail(
|
|
597
|
-
cfg,
|
|
598
|
-
`用法: ${CLI_NAME} add --type <${MEMORY_TYPES.join('|')}> --title <标题> --content <内容>\n` +
|
|
599
|
-
` [--importance n] [--tags a,b] [--source s]\n\n` +
|
|
600
|
-
`示例: ${CLI_NAME} add --type decision --title "采用 SQLite" --content "存储层使用 node:sqlite"`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
if (!MEMORY_TYPES.includes(type)) {
|
|
604
|
-
fail(cfg, `无效的 --type: ${type}(可选: ${MEMORY_TYPES.join(' | ')})`);
|
|
605
|
-
}
|
|
606
|
-
const importanceRaw = flagValue(flags, 'importance');
|
|
607
|
-
let importance;
|
|
608
|
-
if (importanceRaw !== undefined) {
|
|
609
|
-
importance = Number(importanceRaw);
|
|
610
|
-
if (!Number.isInteger(importance) || importance < 1 || importance > 5) {
|
|
611
|
-
fail(cfg, `无效的 --importance: ${importanceRaw}(应为 1-5 的整数)`);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
const tagsRaw = flagValue(flags, 'tags');
|
|
615
|
-
const tags =
|
|
616
|
-
tagsRaw !== undefined
|
|
617
|
-
? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean)
|
|
618
|
-
: undefined;
|
|
619
|
-
const source = flagValue(flags, 'source');
|
|
620
|
-
|
|
621
|
-
const body = { type, title, content };
|
|
622
|
-
if (importance !== undefined) body.importance = importance;
|
|
623
|
-
if (tags) body.tags = tags;
|
|
624
|
-
if (source) body.source = source;
|
|
625
|
-
|
|
626
|
-
const data = await apiRequest(cfg, 'POST', '/memories', { body });
|
|
627
|
-
if (cfg.jsonMode) {
|
|
628
|
-
console.log(stringify(data));
|
|
629
|
-
} else {
|
|
630
|
-
console.log('已添加记忆:');
|
|
631
|
-
printMemoryDetail(data);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async function cmdDelete(cfg, rest) {
|
|
636
|
-
requireToken(cfg);
|
|
637
|
-
const id = rest[0];
|
|
638
|
-
if (!id) {
|
|
639
|
-
fail(cfg, `用法: ${CLI_NAME} delete <id>`);
|
|
640
|
-
}
|
|
641
|
-
const data = await apiRequest(cfg, 'DELETE', `/memories/${encodeURIComponent(id)}`);
|
|
642
|
-
if (cfg.jsonMode) {
|
|
643
|
-
console.log(stringify(data));
|
|
644
|
-
} else {
|
|
645
|
-
console.log(`已删除记忆 ${id}`);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function formatBytes(bytes) {
|
|
650
|
-
if (typeof bytes !== 'number' || !Number.isFinite(bytes)) return '?';
|
|
651
|
-
const mb = bytes / (1024 * 1024);
|
|
652
|
-
if (mb >= 1) return `${mb.toFixed(1)} MB`;
|
|
653
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function printReclaim(report, { applied }) {
|
|
657
|
-
const planned = report?.planned ?? {};
|
|
658
|
-
const cleared = report?.cleared ?? {};
|
|
659
|
-
const columnBytes = report?.column_bytes ?? {};
|
|
660
|
-
const size = report?.size ?? {};
|
|
661
|
-
console.log(`存储无损回收(${applied ? '已执行' : 'dry-run,未改动任何数据'})`);
|
|
662
|
-
console.log(` 输入快照保留窗口: ${report?.olderThanDays ?? '?'} 天(cutoff ${report?.cutoff ?? '?'})`);
|
|
663
|
-
// 第三项(#399):蒸馏原文的到期裁剪同样只删 distill_records,溯源链接刻意保留
|
|
664
|
-
const rows = (o) =>
|
|
665
|
-
`dream_runs.input ${o.dream_run_inputs ?? 0} 行 / 归档行向量 ${o.archived_embeddings ?? 0} 行 / 蒸馏原文 ${o.distill_records ?? 0} 行`;
|
|
666
|
-
console.log(` ${applied ? '已清理' : '将清理'}: ${rows(applied ? cleared : planned)}`);
|
|
667
|
-
console.log(
|
|
668
|
-
` 列文本大小(上界,非回收量): ${formatBytes(columnBytes.dream_run_inputs)} / ${formatBytes(columnBytes.archived_embeddings)} / ${formatBytes(columnBytes.distill_records)}`
|
|
669
|
-
);
|
|
670
|
-
if (!applied) {
|
|
671
|
-
console.log(
|
|
672
|
-
` 库文件: ${formatBytes(size.before_bytes)}(空闲页 ${size.freelist_pages ?? '?'} 页,可立刻回收约 ${formatBytes(size.freelist_bytes)})`
|
|
673
|
-
);
|
|
674
|
-
console.log('');
|
|
675
|
-
console.log(' 上面是 dry-run 的数字。确认后执行:');
|
|
676
|
-
console.log(` ${CLI_NAME} reclaim --apply --vacuum`);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const freed =
|
|
680
|
-
typeof size.before_bytes === 'number' && typeof size.after_bytes === 'number'
|
|
681
|
-
? size.before_bytes - size.after_bytes
|
|
682
|
-
: null;
|
|
683
|
-
if (report?.vacuum?.error) {
|
|
684
|
-
console.log(` VACUUM: 失败(${String(report.vacuum.error).slice(0, 120)})`);
|
|
685
|
-
console.log(' 清理已经完成且不可逆,文件暂时不会变小;库多半被别的进程占着锁,');
|
|
686
|
-
console.log(' 稍后重跑一次 `reclaim --apply --vacuum` 即可(清理是幂等的)。');
|
|
687
|
-
} else {
|
|
688
|
-
console.log(
|
|
689
|
-
` VACUUM: ${report?.vacuum?.ran ? `已跑(${report.vacuum.duration_ms ?? '?'} ms)` : '未跑(未带 --vacuum,文件不会变小)'}`
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
console.log(
|
|
693
|
-
` 库文件: ${formatBytes(size.before_bytes)} → ${formatBytes(size.after_bytes)}` +
|
|
694
|
-
(freed !== null && freed > 0 ? `(释放 ${formatBytes(freed)})` : '')
|
|
695
|
-
);
|
|
696
|
-
console.log(` 耗时: ${report?.duration_ms ?? '?'} ms`);
|
|
697
|
-
if (report?.receipt) console.log(` 回执: llm_audit_logs #${report.receipt}`);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async function cmdReclaim(cfg, flags) {
|
|
701
|
-
requireToken(cfg);
|
|
702
|
-
const applied = flagBool(flags, 'apply');
|
|
703
|
-
const vacuum = flagBool(flags, 'vacuum');
|
|
704
|
-
if (vacuum && !applied) {
|
|
705
|
-
fail(
|
|
706
|
-
cfg,
|
|
707
|
-
`--vacuum 需要配 --apply:dry-run 不改数据,也就没有可回收的页。\n` +
|
|
708
|
-
`先看数字,再执行:${CLI_NAME} reclaim --apply --vacuum`
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
const olderThanDays = parseIntArg(cfg, flagValue(flags, 'older-than', 'olderThanDays'), '--older-than');
|
|
712
|
-
const body = { dryRun: !applied };
|
|
713
|
-
if (applied) {
|
|
714
|
-
// 服务端要求显式确认:这两步不可逆,缺 confirm 只会拿回一份 dry-run 报告。
|
|
715
|
-
body.confirm = true;
|
|
716
|
-
}
|
|
717
|
-
if (olderThanDays !== undefined) body.olderThanDays = olderThanDays;
|
|
718
|
-
if (vacuum) body.vacuum = true;
|
|
719
|
-
const data = await apiRequest(cfg, 'POST', '/maintenance/reclaim', { body });
|
|
720
|
-
if (cfg.jsonMode) {
|
|
721
|
-
console.log(stringify(data));
|
|
722
|
-
} else {
|
|
723
|
-
printReclaim(data, { applied });
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// --out 的例子写的是 `~/Desktop/pack.json`。Git Bash 会替她展开,PowerShell 与 cmd
|
|
728
|
-
// 不会——那两侧 `~/…` 是字面量,writeFileSync 直接 ENOENT,现场只剩一坨堆栈。CLI 不
|
|
729
|
-
// 接管 shell 的活,但引号里这一格得自己补(同 CONFIG_DIR 走 os.homedir())。
|
|
730
|
-
function expandHome(p) {
|
|
731
|
-
if (typeof p !== 'string') return p;
|
|
732
|
-
if (p !== '~' && !p.startsWith('~/') && !p.startsWith('~\\')) return p;
|
|
733
|
-
const rest = p.slice(1).replace(/^[/\\]+/, '');
|
|
734
|
-
return path.join(os.homedir(), rest);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function printShare(pack, { dryRun }) {
|
|
738
|
-
const r = pack?.report ?? {};
|
|
739
|
-
const s = r.skipped ?? {};
|
|
740
|
-
console.log(`${dryRun ? '干跑报告' : '分享包'}: ${r.included ?? 0} 条` +
|
|
741
|
-
(pack?.alias ? ` 来自 ${pack.alias}` : ''));
|
|
742
|
-
const named = [['archived', '归档'], ['forgotten', '已遗忘'], ['readonly_type', '只读指针行'],
|
|
743
|
-
['excluded_type', '不随包走'], ['blank_or_noise', '空白/噪声'], ['sensitive', '密钥/PII']];
|
|
744
|
-
const parts = named.filter(([k]) => (s[k] ?? 0) > 0).map(([k, zh]) => `${zh} ${s[k]}`);
|
|
745
|
-
console.log(` 跳过: ${parts.length ? parts.join(', ') : '无'}`);
|
|
746
|
-
// 折叠不是跳过:那条记忆够格进包,只是包里已经有了它(同标题同正文同内容锚、scope
|
|
747
|
-
// 不同的两条)。不印出来的话 `included` 会比她心里的「够格的条数」少几条,而那正好是
|
|
748
|
-
// 会让人怀疑口径少发了的那类差额。src/share.js 的 report.deduped。
|
|
749
|
-
if ((r.deduped ?? 0) > 0) console.log(` 包内重复已折叠: ${r.deduped} 条(对面只吃一条)`);
|
|
750
|
-
for (const item of (r.skip_list ?? []).slice(0, 20)) {
|
|
751
|
-
// 标题为空有两种:这条本来没标题,或它整行被密钥判据命中、标题在源头就留空了
|
|
752
|
-
// (src/share.js:skip_list 随包外发,而凭据常常就写在标题里)。终端要让人看清
|
|
753
|
-
// 后一种是「有意不给」,不是「数据缺失」。
|
|
754
|
-
console.log(` - [${item.reason}] ${item.title || '(标题不外发)'}`);
|
|
755
|
-
}
|
|
756
|
-
// 判据用「六个计数之和 > 清单长度」而不是「清单等于 50」:前者是「账比清单长」这个事实本身,
|
|
757
|
-
// 恰好 50 条被跳时后者会误报截断(面板侧同一条判据)。50 这个数不在这里出现,
|
|
758
|
-
// 水位改了判据仍然对。
|
|
759
|
-
const skippedTotal = ['archived', 'forgotten', 'readonly_type', 'excluded_type', 'blank_or_noise', 'sensitive']
|
|
760
|
-
.reduce((n, k) => n + (r.skipped?.[k] ?? 0), 0);
|
|
761
|
-
if (skippedTotal > (r.skip_list ?? []).length) console.log(' … 跳过清单已截断,计数仍为全量');
|
|
762
|
-
if (dryRun) console.log(` 没写文件:加 --out <路径> 才会落盘`);
|
|
763
|
-
else console.log(` 发给对方即可,她用自己的 ${CLI_NAME} receive 吃进去`);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
async function cmdShare(cfg, flags) {
|
|
767
|
-
requireToken(cfg);
|
|
768
|
-
const out = expandHome(flagValue(flags, 'out'));
|
|
769
|
-
const dryRun = flagBool(flags, 'dry-run') || flagBool(flags, 'dryrun');
|
|
770
|
-
if (!out && !dryRun) {
|
|
771
|
-
fail(
|
|
772
|
-
cfg,
|
|
773
|
-
`share 要么写文件(--out <路径>),要么只看报告(--dry-run):` +
|
|
774
|
-
`不写文件又把整库打到 stdout 会让报告被 JSON 淹掉。\n` +
|
|
775
|
-
`例子:${CLI_NAME} share --out ~/Desktop/pack.json`
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
const typeRaw = flagValue(flags, 'type');
|
|
779
|
-
const body = {};
|
|
780
|
-
if (typeRaw) body.type = typeRaw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
781
|
-
const alias = flagValue(flags, 'alias');
|
|
782
|
-
if (alias) body.alias = alias;
|
|
783
|
-
const tag = flagValue(flags, 'tag');
|
|
784
|
-
if (tag) body.tag = tag;
|
|
785
|
-
// 超水位那次服务端答 413,apiRequest 对非 2xx 走的是 failHttp:它把响应体里的 hint
|
|
786
|
-
// 与量到的字节转出去再 exit(1)。所以这里**没有**可达的 error 分支——加 `if
|
|
787
|
-
// (pack?.error)` 就是第二份出口,措辞迟早跟拒发那一侧分家(#411 的同一口径)。
|
|
788
|
-
const pack = await apiRequest(cfg, 'POST', '/share', { body });
|
|
789
|
-
const r = pack?.report ?? {};
|
|
790
|
-
if (cfg.jsonMode) {
|
|
791
|
-
console.log(stringify(dryRun ? { report: r, pack } : { report: r, file: out }));
|
|
792
|
-
} else {
|
|
793
|
-
printShare(pack, { dryRun });
|
|
794
|
-
}
|
|
795
|
-
if (dryRun) return;
|
|
796
|
-
fs.writeFileSync(out, JSON.stringify(pack), 'utf8');
|
|
797
|
-
// --json 时 stdout 只许那一份 JSON:后面追一行「已写出」会把 JSON.parse 撞在
|
|
798
|
-
// position 536(jq 同理)。落盘路径已在上面的 payload 里(file 键),这行只对
|
|
799
|
-
// 人有用,就只给人看(同 cmdReclaim:json 模式打印完就结束)。
|
|
800
|
-
if (!cfg.jsonMode) console.log(` 已写出: ${out}`);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
// 入库面(车道 C)。包文件在**本地**读、把包内容整个送上去:服务端永远看不到路径,
|
|
804
|
-
// 也就不需要任何文件系统权限——这是 receive 协议选「传包不传路径」的全部理由。
|
|
805
|
-
// confirm 的脾气照 reclaim:缺省即干跑,写收件人的库(或物删整包)要 --apply 显式确认。
|
|
806
|
-
async function cmdReceive(cfg, rest, flags) {
|
|
807
|
-
requireToken(cfg);
|
|
808
|
-
const file = rest[0];
|
|
809
|
-
if (!file) {
|
|
810
|
-
fail(cfg, `receive 需要包文件:${CLI_NAME} receive <包文件>(不带 --apply 只干跑,一个字都不写)`);
|
|
811
|
-
}
|
|
812
|
-
let pack;
|
|
813
|
-
try {
|
|
814
|
-
pack = JSON.parse(fs.readFileSync(expandHome(file), 'utf8'));
|
|
815
|
-
} catch (e) {
|
|
816
|
-
// 读坏/读不到都在发请求之前拒掉:半读出来的包发过去只会换回一个闸门错,
|
|
817
|
-
// 而那一格现场已经说不清是「文件本来就这样」还是「传输坏了」。
|
|
818
|
-
fail(cfg, `读不到包文件 ${file}:${String(e?.message ?? e)}\n包文件是对方 ${CLI_NAME} share --out 出来的那个 .json。`);
|
|
819
|
-
}
|
|
820
|
-
const applied = flagBool(flags, 'apply');
|
|
821
|
-
const body = { pack };
|
|
822
|
-
if (applied) body.confirm = true;
|
|
823
|
-
const data = await apiRequest(cfg, 'POST', '/receive', { body });
|
|
824
|
-
if (cfg.jsonMode) {
|
|
825
|
-
console.log(stringify(data));
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
printReceive(data, { applied });
|
|
829
|
-
if (!applied) console.log(' 没写库:确认数字之后加 --apply 真的导入');
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// 撤销收的是**包文件**而不是裸 pack_id:让人抄 16 位十六进制去删自己库里的行,
|
|
833
|
-
// 抄错一位就是「删了个没删成的包还自以为收掉了」。pack_id 就写在包文件第一层。
|
|
834
|
-
async function cmdUndoShare(cfg, rest, flags) {
|
|
835
|
-
requireToken(cfg);
|
|
836
|
-
const file = rest[0];
|
|
837
|
-
if (!file) {
|
|
838
|
-
fail(cfg, `undo-share 需要当初那个包文件(pack_id 从文件里取):${CLI_NAME} undo-share <包文件> [--apply]`);
|
|
839
|
-
}
|
|
840
|
-
let pack;
|
|
841
|
-
try {
|
|
842
|
-
pack = JSON.parse(fs.readFileSync(expandHome(file), 'utf8'));
|
|
843
|
-
} catch (e) {
|
|
844
|
-
fail(cfg, `读不到包文件 ${file}:${String(e?.message ?? e)}\n要的是当初 ${CLI_NAME} share --out 出来、发给对面那个 .json。`);
|
|
845
|
-
}
|
|
846
|
-
// trim 在这儿做:只有空格的 pack_id 与「没有 pack_id」是同一种「这个文件收不了」,
|
|
847
|
-
// 放它出门只是绕一圈换回 400 missing-pack-id,而那一格会被读成服务端的问题(服务端
|
|
848
|
-
// parseUndoBody 同样 trim,这里不改它的行为,只是不必为空格 round-trip)。
|
|
849
|
-
const packId = typeof pack?.pack_id === 'string' ? pack.pack_id.trim() : '';
|
|
850
|
-
if (!packId) {
|
|
851
|
-
fail(cfg, `这个文件里没有 pack_id,收起不了:${file}\nundo-share 收的是整包,编号就写在包文件的第一层,不用也不该手抄 16 位十六进制。`);
|
|
852
|
-
}
|
|
853
|
-
const applied = flagBool(flags, 'apply');
|
|
854
|
-
const body = { packId };
|
|
855
|
-
if (applied) body.confirm = true;
|
|
856
|
-
const data = await apiRequest(cfg, 'POST', '/receive/undo', { body });
|
|
857
|
-
if (cfg.jsonMode) {
|
|
858
|
-
console.log(stringify(data));
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
printReceive({ ...data, created: data?.deleted, alias: '' }, { applied, undo: true });
|
|
862
|
-
if (!applied) console.log(' 没删:确认条数之后加 --apply 真的收起这个包');
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function printReceive(d, { applied, undo = false }) {
|
|
866
|
-
const conflicts = Array.isArray(d?.title_conflicts) ? d.title_conflicts : [];
|
|
867
|
-
console.log(`${undo ? (applied ? '已收起' : '将收起') : applied ? '已导入' : '干跑'}: ${d?.created ?? 0} 条` +
|
|
868
|
-
(d?.alias ? ` 来自 ${d.alias}` : '') + (d?.skipped_existing ? ` 已存在 ${d.skipped_existing}` : ''));
|
|
869
|
-
if (undo && applied) {
|
|
870
|
-
// 服务端是物删不是归档(src/share-import.js 的 deleteSharePack):这句话是 --apply
|
|
871
|
-
// 与人之间唯一的说明书,不许软化成「移除」。干跑那一笔一个字都没动,说「直接从库里
|
|
872
|
-
// 带走」是假话,还会把下面的样例压成「已经删掉的这几条」——干跑那侧的话术是 cmdUndo-
|
|
873
|
-
// Share 结尾的「没删:…加 --apply」,两句不许同时出现(同一件事说两遍就没人细看)。
|
|
874
|
-
console.log(' 物删:这些行直接从库里带走,不是归档;包文件还在,重新 receive 一遍即可再来');
|
|
875
|
-
}
|
|
876
|
-
if (conflicts.length) {
|
|
877
|
-
// 撞名是 advisory 读数(src/share-import.js:只对本轮真正会新建的条目判),
|
|
878
|
-
// 所以这里只说「这几条要你看一眼」,永远不许说「除此之外没有别的撞名」。
|
|
879
|
-
console.log(` 撞名提醒:和你自己的条目同名 ${conflicts.length} 条(同类型同标题,不会被并入,需要你处置):`);
|
|
880
|
-
for (const c of conflicts.slice(0, 10)) console.log(` - [${c.type}] ${c.title}`);
|
|
881
|
-
if (conflicts.length > 10) console.log(' … 只列前 10 条');
|
|
882
|
-
}
|
|
883
|
-
const sample = Array.isArray(d?.sample) ? d.sample : [];
|
|
884
|
-
if (sample.length) {
|
|
885
|
-
// 撞名那段有标题、有截断话,样例这段原先两样都没有:服务端把 sample 截到 20
|
|
886
|
-
// (src/share-import.js 的 DELETE_SAMPLE_MAX),CLI 再截到 10,而 deleted 才是全集。
|
|
887
|
-
// 一个 500 行的包就这样留下 10 条无名要点,人会当成「要收走的就是这些」。
|
|
888
|
-
// 全集那一格只报服务端的数,不报「还剩几条」——两次截断混在一起就没人能对得上账。
|
|
889
|
-
console.log(' 样例:下面只是这一批里的几条,不是全部清单');
|
|
890
|
-
const shown = sample.slice(0, 10);
|
|
891
|
-
for (const s of shown) console.log(` · [${s.type}] ${s.title}`);
|
|
892
|
-
const total = typeof d?.deleted === 'number' ? d.deleted : (d?.created ?? 0);
|
|
893
|
-
if (shown.length < total) console.log(` … 只列前 ${shown.length} 条;整批共 ${total} 条(这个数来自服务端计数)`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/* ---------------------------------- 入口 ---------------------------------- */
|
|
898
|
-
async function main(argv) {
|
|
899
|
-
const { positionals, flags } = parseArgv(argv);
|
|
900
|
-
const cmd = positionals[0];
|
|
901
|
-
const rest = positionals.slice(1);
|
|
902
|
-
|
|
903
|
-
if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
904
|
-
console.log(USAGE);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
|
|
908
|
-
console.log(PKG_VERSION);
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const cfg = resolveConfig(flags);
|
|
913
|
-
cfg.jsonMode = flagBool(flags, 'json');
|
|
914
|
-
|
|
915
|
-
switch (cmd) {
|
|
916
|
-
case 'config':
|
|
917
|
-
return cmdConfig(cfg, rest[0], rest.slice(1));
|
|
918
|
-
case 'status':
|
|
919
|
-
return cmdStatus(cfg);
|
|
920
|
-
case 'list':
|
|
921
|
-
case 'ls':
|
|
922
|
-
return cmdList(cfg, flags);
|
|
923
|
-
case 'search':
|
|
924
|
-
return cmdSearch(cfg, rest, flags);
|
|
925
|
-
case 'get':
|
|
926
|
-
return cmdGet(cfg, rest);
|
|
927
|
-
case 'add':
|
|
928
|
-
return cmdAdd(cfg, flags);
|
|
929
|
-
case 'delete':
|
|
930
|
-
case 'rm':
|
|
931
|
-
return cmdDelete(cfg, rest);
|
|
932
|
-
case 'reclaim':
|
|
933
|
-
return cmdReclaim(cfg, flags);
|
|
934
|
-
case 'share':
|
|
935
|
-
return cmdShare(cfg, flags);
|
|
936
|
-
case 'receive':
|
|
937
|
-
return cmdReceive(cfg, rest, flags);
|
|
938
|
-
case 'undo-share':
|
|
939
|
-
return cmdUndoShare(cfg, rest, flags);
|
|
940
|
-
default:
|
|
941
|
-
fail(cfg, `未知命令: ${cmd}\n运行 \`${CLI_NAME} --help\` 查看帮助。`);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// 直接运行才进 main(import 只取 parseArgv 做单测时必须安静):npm 全局安装走
|
|
946
|
-
// node_modules/.bin 的符号链接,argv[1] 是链接路径而非真身,所以要 realpath 两侧
|
|
947
|
-
// 再比,不能用字符串相等。
|
|
948
|
-
const invokedDirectly = (() => {
|
|
949
|
-
if (process.argv[1] == null) return false;
|
|
950
|
-
try {
|
|
951
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
952
|
-
} catch {
|
|
953
|
-
return false;
|
|
954
|
-
}
|
|
955
|
-
})();
|
|
956
|
-
|
|
957
|
-
if (invokedDirectly) {
|
|
958
|
-
main(process.argv.slice(2)).catch((err) => {
|
|
959
|
-
console.error(err?.stack ?? String(err));
|
|
960
|
-
process.exit(1);
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
export { parseArgv, KNOWN_FLAGS, flagValue, flagBool, flagKeyOf };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-mneme CLI —— @modusensus/dsh-mneme 外部 API 命令行客户端
|
|
4
|
+
*
|
|
5
|
+
* 零依赖 ESM(Node >= 20,使用全局 fetch)。
|
|
6
|
+
* 配置优先级:命令行 --url/--token > 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN
|
|
7
|
+
* > 配置文件 ~/.dsh-mneme/cli.json({"url","token"})
|
|
8
|
+
* 默认服务地址:http://127.0.0.1:8790
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import fsp from 'node:fs/promises';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import process from 'node:process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const CLI_NAME = 'dsh-mneme';
|
|
19
|
+
const DEFAULT_URL = 'http://127.0.0.1:8790';
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.dsh-mneme');
|
|
21
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'cli.json');
|
|
22
|
+
// 与 src/store.js 的 TYPES 对齐(CLI 是零依赖 HTTP 客户端,不 import 服务端
|
|
23
|
+
// 模块,所以这里是手写字面量)。document 是唯一例外:它只能经
|
|
24
|
+
// memory_register_document 铸造,saveWithDedupe 会拒绝旁路写入,所以不作为
|
|
25
|
+
// --type 的选项——test/cli-enums.test.js 锁住「差值恰好是 document」。
|
|
26
|
+
const MEMORY_TYPES = ['preference', 'project', 'decision', 'history', 'summary', 'pattern', 'rejected_solution', 'pitfall', 'constraint'];
|
|
27
|
+
// hybrid:API 的 searchMemories 支持(向量先、关键词补位、双路命中做分数混合,
|
|
28
|
+
// 见 src/api.js 的 mode 注释)。此前漏列,search --mode hybrid 会被客户端自己拒掉。
|
|
29
|
+
const SEARCH_MODES = ['keyword', 'vector', 'auto', 'hybrid'];
|
|
30
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
function readPkgVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const pkgPath = path.resolve(
|
|
35
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
36
|
+
'..',
|
|
37
|
+
'package.json'
|
|
38
|
+
);
|
|
39
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? '?';
|
|
40
|
+
} catch {
|
|
41
|
+
return '?';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PKG_VERSION = readPkgVersion();
|
|
46
|
+
|
|
47
|
+
const USAGE = `${CLI_NAME} —— DSH 记忆插件(@modusensus/dsh-mneme)外部 API 命令行客户端
|
|
48
|
+
|
|
49
|
+
用法:
|
|
50
|
+
${CLI_NAME} <命令> [参数] [选项]
|
|
51
|
+
|
|
52
|
+
配置(优先级从高到低):
|
|
53
|
+
1. 命令行参数 --url <url> / --token <token>
|
|
54
|
+
2. 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN
|
|
55
|
+
3. 配置文件 ~/.dsh-mneme/cli.json(由 \`config set\` 写入)
|
|
56
|
+
未配置时使用默认地址:${DEFAULT_URL}
|
|
57
|
+
|
|
58
|
+
命令:
|
|
59
|
+
config set <url> <token> 写入配置文件(token 省略时保留原值)
|
|
60
|
+
config show 查看当前配置(token 打码显示)
|
|
61
|
+
config path 打印配置文件路径
|
|
62
|
+
status 服务状态(版本 / 记忆统计 / 实体 / 运行时长)
|
|
63
|
+
list [--type t] [--min-importance n] [--source s]
|
|
64
|
+
[--limit n] [--offset n] [--json]
|
|
65
|
+
列出记忆(type: preference | project | decision | summary | history)
|
|
66
|
+
search <query> [--mode m] [--topk n] [--json]
|
|
67
|
+
搜索记忆(mode: keyword | vector | auto)
|
|
68
|
+
get <id> [--json] 查看单条记忆
|
|
69
|
+
add --type <t> --title <t> --content <c>
|
|
70
|
+
[--importance n] [--tags a,b] [--source s] [--json]
|
|
71
|
+
新增记忆(type: preference | project | decision | summary | history)
|
|
72
|
+
delete <id> 删除记忆
|
|
73
|
+
reclaim [--older-than n] [--apply] [--vacuum] [--json]
|
|
74
|
+
存储无损回收(默认只报数字,不动数据)
|
|
75
|
+
--older-than n 输入快照保留窗口(天,默认 7;0 = 全部)
|
|
76
|
+
--apply 真的执行(不带就是 dry-run)
|
|
77
|
+
--vacuum 执行后整库 VACUUM(需配 --apply)
|
|
78
|
+
只清「可由记忆库重建的输入快照」与「归档行的向量」,
|
|
79
|
+
一条记忆都不删;收益按 VACUUM 前后体积量
|
|
80
|
+
share [--alias 名字] [--type a,b] [--tag 标签] --out <文件>
|
|
81
|
+
打一个可外发的脱敏分享包(不改库,纯读)
|
|
82
|
+
--alias 名字 在包里署名,对方看到的是这个而不是你的路径
|
|
83
|
+
--type/--tag 收窄到某几类/某个标签(超限分批用这两个)
|
|
84
|
+
--out <文件> 写出包文件;--dry-run 只报数不落盘
|
|
85
|
+
跳过归档/已遗忘/只读指针行/summary,密钥 PII 整条不进包
|
|
86
|
+
receive <包文件> [--apply]
|
|
87
|
+
把对方 share 出来的包吃进自己的库(默认干跑,一个字都不写)
|
|
88
|
+
外来行标 source=imported,不参与本机的巩固/降级,也不并入你的条目
|
|
89
|
+
--apply 才真的写;重复吃同一个包不会多出第二份
|
|
90
|
+
undo-share <包文件> [--apply]
|
|
91
|
+
按 pack_id 收起整包(物删:包还能重新导入,归档位会占住 UNIQUE)
|
|
92
|
+
|
|
93
|
+
选项:
|
|
94
|
+
--url <url> 覆盖服务地址
|
|
95
|
+
--token <token> 覆盖访问 token
|
|
96
|
+
--json 输出原始 JSON
|
|
97
|
+
-h, --help 显示本帮助
|
|
98
|
+
-V, --version 显示版本号
|
|
99
|
+
|
|
100
|
+
说明:
|
|
101
|
+
token 在 DSH 面板「设置 → 外部访问」或插件设置中查看;
|
|
102
|
+
除 GET /health 外,所有外部 API 路由均需 Bearer token。
|
|
103
|
+
|
|
104
|
+
示例:
|
|
105
|
+
${CLI_NAME} config set ${DEFAULT_URL} my-token
|
|
106
|
+
${CLI_NAME} status
|
|
107
|
+
${CLI_NAME} list --type project --limit 10
|
|
108
|
+
${CLI_NAME} search "部署流程" --mode vector --topk 5
|
|
109
|
+
${CLI_NAME} add --type decision --title "采用 SQLite" --content "存储层使用 node:sqlite" --importance 4 --tags 存储,决策
|
|
110
|
+
${CLI_NAME} get 42
|
|
111
|
+
${CLI_NAME} delete 42
|
|
112
|
+
${CLI_NAME} reclaim (先看:会清几行、库多大)
|
|
113
|
+
${CLI_NAME} reclaim --apply --vacuum (确认后执行,并把空间真的收回来)
|
|
114
|
+
${CLI_NAME} share --dry-run (先看:会进几条、为什么跳过那些,不落盘)
|
|
115
|
+
${CLI_NAME} share --out pack.json (写包;~/… 由 CLI 自己展开,PowerShell/cmd 不会)
|
|
116
|
+
${CLI_NAME} receive pack.json (吃包:默认干跑,只报账,一个字都不写)
|
|
117
|
+
${CLI_NAME} receive pack.json --apply (确认数字后真的吃进自己的库;重跑同一个包 created=0)
|
|
118
|
+
${CLI_NAME} undo-share pack.json --apply (按包里那个 pack_id 整包收回:物删,之后还能重新导入)`;
|
|
119
|
+
|
|
120
|
+
/* ---------------------------------- 参数解析 ---------------------------------- */
|
|
121
|
+
|
|
122
|
+
// `--flag value` 形态下,「下一个词以 -- 开头就别当值」这条规则有一个致命反例:
|
|
123
|
+
// 值本身就可能是 `--` 开头的字符串。外部 API 的 token 由
|
|
124
|
+
// `crypto.randomBytes(24).toString("base64url")` 生成(src/api-standalone.js),
|
|
125
|
+
// base64url 的字母表含 `-`,所以约 1/4096 的 token 头两位恰好都是 `-`:
|
|
126
|
+
//
|
|
127
|
+
// dsh-mneme reclaim --url http://127.0.0.1:55423 --token --UYoOHxr6PtE2-bfqW...
|
|
128
|
+
//
|
|
129
|
+
// 旧规则把 `--UYoOHxr6PtE2-bfqW...` 当成一个新旗标(键名 `uyoohxr6pte2-bfqw...`),
|
|
130
|
+
// 于是 token 解析成 `true`,flagValue 视作「没给」→ 退回环境变量/配置文件 → 全空
|
|
131
|
+
// → 报「未配置访问 token」。而 CLI 生成 token 的另一半同样会撞:`--token=--abc`
|
|
132
|
+
// 走等号分支没事,`--token --abc` 就废。这不是测试的随机性,是真实用户的随机性。
|
|
133
|
+
//
|
|
134
|
+
// 修法:区分「已知旗标」与「未知词」。下一个词以 `--` 开头时,只有它确实是一个
|
|
135
|
+
// 已注册的旗标才当作旗标,否则当作当前 flag 的值。于是 `--limit --json` 仍按
|
|
136
|
+
// 旧语义把 --json 当旗标,而 `--token --UYo...` 正确取到 token。
|
|
137
|
+
// 已知旗标表必须与下面 flagValue/flagBool 的实际用法保持同步;漏登记的代价是
|
|
138
|
+
// 该旗标被当成值吞掉,所以 test/cli-flag-value.test.js 用文本锁对齐两者。
|
|
139
|
+
const KNOWN_FLAGS = new Set([
|
|
140
|
+
// 取值的旗标
|
|
141
|
+
'url', 'token', 'type', 'title', 'content', 'importance', 'tags', 'source',
|
|
142
|
+
'limit', 'offset', 'mode', 'topk', 'top-k', 'min-importance', 'minimportance',
|
|
143
|
+
'older-than', 'olderthandays',
|
|
144
|
+
'alias', 'tag', 'out',
|
|
145
|
+
// 布尔旗标
|
|
146
|
+
'apply', 'vacuum', 'json', 'help', 'version',
|
|
147
|
+
'dry-run', 'dryrun',
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
/** `--foo=bar` / `--foo` 取出小写键名;非旗标词返回 null。 */
|
|
151
|
+
function flagKeyOf(word) {
|
|
152
|
+
if (typeof word !== 'string' || !word.startsWith('--') || word === '--') return null;
|
|
153
|
+
const key = word.slice(2);
|
|
154
|
+
const eq = key.indexOf('=');
|
|
155
|
+
return (eq >= 0 ? key.slice(0, eq) : key).toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseArgv(argv) {
|
|
159
|
+
const positionals = [];
|
|
160
|
+
const flags = {};
|
|
161
|
+
for (let i = 0; i < argv.length; i++) {
|
|
162
|
+
const arg = argv[i];
|
|
163
|
+
if (arg === '--') {
|
|
164
|
+
positionals.push(...argv.slice(i + 1));
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (arg.startsWith('--')) {
|
|
168
|
+
let key = arg.slice(2);
|
|
169
|
+
let value;
|
|
170
|
+
const eq = key.indexOf('=');
|
|
171
|
+
if (eq >= 0) {
|
|
172
|
+
value = key.slice(eq + 1);
|
|
173
|
+
key = key.slice(0, eq);
|
|
174
|
+
} else {
|
|
175
|
+
const next = argv[i + 1];
|
|
176
|
+
const nextIsFlag = next !== undefined && flagKeyOf(next) !== null && KNOWN_FLAGS.has(flagKeyOf(next));
|
|
177
|
+
if (next !== undefined && next !== '--' && !nextIsFlag) {
|
|
178
|
+
value = argv[++i];
|
|
179
|
+
} else {
|
|
180
|
+
value = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
flags[key.toLowerCase()] = value;
|
|
184
|
+
} else {
|
|
185
|
+
positionals.push(arg);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { positionals, flags };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function flagValue(flags, ...names) {
|
|
192
|
+
for (const name of names) {
|
|
193
|
+
const value = flags[name.toLowerCase()];
|
|
194
|
+
if (value !== undefined && value !== true && value !== '') return value;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function flagBool(flags, ...names) {
|
|
200
|
+
for (const name of names) {
|
|
201
|
+
const value = flags[name.toLowerCase()];
|
|
202
|
+
if (value !== undefined) return value === true || value === 'true' || value === '';
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ---------------------------------- 配置解析 ---------------------------------- */
|
|
208
|
+
|
|
209
|
+
function readConfigFile() {
|
|
210
|
+
try {
|
|
211
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
212
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
213
|
+
} catch {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function firstNonEmpty(...values) {
|
|
219
|
+
for (const value of values) {
|
|
220
|
+
if (typeof value === 'string' && value.trim() !== '') return value.trim();
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveConfig(flags) {
|
|
226
|
+
const file = readConfigFile();
|
|
227
|
+
const url =
|
|
228
|
+
firstNonEmpty(flagValue(flags, 'url'), process.env.DSH_MNEME_URL, file.url) ?? DEFAULT_URL;
|
|
229
|
+
const token =
|
|
230
|
+
firstNonEmpty(flagValue(flags, 'token'), process.env.DSH_MNEME_TOKEN, file.token) ?? '';
|
|
231
|
+
return { url: url.replace(/\/+$/, ''), token, jsonMode: false };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function maskToken(token) {
|
|
235
|
+
if (!token) return '(未设置)';
|
|
236
|
+
if (token.length <= 8) return '****';
|
|
237
|
+
return `${token.slice(0, 4)}****${token.slice(-4)}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ---------------------------------- 输出与错误 ---------------------------------- */
|
|
241
|
+
|
|
242
|
+
function stringify(value) {
|
|
243
|
+
return JSON.stringify(value, null, 2);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function fail(cfg, message, { rawJson } = {}) {
|
|
247
|
+
if (cfg.jsonMode) {
|
|
248
|
+
console.error(rawJson !== undefined ? rawJson : stringify({ error: message }));
|
|
249
|
+
} else {
|
|
250
|
+
console.error(message);
|
|
251
|
+
}
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function failHttp(cfg, status, pathname, data, text) {
|
|
256
|
+
const serverError =
|
|
257
|
+
data && typeof data === 'object' && data.error
|
|
258
|
+
? String(data.error)
|
|
259
|
+
: (text || '').slice(0, 200);
|
|
260
|
+
let hint = '';
|
|
261
|
+
if (status === 401) {
|
|
262
|
+
hint =
|
|
263
|
+
`\n访问 token 缺失或无效。token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看,\n` +
|
|
264
|
+
`然后用 \`${CLI_NAME} config set <url> <token>\`、环境变量 DSH_MNEME_TOKEN 或 --token 配置。`;
|
|
265
|
+
} else if (status === 404) {
|
|
266
|
+
hint = `\n资源不存在:${pathname}`;
|
|
267
|
+
}
|
|
268
|
+
// 拒答体自己带的下一步必须转出去,不然命令侧只能再写一份错误分支。撞上的这一刀是
|
|
269
|
+
// share 的 413:响应体是 {error:"too-large", bytes, limitBytes, hint, report},
|
|
270
|
+
// 旧实现只取 data.error,于是现场只剩「请求失败(HTTP 413):too-large」——「大了
|
|
271
|
+
// 多少」与「用 --type/--tag 分批」那两句留在体里没人看得见(措辞只许有一份,就在
|
|
272
|
+
// 拒发的那侧,同 RECLAIM_CONFIRM_HINT 的口径)。完整 report(含 skip_list)走
|
|
273
|
+
// --json:下面 rawJson 把整个响应体原样交给 stderr,这里不复制第二份打印器。
|
|
274
|
+
//
|
|
275
|
+
// 已知取舍(C10 记档,见 docs/SHARE.md「已知还没做」):这里只转 hint / bytes /
|
|
276
|
+
// limitBytes 三样,所以 receive 那侧 400 的 `detail`(闸门指到哪一格)与数据面 413 的
|
|
277
|
+
// `limit` 到不了人类可读的输出。刻意不补:补一格就得在这里多一条按路由分支的判断,
|
|
278
|
+
// 而「错误文案只许有一份」是 #411 那条纪律的另一半。要逐格看就加 --json——rawJson
|
|
279
|
+
// 那行给的正是原样响应体,一格都不丢。
|
|
280
|
+
if (data && typeof data === 'object') {
|
|
281
|
+
if (typeof data.hint === 'string' && data.hint.trim()) hint += `\n${data.hint.trim()}`;
|
|
282
|
+
if (Number.isFinite(data.bytes) && Number.isFinite(data.limitBytes)) {
|
|
283
|
+
hint += `\n量到 ${formatBytes(data.bytes)},水位 ${formatBytes(data.limitBytes)}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
fail(cfg, `请求失败(HTTP ${status})${serverError ? `:${serverError}` : ''} [${pathname}]${hint}`, {
|
|
287
|
+
rawJson: cfg.jsonMode && text ? text : undefined,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function failNetwork(cfg, url, err) {
|
|
292
|
+
const timedOut = err && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
293
|
+
const reason = timedOut
|
|
294
|
+
? `请求超时(${REQUEST_TIMEOUT_MS / 1000} 秒)`
|
|
295
|
+
: `无法连接到服务(${err?.code ?? err?.cause?.code ?? err?.message ?? '未知错误'})`;
|
|
296
|
+
fail(
|
|
297
|
+
cfg,
|
|
298
|
+
`${reason}:${url}\n` +
|
|
299
|
+
`请确认 DSH 已启动、插件外部 API 已开启,且地址正确(默认 ${DEFAULT_URL})。`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function requireToken(cfg) {
|
|
304
|
+
if (cfg.token) return;
|
|
305
|
+
fail(
|
|
306
|
+
cfg,
|
|
307
|
+
[
|
|
308
|
+
'未配置访问 token,无法调用该命令(外部 API 需要 Bearer 鉴权)。',
|
|
309
|
+
'',
|
|
310
|
+
'token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看,然后任选其一配置:',
|
|
311
|
+
` 1. ${CLI_NAME} config set <url> <token>`,
|
|
312
|
+
' 2. 环境变量 DSH_MNEME_TOKEN=<token>',
|
|
313
|
+
' 3. 命令行参数 --token <token>',
|
|
314
|
+
].join('\n')
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseIntArg(cfg, value, label) {
|
|
319
|
+
if (value === undefined || value === true || value === '') return undefined;
|
|
320
|
+
const n = Number(value);
|
|
321
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
322
|
+
fail(cfg, `无效的 ${label}: ${value}(应为非负整数)`);
|
|
323
|
+
}
|
|
324
|
+
return n;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ---------------------------------- HTTP 请求 ---------------------------------- */
|
|
328
|
+
|
|
329
|
+
async function apiRequest(cfg, method, pathname, { query, body } = {}) {
|
|
330
|
+
let url = cfg.url + pathname;
|
|
331
|
+
if (query) {
|
|
332
|
+
const params = new URLSearchParams();
|
|
333
|
+
for (const [key, value] of Object.entries(query)) {
|
|
334
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
335
|
+
params.set(key, String(value));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const qs = params.toString();
|
|
339
|
+
if (qs) url += `?${qs}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const headers = { Accept: 'application/json' };
|
|
343
|
+
if (cfg.token) headers.Authorization = `Bearer ${cfg.token}`;
|
|
344
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
345
|
+
|
|
346
|
+
let res;
|
|
347
|
+
try {
|
|
348
|
+
res = await fetch(url, {
|
|
349
|
+
method,
|
|
350
|
+
headers,
|
|
351
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
352
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
353
|
+
});
|
|
354
|
+
} catch (err) {
|
|
355
|
+
failNetwork(cfg, url, err);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const text = await res.text();
|
|
359
|
+
let data = null;
|
|
360
|
+
try {
|
|
361
|
+
data = text ? JSON.parse(text) : null;
|
|
362
|
+
} catch {
|
|
363
|
+
// 非 JSON 响应体,保留原始文本用于报错展示
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!res.ok) {
|
|
367
|
+
failHttp(cfg, res.status, pathname, data, text);
|
|
368
|
+
}
|
|
369
|
+
return data;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* ---------------------------------- 结果格式化 ---------------------------------- */
|
|
373
|
+
|
|
374
|
+
function formatTimestamp(value) {
|
|
375
|
+
if (!value) return '';
|
|
376
|
+
const s = String(value);
|
|
377
|
+
return s.length >= 10 ? s.slice(0, 10) : s;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function formatMemoryLine(item) {
|
|
381
|
+
const id = item?.id ?? '?';
|
|
382
|
+
const type = item?.type ?? '?';
|
|
383
|
+
const importance = item?.importance != null ? ` ★${item.importance}` : '';
|
|
384
|
+
const title = item?.title ? ` ${item.title}` : '';
|
|
385
|
+
const tags =
|
|
386
|
+
Array.isArray(item?.tags) && item.tags.length ? ` [${item.tags.join(',')}]` : '';
|
|
387
|
+
const date = formatTimestamp(item?.created_at ?? item?.createdAt);
|
|
388
|
+
return ` #${id} [${type}]${importance}${title}${tags}${date ? ` (${date})` : ''}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function printMemoryItems(payload, { withMode = false } = {}) {
|
|
392
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
393
|
+
const total = payload?.total ?? items.length;
|
|
394
|
+
if (withMode && payload?.mode) console.log(`搜索模式: ${payload.mode}`);
|
|
395
|
+
console.log(`共 ${total} 条,本次显示 ${items.length} 条`);
|
|
396
|
+
if (!items.length) return;
|
|
397
|
+
console.log('');
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
const score = item?.score ?? item?.similarity;
|
|
400
|
+
const scoreText =
|
|
401
|
+
typeof score === 'number' && Number.isFinite(score) ? ` (score ${score.toFixed(3)})` : '';
|
|
402
|
+
console.log(formatMemoryLine(item) + scoreText);
|
|
403
|
+
const content =
|
|
404
|
+
typeof item?.content === 'string' ? item.content.replace(/\s+/g, ' ').trim() : '';
|
|
405
|
+
if (content) {
|
|
406
|
+
console.log(` ${content.length > 80 ? `${content.slice(0, 80)}…` : content}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function printMemoryDetail(item) {
|
|
412
|
+
if (!item || typeof item !== 'object') {
|
|
413
|
+
console.log(stringify(item ?? null));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const fields = [];
|
|
417
|
+
if (item.id !== undefined) fields.push(`id: ${item.id}`);
|
|
418
|
+
if (item.type !== undefined) fields.push(`类型: ${item.type}`);
|
|
419
|
+
if (item.importance !== undefined) fields.push(`重要性: ${item.importance}`);
|
|
420
|
+
if (item.title !== undefined) fields.push(`标题: ${item.title}`);
|
|
421
|
+
if (item.tags !== undefined) {
|
|
422
|
+
fields.push(`标签: ${Array.isArray(item.tags) ? item.tags.join(', ') : item.tags}`);
|
|
423
|
+
}
|
|
424
|
+
if (item.source !== undefined) fields.push(`来源: ${item.source}`);
|
|
425
|
+
const created = item.created_at ?? item.createdAt;
|
|
426
|
+
if (created) fields.push(`创建时间: ${created}`);
|
|
427
|
+
for (const line of fields) console.log(line);
|
|
428
|
+
if (item.content !== undefined) console.log(`\n${item.content}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function printStatus(data) {
|
|
432
|
+
console.log(`服务版本: ${data?.version ?? '?'}`);
|
|
433
|
+
console.log(`运行时长: ${data?.uptime_s ?? data?.uptimeS ?? '?'} 秒`);
|
|
434
|
+
const memories = data?.memories ?? {};
|
|
435
|
+
console.log(`记忆总数: ${memories.total ?? '?'}`);
|
|
436
|
+
const byType = memories.byType;
|
|
437
|
+
if (Array.isArray(byType)) {
|
|
438
|
+
for (const row of byType) {
|
|
439
|
+
console.log(` - ${row?.type ?? row?.name ?? '?'}: ${row?.count ?? row?.total ?? '?'}`);
|
|
440
|
+
}
|
|
441
|
+
} else if (byType && typeof byType === 'object') {
|
|
442
|
+
for (const [type, count] of Object.entries(byType)) {
|
|
443
|
+
console.log(` - ${type}: ${count}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const entities = data?.entities;
|
|
447
|
+
if (entities != null) {
|
|
448
|
+
const total =
|
|
449
|
+
typeof entities === 'object' && !Array.isArray(entities)
|
|
450
|
+
? entities.total
|
|
451
|
+
: entities;
|
|
452
|
+
console.log(`实体数量: ${total ?? '?'}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* ---------------------------------- 子命令 ---------------------------------- */
|
|
457
|
+
|
|
458
|
+
async function cmdConfig(cfg, sub, rest) {
|
|
459
|
+
const subCmd = sub || 'show';
|
|
460
|
+
|
|
461
|
+
if (subCmd === 'set') {
|
|
462
|
+
const [url, tokenArg] = rest;
|
|
463
|
+
if (!url) {
|
|
464
|
+
fail(
|
|
465
|
+
cfg,
|
|
466
|
+
`用法: ${CLI_NAME} config set <url> <token>\n示例: ${CLI_NAME} config set ${DEFAULT_URL} my-token`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
470
|
+
fail(cfg, `无效的服务地址: ${url}(应以 http:// 或 https:// 开头)`);
|
|
471
|
+
}
|
|
472
|
+
const existing = readConfigFile();
|
|
473
|
+
const token = tokenArg ?? existing.token ?? '';
|
|
474
|
+
await fsp.mkdir(CONFIG_DIR, { recursive: true });
|
|
475
|
+
await fsp.writeFile(CONFIG_PATH, `${JSON.stringify({ url, token }, null, 2)}\n`, 'utf8');
|
|
476
|
+
if (cfg.jsonMode) {
|
|
477
|
+
console.log(stringify({ configPath: CONFIG_PATH, url, tokenConfigured: Boolean(token) }));
|
|
478
|
+
} else {
|
|
479
|
+
console.log(`已写入配置文件: ${CONFIG_PATH}`);
|
|
480
|
+
console.log(` url: ${url}`);
|
|
481
|
+
console.log(` token: ${maskToken(token)}`);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (subCmd === 'show') {
|
|
487
|
+
const hasFile = fs.existsSync(CONFIG_PATH);
|
|
488
|
+
const file = readConfigFile();
|
|
489
|
+
if (cfg.jsonMode) {
|
|
490
|
+
console.log(
|
|
491
|
+
stringify({
|
|
492
|
+
configPath: CONFIG_PATH,
|
|
493
|
+
configExists: hasFile,
|
|
494
|
+
url: file.url ?? DEFAULT_URL,
|
|
495
|
+
token: maskToken(file.token),
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!hasFile) {
|
|
501
|
+
console.log(`尚未创建配置文件(未配置): ${CONFIG_PATH}`);
|
|
502
|
+
console.log('');
|
|
503
|
+
console.log(`可运行 \`${CLI_NAME} config set <url> <token>\` 创建配置;`);
|
|
504
|
+
console.log('token 可在 DSH 面板「设置 → 外部访问」或插件设置中查看。');
|
|
505
|
+
console.log(
|
|
506
|
+
`未配置时使用默认地址 ${DEFAULT_URL},也可用环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN。`
|
|
507
|
+
);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
console.log(`配置文件: ${CONFIG_PATH}`);
|
|
511
|
+
console.log(` url: ${file.url ?? `(未设置,默认 ${DEFAULT_URL})`}`);
|
|
512
|
+
console.log(` token: ${maskToken(file.token)}`);
|
|
513
|
+
console.log('');
|
|
514
|
+
console.log('配置优先级: --url/--token > 环境变量 DSH_MNEME_URL / DSH_MNEME_TOKEN > 配置文件');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (subCmd === 'path') {
|
|
519
|
+
console.log(CONFIG_PATH);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
fail(cfg, `未知的 config 子命令: ${subCmd}\n可用子命令: set / show / path`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function cmdStatus(cfg) {
|
|
527
|
+
requireToken(cfg);
|
|
528
|
+
const data = await apiRequest(cfg, 'GET', '/status');
|
|
529
|
+
if (cfg.jsonMode) {
|
|
530
|
+
console.log(stringify(data));
|
|
531
|
+
} else {
|
|
532
|
+
printStatus(data);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function cmdList(cfg, flags) {
|
|
537
|
+
requireToken(cfg);
|
|
538
|
+
const query = {
|
|
539
|
+
type: flagValue(flags, 'type'),
|
|
540
|
+
minImportance: parseIntArg(cfg, flagValue(flags, 'min-importance', 'minImportance'), '--min-importance'),
|
|
541
|
+
source: flagValue(flags, 'source'),
|
|
542
|
+
limit: parseIntArg(cfg, flagValue(flags, 'limit'), '--limit'),
|
|
543
|
+
offset: parseIntArg(cfg, flagValue(flags, 'offset'), '--offset'),
|
|
544
|
+
};
|
|
545
|
+
const data = await apiRequest(cfg, 'GET', '/memories', { query });
|
|
546
|
+
if (cfg.jsonMode) {
|
|
547
|
+
console.log(stringify(data));
|
|
548
|
+
} else {
|
|
549
|
+
printMemoryItems(data);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function cmdSearch(cfg, rest, flags) {
|
|
554
|
+
requireToken(cfg);
|
|
555
|
+
const q = rest[0];
|
|
556
|
+
if (!q) {
|
|
557
|
+
fail(
|
|
558
|
+
cfg,
|
|
559
|
+
`用法: ${CLI_NAME} search <query> [--mode m] [--topk n]\n` +
|
|
560
|
+
`示例: ${CLI_NAME} search "部署流程" --mode vector --topk 5`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
const mode = flagValue(flags, 'mode');
|
|
564
|
+
if (mode !== undefined && !SEARCH_MODES.includes(mode)) {
|
|
565
|
+
fail(cfg, `无效的 --mode: ${mode}(可选: ${SEARCH_MODES.join(' | ')})`);
|
|
566
|
+
}
|
|
567
|
+
const topK = parseIntArg(cfg, flagValue(flags, 'topk', 'top-k', 'topK'), '--topk');
|
|
568
|
+
const data = await apiRequest(cfg, 'GET', '/search', { query: { q, mode, topK } });
|
|
569
|
+
if (cfg.jsonMode) {
|
|
570
|
+
console.log(stringify(data));
|
|
571
|
+
} else {
|
|
572
|
+
printMemoryItems(data, { withMode: true });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function cmdGet(cfg, rest) {
|
|
577
|
+
requireToken(cfg);
|
|
578
|
+
const id = rest[0];
|
|
579
|
+
if (!id) {
|
|
580
|
+
fail(cfg, `用法: ${CLI_NAME} get <id> [--json]`);
|
|
581
|
+
}
|
|
582
|
+
const data = await apiRequest(cfg, 'GET', `/memories/${encodeURIComponent(id)}`);
|
|
583
|
+
if (cfg.jsonMode) {
|
|
584
|
+
console.log(stringify(data));
|
|
585
|
+
} else {
|
|
586
|
+
printMemoryDetail(data);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function cmdAdd(cfg, flags) {
|
|
591
|
+
requireToken(cfg);
|
|
592
|
+
const type = flagValue(flags, 'type');
|
|
593
|
+
const title = flagValue(flags, 'title');
|
|
594
|
+
const content = flagValue(flags, 'content');
|
|
595
|
+
if (!type || !title || !content) {
|
|
596
|
+
fail(
|
|
597
|
+
cfg,
|
|
598
|
+
`用法: ${CLI_NAME} add --type <${MEMORY_TYPES.join('|')}> --title <标题> --content <内容>\n` +
|
|
599
|
+
` [--importance n] [--tags a,b] [--source s]\n\n` +
|
|
600
|
+
`示例: ${CLI_NAME} add --type decision --title "采用 SQLite" --content "存储层使用 node:sqlite"`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (!MEMORY_TYPES.includes(type)) {
|
|
604
|
+
fail(cfg, `无效的 --type: ${type}(可选: ${MEMORY_TYPES.join(' | ')})`);
|
|
605
|
+
}
|
|
606
|
+
const importanceRaw = flagValue(flags, 'importance');
|
|
607
|
+
let importance;
|
|
608
|
+
if (importanceRaw !== undefined) {
|
|
609
|
+
importance = Number(importanceRaw);
|
|
610
|
+
if (!Number.isInteger(importance) || importance < 1 || importance > 5) {
|
|
611
|
+
fail(cfg, `无效的 --importance: ${importanceRaw}(应为 1-5 的整数)`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const tagsRaw = flagValue(flags, 'tags');
|
|
615
|
+
const tags =
|
|
616
|
+
tagsRaw !== undefined
|
|
617
|
+
? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean)
|
|
618
|
+
: undefined;
|
|
619
|
+
const source = flagValue(flags, 'source');
|
|
620
|
+
|
|
621
|
+
const body = { type, title, content };
|
|
622
|
+
if (importance !== undefined) body.importance = importance;
|
|
623
|
+
if (tags) body.tags = tags;
|
|
624
|
+
if (source) body.source = source;
|
|
625
|
+
|
|
626
|
+
const data = await apiRequest(cfg, 'POST', '/memories', { body });
|
|
627
|
+
if (cfg.jsonMode) {
|
|
628
|
+
console.log(stringify(data));
|
|
629
|
+
} else {
|
|
630
|
+
console.log('已添加记忆:');
|
|
631
|
+
printMemoryDetail(data);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function cmdDelete(cfg, rest) {
|
|
636
|
+
requireToken(cfg);
|
|
637
|
+
const id = rest[0];
|
|
638
|
+
if (!id) {
|
|
639
|
+
fail(cfg, `用法: ${CLI_NAME} delete <id>`);
|
|
640
|
+
}
|
|
641
|
+
const data = await apiRequest(cfg, 'DELETE', `/memories/${encodeURIComponent(id)}`);
|
|
642
|
+
if (cfg.jsonMode) {
|
|
643
|
+
console.log(stringify(data));
|
|
644
|
+
} else {
|
|
645
|
+
console.log(`已删除记忆 ${id}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function formatBytes(bytes) {
|
|
650
|
+
if (typeof bytes !== 'number' || !Number.isFinite(bytes)) return '?';
|
|
651
|
+
const mb = bytes / (1024 * 1024);
|
|
652
|
+
if (mb >= 1) return `${mb.toFixed(1)} MB`;
|
|
653
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function printReclaim(report, { applied }) {
|
|
657
|
+
const planned = report?.planned ?? {};
|
|
658
|
+
const cleared = report?.cleared ?? {};
|
|
659
|
+
const columnBytes = report?.column_bytes ?? {};
|
|
660
|
+
const size = report?.size ?? {};
|
|
661
|
+
console.log(`存储无损回收(${applied ? '已执行' : 'dry-run,未改动任何数据'})`);
|
|
662
|
+
console.log(` 输入快照保留窗口: ${report?.olderThanDays ?? '?'} 天(cutoff ${report?.cutoff ?? '?'})`);
|
|
663
|
+
// 第三项(#399):蒸馏原文的到期裁剪同样只删 distill_records,溯源链接刻意保留
|
|
664
|
+
const rows = (o) =>
|
|
665
|
+
`dream_runs.input ${o.dream_run_inputs ?? 0} 行 / 归档行向量 ${o.archived_embeddings ?? 0} 行 / 蒸馏原文 ${o.distill_records ?? 0} 行`;
|
|
666
|
+
console.log(` ${applied ? '已清理' : '将清理'}: ${rows(applied ? cleared : planned)}`);
|
|
667
|
+
console.log(
|
|
668
|
+
` 列文本大小(上界,非回收量): ${formatBytes(columnBytes.dream_run_inputs)} / ${formatBytes(columnBytes.archived_embeddings)} / ${formatBytes(columnBytes.distill_records)}`
|
|
669
|
+
);
|
|
670
|
+
if (!applied) {
|
|
671
|
+
console.log(
|
|
672
|
+
` 库文件: ${formatBytes(size.before_bytes)}(空闲页 ${size.freelist_pages ?? '?'} 页,可立刻回收约 ${formatBytes(size.freelist_bytes)})`
|
|
673
|
+
);
|
|
674
|
+
console.log('');
|
|
675
|
+
console.log(' 上面是 dry-run 的数字。确认后执行:');
|
|
676
|
+
console.log(` ${CLI_NAME} reclaim --apply --vacuum`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const freed =
|
|
680
|
+
typeof size.before_bytes === 'number' && typeof size.after_bytes === 'number'
|
|
681
|
+
? size.before_bytes - size.after_bytes
|
|
682
|
+
: null;
|
|
683
|
+
if (report?.vacuum?.error) {
|
|
684
|
+
console.log(` VACUUM: 失败(${String(report.vacuum.error).slice(0, 120)})`);
|
|
685
|
+
console.log(' 清理已经完成且不可逆,文件暂时不会变小;库多半被别的进程占着锁,');
|
|
686
|
+
console.log(' 稍后重跑一次 `reclaim --apply --vacuum` 即可(清理是幂等的)。');
|
|
687
|
+
} else {
|
|
688
|
+
console.log(
|
|
689
|
+
` VACUUM: ${report?.vacuum?.ran ? `已跑(${report.vacuum.duration_ms ?? '?'} ms)` : '未跑(未带 --vacuum,文件不会变小)'}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
console.log(
|
|
693
|
+
` 库文件: ${formatBytes(size.before_bytes)} → ${formatBytes(size.after_bytes)}` +
|
|
694
|
+
(freed !== null && freed > 0 ? `(释放 ${formatBytes(freed)})` : '')
|
|
695
|
+
);
|
|
696
|
+
console.log(` 耗时: ${report?.duration_ms ?? '?'} ms`);
|
|
697
|
+
if (report?.receipt) console.log(` 回执: llm_audit_logs #${report.receipt}`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function cmdReclaim(cfg, flags) {
|
|
701
|
+
requireToken(cfg);
|
|
702
|
+
const applied = flagBool(flags, 'apply');
|
|
703
|
+
const vacuum = flagBool(flags, 'vacuum');
|
|
704
|
+
if (vacuum && !applied) {
|
|
705
|
+
fail(
|
|
706
|
+
cfg,
|
|
707
|
+
`--vacuum 需要配 --apply:dry-run 不改数据,也就没有可回收的页。\n` +
|
|
708
|
+
`先看数字,再执行:${CLI_NAME} reclaim --apply --vacuum`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
const olderThanDays = parseIntArg(cfg, flagValue(flags, 'older-than', 'olderThanDays'), '--older-than');
|
|
712
|
+
const body = { dryRun: !applied };
|
|
713
|
+
if (applied) {
|
|
714
|
+
// 服务端要求显式确认:这两步不可逆,缺 confirm 只会拿回一份 dry-run 报告。
|
|
715
|
+
body.confirm = true;
|
|
716
|
+
}
|
|
717
|
+
if (olderThanDays !== undefined) body.olderThanDays = olderThanDays;
|
|
718
|
+
if (vacuum) body.vacuum = true;
|
|
719
|
+
const data = await apiRequest(cfg, 'POST', '/maintenance/reclaim', { body });
|
|
720
|
+
if (cfg.jsonMode) {
|
|
721
|
+
console.log(stringify(data));
|
|
722
|
+
} else {
|
|
723
|
+
printReclaim(data, { applied });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// --out 的例子写的是 `~/Desktop/pack.json`。Git Bash 会替她展开,PowerShell 与 cmd
|
|
728
|
+
// 不会——那两侧 `~/…` 是字面量,writeFileSync 直接 ENOENT,现场只剩一坨堆栈。CLI 不
|
|
729
|
+
// 接管 shell 的活,但引号里这一格得自己补(同 CONFIG_DIR 走 os.homedir())。
|
|
730
|
+
function expandHome(p) {
|
|
731
|
+
if (typeof p !== 'string') return p;
|
|
732
|
+
if (p !== '~' && !p.startsWith('~/') && !p.startsWith('~\\')) return p;
|
|
733
|
+
const rest = p.slice(1).replace(/^[/\\]+/, '');
|
|
734
|
+
return path.join(os.homedir(), rest);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function printShare(pack, { dryRun }) {
|
|
738
|
+
const r = pack?.report ?? {};
|
|
739
|
+
const s = r.skipped ?? {};
|
|
740
|
+
console.log(`${dryRun ? '干跑报告' : '分享包'}: ${r.included ?? 0} 条` +
|
|
741
|
+
(pack?.alias ? ` 来自 ${pack.alias}` : ''));
|
|
742
|
+
const named = [['archived', '归档'], ['forgotten', '已遗忘'], ['readonly_type', '只读指针行'],
|
|
743
|
+
['excluded_type', '不随包走'], ['blank_or_noise', '空白/噪声'], ['sensitive', '密钥/PII']];
|
|
744
|
+
const parts = named.filter(([k]) => (s[k] ?? 0) > 0).map(([k, zh]) => `${zh} ${s[k]}`);
|
|
745
|
+
console.log(` 跳过: ${parts.length ? parts.join(', ') : '无'}`);
|
|
746
|
+
// 折叠不是跳过:那条记忆够格进包,只是包里已经有了它(同标题同正文同内容锚、scope
|
|
747
|
+
// 不同的两条)。不印出来的话 `included` 会比她心里的「够格的条数」少几条,而那正好是
|
|
748
|
+
// 会让人怀疑口径少发了的那类差额。src/share.js 的 report.deduped。
|
|
749
|
+
if ((r.deduped ?? 0) > 0) console.log(` 包内重复已折叠: ${r.deduped} 条(对面只吃一条)`);
|
|
750
|
+
for (const item of (r.skip_list ?? []).slice(0, 20)) {
|
|
751
|
+
// 标题为空有两种:这条本来没标题,或它整行被密钥判据命中、标题在源头就留空了
|
|
752
|
+
// (src/share.js:skip_list 随包外发,而凭据常常就写在标题里)。终端要让人看清
|
|
753
|
+
// 后一种是「有意不给」,不是「数据缺失」。
|
|
754
|
+
console.log(` - [${item.reason}] ${item.title || '(标题不外发)'}`);
|
|
755
|
+
}
|
|
756
|
+
// 判据用「六个计数之和 > 清单长度」而不是「清单等于 50」:前者是「账比清单长」这个事实本身,
|
|
757
|
+
// 恰好 50 条被跳时后者会误报截断(面板侧同一条判据)。50 这个数不在这里出现,
|
|
758
|
+
// 水位改了判据仍然对。
|
|
759
|
+
const skippedTotal = ['archived', 'forgotten', 'readonly_type', 'excluded_type', 'blank_or_noise', 'sensitive']
|
|
760
|
+
.reduce((n, k) => n + (r.skipped?.[k] ?? 0), 0);
|
|
761
|
+
if (skippedTotal > (r.skip_list ?? []).length) console.log(' … 跳过清单已截断,计数仍为全量');
|
|
762
|
+
if (dryRun) console.log(` 没写文件:加 --out <路径> 才会落盘`);
|
|
763
|
+
else console.log(` 发给对方即可,她用自己的 ${CLI_NAME} receive 吃进去`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function cmdShare(cfg, flags) {
|
|
767
|
+
requireToken(cfg);
|
|
768
|
+
const out = expandHome(flagValue(flags, 'out'));
|
|
769
|
+
const dryRun = flagBool(flags, 'dry-run') || flagBool(flags, 'dryrun');
|
|
770
|
+
if (!out && !dryRun) {
|
|
771
|
+
fail(
|
|
772
|
+
cfg,
|
|
773
|
+
`share 要么写文件(--out <路径>),要么只看报告(--dry-run):` +
|
|
774
|
+
`不写文件又把整库打到 stdout 会让报告被 JSON 淹掉。\n` +
|
|
775
|
+
`例子:${CLI_NAME} share --out ~/Desktop/pack.json`
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
const typeRaw = flagValue(flags, 'type');
|
|
779
|
+
const body = {};
|
|
780
|
+
if (typeRaw) body.type = typeRaw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
781
|
+
const alias = flagValue(flags, 'alias');
|
|
782
|
+
if (alias) body.alias = alias;
|
|
783
|
+
const tag = flagValue(flags, 'tag');
|
|
784
|
+
if (tag) body.tag = tag;
|
|
785
|
+
// 超水位那次服务端答 413,apiRequest 对非 2xx 走的是 failHttp:它把响应体里的 hint
|
|
786
|
+
// 与量到的字节转出去再 exit(1)。所以这里**没有**可达的 error 分支——加 `if
|
|
787
|
+
// (pack?.error)` 就是第二份出口,措辞迟早跟拒发那一侧分家(#411 的同一口径)。
|
|
788
|
+
const pack = await apiRequest(cfg, 'POST', '/share', { body });
|
|
789
|
+
const r = pack?.report ?? {};
|
|
790
|
+
if (cfg.jsonMode) {
|
|
791
|
+
console.log(stringify(dryRun ? { report: r, pack } : { report: r, file: out }));
|
|
792
|
+
} else {
|
|
793
|
+
printShare(pack, { dryRun });
|
|
794
|
+
}
|
|
795
|
+
if (dryRun) return;
|
|
796
|
+
fs.writeFileSync(out, JSON.stringify(pack), 'utf8');
|
|
797
|
+
// --json 时 stdout 只许那一份 JSON:后面追一行「已写出」会把 JSON.parse 撞在
|
|
798
|
+
// position 536(jq 同理)。落盘路径已在上面的 payload 里(file 键),这行只对
|
|
799
|
+
// 人有用,就只给人看(同 cmdReclaim:json 模式打印完就结束)。
|
|
800
|
+
if (!cfg.jsonMode) console.log(` 已写出: ${out}`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 入库面(车道 C)。包文件在**本地**读、把包内容整个送上去:服务端永远看不到路径,
|
|
804
|
+
// 也就不需要任何文件系统权限——这是 receive 协议选「传包不传路径」的全部理由。
|
|
805
|
+
// confirm 的脾气照 reclaim:缺省即干跑,写收件人的库(或物删整包)要 --apply 显式确认。
|
|
806
|
+
async function cmdReceive(cfg, rest, flags) {
|
|
807
|
+
requireToken(cfg);
|
|
808
|
+
const file = rest[0];
|
|
809
|
+
if (!file) {
|
|
810
|
+
fail(cfg, `receive 需要包文件:${CLI_NAME} receive <包文件>(不带 --apply 只干跑,一个字都不写)`);
|
|
811
|
+
}
|
|
812
|
+
let pack;
|
|
813
|
+
try {
|
|
814
|
+
pack = JSON.parse(fs.readFileSync(expandHome(file), 'utf8'));
|
|
815
|
+
} catch (e) {
|
|
816
|
+
// 读坏/读不到都在发请求之前拒掉:半读出来的包发过去只会换回一个闸门错,
|
|
817
|
+
// 而那一格现场已经说不清是「文件本来就这样」还是「传输坏了」。
|
|
818
|
+
fail(cfg, `读不到包文件 ${file}:${String(e?.message ?? e)}\n包文件是对方 ${CLI_NAME} share --out 出来的那个 .json。`);
|
|
819
|
+
}
|
|
820
|
+
const applied = flagBool(flags, 'apply');
|
|
821
|
+
const body = { pack };
|
|
822
|
+
if (applied) body.confirm = true;
|
|
823
|
+
const data = await apiRequest(cfg, 'POST', '/receive', { body });
|
|
824
|
+
if (cfg.jsonMode) {
|
|
825
|
+
console.log(stringify(data));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
printReceive(data, { applied });
|
|
829
|
+
if (!applied) console.log(' 没写库:确认数字之后加 --apply 真的导入');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 撤销收的是**包文件**而不是裸 pack_id:让人抄 16 位十六进制去删自己库里的行,
|
|
833
|
+
// 抄错一位就是「删了个没删成的包还自以为收掉了」。pack_id 就写在包文件第一层。
|
|
834
|
+
async function cmdUndoShare(cfg, rest, flags) {
|
|
835
|
+
requireToken(cfg);
|
|
836
|
+
const file = rest[0];
|
|
837
|
+
if (!file) {
|
|
838
|
+
fail(cfg, `undo-share 需要当初那个包文件(pack_id 从文件里取):${CLI_NAME} undo-share <包文件> [--apply]`);
|
|
839
|
+
}
|
|
840
|
+
let pack;
|
|
841
|
+
try {
|
|
842
|
+
pack = JSON.parse(fs.readFileSync(expandHome(file), 'utf8'));
|
|
843
|
+
} catch (e) {
|
|
844
|
+
fail(cfg, `读不到包文件 ${file}:${String(e?.message ?? e)}\n要的是当初 ${CLI_NAME} share --out 出来、发给对面那个 .json。`);
|
|
845
|
+
}
|
|
846
|
+
// trim 在这儿做:只有空格的 pack_id 与「没有 pack_id」是同一种「这个文件收不了」,
|
|
847
|
+
// 放它出门只是绕一圈换回 400 missing-pack-id,而那一格会被读成服务端的问题(服务端
|
|
848
|
+
// parseUndoBody 同样 trim,这里不改它的行为,只是不必为空格 round-trip)。
|
|
849
|
+
const packId = typeof pack?.pack_id === 'string' ? pack.pack_id.trim() : '';
|
|
850
|
+
if (!packId) {
|
|
851
|
+
fail(cfg, `这个文件里没有 pack_id,收起不了:${file}\nundo-share 收的是整包,编号就写在包文件的第一层,不用也不该手抄 16 位十六进制。`);
|
|
852
|
+
}
|
|
853
|
+
const applied = flagBool(flags, 'apply');
|
|
854
|
+
const body = { packId };
|
|
855
|
+
if (applied) body.confirm = true;
|
|
856
|
+
const data = await apiRequest(cfg, 'POST', '/receive/undo', { body });
|
|
857
|
+
if (cfg.jsonMode) {
|
|
858
|
+
console.log(stringify(data));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
printReceive({ ...data, created: data?.deleted, alias: '' }, { applied, undo: true });
|
|
862
|
+
if (!applied) console.log(' 没删:确认条数之后加 --apply 真的收起这个包');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function printReceive(d, { applied, undo = false }) {
|
|
866
|
+
const conflicts = Array.isArray(d?.title_conflicts) ? d.title_conflicts : [];
|
|
867
|
+
console.log(`${undo ? (applied ? '已收起' : '将收起') : applied ? '已导入' : '干跑'}: ${d?.created ?? 0} 条` +
|
|
868
|
+
(d?.alias ? ` 来自 ${d.alias}` : '') + (d?.skipped_existing ? ` 已存在 ${d.skipped_existing}` : ''));
|
|
869
|
+
if (undo && applied) {
|
|
870
|
+
// 服务端是物删不是归档(src/share-import.js 的 deleteSharePack):这句话是 --apply
|
|
871
|
+
// 与人之间唯一的说明书,不许软化成「移除」。干跑那一笔一个字都没动,说「直接从库里
|
|
872
|
+
// 带走」是假话,还会把下面的样例压成「已经删掉的这几条」——干跑那侧的话术是 cmdUndo-
|
|
873
|
+
// Share 结尾的「没删:…加 --apply」,两句不许同时出现(同一件事说两遍就没人细看)。
|
|
874
|
+
console.log(' 物删:这些行直接从库里带走,不是归档;包文件还在,重新 receive 一遍即可再来');
|
|
875
|
+
}
|
|
876
|
+
if (conflicts.length) {
|
|
877
|
+
// 撞名是 advisory 读数(src/share-import.js:只对本轮真正会新建的条目判),
|
|
878
|
+
// 所以这里只说「这几条要你看一眼」,永远不许说「除此之外没有别的撞名」。
|
|
879
|
+
console.log(` 撞名提醒:和你自己的条目同名 ${conflicts.length} 条(同类型同标题,不会被并入,需要你处置):`);
|
|
880
|
+
for (const c of conflicts.slice(0, 10)) console.log(` - [${c.type}] ${c.title}`);
|
|
881
|
+
if (conflicts.length > 10) console.log(' … 只列前 10 条');
|
|
882
|
+
}
|
|
883
|
+
const sample = Array.isArray(d?.sample) ? d.sample : [];
|
|
884
|
+
if (sample.length) {
|
|
885
|
+
// 撞名那段有标题、有截断话,样例这段原先两样都没有:服务端把 sample 截到 20
|
|
886
|
+
// (src/share-import.js 的 DELETE_SAMPLE_MAX),CLI 再截到 10,而 deleted 才是全集。
|
|
887
|
+
// 一个 500 行的包就这样留下 10 条无名要点,人会当成「要收走的就是这些」。
|
|
888
|
+
// 全集那一格只报服务端的数,不报「还剩几条」——两次截断混在一起就没人能对得上账。
|
|
889
|
+
console.log(' 样例:下面只是这一批里的几条,不是全部清单');
|
|
890
|
+
const shown = sample.slice(0, 10);
|
|
891
|
+
for (const s of shown) console.log(` · [${s.type}] ${s.title}`);
|
|
892
|
+
const total = typeof d?.deleted === 'number' ? d.deleted : (d?.created ?? 0);
|
|
893
|
+
if (shown.length < total) console.log(` … 只列前 ${shown.length} 条;整批共 ${total} 条(这个数来自服务端计数)`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* ---------------------------------- 入口 ---------------------------------- */
|
|
898
|
+
async function main(argv) {
|
|
899
|
+
const { positionals, flags } = parseArgv(argv);
|
|
900
|
+
const cmd = positionals[0];
|
|
901
|
+
const rest = positionals.slice(1);
|
|
902
|
+
|
|
903
|
+
if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
904
|
+
console.log(USAGE);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
|
|
908
|
+
console.log(PKG_VERSION);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const cfg = resolveConfig(flags);
|
|
913
|
+
cfg.jsonMode = flagBool(flags, 'json');
|
|
914
|
+
|
|
915
|
+
switch (cmd) {
|
|
916
|
+
case 'config':
|
|
917
|
+
return cmdConfig(cfg, rest[0], rest.slice(1));
|
|
918
|
+
case 'status':
|
|
919
|
+
return cmdStatus(cfg);
|
|
920
|
+
case 'list':
|
|
921
|
+
case 'ls':
|
|
922
|
+
return cmdList(cfg, flags);
|
|
923
|
+
case 'search':
|
|
924
|
+
return cmdSearch(cfg, rest, flags);
|
|
925
|
+
case 'get':
|
|
926
|
+
return cmdGet(cfg, rest);
|
|
927
|
+
case 'add':
|
|
928
|
+
return cmdAdd(cfg, flags);
|
|
929
|
+
case 'delete':
|
|
930
|
+
case 'rm':
|
|
931
|
+
return cmdDelete(cfg, rest);
|
|
932
|
+
case 'reclaim':
|
|
933
|
+
return cmdReclaim(cfg, flags);
|
|
934
|
+
case 'share':
|
|
935
|
+
return cmdShare(cfg, flags);
|
|
936
|
+
case 'receive':
|
|
937
|
+
return cmdReceive(cfg, rest, flags);
|
|
938
|
+
case 'undo-share':
|
|
939
|
+
return cmdUndoShare(cfg, rest, flags);
|
|
940
|
+
default:
|
|
941
|
+
fail(cfg, `未知命令: ${cmd}\n运行 \`${CLI_NAME} --help\` 查看帮助。`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// 直接运行才进 main(import 只取 parseArgv 做单测时必须安静):npm 全局安装走
|
|
946
|
+
// node_modules/.bin 的符号链接,argv[1] 是链接路径而非真身,所以要 realpath 两侧
|
|
947
|
+
// 再比,不能用字符串相等。
|
|
948
|
+
const invokedDirectly = (() => {
|
|
949
|
+
if (process.argv[1] == null) return false;
|
|
950
|
+
try {
|
|
951
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
952
|
+
} catch {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
})();
|
|
956
|
+
|
|
957
|
+
if (invokedDirectly) {
|
|
958
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
959
|
+
console.error(err?.stack ?? String(err));
|
|
960
|
+
process.exit(1);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export { parseArgv, KNOWN_FLAGS, flagValue, flagBool, flagKeyOf };
|