@massu/core 1.10.8 → 1.12.0
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/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +517 -397
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
// ============================================================
|
|
5
|
-
// Memory File Auto-Ingest
|
|
6
|
-
// Shared module for parsing memory/*.md files and ingesting
|
|
7
|
-
// their YAML frontmatter + content into the observations table.
|
|
8
|
-
// Used by: post-tool-use.ts, memory-tools.ts, init.ts
|
|
9
|
-
// ============================================================
|
|
10
|
-
|
|
11
|
-
import type Database from 'better-sqlite3';
|
|
12
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
14
|
-
import { parse as parseYaml } from 'yaml';
|
|
15
|
-
import { addObservation } from './memory-db.ts';
|
|
16
|
-
|
|
17
|
-
export type IngestResult = 'inserted' | 'updated' | 'skipped';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Parse a memory/*.md file's YAML frontmatter and ingest it into the
|
|
21
|
-
* observations table. Deduplicates by title prefix `[memory-file] {name}`.
|
|
22
|
-
*
|
|
23
|
-
* @returns 'inserted' | 'updated' | 'skipped'
|
|
24
|
-
*/
|
|
25
|
-
export function ingestMemoryFile(
|
|
26
|
-
db: Database.Database,
|
|
27
|
-
sessionId: string,
|
|
28
|
-
filePath: string,
|
|
29
|
-
): IngestResult {
|
|
30
|
-
if (!existsSync(filePath)) return 'skipped';
|
|
31
|
-
|
|
32
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
33
|
-
const basename = (filePath.split('/').pop() ?? '').replace('.md', '');
|
|
34
|
-
|
|
35
|
-
// Parse YAML frontmatter (between first --- and second ---)
|
|
36
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
37
|
-
|
|
38
|
-
let name = basename;
|
|
39
|
-
let description = '';
|
|
40
|
-
let type = 'discovery';
|
|
41
|
-
let confidence: number | undefined;
|
|
42
|
-
|
|
43
|
-
if (frontmatterMatch) {
|
|
44
|
-
try {
|
|
45
|
-
// pattern-scanner-allow: yaml-parse — reason: parses YAML FRONTMATTER from markdown memory files (NOT massu.config.yaml). This is document metadata parsing, not application config access; getConfig() does not apply.
|
|
46
|
-
const fm = parseYaml(frontmatterMatch[1]) as Record<string, unknown>;
|
|
47
|
-
name = (fm.name as string) ?? basename;
|
|
48
|
-
description = (fm.description as string) ?? '';
|
|
49
|
-
type = (fm.type as string) ?? 'discovery';
|
|
50
|
-
confidence = fm.confidence != null ? Number(fm.confidence) : undefined;
|
|
51
|
-
} catch {
|
|
52
|
-
// Use defaults if YAML parsing fails
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Map memory types to observation types
|
|
57
|
-
const obsType = mapMemoryTypeToObservationType(type);
|
|
58
|
-
|
|
59
|
-
// Calculate importance from confidence (0.0-1.0 -> 1-5)
|
|
60
|
-
const importance = confidence != null
|
|
61
|
-
? Math.max(1, Math.min(5, Math.round(confidence * 4 + 1)))
|
|
62
|
-
: 4;
|
|
63
|
-
|
|
64
|
-
// Extract body (after second ---)
|
|
65
|
-
const bodyMatch = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)/);
|
|
66
|
-
const body = bodyMatch ? bodyMatch[1].trim().slice(0, 500) : '';
|
|
67
|
-
|
|
68
|
-
const title = `[memory-file] ${name}`;
|
|
69
|
-
const detail = description ? `${description}\n\n${body}` : body;
|
|
70
|
-
|
|
71
|
-
// Deduplicate: check if this exact title exists
|
|
72
|
-
const existing = db.prepare(
|
|
73
|
-
'SELECT id FROM observations WHERE title = ? LIMIT 1'
|
|
74
|
-
).get(title) as { id: number } | undefined;
|
|
75
|
-
|
|
76
|
-
if (existing) {
|
|
77
|
-
db.prepare('UPDATE observations SET detail = ?, importance = ? WHERE id = ?')
|
|
78
|
-
.run(detail, importance, existing.id);
|
|
79
|
-
return 'updated';
|
|
80
|
-
} else {
|
|
81
|
-
addObservation(db, sessionId, obsType, title, detail, { importance });
|
|
82
|
-
return 'inserted';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Bulk-ingest all memory/*.md files from a directory.
|
|
88
|
-
* Skips MEMORY.md (the index file).
|
|
89
|
-
*
|
|
90
|
-
* @returns { inserted, updated, skipped, total }
|
|
91
|
-
*/
|
|
92
|
-
export function backfillMemoryFiles(
|
|
93
|
-
db: Database.Database,
|
|
94
|
-
memoryDir: string,
|
|
95
|
-
sessionId?: string,
|
|
96
|
-
): { inserted: number; updated: number; skipped: number; total: number } {
|
|
97
|
-
const stats = { inserted: 0, updated: 0, skipped: 0, total: 0 };
|
|
98
|
-
|
|
99
|
-
if (!existsSync(memoryDir)) return stats;
|
|
100
|
-
|
|
101
|
-
const files = readdirSync(memoryDir).filter(
|
|
102
|
-
f => f.endsWith('.md') && f !== 'MEMORY.md'
|
|
103
|
-
);
|
|
104
|
-
stats.total = files.length;
|
|
105
|
-
|
|
106
|
-
const sid = sessionId ?? `backfill-${Date.now()}`;
|
|
107
|
-
|
|
108
|
-
for (const file of files) {
|
|
109
|
-
const result = ingestMemoryFile(db, sid, join(memoryDir, file));
|
|
110
|
-
stats[result]++;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return stats;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function mapMemoryTypeToObservationType(memoryType: string): string {
|
|
117
|
-
switch (memoryType) {
|
|
118
|
-
case 'user':
|
|
119
|
-
case 'feedback':
|
|
120
|
-
return 'decision';
|
|
121
|
-
case 'project':
|
|
122
|
-
return 'feature';
|
|
123
|
-
case 'reference':
|
|
124
|
-
return 'discovery';
|
|
125
|
-
default:
|
|
126
|
-
return 'discovery';
|
|
127
|
-
}
|
|
128
|
-
}
|
package/src/memory-tools.ts
DELETED
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import type Database from 'better-sqlite3';
|
|
5
|
-
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
-
import {
|
|
7
|
-
searchObservations,
|
|
8
|
-
getRecentObservations,
|
|
9
|
-
getSessionSummaries,
|
|
10
|
-
getSessionTimeline,
|
|
11
|
-
getFailedAttempts,
|
|
12
|
-
addObservation,
|
|
13
|
-
assignImportance,
|
|
14
|
-
createSession,
|
|
15
|
-
} from './memory-db.ts';
|
|
16
|
-
import { getConfig, getResolvedPaths } from './config.ts';
|
|
17
|
-
import { backfillMemoryFiles } from './memory-file-ingest.ts';
|
|
18
|
-
|
|
19
|
-
/** Prefix a base tool name with the configured tool prefix. */
|
|
20
|
-
function p(baseName: string): string {
|
|
21
|
-
return `${getConfig().toolPrefix}_${baseName}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ============================================================
|
|
25
|
-
// P4-001 through P4-006: MCP Memory Tools
|
|
26
|
-
// ============================================================
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get all memory tool definitions.
|
|
30
|
-
*/
|
|
31
|
-
export function getMemoryToolDefinitions(): ToolDefinition[] {
|
|
32
|
-
return [
|
|
33
|
-
// P4-001: memory_search
|
|
34
|
-
{
|
|
35
|
-
name: p('memory_search'),
|
|
36
|
-
description: 'Search past session observations and decisions using full-text search. Returns compact index of matching observations.',
|
|
37
|
-
inputSchema: {
|
|
38
|
-
type: 'object',
|
|
39
|
-
properties: {
|
|
40
|
-
query: { type: 'string', description: 'Search text (FTS5 query syntax supported)' },
|
|
41
|
-
type: { type: 'string', description: 'Filter by observation type (decision, bugfix, feature, failed_attempt, cr_violation, vr_check, etc.)' },
|
|
42
|
-
cr_rule: { type: 'string', description: 'Filter by CR rule (e.g., CR-9)' },
|
|
43
|
-
date_from: { type: 'string', description: 'Start date (ISO format)' },
|
|
44
|
-
limit: { type: 'number', description: 'Max results (default: 20)' },
|
|
45
|
-
},
|
|
46
|
-
required: ['query'],
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
// P4-002: memory_timeline
|
|
50
|
-
{
|
|
51
|
-
name: p('memory_timeline'),
|
|
52
|
-
description: 'Retrieve episodic memory - chronological context around a specific event. Shows observations before and after the anchor point to reconstruct what happened and why.',
|
|
53
|
-
inputSchema: {
|
|
54
|
-
type: 'object',
|
|
55
|
-
properties: {
|
|
56
|
-
observation_id: { type: 'number', description: 'Anchor observation ID' },
|
|
57
|
-
depth_before: { type: 'number', description: 'Items before (default: 5)' },
|
|
58
|
-
depth_after: { type: 'number', description: 'Items after (default: 5)' },
|
|
59
|
-
},
|
|
60
|
-
required: ['observation_id'],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
// P4-003: memory_detail
|
|
64
|
-
{
|
|
65
|
-
name: p('memory_detail'),
|
|
66
|
-
description: 'Get full observation details by IDs (batch). Includes evidence, files, plan items.',
|
|
67
|
-
inputSchema: {
|
|
68
|
-
type: 'object',
|
|
69
|
-
properties: {
|
|
70
|
-
ids: {
|
|
71
|
-
type: 'array',
|
|
72
|
-
items: { type: 'number' },
|
|
73
|
-
description: 'Observation IDs to retrieve',
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
required: ['ids'],
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
// P4-004: memory_sessions
|
|
80
|
-
{
|
|
81
|
-
name: p('memory_sessions'),
|
|
82
|
-
description: 'List recent sessions with summaries.',
|
|
83
|
-
inputSchema: {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
limit: { type: 'number', description: 'Max sessions (default: 10)' },
|
|
87
|
-
status: { type: 'string', description: 'Filter by status (active, completed, abandoned)' },
|
|
88
|
-
},
|
|
89
|
-
required: [],
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
// P4-005: memory_failures
|
|
93
|
-
{
|
|
94
|
-
name: p('memory_failures'),
|
|
95
|
-
description: 'Get all failed attempts (DON\'T RETRY warnings). Check before attempting a fix to see if it was already tried.',
|
|
96
|
-
inputSchema: {
|
|
97
|
-
type: 'object',
|
|
98
|
-
properties: {
|
|
99
|
-
query: { type: 'string', description: 'Filter by keyword' },
|
|
100
|
-
limit: { type: 'number', description: 'Max results (default: 20)' },
|
|
101
|
-
},
|
|
102
|
-
required: [],
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
// P4-007: memory_backfill
|
|
106
|
-
{
|
|
107
|
-
name: p('memory_backfill'),
|
|
108
|
-
description: 'Scan all memory/*.md files and ingest into database. Run after massu init or to recover from DB loss. Parses YAML frontmatter and deduplicates by title.',
|
|
109
|
-
inputSchema: {
|
|
110
|
-
type: 'object',
|
|
111
|
-
properties: {},
|
|
112
|
-
required: [],
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
// P4-006: memory_ingest
|
|
116
|
-
{
|
|
117
|
-
name: p('memory_ingest'),
|
|
118
|
-
description: 'Manually record an observation that hooks cannot auto-detect. Use for significant decisions, discoveries, or failed attempts mid-session.',
|
|
119
|
-
inputSchema: {
|
|
120
|
-
type: 'object',
|
|
121
|
-
properties: {
|
|
122
|
-
type: {
|
|
123
|
-
type: 'string',
|
|
124
|
-
description: 'Observation type: decision, bugfix, feature, refactor, discovery, cr_violation, vr_check, pattern_compliance, failed_attempt, file_change, incident_near_miss',
|
|
125
|
-
},
|
|
126
|
-
title: { type: 'string', description: 'Short description' },
|
|
127
|
-
detail: { type: 'string', description: 'Full context' },
|
|
128
|
-
importance: { type: 'number', description: 'Override importance (1-5, default: auto-assigned)' },
|
|
129
|
-
cr_rule: { type: 'string', description: 'Link to CR rule (e.g., CR-9)' },
|
|
130
|
-
plan_item: { type: 'string', description: 'Link to plan item (e.g., P2-003)' },
|
|
131
|
-
files: {
|
|
132
|
-
type: 'array',
|
|
133
|
-
items: { type: 'string' },
|
|
134
|
-
description: 'Files involved',
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
required: ['type', 'title'],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Handle a memory tool call.
|
|
145
|
-
*/
|
|
146
|
-
export function handleMemoryToolCall(
|
|
147
|
-
name: string,
|
|
148
|
-
args: Record<string, unknown>,
|
|
149
|
-
memoryDb: Database.Database
|
|
150
|
-
): ToolResult {
|
|
151
|
-
try {
|
|
152
|
-
const prefix = getConfig().toolPrefix + '_';
|
|
153
|
-
const baseName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
154
|
-
|
|
155
|
-
switch (baseName) {
|
|
156
|
-
case 'memory_search':
|
|
157
|
-
return handleSearch(args, memoryDb);
|
|
158
|
-
case 'memory_timeline':
|
|
159
|
-
return handleTimeline(args, memoryDb);
|
|
160
|
-
case 'memory_detail':
|
|
161
|
-
return handleDetail(args, memoryDb);
|
|
162
|
-
case 'memory_sessions':
|
|
163
|
-
return handleSessions(args, memoryDb);
|
|
164
|
-
case 'memory_failures':
|
|
165
|
-
return handleFailures(args, memoryDb);
|
|
166
|
-
case 'memory_ingest':
|
|
167
|
-
return handleIngest(args, memoryDb);
|
|
168
|
-
case 'memory_backfill':
|
|
169
|
-
return handleBackfill(memoryDb);
|
|
170
|
-
default:
|
|
171
|
-
return text(`Unknown memory tool: ${name}`);
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ============================================================
|
|
179
|
-
// Tool Handlers
|
|
180
|
-
// ============================================================
|
|
181
|
-
|
|
182
|
-
function handleSearch(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
183
|
-
const query = args.query as string;
|
|
184
|
-
if (!query) return text('Error: query is required');
|
|
185
|
-
|
|
186
|
-
const results = searchObservations(db, query, {
|
|
187
|
-
type: args.type as string | undefined,
|
|
188
|
-
crRule: args.cr_rule as string | undefined,
|
|
189
|
-
dateFrom: args.date_from as string | undefined,
|
|
190
|
-
limit: args.limit as number | undefined,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (results.length === 0) {
|
|
194
|
-
return text(`No observations found for "${query}".`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const lines = [`## Search Results for "${query}" (${results.length} matches)`, ''];
|
|
198
|
-
lines.push('| ID | Type | Title | Date | Importance |');
|
|
199
|
-
lines.push('|----|------|-------|------|------------|');
|
|
200
|
-
|
|
201
|
-
for (const r of results) {
|
|
202
|
-
lines.push(`| ${r.id} | ${r.type} | ${r.title.slice(0, 80)} | ${r.created_at.split('T')[0]} | ${r.importance} |`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
lines.push('');
|
|
206
|
-
lines.push(`Use ${p('memory_detail')} with IDs for full details.`);
|
|
207
|
-
lines.push(`Use ${p('memory_timeline')} with an ID for chronological context.`);
|
|
208
|
-
|
|
209
|
-
return text(lines.join('\n'));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function handleTimeline(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
213
|
-
const observationId = args.observation_id as number;
|
|
214
|
-
if (!observationId) return text('Error: observation_id is required');
|
|
215
|
-
|
|
216
|
-
const depthBefore = (args.depth_before as number) ?? 5;
|
|
217
|
-
const depthAfter = (args.depth_after as number) ?? 5;
|
|
218
|
-
|
|
219
|
-
// Get the anchor observation
|
|
220
|
-
const anchor = db.prepare('SELECT * FROM observations WHERE id = ?').get(observationId) as Record<string, unknown> | undefined;
|
|
221
|
-
if (!anchor) return text(`Observation ${observationId} not found.`);
|
|
222
|
-
|
|
223
|
-
// Get observations before
|
|
224
|
-
const before = db.prepare(
|
|
225
|
-
'SELECT id, type, title, created_at, importance FROM observations WHERE session_id = ? AND created_at_epoch < ? ORDER BY created_at_epoch DESC LIMIT ?'
|
|
226
|
-
).all(anchor.session_id, anchor.created_at_epoch, depthBefore) as Array<Record<string, unknown>>;
|
|
227
|
-
|
|
228
|
-
// Get observations after
|
|
229
|
-
const after = db.prepare(
|
|
230
|
-
'SELECT id, type, title, created_at, importance FROM observations WHERE session_id = ? AND created_at_epoch > ? ORDER BY created_at_epoch ASC LIMIT ?'
|
|
231
|
-
).all(anchor.session_id, anchor.created_at_epoch, depthAfter) as Array<Record<string, unknown>>;
|
|
232
|
-
|
|
233
|
-
const lines = [`## Timeline around observation #${observationId}`, ''];
|
|
234
|
-
|
|
235
|
-
// Before (reversed to chronological order)
|
|
236
|
-
for (const o of before.reverse()) {
|
|
237
|
-
lines.push(` ${o.id} | ${o.type} | ${(o.title as string).slice(0, 60)} | ${(o.created_at as string).split('T')[0]}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Anchor
|
|
241
|
-
lines.push(`> ${anchor.id} | ${anchor.type} | ${(anchor.title as string).slice(0, 60)} | ${(anchor.created_at as string).split('T')[0]} <-- ANCHOR`);
|
|
242
|
-
|
|
243
|
-
// After
|
|
244
|
-
for (const o of after) {
|
|
245
|
-
lines.push(` ${o.id} | ${o.type} | ${(o.title as string).slice(0, 60)} | ${(o.created_at as string).split('T')[0]}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return text(lines.join('\n'));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function handleDetail(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
252
|
-
const ids = args.ids as number[];
|
|
253
|
-
if (!ids || ids.length === 0) return text('Error: ids array is required');
|
|
254
|
-
|
|
255
|
-
const placeholders = ids.map(() => '?').join(',');
|
|
256
|
-
const observations = db.prepare(
|
|
257
|
-
`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch ASC`
|
|
258
|
-
).all(...ids) as Array<Record<string, unknown>>;
|
|
259
|
-
|
|
260
|
-
if (observations.length === 0) {
|
|
261
|
-
return text('No observations found for the given IDs.');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const lines: string[] = [];
|
|
265
|
-
for (const o of observations) {
|
|
266
|
-
lines.push(`## Observation #${o.id} [${o.type}] (importance: ${o.importance})`);
|
|
267
|
-
lines.push(`**Title**: ${o.title}`);
|
|
268
|
-
lines.push(`**Session**: ${o.session_id}`);
|
|
269
|
-
lines.push(`**Date**: ${o.created_at}`);
|
|
270
|
-
|
|
271
|
-
if (o.detail) lines.push(`**Detail**: ${o.detail}`);
|
|
272
|
-
if (o.files_involved && o.files_involved !== '[]') {
|
|
273
|
-
const files = safeParseJson(o.files_involved as string, []) as string[];
|
|
274
|
-
if (files.length > 0) lines.push(`**Files**: ${files.join(', ')}`);
|
|
275
|
-
}
|
|
276
|
-
if (o.plan_item) lines.push(`**Plan Item**: ${o.plan_item}`);
|
|
277
|
-
if (o.cr_rule) lines.push(`**CR Rule**: ${o.cr_rule}`);
|
|
278
|
-
if (o.vr_type) lines.push(`**VR Type**: ${o.vr_type}`);
|
|
279
|
-
if (o.evidence) lines.push(`**Evidence**: ${(o.evidence as string).slice(0, 500)}`);
|
|
280
|
-
if ((o.recurrence_count as number) > 1) lines.push(`**Recurrence**: ${o.recurrence_count}x`);
|
|
281
|
-
lines.push('');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return text(lines.join('\n'));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function handleSessions(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
288
|
-
const limit = (args.limit as number) ?? 10;
|
|
289
|
-
const status = args.status as string | undefined;
|
|
290
|
-
|
|
291
|
-
let sql = 'SELECT * FROM sessions';
|
|
292
|
-
const params: (string | number)[] = [];
|
|
293
|
-
|
|
294
|
-
if (status) {
|
|
295
|
-
sql += ' WHERE status = ?';
|
|
296
|
-
params.push(status);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
sql += ' ORDER BY started_at_epoch DESC LIMIT ?';
|
|
300
|
-
params.push(limit);
|
|
301
|
-
|
|
302
|
-
const sessions = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
303
|
-
|
|
304
|
-
if (sessions.length === 0) {
|
|
305
|
-
return text('No sessions found.');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const lines = ['## Recent Sessions', ''];
|
|
309
|
-
lines.push('| Session ID | Status | Branch | Started | Plan |');
|
|
310
|
-
lines.push('|------------|--------|--------|---------|------|');
|
|
311
|
-
|
|
312
|
-
for (const s of sessions) {
|
|
313
|
-
const started = (s.started_at as string).split('T')[0];
|
|
314
|
-
const plan = s.plan_file ? (s.plan_file as string).split('/').pop() : '-';
|
|
315
|
-
lines.push(`| ${(s.session_id as string).slice(0, 8)}... | ${s.status} | ${s.git_branch ?? '-'} | ${started} | ${plan} |`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Add summaries for each session
|
|
319
|
-
for (const s of sessions) {
|
|
320
|
-
const summaries = getSessionSummaries(db, 1);
|
|
321
|
-
const summary = summaries.find(sm => sm.session_id === s.session_id);
|
|
322
|
-
if (summary) {
|
|
323
|
-
lines.push('');
|
|
324
|
-
lines.push(`### ${(s.session_id as string).slice(0, 8)}...`);
|
|
325
|
-
if (summary.request) lines.push(`**Task**: ${summary.request.slice(0, 200)}`);
|
|
326
|
-
if (summary.completed) lines.push(`**Completed**: ${summary.completed.slice(0, 200)}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return text(lines.join('\n'));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function handleFailures(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
334
|
-
const query = args.query as string | undefined;
|
|
335
|
-
const limit = (args.limit as number) ?? 20;
|
|
336
|
-
|
|
337
|
-
const failures = getFailedAttempts(db, query, limit);
|
|
338
|
-
|
|
339
|
-
if (failures.length === 0) {
|
|
340
|
-
return text(query ? `No failed attempts found for "${query}".` : 'No failed attempts recorded.');
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const lines = ['## Failed Attempts (DO NOT RETRY)', ''];
|
|
344
|
-
|
|
345
|
-
for (const f of failures) {
|
|
346
|
-
const recurrence = f.recurrence_count > 1 ? ` (occurred ${f.recurrence_count}x across sessions)` : '';
|
|
347
|
-
lines.push(`### #${f.id}: ${f.title}${recurrence}`);
|
|
348
|
-
if (f.detail) lines.push(f.detail.slice(0, 500));
|
|
349
|
-
lines.push(`Session: ${f.session_id.slice(0, 8)}... | Date: ${f.created_at.split('T')[0]}`);
|
|
350
|
-
lines.push('');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return text(lines.join('\n'));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function handleIngest(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
357
|
-
const type = args.type as string;
|
|
358
|
-
const title = args.title as string;
|
|
359
|
-
|
|
360
|
-
if (!type || !title) return text('Error: type and title are required');
|
|
361
|
-
|
|
362
|
-
const validTypes = ['decision', 'bugfix', 'feature', 'refactor', 'discovery',
|
|
363
|
-
'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
|
|
364
|
-
'file_change', 'incident_near_miss'];
|
|
365
|
-
|
|
366
|
-
if (!validTypes.includes(type)) {
|
|
367
|
-
return text(`Error: invalid type "${type}". Valid types: ${validTypes.join(', ')}`);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// We need a session_id - get the most recent active session
|
|
371
|
-
const activeSession = db.prepare(
|
|
372
|
-
"SELECT session_id FROM sessions WHERE status = 'active' ORDER BY started_at_epoch DESC LIMIT 1"
|
|
373
|
-
).get() as { session_id: string } | undefined;
|
|
374
|
-
|
|
375
|
-
if (!activeSession) {
|
|
376
|
-
return text('Error: no active session found. Start a session first.');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const importance = (args.importance as number) ?? assignImportance(type);
|
|
380
|
-
const id = addObservation(db, activeSession.session_id, type, title, (args.detail as string) ?? null, {
|
|
381
|
-
importance,
|
|
382
|
-
crRule: args.cr_rule as string | undefined,
|
|
383
|
-
planItem: args.plan_item as string | undefined,
|
|
384
|
-
filesInvolved: args.files as string[] | undefined,
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
return text(`Observation #${id} recorded successfully.\nType: ${type}\nTitle: ${title}\nImportance: ${importance}\nSession: ${activeSession.session_id.slice(0, 8)}...`);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function handleBackfill(db: Database.Database): ToolResult {
|
|
391
|
-
const memoryDir = getResolvedPaths().memoryDir;
|
|
392
|
-
const stats = backfillMemoryFiles(db, memoryDir);
|
|
393
|
-
|
|
394
|
-
const lines = [
|
|
395
|
-
'## Memory Backfill Results',
|
|
396
|
-
'',
|
|
397
|
-
`- **Total files scanned**: ${stats.total}`,
|
|
398
|
-
`- **Inserted (new)**: ${stats.inserted}`,
|
|
399
|
-
`- **Updated (existing)**: ${stats.updated}`,
|
|
400
|
-
`- **Skipped (not found)**: ${stats.skipped}`,
|
|
401
|
-
'',
|
|
402
|
-
stats.total === 0
|
|
403
|
-
? 'No memory files found in memory directory.'
|
|
404
|
-
: `Successfully processed ${stats.inserted + stats.updated} of ${stats.total} memory files.`,
|
|
405
|
-
];
|
|
406
|
-
|
|
407
|
-
return text(lines.join('\n'));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// ============================================================
|
|
411
|
-
// Helpers
|
|
412
|
-
// ============================================================
|
|
413
|
-
|
|
414
|
-
function text(content: string): ToolResult {
|
|
415
|
-
return { content: [{ type: 'text', text: content }] };
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function safeParseJson(json: string, fallback: unknown): unknown {
|
|
419
|
-
try {
|
|
420
|
-
return JSON.parse(json);
|
|
421
|
-
} catch (_e) {
|
|
422
|
-
return fallback;
|
|
423
|
-
}
|
|
424
|
-
}
|
package/src/middleware-tree.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import type Database from 'better-sqlite3';
|
|
5
|
-
import { getConfig } from './config.ts';
|
|
6
|
-
import { existsSync } from 'fs';
|
|
7
|
-
import { resolve } from 'path';
|
|
8
|
-
import { t } from './lib/sql-table-names.ts';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Build the middleware import tree by tracing all transitive imports
|
|
12
|
-
* from the middleware entry point. Any file in this tree is subject to
|
|
13
|
-
* Edge Runtime restrictions (no Node.js deps).
|
|
14
|
-
*/
|
|
15
|
-
export function buildMiddlewareTree(dataDb: Database.Database): number {
|
|
16
|
-
// Clear existing data
|
|
17
|
-
dataDb.exec(`DELETE FROM ${t('middleware_tree')}`);
|
|
18
|
-
|
|
19
|
-
const config = getConfig();
|
|
20
|
-
const middlewareFile = config.paths.middleware ?? 'src/middleware.ts';
|
|
21
|
-
|
|
22
|
-
// Only build if the middleware file path is configured and exists conceptually
|
|
23
|
-
if (!middlewareFile) return 0;
|
|
24
|
-
|
|
25
|
-
// BFS through import edges starting from middleware.ts
|
|
26
|
-
const visited = new Set<string>();
|
|
27
|
-
const queue: string[] = [middlewareFile];
|
|
28
|
-
visited.add(middlewareFile);
|
|
29
|
-
|
|
30
|
-
while (queue.length > 0) {
|
|
31
|
-
const current = queue.shift()!;
|
|
32
|
-
|
|
33
|
-
const imports = dataDb.prepare(
|
|
34
|
-
`SELECT target_file FROM ${t('imports')} WHERE source_file = ?`
|
|
35
|
-
).all(current) as { target_file: string }[];
|
|
36
|
-
|
|
37
|
-
for (const imp of imports) {
|
|
38
|
-
if (!visited.has(imp.target_file) && imp.target_file.startsWith('src/')) {
|
|
39
|
-
visited.add(imp.target_file);
|
|
40
|
-
queue.push(imp.target_file);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Store the tree
|
|
46
|
-
const insertStmt = dataDb.prepare(`INSERT INTO ${t('middleware_tree')} (file) VALUES (?)`);
|
|
47
|
-
const insertAll = dataDb.transaction(() => {
|
|
48
|
-
for (const file of visited) {
|
|
49
|
-
insertStmt.run(file);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
insertAll();
|
|
53
|
-
|
|
54
|
-
return visited.size;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Check if a file is in the middleware import tree.
|
|
59
|
-
*/
|
|
60
|
-
export function isInMiddlewareTree(dataDb: Database.Database, file: string): boolean {
|
|
61
|
-
const result = dataDb.prepare(`SELECT 1 FROM ${t('middleware_tree')} WHERE file = ?`).get(file);
|
|
62
|
-
return result !== undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get all files in the middleware import tree.
|
|
67
|
-
*/
|
|
68
|
-
export function getMiddlewareTree(dataDb: Database.Database): string[] {
|
|
69
|
-
const rows = dataDb.prepare(`SELECT file FROM ${t('middleware_tree')} ORDER BY file`).all() as { file: string }[];
|
|
70
|
-
return rows.map(r => r.file);
|
|
71
|
-
}
|