claude-memory-layer 1.0.27 → 1.0.29
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/.env.example +7 -0
- package/AGENTS.md +11 -0
- package/README.md +374 -49
- package/benchmarks/replay/anonymized-real-sessions.json +48 -0
- package/dist/cli/index.js +10097 -6003
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +9745 -5587
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +6545 -5270
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +6646 -5354
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +6618 -5347
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +6619 -5354
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +6614 -5325
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +6702 -5356
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +13537 -0
- package/dist/index.js.map +7 -0
- package/dist/mcp/index.js +20770 -0
- package/dist/mcp/index.js.map +7 -0
- package/dist/server/api/index.js +6632 -5319
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +6667 -5340
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +6568 -5350
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/assets/js/bootstrap.js +244 -0
- package/dist/ui/assets/js/chat.js +373 -0
- package/dist/ui/assets/js/disclosure.js +232 -0
- package/dist/ui/assets/js/modals.js +298 -0
- package/dist/ui/assets/js/overview.js +655 -0
- package/dist/ui/assets/js/state.js +72 -0
- package/dist/ui/assets/js/views.js +468 -0
- package/dist/ui/index.html +43 -1
- package/dist/ui/index.ts +3 -0
- package/dist/ui/style.css +222 -0
- package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
- package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
- package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
- package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
- package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
- package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
- package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
- package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
- package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
- package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
- package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
- package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
- package/docs/architecture/comparison-index.md +47 -0
- package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
- package/package.json +12 -5
- package/scripts/build.ts +25 -8
- package/scripts/generate-session-qrels.ts +126 -0
- package/scripts/postinstall-embedding-backend.cjs +142 -0
- package/scripts/replay-retrieval-benchmark.ts +69 -0
- package/specs/thin-core-refactor/context.md +275 -0
- package/specs/thin-core-refactor/plan.md +536 -0
- package/specs/thin-core-refactor/spec.md +465 -0
- package/src/adapters/claude/capture/index.ts +3 -0
- package/src/adapters/claude/context/index.ts +3 -0
- package/src/adapters/claude/hooks/index.ts +21 -0
- package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
- package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
- package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
- package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
- package/src/adapters/claude/hooks/session-end.ts +59 -0
- package/src/adapters/claude/hooks/session-start.ts +73 -0
- package/src/adapters/claude/hooks/stop.ts +128 -0
- package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
- package/src/adapters/claude/index.ts +4 -0
- package/src/adapters/claude/transcript/index.ts +4 -0
- package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
- package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
- package/src/apps/cli/claude-settings-hooks.ts +138 -0
- package/src/apps/cli/codex-import-runner.ts +125 -0
- package/src/apps/cli/codex-validation-output.ts +95 -0
- package/src/apps/cli/hermes-import-runner.ts +130 -0
- package/src/apps/cli/hermes-validation-output.ts +91 -0
- package/src/apps/cli/index.ts +1731 -0
- package/src/apps/cli/mcp-install.ts +106 -0
- package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
- package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
- package/src/apps/dashboard/assets/js/chat.js +373 -0
- package/src/apps/dashboard/assets/js/disclosure.js +232 -0
- package/src/apps/dashboard/assets/js/modals.js +298 -0
- package/src/apps/dashboard/assets/js/overview.js +655 -0
- package/src/apps/dashboard/assets/js/state.js +72 -0
- package/src/apps/dashboard/assets/js/views.js +468 -0
- package/src/{ui → apps/dashboard}/index.html +43 -1
- package/src/apps/dashboard/index.ts +3 -0
- package/src/{ui → apps/dashboard}/style.css +222 -0
- package/src/apps/index.ts +5 -0
- package/src/apps/server/api/chat.ts +244 -0
- package/src/apps/server/api/citations.ts +105 -0
- package/src/apps/server/api/events.ts +137 -0
- package/src/apps/server/api/health.ts +53 -0
- package/src/apps/server/api/index.ts +26 -0
- package/src/apps/server/api/projects.ts +74 -0
- package/src/apps/server/api/search.ts +184 -0
- package/src/apps/server/api/sessions.ts +115 -0
- package/src/apps/server/api/stats.ts +723 -0
- package/src/apps/server/api/turns.ts +143 -0
- package/src/apps/server/api/utils.ts +65 -0
- package/src/apps/server/index.ts +111 -0
- package/src/cli/index.ts +2 -1311
- package/src/cli/retrieval-disclosure-output.ts +2 -0
- package/src/compat/index.ts +5 -0
- package/src/core/derive/fact-deriver.ts +170 -0
- package/src/core/derive/index.ts +2 -0
- package/src/core/derive/summary-deriver.ts +76 -0
- package/src/core/embedder.ts +4 -152
- package/src/core/engine/embedding-maintenance-service.ts +187 -0
- package/src/core/engine/endless-memory-services.ts +4 -0
- package/src/core/engine/index.ts +19 -0
- package/src/core/engine/memory-engine-services.ts +170 -0
- package/src/core/engine/memory-ingest-service.ts +317 -0
- package/src/core/engine/memory-query-service.ts +173 -0
- package/src/core/engine/memory-runtime-service.ts +162 -0
- package/src/core/engine/memory-service-composition.ts +231 -0
- package/src/core/engine/retrieval-analytics-service.ts +181 -0
- package/src/core/engine/retrieval-disclosure-service.ts +420 -0
- package/src/core/engine/retrieval-orchestrator.ts +377 -0
- package/src/core/engine/retrieval-services.ts +176 -0
- package/src/core/engine/shared-memory-services.ts +4 -0
- package/src/core/entity-repo.ts +1 -3
- package/src/core/event-store.ts +3 -3
- package/src/core/evidence-aligner.ts +2 -2
- package/src/core/external-market-context.ts +582 -0
- package/src/core/graduation.ts +2 -3
- package/src/core/index.ts +21 -0
- package/src/core/matcher.ts +2 -4
- package/src/core/model/memory-fact.ts +30 -0
- package/src/core/model/memory-rule.ts +14 -0
- package/src/core/model/memory-summary.ts +21 -0
- package/src/core/model/raw-event.ts +28 -0
- package/src/core/model/retrieval-result.ts +35 -0
- package/src/core/privacy/filter.ts +21 -10
- package/src/core/product-validation-matrix.ts +314 -0
- package/src/core/progressive-retriever.ts +1 -2
- package/src/core/registry/project-path.ts +54 -0
- package/src/core/registry/session-registry.ts +69 -0
- package/src/core/replay-evaluator.ts +625 -0
- package/src/core/retrieval-benchmark.ts +117 -0
- package/src/core/retrieval-quality.ts +109 -0
- package/src/core/retriever.ts +53 -15
- package/src/core/session-qrels.ts +360 -0
- package/src/core/shared-event-store.ts +1 -1
- package/src/core/sqlite-event-store.ts +35 -11
- package/src/core/task/blocker-resolver.ts +2 -2
- package/src/core/task/task-resolver.ts +0 -1
- package/src/core/vector-outbox.ts +1 -10
- package/src/core/vector-worker.ts +1 -1
- package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
- package/src/extensions/endless-memory/index.ts +1 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/mcp/handlers.ts +960 -0
- package/src/extensions/mcp/index.ts +48 -0
- package/src/extensions/mcp/tools.ts +252 -0
- package/src/extensions/shared-memory/index.ts +1 -0
- package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
- package/src/extensions/vector/embedder.ts +197 -0
- package/src/extensions/vector/index.ts +1 -0
- package/src/hooks/post-tool-use.ts +3 -236
- package/src/hooks/semantic-daemon-client.ts +1 -208
- package/src/hooks/semantic-daemon.ts +6 -271
- package/src/hooks/session-end.ts +4 -79
- package/src/hooks/session-start.ts +4 -73
- package/src/hooks/stop.ts +3 -173
- package/src/hooks/user-prompt-submit.ts +3 -338
- package/src/index.ts +13 -0
- package/src/mcp/handlers.ts +2 -212
- package/src/mcp/index.ts +3 -46
- package/src/mcp/tools.ts +2 -78
- package/src/server/api/chat.ts +2 -244
- package/src/server/api/citations.ts +2 -105
- package/src/server/api/events.ts +2 -137
- package/src/server/api/health.ts +2 -53
- package/src/server/api/index.ts +2 -26
- package/src/server/api/projects.ts +2 -74
- package/src/server/api/search.ts +2 -102
- package/src/server/api/sessions.ts +2 -115
- package/src/server/api/stats.ts +2 -724
- package/src/server/api/turns.ts +2 -143
- package/src/server/api/utils.ts +2 -46
- package/src/server/index.ts +2 -100
- package/src/services/bootstrap-organizer.ts +46 -26
- package/src/services/codex-session-history-importer.ts +521 -29
- package/src/services/hermes-session-history-importer.ts +733 -0
- package/src/services/memory-service-config.ts +36 -0
- package/src/services/memory-service-registry.ts +150 -0
- package/src/services/memory-service.ts +211 -1325
- package/src/services/session-history-importer.ts +58 -14
- package/tests/README.md +23 -0
- package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
- package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
- package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
- package/tests/apps/app-layer-boundary.test.ts +48 -0
- package/tests/apps/claude-settings-hooks.test.ts +107 -0
- package/tests/apps/cli-disclosure-output.test.ts +212 -0
- package/tests/apps/codex-import-runner.test.ts +99 -0
- package/tests/apps/codex-validation-output.test.ts +100 -0
- package/tests/apps/hermes-import-runner.test.ts +99 -0
- package/tests/apps/mcp-install-command.test.ts +59 -0
- package/tests/apps/package-build-entrypoints.test.ts +30 -0
- package/tests/apps/postinstall-embedding-backend.test.ts +167 -0
- package/tests/apps/search-api-disclosure.test.ts +162 -0
- package/tests/apps/stats-api-lightweight.test.ts +67 -0
- package/tests/apps/ui-disclosure-output.test.ts +140 -0
- package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
- package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
- package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
- package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
- package/tests/core/embedding-maintenance-service.test.ts +282 -0
- package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
- package/tests/core/external-market-context.test.ts +209 -0
- package/tests/core/fact-deriver.test.ts +79 -0
- package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
- package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
- package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
- package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
- package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
- package/tests/core/memory-engine-services.test.ts +240 -0
- package/tests/core/memory-ingest-service.test.ts +296 -0
- package/tests/core/memory-query-service.test.ts +129 -0
- package/tests/core/memory-runtime-service.test.ts +201 -0
- package/tests/core/memory-service-composition.test.ts +192 -0
- package/tests/core/memory-service-config.test.ts +41 -0
- package/tests/core/memory-service-facade.test.ts +30 -0
- package/tests/core/memory-service-registry.test.ts +206 -0
- package/tests/core/product-validation-matrix.test.ts +61 -0
- package/tests/core/project-registry.test.ts +78 -0
- package/tests/core/replay-evaluator.test.ts +181 -0
- package/tests/core/retrieval-analytics-service.test.ts +210 -0
- package/tests/core/retrieval-benchmark.test.ts +93 -0
- package/tests/core/retrieval-disclosure-service.test.ts +264 -0
- package/tests/core/retrieval-orchestrator.test.ts +403 -0
- package/tests/core/retrieval-quality.test.ts +31 -0
- package/tests/core/retrieval-services.test.ts +185 -0
- package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
- package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
- package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
- package/tests/core/session-history-importer-filter.test.ts +78 -0
- package/tests/core/session-qrels.test.ts +250 -0
- package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
- package/tests/core/summary-deriver.test.ts +66 -0
- package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
- package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
- package/tests/extensions/endless-memory-services.test.ts +325 -0
- package/tests/extensions/mcp-context-tools.test.ts +905 -0
- package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
- package/tests/extensions/mcp-package-build.test.ts +22 -0
- package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
- package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
- package/tests/extensions/shared-memory-services.test.ts +309 -0
- package/tests/extensions/vector-extension-boundary.test.ts +21 -0
- package/.claude/settings.local.json +0 -25
- package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
- package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
- package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
- package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
- package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
- package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
- package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
- package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
- package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
- package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
- package/.npm-cache/_update-notifier-last-checked +0 -0
- package/bootstrap-kb/decisions/decisions.md +0 -244
- package/bootstrap-kb/glossary/glossary.md +0 -46
- package/bootstrap-kb/modules/.claude-plugin.md +0 -22
- package/bootstrap-kb/modules/agents.md.md +0 -15
- package/bootstrap-kb/modules/claude.md.md +0 -15
- package/bootstrap-kb/modules/context.md.md +0 -15
- package/bootstrap-kb/modules/docs.md +0 -18
- package/bootstrap-kb/modules/handoff.md.md +0 -15
- package/bootstrap-kb/modules/package-lock.json.md +0 -15
- package/bootstrap-kb/modules/package.json.md +0 -15
- package/bootstrap-kb/modules/plan.md.md +0 -15
- package/bootstrap-kb/modules/readme.md.md +0 -15
- package/bootstrap-kb/modules/scripts.md +0 -26
- package/bootstrap-kb/modules/spec.md.md +0 -15
- package/bootstrap-kb/modules/specs.md +0 -20
- package/bootstrap-kb/modules/src.md +0 -51
- package/bootstrap-kb/modules/tests.md +0 -42
- package/bootstrap-kb/modules/tsconfig.json.md +0 -15
- package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
- package/bootstrap-kb/overview/overview.md +0 -40
- package/bootstrap-kb/sources/manifest.json +0 -950
- package/bootstrap-kb/sources/manifest.md +0 -227
- package/bootstrap-kb/timeline/timeline.md +0 -57
- package/claude-memory-layer-1.0.14.tgz +0 -0
- package/d.sh +0 -3
- package/deploy.sh +0 -3
- package/dist/ui/app.js +0 -2101
- package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
- package/memory/_index.md +0 -419
- package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
- package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
- package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
- package/memory/agent_response/uncategorized/2026-03-05.md +0 -157
- package/memory/default/uncategorized/2026-02-25.md +0 -4839
- package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
- package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
- package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
- package/memory/specs/citations-system/2026-02-25.md +0 -1121
- package/memory/specs/endless-mode/2026-02-25.md +0 -1392
- package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
- package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
- package/memory/specs/private-tags/2026-02-25.md +0 -1057
- package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
- package/memory/specs/task-entity-system/2026-02-25.md +0 -924
- package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
- package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
- package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
- package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
- package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
- package/memory/tool_observation/uncategorized/2026-03-05.md +0 -33
- package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
- package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
- package/memory/user_prompt/uncategorized/2026-03-05.md +0 -6
- package/specs/optional-duckdb/context.md +0 -77
- package/specs/optional-duckdb/plan.md +0 -142
- package/specs/optional-duckdb/spec.md +0 -35
- package/src/ui/app.js +0 -2101
|
@@ -11,7 +11,8 @@ import * as path from 'path';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import * as readline from 'readline';
|
|
13
13
|
import { randomUUID } from 'crypto';
|
|
14
|
-
import { MemoryService
|
|
14
|
+
import { MemoryService } from './memory-service.js';
|
|
15
|
+
import { registerSession } from '../core/registry/session-registry.js';
|
|
15
16
|
|
|
16
17
|
export type ProgressEvent =
|
|
17
18
|
| { phase: 'scan'; message: string }
|
|
@@ -25,6 +26,8 @@ export interface ImportOptions {
|
|
|
25
26
|
projectPath?: string;
|
|
26
27
|
sessionId?: string;
|
|
27
28
|
limit?: number;
|
|
29
|
+
/** Limit how many matching sessions are imported. Useful for freshness jobs that only need the latest active session. */
|
|
30
|
+
sessionLimit?: number;
|
|
28
31
|
skipExisting?: boolean;
|
|
29
32
|
force?: boolean;
|
|
30
33
|
verbose?: boolean;
|
|
@@ -63,14 +66,48 @@ export interface ClaudeMessage {
|
|
|
63
66
|
* Filter trivial user inputs that aren't worth storing.
|
|
64
67
|
* Mirrors the shouldStorePrompt() logic from user-prompt-submit.ts.
|
|
65
68
|
*/
|
|
66
|
-
function
|
|
69
|
+
export function isClaudeLocalCommandArtifact(content: string): boolean {
|
|
67
70
|
const trimmed = content.trim();
|
|
71
|
+
return (
|
|
72
|
+
/^<local-command-(stdout|stderr)>/.test(trimmed) ||
|
|
73
|
+
/^<command-(name|message)>/.test(trimmed) ||
|
|
74
|
+
(trimmed.includes('<command-name>') && trimmed.includes('<local-command-stdout>'))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isWorthStoringPrompt(content: string): boolean {
|
|
79
|
+
const trimmed = content.trim();
|
|
80
|
+
if (isClaudeLocalCommandArtifact(trimmed)) return false;
|
|
68
81
|
if (trimmed.startsWith('/')) return false;
|
|
69
82
|
if (trimmed.length < 15) return false;
|
|
70
83
|
if (!/[a-zA-Z가-힣]{2,}/.test(trimmed)) return false;
|
|
71
84
|
return true;
|
|
72
85
|
}
|
|
73
86
|
|
|
87
|
+
function getFileMtimeMs(filePath: string): number {
|
|
88
|
+
try {
|
|
89
|
+
return fs.statSync(filePath).mtimeMs;
|
|
90
|
+
} catch {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function selectRecentSessionFiles(files: string[], sessionLimit?: number): string[] {
|
|
96
|
+
if (sessionLimit === undefined) return files;
|
|
97
|
+
return [...files]
|
|
98
|
+
.sort((a, b) => getFileMtimeMs(b) - getFileMtimeMs(a) || b.localeCompare(a))
|
|
99
|
+
.slice(0, sessionLimit);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseSessionLimit(value?: number): number | undefined {
|
|
103
|
+
if (value === undefined) return undefined;
|
|
104
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function applySessionLimit(files: string[], options: ImportOptions): string[] {
|
|
108
|
+
return selectRecentSessionFiles(files, parseSessionLimit(options.sessionLimit));
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
function classifyEntry(entry: ClaudeMessage): 'user_prompt' | 'tool_result' | 'agent_text' | 'tool_use' | 'thinking' | 'skip' {
|
|
75
112
|
if (entry.type !== 'user' && entry.type !== 'assistant') {
|
|
76
113
|
return 'skip';
|
|
@@ -151,7 +188,8 @@ export class SessionHistoryImporter {
|
|
|
151
188
|
allSessionFiles.push(...files);
|
|
152
189
|
}
|
|
153
190
|
const sessionFiles = [...new Set(allSessionFiles)];
|
|
154
|
-
|
|
191
|
+
const selectedSessionFiles = applySessionLimit(sessionFiles, options);
|
|
192
|
+
result.totalSessions = selectedSessionFiles.length;
|
|
155
193
|
onProgress?.({
|
|
156
194
|
phase: 'scan',
|
|
157
195
|
message: `Found ${sessionFiles.length} sessions in ${projectDirs.length} matched project folder(s)`
|
|
@@ -165,11 +203,11 @@ export class SessionHistoryImporter {
|
|
|
165
203
|
console.log(`Found ${sessionFiles.length} session files across matched folders`);
|
|
166
204
|
}
|
|
167
205
|
|
|
168
|
-
// Import each session
|
|
169
|
-
for (let i = 0; i <
|
|
170
|
-
const sessionFile =
|
|
206
|
+
// Import each selected session
|
|
207
|
+
for (let i = 0; i < selectedSessionFiles.length; i++) {
|
|
208
|
+
const sessionFile = selectedSessionFiles[i];
|
|
171
209
|
try {
|
|
172
|
-
onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions:
|
|
210
|
+
onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: selectedSessionFiles.length, filePath: sessionFile });
|
|
173
211
|
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
174
212
|
...options,
|
|
175
213
|
_sessionIndex: i,
|
|
@@ -270,7 +308,7 @@ export class SessionHistoryImporter {
|
|
|
270
308
|
{ importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
|
|
271
309
|
);
|
|
272
310
|
|
|
273
|
-
if (appendResult.isDuplicate) {
|
|
311
|
+
if (appendResult.success && appendResult.isDuplicate) {
|
|
274
312
|
result.skippedDuplicates++;
|
|
275
313
|
} else {
|
|
276
314
|
result.importedResponses++;
|
|
@@ -310,7 +348,7 @@ export class SessionHistoryImporter {
|
|
|
310
348
|
{ importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
|
|
311
349
|
);
|
|
312
350
|
|
|
313
|
-
if (appendResult.isDuplicate) {
|
|
351
|
+
if (appendResult.success && appendResult.isDuplicate) {
|
|
314
352
|
result.skippedDuplicates++;
|
|
315
353
|
} else {
|
|
316
354
|
result.importedPrompts++;
|
|
@@ -401,16 +439,22 @@ export class SessionHistoryImporter {
|
|
|
401
439
|
console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
|
|
402
440
|
}
|
|
403
441
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
442
|
+
const selectedSessionFiles = applySessionLimit(allSessionFiles, options);
|
|
443
|
+
result.totalSessions = selectedSessionFiles.length;
|
|
444
|
+
onProgress?.({
|
|
445
|
+
phase: 'scan',
|
|
446
|
+
message: `Selected ${selectedSessionFiles.length} of ${allSessionFiles.length} session(s) for import`
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Import selected session files with progress tracking
|
|
450
|
+
for (let i = 0; i < selectedSessionFiles.length; i++) {
|
|
451
|
+
const sessionFile = selectedSessionFiles[i];
|
|
407
452
|
try {
|
|
408
|
-
onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions:
|
|
453
|
+
onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: selectedSessionFiles.length, filePath: sessionFile });
|
|
409
454
|
const sessionResult = await this.importSessionFile(sessionFile, {
|
|
410
455
|
...options,
|
|
411
456
|
_sessionIndex: i,
|
|
412
457
|
} as ImportOptions & { _sessionIndex: number });
|
|
413
|
-
result.totalSessions++;
|
|
414
458
|
result.totalMessages += sessionResult.totalMessages;
|
|
415
459
|
result.importedPrompts += sessionResult.importedPrompts;
|
|
416
460
|
result.importedResponses += sessionResult.importedResponses;
|
package/tests/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Test Matrix
|
|
2
|
+
|
|
3
|
+
The test suite mirrors the thin-core architecture so regressions are easier to localize.
|
|
4
|
+
|
|
5
|
+
- `core/` — core models, stores, retrieval, ingestion, derivation, runtime services, and service facades.
|
|
6
|
+
- `adapters/claude/` — Claude Code hook and transcript adapter behavior.
|
|
7
|
+
- `extensions/` — optional vector, shared-memory, endless-memory, and MCP extension boundaries/services.
|
|
8
|
+
- `apps/` — CLI, server/API, dashboard/UI, and packaging-facing application behavior.
|
|
9
|
+
|
|
10
|
+
Run all tests with:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm test -- --run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run one architecture slice with:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm test -- --run tests/core
|
|
20
|
+
npm test -- --run tests/adapters/claude
|
|
21
|
+
npm test -- --run tests/extensions
|
|
22
|
+
npm test -- --run tests/apps
|
|
23
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
handleSemanticDaemonRequest,
|
|
4
|
+
isValidSemanticDaemonRequest,
|
|
5
|
+
isVectorSessionFilterError,
|
|
6
|
+
makeSemanticDaemonErrorResponse,
|
|
7
|
+
parseSemanticDaemonRequest
|
|
8
|
+
} from '../../../src/adapters/claude/hooks/semantic-daemon.js';
|
|
9
|
+
|
|
10
|
+
describe('Claude semantic daemon adapter', () => {
|
|
11
|
+
it('parses JSON requests and treats malformed payloads as empty requests', () => {
|
|
12
|
+
expect(parseSemanticDaemonRequest('{"type":"retrieve","sessionId":"s1"}')).toEqual({
|
|
13
|
+
type: 'retrieve',
|
|
14
|
+
sessionId: 's1'
|
|
15
|
+
});
|
|
16
|
+
expect(parseSemanticDaemonRequest('{not-json')).toEqual({});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('validates retrieve requests before touching MemoryService', () => {
|
|
20
|
+
expect(isValidSemanticDaemonRequest({
|
|
21
|
+
type: 'retrieve',
|
|
22
|
+
sessionId: 'session-1',
|
|
23
|
+
prompt: 'find checkout fix',
|
|
24
|
+
topK: 5,
|
|
25
|
+
minScore: 0.2
|
|
26
|
+
})).toBe(true);
|
|
27
|
+
|
|
28
|
+
expect(isValidSemanticDaemonRequest({
|
|
29
|
+
type: 'retrieve',
|
|
30
|
+
sessionId: 'session-1',
|
|
31
|
+
prompt: 'find checkout fix',
|
|
32
|
+
topK: Number.NaN,
|
|
33
|
+
minScore: 0.2
|
|
34
|
+
})).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns a deterministic invalid request response without initializing retrieval', async () => {
|
|
38
|
+
await expect(handleSemanticDaemonRequest('{"type":"retrieve"}')).resolves.toEqual({
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'invalid request'
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('detects LanceDB sessionId field-case filter failures', () => {
|
|
45
|
+
expect(isVectorSessionFilterError(new Error('No field named sessionId in schema'))).toBe(true);
|
|
46
|
+
expect(isVectorSessionFilterError(new Error('connection refused'))).toBe(false);
|
|
47
|
+
expect(isVectorSessionFilterError('no field named sessionId')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('formats daemon errors without leaking non-Error values', () => {
|
|
51
|
+
expect(makeSemanticDaemonErrorResponse(new Error('boom'))).toEqual({ ok: false, error: 'boom' });
|
|
52
|
+
expect(makeSemanticDaemonErrorResponse('boom')).toEqual({ ok: false, error: 'unknown daemon error' });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { readTranscriptTailEntries } from '../../../src/adapters/claude/transcript/transcript-reader.js';
|
|
6
|
+
import {
|
|
7
|
+
extractAssistantTextMessages,
|
|
8
|
+
extractAssistantMessages,
|
|
9
|
+
generateSessionSummary
|
|
10
|
+
} from '../../../src/adapters/claude/transcript/turn-reconstructor.js';
|
|
11
|
+
|
|
12
|
+
describe('Claude transcript reader', () => {
|
|
13
|
+
it('reads JSONL transcript tail entries and skips malformed partial lines', async () => {
|
|
14
|
+
const dir = await mkdtemp(join(tmpdir(), 'cml-transcript-'));
|
|
15
|
+
const transcriptPath = join(dir, 'transcript.jsonl');
|
|
16
|
+
await writeFile(
|
|
17
|
+
transcriptPath,
|
|
18
|
+
[
|
|
19
|
+
'{"type":"user","message":{"content":"hello"}}',
|
|
20
|
+
'not-json',
|
|
21
|
+
'null',
|
|
22
|
+
'42',
|
|
23
|
+
'{"type":"assistant","message":{"content":[{"type":"text","text":"answer"}]}}'
|
|
24
|
+
].join('\n'),
|
|
25
|
+
'utf8'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const entries = await readTranscriptTailEntries(transcriptPath);
|
|
29
|
+
|
|
30
|
+
expect(entries.map((entry) => entry.type)).toEqual(['user', 'assistant']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('skips a partial first line when tail reading starts mid-record', async () => {
|
|
34
|
+
const dir = await mkdtemp(join(tmpdir(), 'cml-transcript-'));
|
|
35
|
+
const transcriptPath = join(dir, 'transcript.jsonl');
|
|
36
|
+
const firstLine = `{"type":"user","message":{"content":"${'x'.repeat(500)}"}}`;
|
|
37
|
+
const secondLine = '{"type":"user","message":{"content":"recent question"}}';
|
|
38
|
+
const thirdLine = '{"type":"assistant","message":{"content":[{"type":"text","text":"recent answer"}]}}';
|
|
39
|
+
await writeFile(transcriptPath, [firstLine, secondLine, thirdLine].join('\n'), 'utf8');
|
|
40
|
+
|
|
41
|
+
const maxBytes = Buffer.byteLength(`${secondLine}\n${thirdLine}`) + 10;
|
|
42
|
+
const entries = await readTranscriptTailEntries(transcriptPath, { maxBytes });
|
|
43
|
+
|
|
44
|
+
expect(entries.map((entry) => entry.type)).toEqual(['user', 'assistant']);
|
|
45
|
+
expect(entries[0]?.message?.content).toBe('recent question');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns an empty entry list for missing transcript files', async () => {
|
|
49
|
+
await expect(readTranscriptTailEntries('/tmp/non-existent-claude-transcript.jsonl')).resolves.toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('Claude turn reconstructor', () => {
|
|
54
|
+
it('extracts assistant text blocks and joins multi-part text content', async () => {
|
|
55
|
+
const messages = extractAssistantTextMessages([
|
|
56
|
+
{ type: 'user', message: { content: 'ignored' } },
|
|
57
|
+
{
|
|
58
|
+
type: 'assistant',
|
|
59
|
+
message: {
|
|
60
|
+
content: [
|
|
61
|
+
{ type: 'text', text: 'first' },
|
|
62
|
+
{ type: 'tool_use', id: 'tool-1' },
|
|
63
|
+
{ type: 'text', text: 'second' }
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tool-2' }] } }
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
expect(messages).toEqual(['first\nsecond']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('reads assistant text messages from a transcript file', async () => {
|
|
74
|
+
const dir = await mkdtemp(join(tmpdir(), 'cml-transcript-'));
|
|
75
|
+
const transcriptPath = join(dir, 'transcript.jsonl');
|
|
76
|
+
await writeFile(
|
|
77
|
+
transcriptPath,
|
|
78
|
+
'{"type":"assistant","message":{"content":[{"type":"text","text":"final answer"}]}}\n',
|
|
79
|
+
'utf8'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await expect(extractAssistantMessages(transcriptPath)).resolves.toEqual(['final answer']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('generates deterministic summaries from session events', () => {
|
|
86
|
+
const summary = generateSessionSummary([
|
|
87
|
+
{ eventType: 'user_prompt', content: 'How should we refactor transcript parsing?' },
|
|
88
|
+
{ eventType: 'agent_response', content: 'Move it behind adapter modules.' },
|
|
89
|
+
{ eventType: 'tool_observation', content: 'ignored' }
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
expect(summary).toBe(
|
|
93
|
+
'Session with 1 user prompts and 1 responses.\n' +
|
|
94
|
+
'Topics discussed:\n' +
|
|
95
|
+
'- How should we refactor transcript parsing?'
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { MemoryQueryService } from '../../src/core/engine/memory-query-service.js';
|
|
4
|
+
import type { MemoryEvent } from '../../src/core/types.js';
|
|
5
|
+
import {
|
|
6
|
+
filterHookInjectableMemories,
|
|
7
|
+
getHookInjectionPolicy,
|
|
8
|
+
summarizeHookInjectionConfidence,
|
|
9
|
+
type HookMemoryCandidate
|
|
10
|
+
} from '../../src/adapters/claude/hooks/prompt-injection-policy.js';
|
|
11
|
+
|
|
12
|
+
function event(id: string, content = 'low confidence keyword result'): MemoryEvent {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
eventType: 'user_prompt',
|
|
16
|
+
sessionId: 'session-1',
|
|
17
|
+
timestamp: new Date('2026-05-02T00:00:00.000Z'),
|
|
18
|
+
content,
|
|
19
|
+
canonicalKey: `test/${id}`,
|
|
20
|
+
dedupeKey: `session-1:${id}`,
|
|
21
|
+
metadata: {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Claude hook prompt injection policy', () => {
|
|
26
|
+
it('filters low-confidence hook candidates before prompt injection', () => {
|
|
27
|
+
const candidates: HookMemoryCandidate[] = [
|
|
28
|
+
{ id: 'low-semantic', type: 'agent_response', content: 'maybe related', score: 0.58, source: 'semantic' },
|
|
29
|
+
{ id: 'low-keyword', type: 'user_prompt', content: 'weak keyword rescue', score: 0.49, source: 'keyword' },
|
|
30
|
+
{ id: 'high-keyword', type: 'user_prompt', content: 'exact high-confidence keyword', score: 0.84, source: 'keyword' },
|
|
31
|
+
{ id: 'high-semantic', type: 'session_summary', content: 'high semantic match', score: 0.76, source: 'semantic' }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
expect(filterHookInjectableMemories(candidates, getHookInjectionPolicy())).toEqual([
|
|
35
|
+
candidates[2],
|
|
36
|
+
candidates[3]
|
|
37
|
+
]);
|
|
38
|
+
expect(summarizeHookInjectionConfidence(candidates)).toBe('high');
|
|
39
|
+
expect(summarizeHookInjectionConfidence([])).toBe('none');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('limits injected hook memories by highest confidence rather than first passing candidate', () => {
|
|
43
|
+
const candidates: HookMemoryCandidate[] = [
|
|
44
|
+
{ id: 'medium-first', type: 'session_summary', content: 'passes but weaker', score: 0.67, source: 'semantic' },
|
|
45
|
+
{ id: 'best-later', type: 'user_prompt', content: 'best exact keyword', score: 0.93, source: 'keyword' },
|
|
46
|
+
{ id: 'second-best-later', type: 'agent_response', content: 'strong semantic match', score: 0.88, source: 'semantic' }
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
expect(filterHookInjectableMemories(candidates, { ...getHookInjectionPolicy(), maxMemories: 2 }))
|
|
50
|
+
.toEqual([candidates[1], candidates[2]]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses bounded hook policy thresholds so unsafe env overrides cannot inject weak memories', () => {
|
|
54
|
+
const policy = getHookInjectionPolicy({
|
|
55
|
+
CLAUDE_MEMORY_HOOK_INJECTION_MIN_SCORE: '-1',
|
|
56
|
+
CLAUDE_MEMORY_HOOK_SEMANTIC_MIN_SCORE: '2',
|
|
57
|
+
CLAUDE_MEMORY_HOOK_KEYWORD_MIN_SCORE: 'not-a-number',
|
|
58
|
+
CLAUDE_MEMORY_HOOK_FALLBACK_KEYWORD_MIN_SCORE: '-0.2',
|
|
59
|
+
CLAUDE_MEMORY_HOOK_MAX_INJECTED: '2'
|
|
60
|
+
} as NodeJS.ProcessEnv);
|
|
61
|
+
|
|
62
|
+
expect(policy).toEqual({
|
|
63
|
+
minScore: 0.65,
|
|
64
|
+
semanticMinScore: 0.65,
|
|
65
|
+
keywordMinScore: 0.7,
|
|
66
|
+
fallbackKeywordMinScore: 0.8,
|
|
67
|
+
maxMemories: 2
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(filterHookInjectableMemories([
|
|
71
|
+
{ id: 'weak-unknown', type: 'user_prompt', content: 'weak unknown source', score: 0.1, source: 'unknown' },
|
|
72
|
+
{ id: 'fallback-too-weak', type: 'user_prompt', content: 'weak fallback', score: 0.79, source: 'keyword', fallback: true }
|
|
73
|
+
], policy)).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('keeps regular CLI/query search behavior independent from hook injection policy', async () => {
|
|
77
|
+
const memory = event('regular-low');
|
|
78
|
+
const service = new MemoryQueryService(
|
|
79
|
+
async () => {},
|
|
80
|
+
{
|
|
81
|
+
keywordSearch: async () => [{ event: memory, rank: -0.5 }],
|
|
82
|
+
getSessionEvents: async () => [memory],
|
|
83
|
+
getRecentEvents: async () => [memory]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const regularResults = await service.keywordSearch('weak keyword rescue', { topK: 1, minScore: 0.2 });
|
|
88
|
+
expect(regularResults).toEqual([{ event: memory, score: 1 }]);
|
|
89
|
+
|
|
90
|
+
const hookCandidates: HookMemoryCandidate[] = regularResults.map((result) => ({
|
|
91
|
+
id: result.event.id,
|
|
92
|
+
type: result.event.eventType,
|
|
93
|
+
content: result.event.content,
|
|
94
|
+
score: 0.5,
|
|
95
|
+
source: 'keyword'
|
|
96
|
+
}));
|
|
97
|
+
expect(filterHookInjectableMemories(hookCandidates, getHookInjectionPolicy())).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const apiModules = [
|
|
5
|
+
'chat',
|
|
6
|
+
'citations',
|
|
7
|
+
'events',
|
|
8
|
+
'health',
|
|
9
|
+
'projects',
|
|
10
|
+
'search',
|
|
11
|
+
'sessions',
|
|
12
|
+
'stats',
|
|
13
|
+
'turns',
|
|
14
|
+
'utils'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
describe('app-layer entrypoint boundaries', () => {
|
|
18
|
+
it('keeps CLI implementation under src/apps/cli while preserving src/cli compatibility', () => {
|
|
19
|
+
const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
|
|
20
|
+
scripts?: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
const buildScript = readFileSync('scripts/build.ts', 'utf8');
|
|
23
|
+
const cliCompatSource = readFileSync('src/cli/index.ts', 'utf8');
|
|
24
|
+
const cliDisclosureCompatSource = readFileSync('src/cli/retrieval-disclosure-output.ts', 'utf8');
|
|
25
|
+
|
|
26
|
+
expect(packageJson.scripts?.dev).toBe('tsx src/apps/cli/index.ts');
|
|
27
|
+
expect(buildScript).toContain("entryPoints: ['src/apps/cli/index.ts']");
|
|
28
|
+
expect(cliCompatSource).toContain("../apps/cli/index.js");
|
|
29
|
+
expect(cliDisclosureCompatSource).toContain("../apps/cli/retrieval-disclosure-output.js");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps server implementation under src/apps/server while preserving src/server compatibility', () => {
|
|
33
|
+
const buildScript = readFileSync('scripts/build.ts', 'utf8');
|
|
34
|
+
const serverCompatSource = readFileSync('src/server/index.ts', 'utf8');
|
|
35
|
+
const apiIndexCompatSource = readFileSync('src/server/api/index.ts', 'utf8');
|
|
36
|
+
|
|
37
|
+
expect(buildScript).toContain("entryPoints: ['src/apps/server/index.ts']");
|
|
38
|
+
expect(buildScript).toContain("entryPoints: ['src/apps/server/api/index.ts']");
|
|
39
|
+
expect(serverCompatSource).toContain("../apps/server/index.js");
|
|
40
|
+
expect(apiIndexCompatSource).toContain("../../apps/server/api/index.js");
|
|
41
|
+
|
|
42
|
+
for (const moduleName of apiModules) {
|
|
43
|
+
const compatPath = `src/server/api/${moduleName}.ts`;
|
|
44
|
+
expect(existsSync(compatPath), `${compatPath} should exist`).toBe(true);
|
|
45
|
+
expect(readFileSync(compatPath, 'utf8')).toContain(`../../apps/server/api/${moduleName}.js`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildHookCommand,
|
|
5
|
+
getHooksConfig,
|
|
6
|
+
mergePluginHooksIntoSettings,
|
|
7
|
+
removePluginHooksFromSettings,
|
|
8
|
+
type ClaudeSettingsWithHooks
|
|
9
|
+
} from '../../src/apps/cli/claude-settings-hooks.js';
|
|
10
|
+
|
|
11
|
+
describe('Claude Code hook settings helpers', () => {
|
|
12
|
+
it('quotes hook paths so plugin installs work when the path contains spaces', () => {
|
|
13
|
+
expect(buildHookCommand('/tmp/project with spaces/dist', 'user-prompt-submit.js'))
|
|
14
|
+
.toBe("node '/tmp/project with spaces/dist/hooks/user-prompt-submit.js'");
|
|
15
|
+
|
|
16
|
+
const complexCommand = buildHookCommand("/tmp/project with $dollars 'quotes' and `ticks`/dist", 'stop.js');
|
|
17
|
+
expect(complexCommand).toContain("'\\''quotes'\\''");
|
|
18
|
+
expect(complexCommand).toContain('$dollars');
|
|
19
|
+
expect(complexCommand).toContain('`ticks`');
|
|
20
|
+
|
|
21
|
+
expect(getHooksConfig('/tmp/project with spaces/dist').UserPromptSubmit?.[0].hooks[0].command)
|
|
22
|
+
.toBe("node '/tmp/project with spaces/dist/hooks/user-prompt-submit.js'");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('merges plugin hooks without replacing unrelated hooks in the same categories', () => {
|
|
26
|
+
const settings: ClaudeSettingsWithHooks = {
|
|
27
|
+
theme: 'dark',
|
|
28
|
+
hooks: {
|
|
29
|
+
UserPromptSubmit: [
|
|
30
|
+
{
|
|
31
|
+
matcher: 'existing',
|
|
32
|
+
hooks: [{ type: 'command', command: 'node /other/plugin.js' }]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
Stop: [
|
|
36
|
+
{
|
|
37
|
+
matcher: 'old-plugin',
|
|
38
|
+
hooks: [{ type: 'command', command: 'node /old/claude-memory-layer/dist/hooks/stop.js' }]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const merged = mergePluginHooksIntoSettings(settings, '/new/plugin dist');
|
|
45
|
+
|
|
46
|
+
expect(merged.theme).toBe('dark');
|
|
47
|
+
expect(merged.hooks?.UserPromptSubmit).toEqual([
|
|
48
|
+
{
|
|
49
|
+
matcher: 'existing',
|
|
50
|
+
hooks: [{ type: 'command', command: 'node /other/plugin.js' }]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
matcher: '',
|
|
54
|
+
hooks: [{ type: 'command', command: "node '/new/plugin dist/hooks/user-prompt-submit.js'" }]
|
|
55
|
+
}
|
|
56
|
+
]);
|
|
57
|
+
expect(merged.hooks?.Stop).toEqual([
|
|
58
|
+
{
|
|
59
|
+
matcher: '',
|
|
60
|
+
hooks: [{ type: 'command', command: "node '/new/plugin dist/hooks/stop.js'" }]
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('uninstall removes only claude-memory-layer hook commands and leaves other hooks intact', () => {
|
|
66
|
+
const settings: ClaudeSettingsWithHooks = {
|
|
67
|
+
hooks: {
|
|
68
|
+
SessionStart: [
|
|
69
|
+
{
|
|
70
|
+
matcher: 'keep-and-remove',
|
|
71
|
+
hooks: [
|
|
72
|
+
{ type: 'command', command: 'node /opt/claude-memory-layer/dist/hooks/session-start.js' },
|
|
73
|
+
{ type: 'command', command: 'node /other/session-start-helper.js' }
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
PostToolUse: [
|
|
78
|
+
{
|
|
79
|
+
matcher: 'keep',
|
|
80
|
+
hooks: [
|
|
81
|
+
{ type: 'command', command: 'node /other/post-tool-use-helper.js' },
|
|
82
|
+
{ type: 'command', command: 'node /other-plugin/hooks/post-tool-use.js' }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const removed = removePluginHooksFromSettings(settings, '/opt/claude-memory-layer/dist');
|
|
90
|
+
|
|
91
|
+
expect(removed.hooks?.SessionStart).toEqual([
|
|
92
|
+
{
|
|
93
|
+
matcher: 'keep-and-remove',
|
|
94
|
+
hooks: [{ type: 'command', command: 'node /other/session-start-helper.js' }]
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
expect(removed.hooks?.PostToolUse).toEqual([
|
|
98
|
+
{
|
|
99
|
+
matcher: 'keep',
|
|
100
|
+
hooks: [
|
|
101
|
+
{ type: 'command', command: 'node /other/post-tool-use-helper.js' },
|
|
102
|
+
{ type: 'command', command: 'node /other-plugin/hooks/post-tool-use.js' }
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|