recallx 1.1.0 → 1.2.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/README.md +33 -1
- package/app/cli/src/cli.js +32 -1
- package/app/cli/src/format.js +14 -0
- package/app/mcp/server.js +248 -127
- package/app/server/app.js +412 -4
- package/app/server/config.js +4 -2
- package/app/server/index.js +12 -1
- package/app/server/project-graph.js +13 -6
- package/app/server/repositories.js +120 -8
- package/app/server/sqlite-errors.js +10 -0
- package/app/server/workspace-import-helpers.js +161 -0
- package/app/server/workspace-import.js +572 -0
- package/app/server/workspace-ops.js +249 -0
- package/app/server/workspace-session.js +118 -6
- package/app/shared/contracts.js +41 -0
- package/app/shared/version.js +1 -1
- package/dist/renderer/assets/{ProjectGraphCanvas-WP0YEOpB.js → ProjectGraphCanvas-B9-L83dL.js} +1 -1
- package/dist/renderer/assets/index-CNeaY_5l.js +69 -0
- package/dist/renderer/assets/index-Dz33nPCb.css +1 -0
- package/dist/renderer/index.html +2 -2
- package/package.json +1 -1
- package/dist/renderer/assets/index-5rwy6MBF.js +0 -69
- package/dist/renderer/assets/index-C2-KXqBO.css +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "./errors.js";
|
|
5
|
+
const BACKUP_MANIFEST_FILE = "manifest.json";
|
|
6
|
+
const SESSION_METADATA_FILE = "workspace-session.json";
|
|
7
|
+
const LOCK_METADATA_FILE = "workspace.lock.json";
|
|
8
|
+
const WORKSPACE_ACTIVITY_THRESHOLD_MS = 10 * 60 * 1000;
|
|
9
|
+
function readJsonFile(filePath) {
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeJsonFile(filePath, value) {
|
|
21
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
23
|
+
}
|
|
24
|
+
function sanitizeLabel(value, fallback) {
|
|
25
|
+
const trimmed = value?.trim();
|
|
26
|
+
if (!trimmed) {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
return trimmed.replace(/[<>:"/\\|?*\x00-\x1f]+/g, "-").replace(/\s+/g, " ").trim() || fallback;
|
|
30
|
+
}
|
|
31
|
+
function sessionMetadataPath(paths) {
|
|
32
|
+
return path.join(paths.configDir, SESSION_METADATA_FILE);
|
|
33
|
+
}
|
|
34
|
+
function lockMetadataPath(paths) {
|
|
35
|
+
return path.join(paths.configDir, LOCK_METADATA_FILE);
|
|
36
|
+
}
|
|
37
|
+
function readSessionMetadata(paths) {
|
|
38
|
+
return readJsonFile(sessionMetadataPath(paths));
|
|
39
|
+
}
|
|
40
|
+
function readLockMetadata(paths) {
|
|
41
|
+
return readJsonFile(lockMetadataPath(paths));
|
|
42
|
+
}
|
|
43
|
+
function buildSafetyWarnings(previous, lock, machineId, sessionId) {
|
|
44
|
+
const warnings = [];
|
|
45
|
+
const isSameActiveSession = previous?.sessionId === sessionId &&
|
|
46
|
+
previous.machineId === machineId &&
|
|
47
|
+
lock?.sessionId === sessionId &&
|
|
48
|
+
lock.machineId === machineId;
|
|
49
|
+
if (lock && !isSameActiveSession) {
|
|
50
|
+
warnings.push({
|
|
51
|
+
code: "active_lock",
|
|
52
|
+
message: `Workspace lock marker is still present from ${lock.machineId}. Another session may still be active.`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (previous &&
|
|
56
|
+
!isSameActiveSession &&
|
|
57
|
+
(!previous.lastCleanCloseAt || previous.lastCleanCloseAt < previous.lastOpenedAt)) {
|
|
58
|
+
warnings.push({
|
|
59
|
+
code: "unclean_shutdown",
|
|
60
|
+
message: "The previous session does not appear to have closed cleanly. Create a backup before heavy edits."
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (previous &&
|
|
64
|
+
previous.machineId !== machineId &&
|
|
65
|
+
Date.now() - Date.parse(previous.lastOpenedAt) <= WORKSPACE_ACTIVITY_THRESHOLD_MS) {
|
|
66
|
+
warnings.push({
|
|
67
|
+
code: "recent_other_machine",
|
|
68
|
+
message: `This workspace was opened recently on ${previous.machineId}. Treat multi-device access as single-writer only.`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return warnings;
|
|
72
|
+
}
|
|
73
|
+
export function beginWorkspaceSession(paths, params) {
|
|
74
|
+
const machineId = os.hostname() || "unknown-machine";
|
|
75
|
+
const previous = readSessionMetadata(paths);
|
|
76
|
+
const lock = readLockMetadata(paths);
|
|
77
|
+
const warnings = buildSafetyWarnings(previous, lock, machineId, params.sessionId);
|
|
78
|
+
const nextSession = {
|
|
79
|
+
machineId,
|
|
80
|
+
sessionId: params.sessionId,
|
|
81
|
+
appVersion: params.appVersion,
|
|
82
|
+
lastOpenedAt: params.now,
|
|
83
|
+
lastCleanCloseAt: previous?.lastCleanCloseAt ?? null
|
|
84
|
+
};
|
|
85
|
+
const nextLock = {
|
|
86
|
+
machineId,
|
|
87
|
+
sessionId: params.sessionId,
|
|
88
|
+
appVersion: params.appVersion,
|
|
89
|
+
lockUpdatedAt: params.now
|
|
90
|
+
};
|
|
91
|
+
writeJsonFile(sessionMetadataPath(paths), nextSession);
|
|
92
|
+
writeJsonFile(lockMetadataPath(paths), nextLock);
|
|
93
|
+
return {
|
|
94
|
+
machineId,
|
|
95
|
+
sessionId: params.sessionId,
|
|
96
|
+
lastOpenedAt: params.now,
|
|
97
|
+
lastCleanCloseAt: nextSession.lastCleanCloseAt,
|
|
98
|
+
lockPresent: true,
|
|
99
|
+
lockUpdatedAt: params.now,
|
|
100
|
+
activeSessionMachineId: machineId,
|
|
101
|
+
warnings
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function endWorkspaceSession(paths, params) {
|
|
105
|
+
const machineId = os.hostname() || "unknown-machine";
|
|
106
|
+
const previous = readSessionMetadata(paths);
|
|
107
|
+
const nextSession = {
|
|
108
|
+
machineId,
|
|
109
|
+
sessionId: params.sessionId,
|
|
110
|
+
appVersion: params.appVersion,
|
|
111
|
+
lastOpenedAt: previous?.lastOpenedAt ?? params.now,
|
|
112
|
+
lastCleanCloseAt: params.now
|
|
113
|
+
};
|
|
114
|
+
writeJsonFile(sessionMetadataPath(paths), nextSession);
|
|
115
|
+
const lock = readLockMetadata(paths);
|
|
116
|
+
if (lock?.sessionId === params.sessionId && existsSync(lockMetadataPath(paths))) {
|
|
117
|
+
unlinkSync(lockMetadataPath(paths));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function formatArtifactSize(bytes) {
|
|
121
|
+
if (bytes < 1024) {
|
|
122
|
+
return `${bytes} B`;
|
|
123
|
+
}
|
|
124
|
+
if (bytes < 1024 * 1024) {
|
|
125
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
126
|
+
}
|
|
127
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
128
|
+
}
|
|
129
|
+
function copyWorkspaceSnapshot(sourcePaths, backupDir) {
|
|
130
|
+
mkdirSync(backupDir, { recursive: true });
|
|
131
|
+
if (existsSync(sourcePaths.dbPath)) {
|
|
132
|
+
copyFileSync(sourcePaths.dbPath, path.join(backupDir, "workspace.db"));
|
|
133
|
+
}
|
|
134
|
+
for (const [from, name] of [
|
|
135
|
+
[sourcePaths.artifactsDir, "artifacts"],
|
|
136
|
+
[sourcePaths.exportsDir, "exports"],
|
|
137
|
+
]) {
|
|
138
|
+
if (existsSync(from)) {
|
|
139
|
+
cpSync(from, path.join(backupDir, name), { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (existsSync(sourcePaths.configDir)) {
|
|
143
|
+
const targetConfigDir = path.join(backupDir, "config");
|
|
144
|
+
mkdirSync(targetConfigDir, { recursive: true });
|
|
145
|
+
for (const entry of readdirSync(sourcePaths.configDir, { withFileTypes: true })) {
|
|
146
|
+
if (entry.name === SESSION_METADATA_FILE || entry.name === LOCK_METADATA_FILE) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const sourcePath = path.join(sourcePaths.configDir, entry.name);
|
|
150
|
+
const destinationPath = path.join(targetConfigDir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
cpSync(sourcePath, destinationPath, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
copyFileSync(sourcePath, destinationPath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function createWorkspaceBackup(paths, params) {
|
|
161
|
+
const id = `${params.now.replace(/[-:.TZ]/g, "").slice(0, 14)}-${sanitizeLabel(params.label, "snapshot").replace(/\s+/g, "-").toLowerCase()}`;
|
|
162
|
+
const backupDir = path.join(paths.backupsDir, id);
|
|
163
|
+
if (existsSync(backupDir)) {
|
|
164
|
+
throw new AppError(409, "BACKUP_EXISTS", `Backup already exists: ${id}`);
|
|
165
|
+
}
|
|
166
|
+
copyWorkspaceSnapshot(paths, backupDir);
|
|
167
|
+
const manifest = {
|
|
168
|
+
id,
|
|
169
|
+
label: sanitizeLabel(params.label, "Manual snapshot"),
|
|
170
|
+
createdAt: params.now,
|
|
171
|
+
backupPath: backupDir,
|
|
172
|
+
workspaceRoot: paths.root,
|
|
173
|
+
workspaceName: params.workspaceName,
|
|
174
|
+
appVersion: params.appVersion
|
|
175
|
+
};
|
|
176
|
+
writeJsonFile(path.join(backupDir, BACKUP_MANIFEST_FILE), manifest);
|
|
177
|
+
return manifest;
|
|
178
|
+
}
|
|
179
|
+
export function listWorkspaceBackups(paths) {
|
|
180
|
+
if (!existsSync(paths.backupsDir)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return readdirSync(paths.backupsDir, { withFileTypes: true })
|
|
184
|
+
.filter((entry) => entry.isDirectory())
|
|
185
|
+
.map((entry) => readJsonFile(path.join(paths.backupsDir, entry.name, BACKUP_MANIFEST_FILE)))
|
|
186
|
+
.filter((entry) => Boolean(entry))
|
|
187
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
188
|
+
}
|
|
189
|
+
export function restoreWorkspaceBackup(paths, params) {
|
|
190
|
+
const manifest = listWorkspaceBackups(paths).find((item) => item.id === params.backupId);
|
|
191
|
+
if (!manifest) {
|
|
192
|
+
throw new AppError(404, "BACKUP_NOT_FOUND", `Backup ${params.backupId} not found.`);
|
|
193
|
+
}
|
|
194
|
+
const targetRoot = path.resolve(params.targetRootPath);
|
|
195
|
+
if (targetRoot === path.resolve(paths.root)) {
|
|
196
|
+
throw new AppError(400, "INVALID_INPUT", "Restore target must be a different workspace root.");
|
|
197
|
+
}
|
|
198
|
+
if (existsSync(targetRoot)) {
|
|
199
|
+
const existingEntries = readdirSync(targetRoot);
|
|
200
|
+
if (existingEntries.length > 0) {
|
|
201
|
+
throw new AppError(409, "RESTORE_TARGET_NOT_EMPTY", "Restore target root must be empty or not exist.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
206
|
+
}
|
|
207
|
+
for (const entry of ["workspace.db", "artifacts", "exports", "config"]) {
|
|
208
|
+
const sourcePath = path.join(manifest.backupPath, entry);
|
|
209
|
+
const destinationPath = path.join(targetRoot, entry);
|
|
210
|
+
if (!existsSync(sourcePath)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (entry === "workspace.db") {
|
|
214
|
+
copyFileSync(sourcePath, destinationPath);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
cpSync(sourcePath, destinationPath, { recursive: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return manifest;
|
|
221
|
+
}
|
|
222
|
+
export function exportWorkspaceSnapshot(paths, params) {
|
|
223
|
+
const id = `${params.now.replace(/[-:.TZ]/g, "").slice(0, 14)}-workspace-export`;
|
|
224
|
+
const extension = params.format === "json" ? "json" : "md";
|
|
225
|
+
const exportPath = path.join(paths.exportsDir, `${id}.${extension}`);
|
|
226
|
+
const body = params.format === "json" ? `${JSON.stringify(params.payload, null, 2)}\n` : `${params.markdown}\n`;
|
|
227
|
+
writeFileSync(exportPath, body, "utf8");
|
|
228
|
+
const manifest = {
|
|
229
|
+
id,
|
|
230
|
+
format: params.format,
|
|
231
|
+
createdAt: params.now,
|
|
232
|
+
exportPath,
|
|
233
|
+
workspaceRoot: paths.root,
|
|
234
|
+
workspaceName: params.workspaceName,
|
|
235
|
+
appVersion: params.appVersion
|
|
236
|
+
};
|
|
237
|
+
writeJsonFile(path.join(paths.exportsDir, `${id}.manifest.json`), manifest);
|
|
238
|
+
return manifest;
|
|
239
|
+
}
|
|
240
|
+
export function summarizeBackupRecord(record) {
|
|
241
|
+
const stats = existsSync(record.backupPath) ? statSync(record.backupPath) : null;
|
|
242
|
+
const sizeLabel = stats ? formatArtifactSize(stats.size) : "unknown";
|
|
243
|
+
return `${record.label} (${record.id}) at ${record.createdAt} -> ${record.backupPath} [${sizeLabel}]`;
|
|
244
|
+
}
|
|
245
|
+
export function removeWorkspaceDirectory(rootPath) {
|
|
246
|
+
if (existsSync(rootPath)) {
|
|
247
|
+
rmSync(rootPath, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { workspaceInfo } from "./config.js";
|
|
4
5
|
import { openDatabase } from "./db.js";
|
|
5
6
|
import { AppError } from "./errors.js";
|
|
6
7
|
import { bootstrapAutomaticGovernance } from "./governance.js";
|
|
7
8
|
import { RecallXRepository } from "./repositories.js";
|
|
9
|
+
import { beginWorkspaceSession, createWorkspaceBackup, endWorkspaceSession, exportWorkspaceSnapshot, listWorkspaceBackups, restoreWorkspaceBackup, } from "./workspace-ops.js";
|
|
10
|
+
import { importIntoWorkspace, previewImportIntoWorkspace } from "./workspace-import.js";
|
|
8
11
|
import { defaultWorkspaceName, ensureWorkspace } from "./workspace.js";
|
|
9
12
|
import { RECALLX_VERSION } from "../shared/version.js";
|
|
10
13
|
export class WorkspaceSessionManager {
|
|
@@ -12,6 +15,7 @@ export class WorkspaceSessionManager {
|
|
|
12
15
|
authMode;
|
|
13
16
|
currentState;
|
|
14
17
|
history = new Map();
|
|
18
|
+
processSessionId = randomUUID();
|
|
15
19
|
constructor(serverConfig, initialWorkspaceRoot, authMode) {
|
|
16
20
|
this.serverConfig = serverConfig;
|
|
17
21
|
this.authMode = authMode;
|
|
@@ -44,12 +48,97 @@ export class WorkspaceSessionManager {
|
|
|
44
48
|
requireExistingRoot: true,
|
|
45
49
|
});
|
|
46
50
|
}
|
|
51
|
+
listBackups() {
|
|
52
|
+
return listWorkspaceBackups(this.currentState.paths);
|
|
53
|
+
}
|
|
54
|
+
createBackup(label) {
|
|
55
|
+
return createWorkspaceBackup(this.currentState.paths, {
|
|
56
|
+
workspaceName: this.currentState.workspaceInfo.workspaceName,
|
|
57
|
+
appVersion: RECALLX_VERSION,
|
|
58
|
+
label,
|
|
59
|
+
now: new Date().toISOString(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
exportWorkspace(format) {
|
|
63
|
+
const repository = this.currentState.repository;
|
|
64
|
+
const payload = {
|
|
65
|
+
workspace: this.currentState.workspaceInfo,
|
|
66
|
+
nodes: repository.listAllNodes(),
|
|
67
|
+
relations: repository.listAllRelations(),
|
|
68
|
+
activities: repository.listAllActivities(),
|
|
69
|
+
artifacts: repository.listAllArtifacts(),
|
|
70
|
+
integrations: repository.listIntegrations(),
|
|
71
|
+
settings: repository.getSettings(),
|
|
72
|
+
};
|
|
73
|
+
const markdown = [
|
|
74
|
+
`# ${this.currentState.workspaceInfo.workspaceName}`,
|
|
75
|
+
"",
|
|
76
|
+
`- exportedAt: ${new Date().toISOString()}`,
|
|
77
|
+
`- workspaceRoot: ${this.currentState.workspaceRoot}`,
|
|
78
|
+
`- nodes: ${payload.nodes.length}`,
|
|
79
|
+
`- relations: ${payload.relations.length}`,
|
|
80
|
+
`- activities: ${payload.activities.length}`,
|
|
81
|
+
`- artifacts: ${payload.artifacts.length}`,
|
|
82
|
+
`- integrations: ${payload.integrations.length}`,
|
|
83
|
+
"",
|
|
84
|
+
"## Recent Nodes",
|
|
85
|
+
...payload.nodes.slice(0, 20).map((node) => `- ${node.title ?? node.id} (${node.type})`),
|
|
86
|
+
].join("\n");
|
|
87
|
+
return exportWorkspaceSnapshot(this.currentState.paths, {
|
|
88
|
+
workspaceName: this.currentState.workspaceInfo.workspaceName,
|
|
89
|
+
appVersion: RECALLX_VERSION,
|
|
90
|
+
now: new Date().toISOString(),
|
|
91
|
+
format,
|
|
92
|
+
payload,
|
|
93
|
+
markdown,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
previewImportWorkspace(format, sourcePath, label, options) {
|
|
97
|
+
return previewImportIntoWorkspace({
|
|
98
|
+
repository: this.currentState.repository,
|
|
99
|
+
format,
|
|
100
|
+
sourcePath,
|
|
101
|
+
label,
|
|
102
|
+
options,
|
|
103
|
+
now: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
importWorkspace(format, sourcePath, label, options) {
|
|
107
|
+
const backup = this.createBackup(label ? `before-import ${label}` : "before-import");
|
|
108
|
+
return importIntoWorkspace({
|
|
109
|
+
repository: this.currentState.repository,
|
|
110
|
+
paths: this.currentState.paths,
|
|
111
|
+
format,
|
|
112
|
+
sourcePath,
|
|
113
|
+
label,
|
|
114
|
+
options,
|
|
115
|
+
now: new Date().toISOString(),
|
|
116
|
+
backup,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
restoreBackup(backupId, targetRootPath, workspaceName) {
|
|
120
|
+
const autoBackup = this.createBackup(workspaceName ? `before-restore ${workspaceName}` : "before-restore");
|
|
121
|
+
const manifest = restoreWorkspaceBackup(this.currentState.paths, {
|
|
122
|
+
backupId,
|
|
123
|
+
targetRootPath,
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
workspace: this.swapWorkspace(targetRootPath, {
|
|
127
|
+
workspaceName: workspaceName?.trim() || manifest.workspaceName,
|
|
128
|
+
requireExistingRoot: true,
|
|
129
|
+
}),
|
|
130
|
+
autoBackup,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
shutdown() {
|
|
134
|
+
this.closeState(this.currentState);
|
|
135
|
+
}
|
|
47
136
|
swapWorkspace(rootPath, options) {
|
|
48
137
|
const nextState = this.loadWorkspace(rootPath, options);
|
|
49
138
|
const previousState = this.currentState;
|
|
50
139
|
this.currentState = nextState;
|
|
51
140
|
this.remember(nextState);
|
|
52
|
-
|
|
141
|
+
this.closeState(previousState);
|
|
53
142
|
return this.getWorkspaceCatalogItem(nextState);
|
|
54
143
|
}
|
|
55
144
|
loadWorkspace(rootPath, options) {
|
|
@@ -61,9 +150,10 @@ export class WorkspaceSessionManager {
|
|
|
61
150
|
const db = openDatabase(paths);
|
|
62
151
|
const repository = new RecallXRepository(db, resolvedRoot);
|
|
63
152
|
const storedSettings = repository.getSettings(["workspace.name"]);
|
|
64
|
-
const resolvedName =
|
|
65
|
-
|
|
66
|
-
|
|
153
|
+
const resolvedName = options.workspaceName?.trim() ||
|
|
154
|
+
(typeof storedSettings["workspace.name"] === "string" && storedSettings["workspace.name"].trim()
|
|
155
|
+
? String(storedSettings["workspace.name"])
|
|
156
|
+
: defaultWorkspaceName(resolvedRoot));
|
|
67
157
|
repository.setSetting("workspace.name", resolvedName);
|
|
68
158
|
repository.setSetting("workspace.version", RECALLX_VERSION);
|
|
69
159
|
repository.setSetting("api.bind", `${this.serverConfig.bindAddress}:${this.serverConfig.port}`);
|
|
@@ -105,6 +195,12 @@ export class WorkspaceSessionManager {
|
|
|
105
195
|
repository.ensureSearchTagIndex();
|
|
106
196
|
repository.ensureActivitySearchIndex();
|
|
107
197
|
bootstrapAutomaticGovernance(repository);
|
|
198
|
+
const now = new Date().toISOString();
|
|
199
|
+
const safety = beginWorkspaceSession(paths, {
|
|
200
|
+
sessionId: this.processSessionId,
|
|
201
|
+
appVersion: RECALLX_VERSION,
|
|
202
|
+
now,
|
|
203
|
+
});
|
|
108
204
|
return {
|
|
109
205
|
db,
|
|
110
206
|
repository,
|
|
@@ -113,9 +209,25 @@ export class WorkspaceSessionManager {
|
|
|
113
209
|
workspaceInfo: workspaceInfo(resolvedRoot, {
|
|
114
210
|
...this.serverConfig,
|
|
115
211
|
workspaceName: resolvedName,
|
|
116
|
-
}, this.authMode
|
|
212
|
+
}, this.authMode, {
|
|
213
|
+
dbPath: paths.dbPath,
|
|
214
|
+
artifactsDir: paths.artifactsDir,
|
|
215
|
+
exportsDir: paths.exportsDir,
|
|
216
|
+
importsDir: paths.importsDir,
|
|
217
|
+
backupsDir: paths.backupsDir,
|
|
218
|
+
configDir: paths.configDir,
|
|
219
|
+
cacheDir: paths.cacheDir,
|
|
220
|
+
}, safety),
|
|
117
221
|
};
|
|
118
222
|
}
|
|
223
|
+
closeState(state) {
|
|
224
|
+
endWorkspaceSession(state.paths, {
|
|
225
|
+
sessionId: this.processSessionId,
|
|
226
|
+
appVersion: RECALLX_VERSION,
|
|
227
|
+
now: new Date().toISOString(),
|
|
228
|
+
});
|
|
229
|
+
state.db.close();
|
|
230
|
+
}
|
|
119
231
|
remember(state) {
|
|
120
232
|
this.history.set(state.workspaceRoot, this.getWorkspaceCatalogItem(state));
|
|
121
233
|
}
|
|
@@ -123,7 +235,7 @@ export class WorkspaceSessionManager {
|
|
|
123
235
|
return {
|
|
124
236
|
...state.workspaceInfo,
|
|
125
237
|
isCurrent: state.workspaceRoot === this.currentState?.workspaceRoot,
|
|
126
|
-
lastOpenedAt: new Date().toISOString(),
|
|
238
|
+
lastOpenedAt: state.workspaceInfo.safety?.lastOpenedAt ?? new Date().toISOString(),
|
|
127
239
|
};
|
|
128
240
|
}
|
|
129
241
|
}
|
package/app/shared/contracts.js
CHANGED
|
@@ -43,6 +43,7 @@ export const searchFeedbackResultTypes = ["node", "activity"];
|
|
|
43
43
|
export const searchFeedbackVerdicts = ["useful", "not_useful", "uncertain"];
|
|
44
44
|
export const governanceEntityTypes = ["node", "relation"];
|
|
45
45
|
export const governanceStates = ["healthy", "low_confidence", "contested"];
|
|
46
|
+
export const governanceDecisionActions = ["promote", "contest", "archive", "accept", "reject"];
|
|
46
47
|
export const governanceEventTypes = [
|
|
47
48
|
"evaluated",
|
|
48
49
|
"promoted",
|
|
@@ -165,6 +166,11 @@ export const governanceIssuesQuerySchema = z.object({
|
|
|
165
166
|
states: z.array(z.enum(governanceStates)).optional(),
|
|
166
167
|
limit: z.number().int().min(1).max(100).default(20)
|
|
167
168
|
});
|
|
169
|
+
export const governanceEventsQuerySchema = z.object({
|
|
170
|
+
entityTypes: z.array(z.enum(governanceEntityTypes)).optional(),
|
|
171
|
+
actions: z.array(z.enum(governanceDecisionActions)).optional(),
|
|
172
|
+
limit: z.number().int().min(1).max(100).default(12)
|
|
173
|
+
});
|
|
168
174
|
export const recomputeGovernanceSchema = z.object({
|
|
169
175
|
entityType: z.enum(governanceEntityTypes).optional(),
|
|
170
176
|
entityIds: z.array(z.string().min(1)).max(200).optional(),
|
|
@@ -192,6 +198,18 @@ export const updateNodeSchema = z.object({
|
|
|
192
198
|
metadata: z.record(z.any()).optional(),
|
|
193
199
|
status: z.enum(nodeStatuses).optional()
|
|
194
200
|
});
|
|
201
|
+
export const governanceNodeActionSchema = z.object({
|
|
202
|
+
action: z.enum(["promote", "contest", "archive"]),
|
|
203
|
+
note: z.string().max(280).optional(),
|
|
204
|
+
source: sourceSchema,
|
|
205
|
+
metadata: z.record(z.any()).default({})
|
|
206
|
+
});
|
|
207
|
+
export const governanceRelationActionSchema = z.object({
|
|
208
|
+
action: z.enum(["accept", "reject", "archive"]),
|
|
209
|
+
note: z.string().max(280).optional(),
|
|
210
|
+
source: sourceSchema,
|
|
211
|
+
metadata: z.record(z.any()).default({})
|
|
212
|
+
});
|
|
195
213
|
export const createRelationSchema = z.object({
|
|
196
214
|
fromNodeId: z.string().min(1),
|
|
197
215
|
toNodeId: z.string().min(1),
|
|
@@ -314,3 +332,26 @@ export const createWorkspaceSchema = z.object({
|
|
|
314
332
|
export const openWorkspaceSchema = z.object({
|
|
315
333
|
rootPath: z.string().min(1)
|
|
316
334
|
});
|
|
335
|
+
export const createWorkspaceBackupSchema = z.object({
|
|
336
|
+
label: z.string().min(1).max(80).optional()
|
|
337
|
+
});
|
|
338
|
+
export const exportWorkspaceSchema = z.object({
|
|
339
|
+
format: z.enum(["json", "markdown"]).default("json")
|
|
340
|
+
});
|
|
341
|
+
export const workspaceImportOptionsSchema = z.object({
|
|
342
|
+
normalizeTitleWhitespace: z.boolean().optional(),
|
|
343
|
+
trimBodyWhitespace: z.boolean().optional(),
|
|
344
|
+
duplicateMode: z.enum(["warn", "skip_exact"]).optional()
|
|
345
|
+
});
|
|
346
|
+
export const importWorkspacePreviewSchema = z.object({
|
|
347
|
+
format: z.enum(["recallx_json", "markdown"]),
|
|
348
|
+
sourcePath: z.string().min(1),
|
|
349
|
+
label: z.string().min(1).max(120).optional(),
|
|
350
|
+
options: workspaceImportOptionsSchema.optional()
|
|
351
|
+
});
|
|
352
|
+
export const importWorkspaceSchema = importWorkspacePreviewSchema;
|
|
353
|
+
export const restoreWorkspaceBackupSchema = z.object({
|
|
354
|
+
backupId: z.string().min(1),
|
|
355
|
+
targetRootPath: z.string().min(1),
|
|
356
|
+
workspaceName: z.string().min(1).optional()
|
|
357
|
+
});
|
package/app/shared/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RECALLX_VERSION = "1.
|
|
1
|
+
export const RECALLX_VERSION = "1.2.0";
|