| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609 |
- import express from 'express';
- import fs from 'fs';
- import path from 'path';
- import { fileURLToPath } from 'url';
- import {
- MemoryBundleValidationError,
- } from '../../../src/context/memory/edgeclaw-memory-core/lib/index.js';
- import {
- readPilotDeckConfigFile,
- writePilotDeckConfig,
- } from '../services/pilotdeckConfig.js';
- import { reloadPilotDeckConfig } from '../services/pilotdeckConfigReloader.js';
- import { suppressNextWatchEvent } from '../services/pilotdeckConfigWatcher.js';
- import {
- clearAllMemoryData,
- exportAllProjectsMemoryBundle,
- getMemoryServiceForRequest,
- getMemorySchedulerStatus,
- importAllProjectsMemoryBundle,
- rollbackLastMemoryDream,
- runManualMemoryDream,
- runManualMemoryFlush,
- } from '../services/memoryService.js';
- const router = express.Router();
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- export const MEMORY_DASHBOARD_DIR = path.resolve(
- __dirname,
- '../../../src/context/memory/edgeclaw-memory-core/ui-source',
- );
- function parseLimit(value, fallback) {
- const parsed = Number.parseInt(String(value ?? ''), 10);
- if (!Number.isFinite(parsed)) return fallback;
- return Math.max(1, Math.min(200, parsed));
- }
- function parseOffset(value, fallback = 0) {
- const parsed = Number.parseInt(String(value ?? ''), 10);
- if (!Number.isFinite(parsed)) return fallback;
- return Math.max(0, parsed);
- }
- function parseMemoryKind(value) {
- return value === 'user' || value === 'feedback' || value === 'project' || value === 'general_project_meta'
- ? value
- : 'all';
- }
- function normalizeMemoryInterval(value, fallback) {
- const parsed = Number.parseInt(String(value ?? ''), 10);
- if (!Number.isFinite(parsed)) return fallback;
- return Math.max(0, Math.min(10_080, Math.floor(parsed)));
- }
- function getGlobalMemorySettingsFromConfig(config) {
- const memory = config?.memory ?? {};
- const reasoningMode = memory.reasoningMode === 'accuracy_first' ? 'accuracy_first' : 'answer_first';
- return {
- reasoningMode,
- autoIndexIntervalMinutes: normalizeMemoryInterval(memory.autoIndexIntervalMinutes, 30),
- autoDreamIntervalMinutes: normalizeMemoryInterval(memory.autoDreamIntervalMinutes, 60),
- };
- }
- function getGlobalMemorySettings() {
- return getGlobalMemorySettingsFromConfig(readPilotDeckConfigFile().config);
- }
- async function saveGlobalMemorySettings(partial = {}) {
- const { config } = readPilotDeckConfigFile();
- const current = getGlobalMemorySettingsFromConfig(config);
- const next = {
- reasoningMode: partial.reasoningMode === 'accuracy_first'
- ? 'accuracy_first'
- : partial.reasoningMode === 'answer_first'
- ? 'answer_first'
- : current.reasoningMode,
- autoIndexIntervalMinutes: normalizeMemoryInterval(
- partial.autoIndexIntervalMinutes,
- current.autoIndexIntervalMinutes,
- ),
- autoDreamIntervalMinutes: normalizeMemoryInterval(
- partial.autoDreamIntervalMinutes,
- current.autoDreamIntervalMinutes,
- ),
- };
- const nextConfig = {
- ...config,
- memory: {
- ...(config.memory ?? {}),
- ...next,
- },
- };
- suppressNextWatchEvent();
- const saved = await writePilotDeckConfig(nextConfig);
- await reloadPilotDeckConfig(saved.config);
- return getGlobalMemorySettingsFromConfig(saved.config);
- }
- function normalizeSearchText(value) {
- return String(value || '').toLowerCase().replace(/\s+/g, ' ').trim();
- }
- function isExternalRecordPath(relativePath) {
- return typeof relativePath === 'string' && relativePath.startsWith('external:');
- }
- function summarizeEntries(entries) {
- const projectEntries = entries.filter((entry) => entry.type === 'project');
- const feedbackEntries = entries.filter((entry) => entry.type === 'feedback');
- const latestMemoryAt = entries
- .map((entry) => entry.updatedAt)
- .filter(Boolean)
- .sort()
- .at(-1);
- return {
- totalEntries: entries.length,
- projectEntries: projectEntries.length,
- feedbackEntries: feedbackEntries.length,
- ...(latestMemoryAt ? { latestMemoryAt } : {}),
- };
- }
- function normalizeGeneralDisplayProject(repository, project) {
- const localEntries = repository.listReadableProjectEntries(project.logicalProjectId, {
- kinds: ['project', 'feedback'],
- includeDeprecated: false,
- includeExternal: false,
- });
- const {
- sourceWorkspacePath,
- sourceProjectId,
- externalLogicalProjectId,
- localMirrorProjectId,
- ...rest
- } = project;
- return {
- ...rest,
- sourceType: 'general_local',
- readOnly: false,
- hasLocalMirror: false,
- summary: summarizeEntries(localEntries),
- };
- }
- function annotateWorkspaceEntries(entries) {
- return entries.map((entry) => ({
- ...entry,
- sourceType: 'general_local',
- readOnly: false,
- }));
- }
- function buildWorkspaceSnapshot(repository, { query = '', limit = 100, offset = 0, selectedProjectId = '' } = {}) {
- const store = repository.getFileMemoryStore();
- const workspaceMode = typeof repository.getWorkspaceMode === 'function'
- ? repository.getWorkspaceMode()
- : store.getWorkspaceMode();
- const manifestPath = path.join(store.getRootDir(), 'MEMORY.md');
- if (workspaceMode === 'general') {
- const generalProjects = repository
- .listReadableProjectCatalog()
- .filter((entry) => entry.sourceType !== 'workspace_external')
- .map((entry) => normalizeGeneralDisplayProject(repository, entry));
- const selectedProject = generalProjects.find((entry) => entry.logicalProjectId === selectedProjectId)
- || generalProjects[0]
- || null;
- const allEntries = selectedProject
- ? repository.listReadableProjectEntries(selectedProject.logicalProjectId, {
- kinds: ['project', 'feedback'],
- includeDeprecated: true,
- includeExternal: false,
- ...(query ? { query } : {}),
- })
- : [];
- const activeEntries = allEntries.filter((entry) => !entry.deprecated);
- const deprecatedEntries = allEntries.filter((entry) => entry.deprecated);
- const activePage = annotateWorkspaceEntries(
- activeEntries
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
- .slice(offset, offset + limit),
- );
- const deprecatedPage = annotateWorkspaceEntries(
- deprecatedEntries
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
- .slice(offset, offset + limit),
- );
- return {
- workspaceMode,
- generalProjects,
- selectedProjectId: selectedProject?.logicalProjectId ?? null,
- selectedProjectSource: selectedProject ? 'general_local' : null,
- selectedProject,
- projectMetaPath: selectedProject && !selectedProject.readOnly ? selectedProject.relativePath : null,
- projectMeta: selectedProject && !selectedProject.readOnly ? selectedProject : null,
- manifestPath: 'MEMORY.md',
- manifestContent: (() => {
- try {
- return fs.readFileSync(manifestPath, 'utf-8');
- } catch {
- return '';
- }
- })(),
- totalFiles: activeEntries.length,
- totalProjects: activeEntries.filter((record) => record.type === 'project').length,
- totalFeedback: activeEntries.filter((record) => record.type === 'feedback').length,
- projectEntries: activePage.filter((record) => record.type === 'project'),
- feedbackEntries: activePage.filter((record) => record.type === 'feedback'),
- deprecatedProjectEntries: deprecatedPage.filter((record) => record.type === 'project'),
- deprecatedFeedbackEntries: deprecatedPage.filter((record) => record.type === 'feedback'),
- };
- }
- const projectMeta = store.getProjectMeta() ?? null;
- const manifestEntries = repository.listMemoryEntries({
- scope: 'project',
- includeDeprecated: true,
- limit: 1000,
- });
- const records = repository.getMemoryRecordsByIds(
- manifestEntries.map((entry) => entry.relativePath),
- 5000,
- );
- const normalizedQuery = normalizeSearchText(query);
- const filtered = !normalizedQuery
- ? records
- : records.filter((record) =>
- normalizeSearchText(
- [
- record.name,
- record.description,
- record.relativePath,
- record.preview,
- record.sourceSessionKey ?? '',
- ].join(' '),
- ).includes(normalizedQuery),
- );
- const activeFiltered = filtered.filter((record) => !record.deprecated);
- const page = filtered
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
- .slice(offset, offset + limit);
- return {
- workspaceMode,
- projectMetaPath: projectMeta ? 'project.meta.md' : null,
- projectMeta,
- manifestPath: 'MEMORY.md',
- manifestContent: (() => {
- try {
- return fs.readFileSync(manifestPath, 'utf-8');
- } catch {
- return '';
- }
- })(),
- totalFiles: activeFiltered.length,
- totalProjects: activeFiltered.filter((record) => record.type === 'project').length,
- totalFeedback: activeFiltered.filter((record) => record.type === 'feedback').length,
- projectEntries: page.filter((record) => record.type === 'project' && !record.deprecated),
- feedbackEntries: page.filter((record) => record.type === 'feedback' && !record.deprecated),
- deprecatedProjectEntries: page.filter((record) => record.type === 'project' && record.deprecated),
- deprecatedFeedbackEntries: page.filter((record) => record.type === 'feedback' && record.deprecated),
- };
- }
- function buildDashboardSnapshot(service, repository, { query = '', selectedProjectId = '' } = {}) {
- return {
- overview: {
- ...service.overview(),
- scheduler: getMemorySchedulerStatus(),
- },
- settings: getGlobalMemorySettings(),
- workspace: buildWorkspaceSnapshot(repository, {
- query,
- limit: 200,
- offset: 0,
- selectedProjectId,
- }),
- userSummary: service.getUserSummary(),
- caseTraces: service.listCaseTraces(12),
- indexTraces: service.listIndexTraces(10),
- dreamTraces: service.listDreamTraces(10),
- };
- }
- function getQuery(req) {
- return typeof req.query.q === 'string' ? req.query.q.trim() : '';
- }
- function getSelectedProjectId(req) {
- return typeof req.query.selectedProjectId === 'string'
- ? req.query.selectedProjectId.trim()
- : '';
- }
- async function withMemoryService(req, res, fn) {
- try {
- const { projectPath, dataDir, service } = await getMemoryServiceForRequest(req);
- return await fn({ projectPath, dataDir, service, repository: service.repository });
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- return res.status(400).json({ error: message });
- }
- }
- function buildDownloadFileName(prefix, exportedAt) {
- const safe = String(exportedAt || '')
- .replace(/[^\dTZ-]/g, '-')
- .replace(/-+/g, '-');
- return `${prefix}-${safe || 'export'}.json`;
- }
- function sendBundleDownload(res, bundle, prefix) {
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
- res.setHeader(
- 'Content-Disposition',
- `attachment; filename="${buildDownloadFileName(prefix, bundle.exportedAt)}"`,
- );
- res.send(JSON.stringify(bundle, null, 2));
- }
- router.get('/overview', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- res.json({
- ...service.overview(),
- scheduler: getMemorySchedulerStatus(),
- });
- }),
- );
- router.route('/settings')
- .get(async (req, res) =>
- withMemoryService(req, res, async () => {
- res.json(getGlobalMemorySettings());
- }))
- .post(async (req, res) =>
- withMemoryService(req, res, async () => {
- res.json(await saveGlobalMemorySettings(req.body ?? {}));
- }));
- router.post('/index/run', async (req, res) =>
- withMemoryService(req, res, async ({ dataDir, service, repository }) => {
- const result = await runManualMemoryFlush(service, dataDir, { reason: 'manual' });
- res.json({
- ...result,
- dashboard: buildDashboardSnapshot(service, repository, {
- query: getQuery(req),
- selectedProjectId: getSelectedProjectId(req),
- }),
- });
- }),
- );
- router.post('/dream/run', async (req, res) =>
- withMemoryService(req, res, async ({ dataDir, service, repository }) => {
- const result = await runManualMemoryDream(service, dataDir);
- res.json({
- ...result,
- dashboard: buildDashboardSnapshot(service, repository, {
- query: getQuery(req),
- selectedProjectId: getSelectedProjectId(req),
- }),
- });
- }),
- );
- router.post('/dream/rollback-last', async (req, res) =>
- withMemoryService(req, res, async ({ dataDir, service, repository }) => {
- const result = await rollbackLastMemoryDream(service, dataDir);
- res.json({
- ...result,
- dashboard: buildDashboardSnapshot(service, repository, {
- query: getQuery(req),
- selectedProjectId: getSelectedProjectId(req),
- }),
- });
- }),
- );
- router.get('/snapshot', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- res.json(service.snapshot(parseLimit(req.query.limit, 24)));
- }),
- );
- router.get('/memory/list', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const kind = parseMemoryKind(req.query.kind);
- const query = typeof req.query.query === 'string' ? req.query.query.trim() : '';
- const limit = parseLimit(req.query.limit, 10);
- const offset = parseOffset(req.query.offset, 0);
- const items = service.list({
- ...(kind !== 'all' ? { kinds: [kind] } : {}),
- ...(query ? { query } : {}),
- limit,
- offset,
- });
- res.json(items);
- }),
- );
- router.get('/memory/get', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const ids = String(req.query.ids || '')
- .split(',')
- .map((value) => value.trim())
- .filter(Boolean);
- if (ids.length === 0) {
- return res.status(400).json({ error: 'ids query parameter is required' });
- }
- res.json(service.get(ids, 5000));
- }),
- );
- router.post('/memory/actions', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- try {
- res.json(service.act(req.body ?? {}));
- } catch (error) {
- res.status(400).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- }),
- );
- router.get('/memory/user-summary', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- res.json(service.getUserSummary());
- }),
- );
- router.route('/project-meta')
- .get(async (req, res) =>
- withMemoryService(req, res, async ({ service, repository }) => {
- const selected = getSelectedProjectId(req);
- if (service.getWorkspaceMode() === 'general' && selected) {
- const readableProject = service.getReadableProject(selected);
- if (!readableProject || readableProject.readOnly) {
- return res.json(null);
- }
- return res.json(repository.getFileMemoryStore().getProjectMeta(readableProject.projectId) ?? readableProject);
- }
- res.json(service.getProjectMeta());
- }))
- .post(async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- try {
- res.json(service.updateProjectMeta(req.body ?? {}));
- } catch (error) {
- res.status(400).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- }));
- router.get('/workspace', async (req, res) =>
- withMemoryService(req, res, async ({ repository }) => {
- res.json(
- buildWorkspaceSnapshot(repository, {
- query: getQuery(req),
- limit: parseLimit(req.query.limit, 100),
- offset: parseOffset(req.query.offset, 0),
- selectedProjectId: getSelectedProjectId(req),
- }),
- );
- }),
- );
- router.get('/cases', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- res.json(service.listCaseTraces(parseLimit(req.query.limit, 12)));
- }),
- );
- router.get('/cases/:caseId', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const record = service.getCaseTrace(req.params.caseId);
- if (!record) {
- return res.status(404).json({ error: 'Not found' });
- }
- res.json(record);
- }),
- );
- router.get('/index-traces', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- res.json(service.listIndexTraces(parseLimit(req.query.limit, 30)));
- }),
- );
- router.get('/index-traces/:indexTraceId', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const record = service.getIndexTrace(req.params.indexTraceId);
- if (!record) {
- return res.status(404).json({ error: 'Not found' });
- }
- res.json(record);
- }),
- );
- router.get('/dream-traces', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- res.json(service.listDreamTraces(parseLimit(req.query.limit, 30)));
- }),
- );
- router.get('/dream-traces/:dreamTraceId', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const record = service.getDreamTrace(req.params.dreamTraceId);
- if (!record) {
- return res.status(404).json({ error: 'Not found' });
- }
- res.json(record);
- }),
- );
- router.get('/export/current-project', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const bundle = service.exportBundle();
- sendBundleDownload(res, bundle, 'pilotdeck-memory-current-project');
- }),
- );
- router.get('/export/all-projects', async (_req, res) => {
- try {
- const bundle = await exportAllProjectsMemoryBundle();
- sendBundleDownload(res, bundle, 'pilotdeck-memory-all-projects');
- } catch (error) {
- res.status(500).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- });
- router.post('/import/current-project', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- try {
- res.json(service.importBundle(req.body));
- } catch (error) {
- const status = error instanceof MemoryBundleValidationError ? 400 : 500;
- res.status(status).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- }),
- );
- router.post('/import/all-projects', async (req, res) => {
- try {
- res.json(await importAllProjectsMemoryBundle(req.body));
- } catch (error) {
- const status = error instanceof MemoryBundleValidationError ? 400 : 500;
- res.status(status).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- });
- router.get('/export', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- const bundle = service.exportBundle();
- sendBundleDownload(res, bundle, 'pilotdeck-memory-current-project');
- }),
- );
- router.post('/import', async (req, res) =>
- withMemoryService(req, res, async ({ service }) => {
- try {
- res.json(service.importBundle(req.body));
- } catch (error) {
- const status = error instanceof MemoryBundleValidationError ? 400 : 500;
- res.status(status).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- }),
- );
- router.post('/clear', async (req, res) => {
- const scope = req.body?.scope === 'all_memory' ? 'all_memory' : 'current_project';
- if (scope === 'all_memory') {
- try {
- res.json(await clearAllMemoryData());
- } catch (error) {
- res.status(500).json({
- error: error instanceof Error ? error.message : String(error),
- });
- }
- return;
- }
- return withMemoryService(req, res, async ({ service, repository }) => {
- const result = service.clear(scope);
- res.json({
- ...result,
- dashboard: buildDashboardSnapshot(service, repository, {
- query: getQuery(req),
- selectedProjectId: getSelectedProjectId(req),
- }),
- });
- });
- });
- export default router;
|