memorix 1.7.2 → 1.8.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 +59 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/assets/star-history-dark.svg +4 -4
- package/assets/star-history-light.svg +4 -4
- package/assets/star-history.json +47 -3
- package/dist/cli/index.js +1801 -254
- package/dist/dashboard/static/app.js +88 -4
- package/dist/dashboard/static/index.html +10 -0
- package/dist/dashboard/static/style.css +86 -0
- package/dist/index.js +1241 -224
- package/dist/maintenance-runner.js +972 -126
- package/dist/media-audio-runner.js +967 -121
- package/dist/media-video-runner.js +972 -126
- package/dist/memcode-runtime/CHANGELOG.md +59 -1
- package/dist/sdk.js +1240 -223
- package/dist/types.d.ts +8 -0
- package/dist/vector-backfill-runner.js +972 -126
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
- package/docs/API_REFERENCE.md +37 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +57 -1
- package/docs/DASHBOARD.md +10 -0
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/PERFORMANCE.md +3 -1
- package/docs/README.md +11 -1
- package/docs/SETUP.md +2 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/rules/memorix.md +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/SKILL.md +1 -1
- package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/gemini/memorix/GEMINI.md +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/server.json +2 -2
- package/src/cli/capability-map.ts +2 -0
- package/src/cli/commands/config-migrate.ts +7 -0
- package/src/cli/commands/context.ts +11 -0
- package/src/cli/commands/doctor.ts +5 -1
- package/src/cli/commands/evidence.ts +56 -0
- package/src/cli/commands/feedback.ts +58 -0
- package/src/cli/commands/init.ts +17 -0
- package/src/cli/commands/resume.ts +2 -0
- package/src/cli/commands/serve-http.ts +201 -12
- package/src/cli/commands/status.ts +2 -0
- package/src/cli/fallback-budget.ts +59 -0
- package/src/cli/index.ts +5 -1
- package/src/compact/engine.ts +26 -1
- package/src/config/dotenv-loader.ts +17 -0
- package/src/config/resolved-config.ts +110 -12
- package/src/config/toml-loader.ts +8 -0
- package/src/config/yaml-loader.ts +9 -0
- package/src/dashboard/config-provenance.ts +17 -0
- package/src/dashboard/evidence.ts +63 -0
- package/src/dashboard/server.ts +93 -68
- package/src/dashboard/static/index.html +10 -0
- package/src/dashboard/static/style.css +86 -0
- package/src/hooks/installers/index.ts +1 -1
- package/src/hooks/official-skills.ts +3 -3
- package/src/hooks/rules/memorix-agent-rules.md +1 -1
- package/src/llm/provider.ts +33 -7
- package/src/memory/feedback-model.ts +4 -0
- package/src/memory/feedback.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/memory/retention.ts +38 -5
- package/src/multimodal/image-loader.ts +6 -3
- package/src/rerank/http-provider.ts +125 -0
- package/src/rerank/index.ts +81 -0
- package/src/rerank/policy.ts +88 -0
- package/src/server/mcp-binding-store.ts +79 -0
- package/src/server/mcp-diagnostics.ts +40 -0
- package/src/server/tool-profile.ts +4 -0
- package/src/server.ts +121 -1
- package/src/store/evidence-store.ts +261 -0
- package/src/store/orama-store.ts +79 -53
- package/src/store/sqlite-db.ts +104 -0
- package/src/types.ts +8 -0
|
@@ -69,6 +69,13 @@ export function serializeResolvedConfigToToml(
|
|
|
69
69
|
dimensions: config.embedding.dimensions,
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
writeSection(lines, 'rerank', {
|
|
73
|
+
provider: config.rerank.provider,
|
|
74
|
+
model: config.rerank.model,
|
|
75
|
+
base_url: config.rerank.baseUrl,
|
|
76
|
+
api_key: includeSecrets ? config.rerank.apiKey : undefined,
|
|
77
|
+
});
|
|
78
|
+
|
|
72
79
|
writeSection(lines, 'memory', {
|
|
73
80
|
inject: config.memory.inject,
|
|
74
81
|
formation: config.memory.formation,
|
|
@@ -11,6 +11,7 @@ import { buildBoundedContextReceipt } from '../../knowledge/context-receipt.js';
|
|
|
11
11
|
import { WORKFLOW_AGENT_TARGETS } from '../../knowledge/workflows.js';
|
|
12
12
|
import type { AgentTarget } from '../../types.js';
|
|
13
13
|
import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
|
|
14
|
+
import { CliFallbackBudget } from '../fallback-budget.js';
|
|
14
15
|
|
|
15
16
|
function coerceRefreshMode(input?: string): AutoContextRefreshMode {
|
|
16
17
|
const value = (input ?? 'auto').trim().toLowerCase();
|
|
@@ -42,6 +43,7 @@ export default defineCommand({
|
|
|
42
43
|
agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
|
|
43
44
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
44
45
|
briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
|
|
46
|
+
fallback: { type: 'boolean', description: 'Mark this as the one-shot MCP-unavailable fallback for the task' },
|
|
45
47
|
},
|
|
46
48
|
run: async ({ args }) => {
|
|
47
49
|
const asJson = !!args.json || !!args.briefJson;
|
|
@@ -51,6 +53,15 @@ export default defineCommand({
|
|
|
51
53
|
|
|
52
54
|
try {
|
|
53
55
|
const { project, dataDir, reader } = await getCliProjectContext();
|
|
56
|
+
if (args.fallback) {
|
|
57
|
+
const budget = new CliFallbackBudget();
|
|
58
|
+
await budget.init(dataDir);
|
|
59
|
+
const claim = budget.claim(project.id, task);
|
|
60
|
+
if (!claim.allowed) {
|
|
61
|
+
emitError(claim.reason ?? 'CLI fallback budget exhausted for this task.', asJson);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
54
65
|
const context = await buildAutoProjectContext({
|
|
55
66
|
project,
|
|
56
67
|
dataDir,
|
|
@@ -517,7 +517,11 @@ export default defineCommand({
|
|
|
517
517
|
if (isLLMEnabled()) {
|
|
518
518
|
const config = getLLMConfig();
|
|
519
519
|
lines.push(ok(`Provider: ${config?.provider}/${config?.model}`));
|
|
520
|
-
|
|
520
|
+
const { isNeuralRerankEnabled } = await import('../../rerank/index.js');
|
|
521
|
+
const rerankCap = isNeuralRerankEnabled()
|
|
522
|
+
? 'HTTP rerank'
|
|
523
|
+
: 'LLM rerank';
|
|
524
|
+
lines.push(info(`Capabilities: fact extraction, auto-dedup, ${rerankCap}`));
|
|
521
525
|
report.llm = { enabled: true, provider: config?.provider, model: config?.model };
|
|
522
526
|
} else {
|
|
523
527
|
lines.push(info('LLM mode: off (heuristic dedup only)'));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { getObservationStore } from '../../store/obs-store.js';
|
|
3
|
+
import { EvidenceCardStore } from '../../store/evidence-store.js';
|
|
4
|
+
import { emitError, emitResult, getCliProjectContext, getCliReadContext } from './operator-shared.js';
|
|
5
|
+
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
meta: { name: 'evidence', description: 'Inspect project-scoped evidence cards and provenance' },
|
|
8
|
+
args: {
|
|
9
|
+
action: { type: 'positional', description: 'list, get, sync, stale, or events', required: false },
|
|
10
|
+
id: { type: 'string', description: 'Candidate id, usually an observation id' },
|
|
11
|
+
paths: { type: 'string', description: 'Comma-separated changed paths for stale' },
|
|
12
|
+
reason: { type: 'string', description: 'Why the source became stale' },
|
|
13
|
+
query: { type: 'string', description: 'Search title/source fields' },
|
|
14
|
+
limit: { type: 'string', description: 'Maximum cards/events' },
|
|
15
|
+
json: { type: 'boolean', description: 'Emit JSON' },
|
|
16
|
+
},
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
const action = (args.action as string | undefined) || 'list';
|
|
19
|
+
const asJson = !!args.json;
|
|
20
|
+
try {
|
|
21
|
+
const context = action === 'list' || action === 'get' || action === 'events'
|
|
22
|
+
? await getCliReadContext()
|
|
23
|
+
: await getCliProjectContext({ searchIndex: false });
|
|
24
|
+
const { project, dataDir } = context;
|
|
25
|
+
const store = new EvidenceCardStore();
|
|
26
|
+
await store.init(dataDir);
|
|
27
|
+
const limit = Math.min(100, Math.max(1, Number(args.limit || 20)));
|
|
28
|
+
if (action === 'sync') {
|
|
29
|
+
const synced = store.syncObservations(await getObservationStore().loadByProject(project.id));
|
|
30
|
+
emitResult({ project, synced: synced.length }, `Synced ${synced.length} evidence card(s).`, asJson);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (action === 'stale') {
|
|
34
|
+
const paths = String(args.paths || '').split(',').map((value) => value.trim()).filter(Boolean);
|
|
35
|
+
const changed = store.markStaleForPaths(project.id, paths, String(args.reason || 'source file changed'));
|
|
36
|
+
emitResult({ project, staleMarked: changed, paths }, `Marked ${changed} evidence card(s) stale.`, asJson);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const id = String(args.id || '').trim();
|
|
40
|
+
if (action === 'get' || action === 'events') {
|
|
41
|
+
if (!id) { emitError(`--id is required for evidence ${action}.`, asJson); return; }
|
|
42
|
+
const card = store.get(project.id, 'observation', id);
|
|
43
|
+
if (!card) { emitError(`Evidence card for observation ${id} was not found.`, asJson); return; }
|
|
44
|
+
const result = action === 'events' ? store.listEvents(card.id, limit) : { card, events: store.listEvents(card.id) };
|
|
45
|
+
emitResult({ project, result }, JSON.stringify(result, null, 2), asJson);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (action !== 'list') { emitError('action must be list, get, sync, stale, or events.', asJson); return; }
|
|
49
|
+
const query = String(args.query || '').trim().toLowerCase();
|
|
50
|
+
const cards = store.list(project.id, { limit: 100 }).filter((card) => !query || [card.title, card.summary, card.sourceRef, card.locator || ''].join(' ').toLowerCase().includes(query)).slice(0, limit);
|
|
51
|
+
emitResult({ project, cards }, cards.length === 0 ? 'No evidence cards found.' : cards.map((card) => `- ${card.candidateId} ${card.title} [${card.freshness}] ${card.sourceRef}`).join('\n'), asJson);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { MemoryFeedbackStore, type FeedbackCandidateKind, type MemoryFeedbackSignal } from '../../memory/feedback.js';
|
|
3
|
+
import { emitError, emitResult, getCliProjectContext, getCliReadContext } from './operator-shared.js';
|
|
4
|
+
|
|
5
|
+
const signals: MemoryFeedbackSignal[] = ['user-correction', 'verification-success', 'verification-failure', 'code-change', 'used', 'not-used', 'code-conflict', 'strengthen', 'weaken', 'revoke'];
|
|
6
|
+
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
meta: { name: 'feedback', description: 'Record and audit memory feedback' },
|
|
9
|
+
args: {
|
|
10
|
+
action: { type: 'positional', description: 'record, show, or audit', required: false },
|
|
11
|
+
id: { type: 'string', description: 'Candidate id' },
|
|
12
|
+
kind: { type: 'string', description: 'observation, claim, durable-memory, or workflow' },
|
|
13
|
+
signal: { type: 'string', description: 'Feedback signal' },
|
|
14
|
+
source: { type: 'string', description: 'Source reference for the feedback' },
|
|
15
|
+
actor: { type: 'string', description: 'Actor id or human label' },
|
|
16
|
+
note: { type: 'string', description: 'Short audit note' },
|
|
17
|
+
targetEventId: { type: 'string', description: 'Event id to revoke' },
|
|
18
|
+
limit: { type: 'string', description: 'Maximum audit rows' },
|
|
19
|
+
json: { type: 'boolean', description: 'Emit JSON' },
|
|
20
|
+
},
|
|
21
|
+
async run({ args }) {
|
|
22
|
+
const action = (args.action as string | undefined) || 'show';
|
|
23
|
+
const asJson = !!args.json;
|
|
24
|
+
try {
|
|
25
|
+
const context = action === 'show' || action === 'audit' ? await getCliReadContext() : await getCliProjectContext({ searchIndex: false });
|
|
26
|
+
const { project, dataDir } = context;
|
|
27
|
+
const id = String(args.id || '').trim();
|
|
28
|
+
const kind = (String(args.kind || 'observation').trim() || 'observation') as FeedbackCandidateKind;
|
|
29
|
+
if (!id) { emitError('--id is required.', asJson); return; }
|
|
30
|
+
if (!['observation', 'claim', 'durable-memory', 'workflow'].includes(kind)) { emitError('Invalid --kind.', asJson); return; }
|
|
31
|
+
const store = new MemoryFeedbackStore();
|
|
32
|
+
await store.init(dataDir);
|
|
33
|
+
if (action === 'show') {
|
|
34
|
+
const result = store.getState(project.id, kind, id);
|
|
35
|
+
emitResult({ project, result }, JSON.stringify(result, null, 2), asJson);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (action === 'audit') {
|
|
39
|
+
const result = store.audit(project.id, kind, id, Number(args.limit || 100));
|
|
40
|
+
emitResult({ project, result }, JSON.stringify(result, null, 2), asJson);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (action !== 'record') { emitError('action must be record, show, or audit.', asJson); return; }
|
|
44
|
+
const signal = String(args.signal || '').trim() as MemoryFeedbackSignal;
|
|
45
|
+
const sourceRef = String(args.source || '').trim();
|
|
46
|
+
if (!signals.includes(signal) || !sourceRef) { emitError(`record requires --signal (${signals.join(', ')}) and --source.`, asJson); return; }
|
|
47
|
+
const result = store.record({
|
|
48
|
+
projectId: project.id, candidateKind: kind, candidateId: id, signal, sourceRef,
|
|
49
|
+
...(args.actor ? { actor: String(args.actor) } : {}),
|
|
50
|
+
...(args.note ? { note: String(args.note) } : {}),
|
|
51
|
+
...(args.targetEventId ? { targetEventId: String(args.targetEventId) } : {}),
|
|
52
|
+
});
|
|
53
|
+
emitResult({ project, result }, `Recorded ${signal} for ${kind}:${id}; weight=${result.state.weight.toFixed(2)}.`, asJson);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
emitError(error instanceof Error ? error.message : String(error), asJson);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -183,6 +183,12 @@ export default defineCommand({
|
|
|
183
183
|
envLines.push('');
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
envLines.push('# Optional HTTP rerank (Cohere-compatible POST {base_url}/rerank)');
|
|
187
|
+
envLines.push('# MEMORIX_RERANK_PROVIDER=http');
|
|
188
|
+
envLines.push('# MEMORIX_RERANK_MODEL=rerank-model');
|
|
189
|
+
envLines.push('# MEMORIX_RERANK_BASE_URL=https://api.example.com/v1');
|
|
190
|
+
envLines.push('# MEMORIX_RERANK_API_KEY= # required when BASE_URL is not the memory LLM endpoint');
|
|
191
|
+
envLines.push('');
|
|
186
192
|
envLines.push('# Optional memory LLM simple key (does not apply to embedding or agent lanes)');
|
|
187
193
|
envLines.push('# MEMORIX_API_KEY=sk-your-key-here');
|
|
188
194
|
envLines.push('');
|
|
@@ -310,6 +316,17 @@ export function buildInitTomlConfig(options: {
|
|
|
310
316
|
}
|
|
311
317
|
lines.push('');
|
|
312
318
|
|
|
319
|
+
lines.push('[rerank]');
|
|
320
|
+
lines.push('# Optional HTTP rerank against any Cohere-compatible /rerank API.');
|
|
321
|
+
lines.push('# provider = "http"');
|
|
322
|
+
lines.push('# model = "rerank-model"');
|
|
323
|
+
lines.push('# base_url inherits [memory.llm] when unset.');
|
|
324
|
+
if (isGlobal) {
|
|
325
|
+
lines.push('# Bearer inherits the memory LLM key only when that endpoint matches.');
|
|
326
|
+
lines.push('# A different rerank URL needs MEMORIX_RERANK_API_KEY.');
|
|
327
|
+
}
|
|
328
|
+
lines.push('');
|
|
329
|
+
|
|
313
330
|
lines.push('[git]');
|
|
314
331
|
lines.push(`auto_hook = ${options.gitAutoHook}`);
|
|
315
332
|
lines.push('ingest_on_commit = true');
|
|
@@ -16,6 +16,7 @@ export default defineCommand({
|
|
|
16
16
|
agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
|
|
17
17
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
18
18
|
briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
|
|
19
|
+
fallback: { type: 'boolean', description: 'Mark this as the one-shot MCP-unavailable fallback for the task' },
|
|
19
20
|
},
|
|
20
21
|
async run({ args }) {
|
|
21
22
|
await contextCommand.run?.({
|
|
@@ -26,6 +27,7 @@ export default defineCommand({
|
|
|
26
27
|
agent: args.agent,
|
|
27
28
|
json: args.json,
|
|
28
29
|
briefJson: args.briefJson,
|
|
30
|
+
fallback: args.fallback,
|
|
29
31
|
resume: true,
|
|
30
32
|
},
|
|
31
33
|
rawArgs: [],
|
|
@@ -26,6 +26,7 @@ import { resolveToolProfile } from '../../server/tool-profile.js';
|
|
|
26
26
|
import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
|
|
27
27
|
import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
|
|
28
28
|
import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
|
|
29
|
+
import { CURRENT_MCP_PROTOCOL_VERSION, MCP_SERVER_NAME, getMcpDiagnostics } from '../../server/mcp-diagnostics.js';
|
|
29
30
|
import { parseTcpPortOrReport } from '../port.js';
|
|
30
31
|
import { mcpFileUriToPath } from '../mcp-root-path.js';
|
|
31
32
|
|
|
@@ -36,6 +37,8 @@ import { mcpFileUriToPath } from '../mcp-root-path.js';
|
|
|
36
37
|
*/
|
|
37
38
|
export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
|
|
38
39
|
export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
|
|
40
|
+
/** Pinned SDK protocol accepted internally while the HTTP boundary speaks the current stateless contract. */
|
|
41
|
+
export const MCP_SDK_COMPAT_PROTOCOL_VERSION = '2025-11-25';
|
|
39
42
|
|
|
40
43
|
export function parseSessionTimeoutMs(raw: string | undefined): number {
|
|
41
44
|
const value = raw?.trim();
|
|
@@ -116,6 +119,11 @@ export default defineCommand({
|
|
|
116
119
|
const { findGitInSubdirs } = await import('../../project/detector.js');
|
|
117
120
|
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
118
121
|
const { homedir } = await import('node:os');
|
|
122
|
+
// Keep SQLite-backed control-plane helpers lazy. Besides reducing the
|
|
123
|
+
// handshake import graph, this prevents agent integration tests that mock
|
|
124
|
+
// node:fs from being evaluated before their mock factories initialize.
|
|
125
|
+
const { McpBindingStore } = await import('../../server/mcp-binding-store.js');
|
|
126
|
+
const { MemoryFeedbackStore } = await import('../../memory/feedback.js');
|
|
119
127
|
|
|
120
128
|
// The [server] config section feeds startup defaults; CLI flags win.
|
|
121
129
|
let configuredDefaults: ServeHttpConfiguredDefaults = {};
|
|
@@ -176,11 +184,12 @@ export default defineCommand({
|
|
|
176
184
|
return store;
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
187
|
+
// TeamStore is resolved per MCP session/project. Sharing one mutable
|
|
188
|
+
// singleton here made project A's coordination state visible to project B
|
|
189
|
+
// after a roots switch. The cache still reuses the same project's SQLite
|
|
190
|
+
// handle without crossing project data directories.
|
|
191
|
+
const statelessBindingStore = new McpBindingStore();
|
|
192
|
+
await statelessBindingStore.init(persistMod.getBaseDataDir());
|
|
184
193
|
|
|
185
194
|
type SessionState = {
|
|
186
195
|
transport: InstanceType<typeof StreamableHTTPServerTransport>;
|
|
@@ -396,8 +405,95 @@ export default defineCommand({
|
|
|
396
405
|
// No Access-Control-Allow-Origin at all for disallowed origins —
|
|
397
406
|
// browser will block the response (fail-closed).
|
|
398
407
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
399
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Last-Event-Id');
|
|
400
|
-
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
408
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
|
|
409
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, Mcp-Project-Handle, Mcp-Protocol-Version, Mcp-Method, Mcp-Name');
|
|
410
|
+
res.setHeader('Mcp-Protocol-Version', req.headers['mcp-protocol-version'] || CURRENT_MCP_PROTOCOL_VERSION);
|
|
411
|
+
res.setHeader('Mcp-Method', req.method || 'UNKNOWN');
|
|
412
|
+
res.setHeader('Mcp-Name', MCP_SERVER_NAME);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Handle one 2026 stateless MCP request with a fresh server/transport. */
|
|
416
|
+
async function handleStatelessPost(req: IncomingMessage, res: ServerResponse, body: unknown): Promise<void> {
|
|
417
|
+
const handleId = typeof req.headers['mcp-project-handle'] === 'string'
|
|
418
|
+
? req.headers['mcp-project-handle']
|
|
419
|
+
: undefined;
|
|
420
|
+
const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
|
|
421
|
+
if (handleId && !persisted) {
|
|
422
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
423
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'MCP project handle not found or expired; initialize again.' }, id: null }));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const requestedRoot = typeof req.headers['mcp-project-root'] === 'string'
|
|
427
|
+
? req.headers['mcp-project-root']
|
|
428
|
+
: undefined;
|
|
429
|
+
const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
|
|
430
|
+
if (!persisted && !isInitializeRequest(body)) {
|
|
431
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
432
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Stateless MCP requests require Mcp-Project-Handle; initialize first.' }, id: null }));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const { createMemorixServer } = await import('../../server.js');
|
|
437
|
+
const { createProjectBindingController } = await import('../../server/request-context.js');
|
|
438
|
+
const binding = createProjectBindingController(root);
|
|
439
|
+
const { server, projectId, handleTransportClose } = await createMemorixServer(
|
|
440
|
+
root,
|
|
441
|
+
undefined,
|
|
442
|
+
undefined,
|
|
443
|
+
{
|
|
444
|
+
allowUntrackedFallback: false,
|
|
445
|
+
deferProjectInitUntilBound: true,
|
|
446
|
+
deferProjectRuntimeInit: true,
|
|
447
|
+
dashboardMode: 'control-plane',
|
|
448
|
+
dashboardPort: port,
|
|
449
|
+
toolProfile,
|
|
450
|
+
projectBinding: binding,
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
const transport = new StreamableHTTPServerTransport({
|
|
454
|
+
// No Mcp-Session-Id: the protocol state is carried by our explicit
|
|
455
|
+
// durable project handle and reconstructed on every request.
|
|
456
|
+
sessionIdGenerator: undefined,
|
|
457
|
+
enableJsonResponse: true,
|
|
458
|
+
});
|
|
459
|
+
try {
|
|
460
|
+
await server.connect(transport);
|
|
461
|
+
let effectiveHandle = persisted;
|
|
462
|
+
if (!effectiveHandle && projectId !== '__unresolved__') {
|
|
463
|
+
effectiveHandle = statelessBindingStore.create({
|
|
464
|
+
projectId,
|
|
465
|
+
projectRoot: root,
|
|
466
|
+
dataDir: await persistMod.getProjectDataDir(projectId),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (effectiveHandle) res.setHeader('Mcp-Project-Handle', effectiveHandle.handleId);
|
|
470
|
+
// The 1.30 SDK predates the 2026-07-28 version string. Keep the
|
|
471
|
+
// external stateless headers at the current contract, but normalize
|
|
472
|
+
// only the SDK-facing request and initialize params to its newest
|
|
473
|
+
// accepted wire version. This is a real compatibility adapter, not a
|
|
474
|
+
// claim that the pinned SDK implements newer optional extensions.
|
|
475
|
+
const sdkBody = body && typeof body === 'object'
|
|
476
|
+
? JSON.parse(JSON.stringify(body)) as Record<string, any>
|
|
477
|
+
: body;
|
|
478
|
+
const sdkRequest = sdkBody as Record<string, any> | undefined;
|
|
479
|
+
if (sdkRequest && typeof sdkRequest === 'object' && sdkRequest.method === 'initialize' && sdkRequest.params) {
|
|
480
|
+
sdkRequest.params.protocolVersion = MCP_SDK_COMPAT_PROTOCOL_VERSION;
|
|
481
|
+
}
|
|
482
|
+
if (req.headers['mcp-protocol-version']) {
|
|
483
|
+
req.headers['mcp-protocol-version'] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
|
|
484
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
485
|
+
if (req.rawHeaders[i].toLowerCase() === 'mcp-protocol-version') {
|
|
486
|
+
req.rawHeaders[i + 1] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
patchMcpResponse(res);
|
|
492
|
+
await transport.handleRequest(req, res, sdkBody);
|
|
493
|
+
} finally {
|
|
494
|
+
handleTransportClose();
|
|
495
|
+
await transport.close().catch(() => undefined);
|
|
496
|
+
}
|
|
401
497
|
}
|
|
402
498
|
|
|
403
499
|
/**
|
|
@@ -407,6 +503,14 @@ export default defineCommand({
|
|
|
407
503
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
408
504
|
const body = await parseBody(req);
|
|
409
505
|
|
|
506
|
+
const requestedProtocol = req.headers['mcp-protocol-version'];
|
|
507
|
+
const statelessRequested = req.headers['mcp-stateless'] === 'true'
|
|
508
|
+
|| requestedProtocol === CURRENT_MCP_PROTOCOL_VERSION;
|
|
509
|
+
if (!sessionId && statelessRequested) {
|
|
510
|
+
await handleStatelessPost(req, res, body);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
410
514
|
if (sessionId && sessions.has(sessionId)) {
|
|
411
515
|
// Existing session — route to its transport
|
|
412
516
|
sessionLastActivity.set(sessionId, Date.now());
|
|
@@ -464,7 +568,7 @@ export default defineCommand({
|
|
|
464
568
|
const { server, switchProject, handleTransportClose } = await createMemorixServer(
|
|
465
569
|
projectRoot,
|
|
466
570
|
undefined,
|
|
467
|
-
|
|
571
|
+
undefined,
|
|
468
572
|
{
|
|
469
573
|
allowUntrackedFallback: false,
|
|
470
574
|
deferProjectInitUntilBound: true,
|
|
@@ -660,6 +764,20 @@ export default defineCommand({
|
|
|
660
764
|
);
|
|
661
765
|
}
|
|
662
766
|
|
|
767
|
+
async function loadProjectFeedback(dataDir: string, projectId: string): Promise<Map<string, { weight: number; status: any }>> {
|
|
768
|
+
const result = new Map<string, { weight: number; status: any }>();
|
|
769
|
+
try {
|
|
770
|
+
const feedback = new MemoryFeedbackStore();
|
|
771
|
+
await feedback.init(dataDir);
|
|
772
|
+
for (const state of feedback.listStates(projectId, 'observation')) {
|
|
773
|
+
result.set(state.candidateId, { weight: state.weight, status: state.status });
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
// Older stores without the 1.8 tables retain the pre-feedback posture.
|
|
777
|
+
}
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
|
|
663
781
|
// Resolve static directory (dist/dashboard/static)
|
|
664
782
|
const cliDir = pathModule.default.dirname(fileURLToPath(import.meta.url));
|
|
665
783
|
const dashStaticDir = pathModule.default.join(cliDir, '..', 'dashboard', 'static');
|
|
@@ -727,10 +845,53 @@ export default defineCommand({
|
|
|
727
845
|
return;
|
|
728
846
|
}
|
|
729
847
|
|
|
848
|
+
const maintenanceMatch = apiPath.match(/^\/maintenance\/(cleanup|deduplicate|consolidate|retention)\/(preview|execute)$/);
|
|
849
|
+
if (maintenanceMatch) {
|
|
850
|
+
if (req.method !== 'POST') {
|
|
851
|
+
sendJson({ error: 'Maintenance actions require POST.' }, 405);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const bodyValue = await parseBody(req);
|
|
855
|
+
const body = bodyValue && typeof bodyValue === 'object' && !Array.isArray(bodyValue)
|
|
856
|
+
? bodyValue as Record<string, unknown>
|
|
857
|
+
: {};
|
|
858
|
+
const { projectId: maintenanceProjectId, dataDir: maintenanceDataDir } = await resolveRequestProject(url);
|
|
859
|
+
const maintenanceStore = await getDashboardObservationStore(maintenanceDataDir);
|
|
860
|
+
const context = {
|
|
861
|
+
dataDir: maintenanceDataDir,
|
|
862
|
+
projectId: maintenanceProjectId,
|
|
863
|
+
projectRoot: maintenanceProjectId === defaultProject.id ? projectRoot : null,
|
|
864
|
+
store: maintenanceStore,
|
|
865
|
+
};
|
|
866
|
+
const maintenance = await import('../../dashboard/maintenance.js');
|
|
867
|
+
const action = maintenanceMatch[1];
|
|
868
|
+
const phase = maintenanceMatch[2];
|
|
869
|
+
let result: unknown;
|
|
870
|
+
if (action === 'cleanup') {
|
|
871
|
+
result = phase === 'preview'
|
|
872
|
+
? await maintenance.previewCleanup(context, body.includeNoise === true)
|
|
873
|
+
: await maintenance.executeCleanup(context, body.payload, body.token);
|
|
874
|
+
} else if (action === 'deduplicate') {
|
|
875
|
+
result = phase === 'preview'
|
|
876
|
+
? await maintenance.previewDeduplicate(context)
|
|
877
|
+
: await maintenance.executeDeduplicate(context, body.payload, body.token);
|
|
878
|
+
} else if (action === 'consolidate') {
|
|
879
|
+
result = phase === 'preview'
|
|
880
|
+
? await maintenance.previewConsolidate(context)
|
|
881
|
+
: await maintenance.executeConsolidate(context, body.payload, body.token);
|
|
882
|
+
} else {
|
|
883
|
+
result = phase === 'preview'
|
|
884
|
+
? await maintenance.previewRetentionArchive(context)
|
|
885
|
+
: await maintenance.executeRetentionArchive(context, body.payload, body.token);
|
|
886
|
+
}
|
|
887
|
+
sendJson(result);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
730
891
|
if (apiPath === '/team') {
|
|
731
892
|
// Phase 4a: All team state is in SQLite via TeamStore
|
|
732
|
-
const { projectId: teamProjectId } = await resolveRequestProject(url);
|
|
733
|
-
const ts =
|
|
893
|
+
const { projectId: teamProjectId, dataDir: teamDataDir } = await resolveRequestProject(url);
|
|
894
|
+
const ts = await getTeamStore(teamDataDir);
|
|
734
895
|
const scope = url.searchParams.get('scope') || 'project';
|
|
735
896
|
|
|
736
897
|
// Determine effective project ID for query
|
|
@@ -847,6 +1008,7 @@ export default defineCommand({
|
|
|
847
1008
|
const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
848
1009
|
|
|
849
1010
|
const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
|
|
1011
|
+
const feedback = await loadProjectFeedback(statsDataDir, statsProjectId);
|
|
850
1012
|
const statsStore = await getDashboardObservationStore(statsDataDir);
|
|
851
1013
|
const nextId = await statsStore.loadIdCounter();
|
|
852
1014
|
const typeCounts: Record<string, number> = {};
|
|
@@ -887,7 +1049,10 @@ export default defineCommand({
|
|
|
887
1049
|
const retention = summarizeRetentionProjections(
|
|
888
1050
|
observations
|
|
889
1051
|
.filter((observation) => observation.type !== 'probe')
|
|
890
|
-
.map((observation) => projectObservationRetention(observation, {
|
|
1052
|
+
.map((observation) => projectObservationRetention(observation, {
|
|
1053
|
+
referenceTime: new Date(now),
|
|
1054
|
+
feedback: feedback.get(String(observation.id)),
|
|
1055
|
+
})),
|
|
891
1056
|
);
|
|
892
1057
|
const retentionSummary = {
|
|
893
1058
|
active: retention.active,
|
|
@@ -1006,6 +1171,23 @@ export default defineCommand({
|
|
|
1006
1171
|
return;
|
|
1007
1172
|
}
|
|
1008
1173
|
|
|
1174
|
+
if (apiPath === '/evidence') {
|
|
1175
|
+
const { projectId: evidenceProjectId, dataDir: evidenceDataDir } = await resolveRequestProject(url);
|
|
1176
|
+
const { EvidenceCardStore } = await import('../../store/evidence-store.js');
|
|
1177
|
+
const evidenceStore = new EvidenceCardStore();
|
|
1178
|
+
await evidenceStore.init(evidenceDataDir);
|
|
1179
|
+
const observations = await loadDashboardProjectObservations(evidenceDataDir, evidenceProjectId, 'active');
|
|
1180
|
+
evidenceStore.syncObservations(observations);
|
|
1181
|
+
const query = (url.searchParams.get('q') || '').trim().toLowerCase();
|
|
1182
|
+
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit') || 20)));
|
|
1183
|
+
const cards = evidenceStore.list(evidenceProjectId, { limit: 100 })
|
|
1184
|
+
.filter((card) => !query || [card.title, card.summary, card.sourceRef, card.locator ?? ''].join(' ').toLowerCase().includes(query))
|
|
1185
|
+
.slice(0, limit)
|
|
1186
|
+
.map((card) => ({ ...card, events: evidenceStore.listEvents(card.id, 8) }));
|
|
1187
|
+
sendJson({ projectId: evidenceProjectId, cards });
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1009
1191
|
if (apiPath === '/graph') {
|
|
1010
1192
|
const { projectId: graphProjectId, dataDir: graphDataDir } = await resolveRequestProject(url);
|
|
1011
1193
|
const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
|
|
@@ -1030,12 +1212,13 @@ export default defineCommand({
|
|
|
1030
1212
|
if (apiPath === '/retention') {
|
|
1031
1213
|
const { projectId: retProjectId, dataDir: retDataDir } = await resolveRequestProject(url);
|
|
1032
1214
|
const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active');
|
|
1215
|
+
const feedback = await loadProjectFeedback(retDataDir, retProjectId);
|
|
1033
1216
|
const referenceTime = new Date();
|
|
1034
1217
|
const rows = observations
|
|
1035
1218
|
.filter((observation) => observation.type !== 'probe')
|
|
1036
1219
|
.map((observation) => ({
|
|
1037
1220
|
observation,
|
|
1038
|
-
retention: projectObservationRetention(observation, { referenceTime }),
|
|
1221
|
+
retention: projectObservationRetention(observation, { referenceTime, feedback: feedback.get(String(observation.id)) }),
|
|
1039
1222
|
}))
|
|
1040
1223
|
.sort((a, b) => b.retention.displayScore - a.retention.displayScore);
|
|
1041
1224
|
const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
|
|
@@ -1499,6 +1682,12 @@ export default defineCommand({
|
|
|
1499
1682
|
return;
|
|
1500
1683
|
}
|
|
1501
1684
|
|
|
1685
|
+
if (url.pathname === '/protocol-diagnostics') {
|
|
1686
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'public, max-age=60' });
|
|
1687
|
+
res.end(JSON.stringify(getMcpDiagnostics()));
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1502
1691
|
// MCP endpoint
|
|
1503
1692
|
if (url.pathname === '/mcp') {
|
|
1504
1693
|
// Ensure Accept header includes both application/json and text/event-stream.
|
|
@@ -104,6 +104,7 @@ export default defineCommand({
|
|
|
104
104
|
diagLines.push(` Agent lane: ${formatLane(agent.provider, agent.model, agent.baseUrl, agent.apiKey)}`);
|
|
105
105
|
diagLines.push(` Memory LLM lane: ${formatLane(resolved.memory.llm.provider, resolved.memory.llm.model, resolved.memory.llm.baseUrl, resolved.memory.llm.apiKey)}`);
|
|
106
106
|
diagLines.push(` Embedding lane: ${formatLane(resolved.embedding.provider, resolved.embedding.model, resolved.embedding.baseUrl, resolved.embedding.apiKey)}`);
|
|
107
|
+
diagLines.push(` Rerank lane: ${formatLane(resolved.rerank.provider, resolved.rerank.model, resolved.rerank.baseUrl, resolved.rerank.apiKey)}`);
|
|
107
108
|
diagLines.push(` Memory behavior: inject=${resolved.memory.inject ?? 'default'}, formation=${resolved.memory.formation ?? 'default'}, autoCleanup=${resolved.memory.autoCleanup ?? 'default'}`);
|
|
108
109
|
diagLines.push(` Git behavior: autoHook=${resolved.git.autoHook ?? 'default'}, ingestOnCommit=${resolved.git.ingestOnCommit ?? 'default'}, maxDiffSize=${resolved.git.maxDiffSize ?? 'default'}, skipMergeCommits=${resolved.git.skipMergeCommits ?? 'default'}`);
|
|
109
110
|
diagLines.push(` Server: transport=${resolved.server.transport ?? 'default'}, dashboard=${resolved.server.dashboard ?? 'default'}, dashboardPort=${resolved.server.dashboardPort ?? 'default'}`);
|
|
@@ -141,6 +142,7 @@ export default defineCommand({
|
|
|
141
142
|
agent: summarizeLane(agent.provider, agent.model, agent.baseUrl, agent.apiKey),
|
|
142
143
|
memoryLlm: summarizeLane(resolved.memory.llm.provider, resolved.memory.llm.model, resolved.memory.llm.baseUrl, resolved.memory.llm.apiKey),
|
|
143
144
|
embedding: summarizeLane(resolved.embedding.provider, resolved.embedding.model, resolved.embedding.baseUrl, resolved.embedding.apiKey),
|
|
145
|
+
rerank: summarizeLane(resolved.rerank.provider, resolved.rerank.model, resolved.rerank.baseUrl, resolved.rerank.apiKey),
|
|
144
146
|
},
|
|
145
147
|
memory: {
|
|
146
148
|
inject: resolved.memory.inject ?? 'default',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getDatabase } from '../store/sqlite-db.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_FALLBACK_TTL_MS = 10 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
export interface FallbackClaim {
|
|
7
|
+
allowed: boolean;
|
|
8
|
+
key: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function fallbackRequestKey(projectId: string, task: string | undefined): string {
|
|
14
|
+
const normalized = (task ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
15
|
+
return createHash('sha256').update(`${projectId}\n${normalized}`).digest('hex').slice(0, 32);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Durable one-shot fallback guard. A failed MCP discovery must not turn into a
|
|
20
|
+
* loop of help/search/status probes; one bounded context request is enough.
|
|
21
|
+
*/
|
|
22
|
+
export class CliFallbackBudget {
|
|
23
|
+
private db: any = null;
|
|
24
|
+
|
|
25
|
+
async init(dataDir: string): Promise<void> {
|
|
26
|
+
this.db = getDatabase(dataDir);
|
|
27
|
+
this.db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS cli_fallback_attempts (
|
|
29
|
+
project_id TEXT NOT NULL,
|
|
30
|
+
request_key TEXT NOT NULL,
|
|
31
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
last_attempt_at INTEGER NOT NULL,
|
|
33
|
+
expires_at INTEGER NOT NULL,
|
|
34
|
+
PRIMARY KEY (project_id, request_key)
|
|
35
|
+
)
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
claim(projectId: string, task: string | undefined, now = Date.now(), ttlMs = DEFAULT_FALLBACK_TTL_MS): FallbackClaim {
|
|
40
|
+
if (!this.db) throw new Error('CliFallbackBudget is not initialized.');
|
|
41
|
+
const key = fallbackRequestKey(projectId, task);
|
|
42
|
+
const current = this.db.prepare(`
|
|
43
|
+
SELECT count, expires_at FROM cli_fallback_attempts WHERE project_id = ? AND request_key = ?
|
|
44
|
+
`).get(projectId, key);
|
|
45
|
+
const count = Number(current?.count ?? 0);
|
|
46
|
+
if (current && Number(current.expires_at) > now && count >= 1) {
|
|
47
|
+
return { allowed: false, key, count, reason: 'Fallback budget exhausted for this task. Stop probing and continue with the current project files.' };
|
|
48
|
+
}
|
|
49
|
+
this.db.prepare(`
|
|
50
|
+
INSERT INTO cli_fallback_attempts (project_id, request_key, count, last_attempt_at, expires_at)
|
|
51
|
+
VALUES (?, ?, 1, ?, ?)
|
|
52
|
+
ON CONFLICT(project_id, request_key) DO UPDATE SET
|
|
53
|
+
count = CASE WHEN excluded.expires_at > cli_fallback_attempts.expires_at THEN 1 ELSE cli_fallback_attempts.count + 1 END,
|
|
54
|
+
last_attempt_at = excluded.last_attempt_at,
|
|
55
|
+
expires_at = CASE WHEN excluded.expires_at > cli_fallback_attempts.expires_at THEN excluded.expires_at ELSE cli_fallback_attempts.expires_at END
|
|
56
|
+
`).run(projectId, key, now, now + Math.max(1_000, ttlMs));
|
|
57
|
+
return { allowed: true, key, count: 1 };
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -248,6 +248,8 @@ const main = defineCommand({
|
|
|
248
248
|
codegraph: () => import('./commands/codegraph.js').then(m => m.default),
|
|
249
249
|
checkpoint: () => import('./commands/checkpoint.js').then(m => m.default),
|
|
250
250
|
knowledge: () => import('./commands/knowledge.js').then(m => m.default),
|
|
251
|
+
evidence: () => import('./commands/evidence.js').then(m => m.default),
|
|
252
|
+
feedback: () => import('./commands/feedback.js').then(m => m.default),
|
|
251
253
|
reasoning: () => import('./commands/reasoning.js').then(m => m.default),
|
|
252
254
|
retention: () => import('./commands/retention.js').then(m => m.default),
|
|
253
255
|
formation: () => import('./commands/formation.js').then(m => m.default),
|
|
@@ -318,7 +320,7 @@ const main = defineCommand({
|
|
|
318
320
|
const firstArg = process.argv[2];
|
|
319
321
|
const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
|
|
320
322
|
'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'checkpoint', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
|
|
321
|
-
'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
|
|
323
|
+
'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll', 'evidence', 'feedback',
|
|
322
324
|
'receipt',
|
|
323
325
|
'serve', 'serve-http', 'status', 'sync',
|
|
324
326
|
'hook', 'hooks', 'ingest', 'media', 'git-hook', 'git-hook-uninstall',
|
|
@@ -362,6 +364,8 @@ const main = defineCommand({
|
|
|
362
364
|
console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
|
|
363
365
|
console.error(' checkpoint Inspect native compact continuity checkpoints');
|
|
364
366
|
console.error(' knowledge Review source-backed knowledge pages and project workflows');
|
|
367
|
+
console.error(' evidence Inspect source-backed evidence cards');
|
|
368
|
+
console.error(' feedback Record or audit memory feedback');
|
|
365
369
|
console.error(' reasoning Store/search decision rationale');
|
|
366
370
|
console.error(' retention Inspect stale/archive status');
|
|
367
371
|
console.error(' formation Inspect Memory Formation metrics');
|