fraim 2.0.203 → 2.0.204
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 +3 -3
- package/bin/fraim.js +8 -18
- package/dist/src/cli/commands/first-run.js +2 -37
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +1 -1
- package/dist/src/cli/fraim.js +0 -2
- package/dist/src/cli/setup/ide-invocation-surfaces.js +6 -6
- package/dist/src/cli/utils/managed-agent-paths.js +76 -6
- package/dist/src/cli/utils/version-utils.js +2 -2
- package/dist/src/config/persona-capability-bundles.js +3 -3
- package/dist/src/core/job-phases.js +168 -0
- package/dist/src/core/utils/ports.js +32 -0
- package/dist/src/core/utils/stub-generator.js +24 -2
- package/dist/src/first-run/session-service.js +106 -24
- package/dist/src/local-mcp-server/learning-context-builder.js +1 -1
- package/dist/src/local-mcp-server/stdio-server.js +41 -9
- package/dist/src/routes/analytics.js +2 -2
- package/dist/src/services/registry-service.js +2 -2
- package/index.js +1 -1
- package/package.json +61 -181
- package/dist/src/ai-hub/catalog.js +0 -406
- package/dist/src/ai-hub/cert-store.js +0 -70
- package/dist/src/ai-hub/conversation-store.js +0 -359
- package/dist/src/ai-hub/desktop-main.js +0 -449
- package/dist/src/ai-hub/hosts.js +0 -1664
- package/dist/src/ai-hub/hub-latest-version.js +0 -52
- package/dist/src/ai-hub/hub-launch-decision.js +0 -57
- package/dist/src/ai-hub/hub-runtime-file.js +0 -43
- package/dist/src/ai-hub/managed-browser.js +0 -269
- package/dist/src/ai-hub/manager-turns.js +0 -64
- package/dist/src/ai-hub/office-sideload.js +0 -156
- package/dist/src/ai-hub/openclaw-bridge.js +0 -250
- package/dist/src/ai-hub/preferences.js +0 -201
- package/dist/src/ai-hub/remote-hub-gateway.js +0 -88
- package/dist/src/ai-hub/server.js +0 -3827
- package/dist/src/ai-hub/types.js +0 -2
- package/dist/src/ai-hub/word-sideload.js +0 -95
- package/dist/src/cli/commands/hub.js +0 -238
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
- package/dist/src/services/email-service-clean.js +0 -782
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +0 -33
- package/extensions/office-word/taskpane.html +0 -242
- package/public/ai-hub/index.html +0 -894
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +0 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +0 -30
- package/public/ai-hub/review.css +0 -381
- package/public/ai-hub/script.js +0 -11015
- package/public/ai-hub/styles.css +0 -4873
- package/public/portfolio/ashley.html +0 -523
- package/public/portfolio/auditya.html +0 -83
- package/public/portfolio/banke.html +0 -83
- package/public/portfolio/beza.html +0 -659
- package/public/portfolio/careena.html +0 -632
- package/public/portfolio/casey.html +0 -568
- package/public/portfolio/celia.html +0 -490
- package/public/portfolio/deidre.html +0 -642
- package/public/portfolio/gautam.html +0 -597
- package/public/portfolio/hari.html +0 -469
- package/public/portfolio/huxley.html +0 -1354
- package/public/portfolio/index.html +0 -741
- package/public/portfolio/maestro.html +0 -518
- package/public/portfolio/mandy.html +0 -590
- package/public/portfolio/mona.html +0 -597
- package/public/portfolio/pam.html +0 -887
- package/public/portfolio/procella.html +0 -107
- package/public/portfolio/qasm.html +0 -569
- package/public/portfolio/ricardo.html +0 -489
- package/public/portfolio/sade.html +0 -560
- package/public/portfolio/sam.html +0 -654
- package/public/portfolio/sechar.html +0 -580
- package/public/portfolio/sreya.html +0 -599
- package/public/portfolio/swen.html +0 -601
- /package/dist/src/{ai-hub → core}/brand-store.js +0 -0
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AiHubConversationStore = exports.COMPANY_SCOPE_KEY = exports.MANAGER_SCOPE_KEY = void 0;
|
|
7
|
-
exports.conversationScopeKey = conversationScopeKey;
|
|
8
|
-
exports.migrateConversationStore = migrateConversationStore;
|
|
9
|
-
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const path_1 = __importDefault(require("path"));
|
|
11
|
-
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
12
|
-
// Issue #708: reserved, project-independent bucket keys for non-project scopes.
|
|
13
|
-
// They are NOT filesystem paths and must never be passed through path.resolve.
|
|
14
|
-
exports.MANAGER_SCOPE_KEY = '@manager';
|
|
15
|
-
exports.COMPANY_SCOPE_KEY = '@company';
|
|
16
|
-
/**
|
|
17
|
-
* Issue #708: resolve the conversation store bucket key for a given scope.
|
|
18
|
-
* - 'manager'/'company' → a stable sentinel key (project-independent home).
|
|
19
|
-
* - 'project' (or undefined) → the resolved project path (existing behavior).
|
|
20
|
-
*/
|
|
21
|
-
function conversationScopeKey(scope, projectPath) {
|
|
22
|
-
if (scope === 'manager')
|
|
23
|
-
return exports.MANAGER_SCOPE_KEY;
|
|
24
|
-
if (scope === 'company')
|
|
25
|
-
return exports.COMPANY_SCOPE_KEY;
|
|
26
|
-
return normalizeConversationKey(projectPath);
|
|
27
|
-
}
|
|
28
|
-
const emptyProjectState = () => ({
|
|
29
|
-
activeId: null,
|
|
30
|
-
conversations: [],
|
|
31
|
-
});
|
|
32
|
-
function timestampValue(value) {
|
|
33
|
-
if (typeof value === 'number')
|
|
34
|
-
return Number.isFinite(value) ? value : 0;
|
|
35
|
-
if (typeof value === 'string') {
|
|
36
|
-
const parsed = Date.parse(value);
|
|
37
|
-
if (Number.isFinite(parsed))
|
|
38
|
-
return parsed;
|
|
39
|
-
const numeric = Number(value);
|
|
40
|
-
return Number.isFinite(numeric) ? numeric : 0;
|
|
41
|
-
}
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
// Issue #708: sentinel scope keys (e.g. '@manager') are stable bucket keys, not paths —
|
|
45
|
-
// pass them through untouched; resolve everything else as a real project path.
|
|
46
|
-
function normalizeConversationKey(key) {
|
|
47
|
-
if (typeof key === 'string' && (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY)) {
|
|
48
|
-
return key;
|
|
49
|
-
}
|
|
50
|
-
return path_1.default.resolve(key || process.cwd());
|
|
51
|
-
}
|
|
52
|
-
// Retained name for project-path stamping; now sentinel-aware so manager/company
|
|
53
|
-
// records keep their scope key rather than being resolved into cwd/@manager.
|
|
54
|
-
function normalizeProjectPath(projectPath) {
|
|
55
|
-
return normalizeConversationKey(projectPath);
|
|
56
|
-
}
|
|
57
|
-
// Issue #708: one-time, idempotent migration. Legacy manager/company-invoked runs were
|
|
58
|
-
// stored inside project buckets and distinguished only by the client-side `invokedArea`
|
|
59
|
-
// field. Fold them into the reserved scope buckets (stamping `scope`) so they have a
|
|
60
|
-
// stable, project-independent home. Runs already in a scope bucket are left untouched.
|
|
61
|
-
function migrateScopeBuckets(store) {
|
|
62
|
-
const projects = {};
|
|
63
|
-
const ensure = (key) => {
|
|
64
|
-
if (!projects[key])
|
|
65
|
-
projects[key] = { activeId: null, conversations: [] };
|
|
66
|
-
return projects[key];
|
|
67
|
-
};
|
|
68
|
-
// Seed existing scope buckets first so their activeId is preserved.
|
|
69
|
-
for (const [key, state] of Object.entries(store.projects || {})) {
|
|
70
|
-
if (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY) {
|
|
71
|
-
ensure(key).conversations.push(...(state.conversations || []));
|
|
72
|
-
projects[key].activeId = state.activeId ?? null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
for (const [key, state] of Object.entries(store.projects || {})) {
|
|
76
|
-
if (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY)
|
|
77
|
-
continue;
|
|
78
|
-
const stay = [];
|
|
79
|
-
for (const conv of (state.conversations || [])) {
|
|
80
|
-
const scope = conv.scope
|
|
81
|
-
?? conv.invokedArea;
|
|
82
|
-
if (scope === 'manager' || scope === 'company') {
|
|
83
|
-
const bucketKey = scope === 'manager' ? exports.MANAGER_SCOPE_KEY : exports.COMPANY_SCOPE_KEY;
|
|
84
|
-
const bucket = ensure(bucketKey);
|
|
85
|
-
if (!bucket.conversations.some((c) => c && c.id === conv.id)) {
|
|
86
|
-
bucket.conversations.push({ ...conv, scope: scope });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
stay.push(conv);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
projects[key] = { activeId: state.activeId ?? null, conversations: stay };
|
|
94
|
-
}
|
|
95
|
-
return { version: 1, projects };
|
|
96
|
-
}
|
|
97
|
-
// Canonical comparison key for a project path (case-insensitive on win32), matching the
|
|
98
|
-
// convention used by removeProject and the preferences store.
|
|
99
|
-
function canonicalProjectPathKey(value) {
|
|
100
|
-
const resolved = path_1.default.resolve(value);
|
|
101
|
-
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
102
|
-
}
|
|
103
|
-
// How much run history a record carries. Used to decide the winner when the same conversation id
|
|
104
|
-
// exists in two buckets: the copy with real history must never be overwritten by a header-only
|
|
105
|
-
// shell (which is exactly what a mis-filed slim PUT produces).
|
|
106
|
-
function conversationRichness(conv) {
|
|
107
|
-
const value = conv;
|
|
108
|
-
const messages = Array.isArray(value?.messages) ? value.messages.length : 0;
|
|
109
|
-
const events = Array.isArray(value?.events) ? value.events.length : 0;
|
|
110
|
-
return messages + events;
|
|
111
|
-
}
|
|
112
|
-
// Place a raw conversation into a bucket, deduping by id. When the id already exists, keep the
|
|
113
|
-
// richer copy (more run history); tie-break on newer lastUpdatedAt. Order-independent.
|
|
114
|
-
function placeConversationInBucket(bucket, conv) {
|
|
115
|
-
const value = conv;
|
|
116
|
-
const id = value && typeof value.id === 'string' ? value.id : '';
|
|
117
|
-
if (!id) {
|
|
118
|
-
bucket.conversations.push(conv);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const idx = bucket.conversations.findIndex((entry) => entry && entry.id === id);
|
|
122
|
-
if (idx < 0) {
|
|
123
|
-
bucket.conversations.push(conv);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const existing = bucket.conversations[idx];
|
|
127
|
-
const existingScore = conversationRichness(existing);
|
|
128
|
-
const incomingScore = conversationRichness(conv);
|
|
129
|
-
if (incomingScore > existingScore) {
|
|
130
|
-
bucket.conversations[idx] = conv;
|
|
131
|
-
}
|
|
132
|
-
else if (incomingScore === existingScore
|
|
133
|
-
&& timestampValue(value?.lastUpdatedAt) > timestampValue(existing.lastUpdatedAt)) {
|
|
134
|
-
bucket.conversations[idx] = conv;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// A conversation's identity is unique only WITHIN a bucket, and a bucket key is a project path.
|
|
138
|
-
// The buggy client PUT path could file a conversation whose own projectPath points at project B
|
|
139
|
-
// into project A's bucket (see docs/rca/hub-conversation-cross-project-leak.md), so the same run
|
|
140
|
-
// appeared under two projects and the mis-filed copy was a header-only shell. This one-time,
|
|
141
|
-
// idempotent, read-time migration relocates every project-scoped record back to the bucket that
|
|
142
|
-
// matches its own projectPath, deduping so the copy with real run history wins. A bucket activeId
|
|
143
|
-
// that pointed at a relocated record is cleared. Sentinel scope buckets (@manager/@company)
|
|
144
|
-
// legitimately hold records whose projectPath points at a project, so they are left untouched.
|
|
145
|
-
function migrateProjectBuckets(store) {
|
|
146
|
-
const projects = {};
|
|
147
|
-
const ensure = (key) => {
|
|
148
|
-
if (!projects[key])
|
|
149
|
-
projects[key] = { activeId: null, conversations: [] };
|
|
150
|
-
return projects[key];
|
|
151
|
-
};
|
|
152
|
-
// Preserve every existing bucket key and its activeId up front.
|
|
153
|
-
for (const [key, state] of Object.entries(store.projects || {})) {
|
|
154
|
-
ensure(key).activeId = state.activeId ?? null;
|
|
155
|
-
}
|
|
156
|
-
for (const [bucketKey, state] of Object.entries(store.projects || {})) {
|
|
157
|
-
const isSentinel = bucketKey === exports.MANAGER_SCOPE_KEY || bucketKey === exports.COMPANY_SCOPE_KEY;
|
|
158
|
-
for (const conv of (state.conversations || [])) {
|
|
159
|
-
if (!conv || typeof conv !== 'object')
|
|
160
|
-
continue;
|
|
161
|
-
const scope = conv.scope
|
|
162
|
-
?? conv.invokedArea;
|
|
163
|
-
const ownPath = typeof conv.projectPath === 'string'
|
|
164
|
-
? conv.projectPath
|
|
165
|
-
: '';
|
|
166
|
-
const mismatched = !isSentinel
|
|
167
|
-
&& scope !== 'manager' && scope !== 'company'
|
|
168
|
-
&& ownPath.length > 0
|
|
169
|
-
&& canonicalProjectPathKey(ownPath) !== canonicalProjectPathKey(bucketKey);
|
|
170
|
-
if (!mismatched) {
|
|
171
|
-
placeConversationInBucket(ensure(bucketKey), conv);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
// Relocate to the record's own project bucket.
|
|
175
|
-
placeConversationInBucket(ensure(path_1.default.resolve(ownPath)), conv);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
// Any activeId that no longer resolves to a conversation in its bucket (because that record was
|
|
179
|
-
// relocated out) is stale — clear it so a project never points at another project's run.
|
|
180
|
-
for (const state of Object.values(projects)) {
|
|
181
|
-
if (state.activeId && !state.conversations.some((entry) => entry && entry.id === state.activeId)) {
|
|
182
|
-
state.activeId = null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return { version: 1, projects };
|
|
186
|
-
}
|
|
187
|
-
// The full on-read migration pipeline, exported so a one-off maintenance/cleanup pass can heal an
|
|
188
|
-
// existing store file through the EXACT same code path the store uses at runtime (rather than a
|
|
189
|
-
// duplicated, drift-prone reimplementation). Operates on raw records, so it never drops a record
|
|
190
|
-
// via normalization.
|
|
191
|
-
function migrateConversationStore(store) {
|
|
192
|
-
return migrateProjectBuckets(migrateScopeBuckets(store));
|
|
193
|
-
}
|
|
194
|
-
function normalizeConversation(projectPath, raw) {
|
|
195
|
-
if (!raw || typeof raw !== 'object')
|
|
196
|
-
return null;
|
|
197
|
-
const value = raw;
|
|
198
|
-
if (typeof value.id !== 'string' || value.id.length === 0)
|
|
199
|
-
return null;
|
|
200
|
-
if (typeof value.title !== 'string')
|
|
201
|
-
return null;
|
|
202
|
-
if (typeof value.jobId !== 'string')
|
|
203
|
-
return null;
|
|
204
|
-
if (value.agentName !== 'codex' && value.agentName !== 'claude' && value.agentName !== 'gemini')
|
|
205
|
-
return null;
|
|
206
|
-
if (value.status !== 'running' && value.status !== 'completed' && value.status !== 'failed')
|
|
207
|
-
return null;
|
|
208
|
-
return {
|
|
209
|
-
...value,
|
|
210
|
-
id: value.id,
|
|
211
|
-
projectPath: normalizeProjectPath(typeof value.projectPath === 'string' ? value.projectPath : projectPath),
|
|
212
|
-
title: value.title,
|
|
213
|
-
jobId: value.jobId,
|
|
214
|
-
agentName: value.agentName,
|
|
215
|
-
status: value.status,
|
|
216
|
-
createdAt: value.createdAt ?? new Date().toISOString(),
|
|
217
|
-
lastUpdatedAt: value.lastUpdatedAt ?? value.createdAt ?? new Date().toISOString(),
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
function normalizeProjectState(projectPath, raw) {
|
|
221
|
-
if (!raw || typeof raw !== 'object')
|
|
222
|
-
return emptyProjectState();
|
|
223
|
-
const value = raw;
|
|
224
|
-
const conversations = Array.isArray(value.conversations)
|
|
225
|
-
? value.conversations.map((entry) => normalizeConversation(projectPath, entry)).filter((entry) => Boolean(entry))
|
|
226
|
-
: [];
|
|
227
|
-
const activeId = typeof value.activeId === 'string' && conversations.some((entry) => entry.id === value.activeId)
|
|
228
|
-
? value.activeId
|
|
229
|
-
: null;
|
|
230
|
-
return { activeId, conversations };
|
|
231
|
-
}
|
|
232
|
-
class AiHubConversationStore {
|
|
233
|
-
constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-conversations.json')) {
|
|
234
|
-
this.stateFilePath = stateFilePath;
|
|
235
|
-
}
|
|
236
|
-
listProjectPaths() {
|
|
237
|
-
const state = this.readStore();
|
|
238
|
-
return Object.keys(state.projects || {}).map((projectPath) => normalizeProjectPath(projectPath));
|
|
239
|
-
}
|
|
240
|
-
loadProject(projectPath) {
|
|
241
|
-
const state = this.readStore();
|
|
242
|
-
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
243
|
-
return normalizeProjectState(normalizedProjectPath, state.projects[normalizedProjectPath]);
|
|
244
|
-
}
|
|
245
|
-
// Issue #719: delete a project's KEY from the store entirely. Writing an empty
|
|
246
|
-
// conversation list is not enough — knownProjects re-derives a project from
|
|
247
|
-
// every stored key (listProjectPaths), so removal must drop the key itself.
|
|
248
|
-
// Sentinel scope buckets (@manager/@company) are not projects and never removable.
|
|
249
|
-
removeProject(projectPath) {
|
|
250
|
-
if (projectPath === exports.MANAGER_SCOPE_KEY || projectPath === exports.COMPANY_SCOPE_KEY)
|
|
251
|
-
return false;
|
|
252
|
-
const canonicalKey = (value) => {
|
|
253
|
-
const resolved = path_1.default.resolve(value);
|
|
254
|
-
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
255
|
-
};
|
|
256
|
-
const target = canonicalKey(projectPath);
|
|
257
|
-
const state = this.readStore();
|
|
258
|
-
let removed = false;
|
|
259
|
-
for (const key of Object.keys(state.projects)) {
|
|
260
|
-
if (key === exports.MANAGER_SCOPE_KEY || key === exports.COMPANY_SCOPE_KEY)
|
|
261
|
-
continue;
|
|
262
|
-
if (canonicalKey(key) === target) {
|
|
263
|
-
delete state.projects[key];
|
|
264
|
-
removed = true;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (removed)
|
|
268
|
-
this.writeStore(state);
|
|
269
|
-
return removed;
|
|
270
|
-
}
|
|
271
|
-
replaceProject(projectPath, next) {
|
|
272
|
-
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
273
|
-
const normalized = normalizeProjectState(normalizedProjectPath, next);
|
|
274
|
-
const state = this.readStore();
|
|
275
|
-
state.projects[normalizedProjectPath] = normalized;
|
|
276
|
-
this.writeStore(state);
|
|
277
|
-
return normalized;
|
|
278
|
-
}
|
|
279
|
-
upsertConversation(projectPath, conversation, activeId) {
|
|
280
|
-
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
281
|
-
const normalizedConversation = normalizeConversation(normalizedProjectPath, conversation);
|
|
282
|
-
if (!normalizedConversation) {
|
|
283
|
-
return this.loadProject(normalizedProjectPath);
|
|
284
|
-
}
|
|
285
|
-
const current = this.loadProject(normalizedProjectPath);
|
|
286
|
-
const idx = current.conversations.findIndex((entry) => entry.id === normalizedConversation.id);
|
|
287
|
-
if (idx >= 0) {
|
|
288
|
-
const existing = current.conversations[idx];
|
|
289
|
-
if (timestampValue(normalizedConversation.lastUpdatedAt) < timestampValue(existing.lastUpdatedAt)) {
|
|
290
|
-
return current;
|
|
291
|
-
}
|
|
292
|
-
current.conversations[idx] = normalizedConversation;
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
current.conversations.unshift(normalizedConversation);
|
|
296
|
-
}
|
|
297
|
-
current.activeId = activeId === undefined ? current.activeId : activeId;
|
|
298
|
-
if (current.activeId && !current.conversations.some((entry) => entry.id === current.activeId)) {
|
|
299
|
-
current.activeId = null;
|
|
300
|
-
}
|
|
301
|
-
return this.replaceProject(normalizedProjectPath, current);
|
|
302
|
-
}
|
|
303
|
-
patchConversation(projectPath, conversationId, patch) {
|
|
304
|
-
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
305
|
-
const current = this.loadProject(normalizedProjectPath);
|
|
306
|
-
const idx = current.conversations.findIndex((entry) => entry.id === conversationId);
|
|
307
|
-
if (idx < 0) {
|
|
308
|
-
const candidate = normalizeConversation(normalizedProjectPath, {
|
|
309
|
-
...patch,
|
|
310
|
-
id: conversationId,
|
|
311
|
-
projectPath: normalizedProjectPath,
|
|
312
|
-
});
|
|
313
|
-
if (candidate) {
|
|
314
|
-
current.conversations.unshift(candidate);
|
|
315
|
-
return this.replaceProject(normalizedProjectPath, current);
|
|
316
|
-
}
|
|
317
|
-
return current;
|
|
318
|
-
}
|
|
319
|
-
const existing = current.conversations[idx];
|
|
320
|
-
if (patch.lastUpdatedAt !== undefined
|
|
321
|
-
&& timestampValue(patch.lastUpdatedAt) < timestampValue(existing.lastUpdatedAt)) {
|
|
322
|
-
return current;
|
|
323
|
-
}
|
|
324
|
-
current.conversations[idx] = {
|
|
325
|
-
...existing,
|
|
326
|
-
...patch,
|
|
327
|
-
id: existing.id,
|
|
328
|
-
projectPath: normalizedProjectPath,
|
|
329
|
-
agentName: patch.agentName || existing.agentName,
|
|
330
|
-
lastUpdatedAt: patch.lastUpdatedAt ?? new Date().toISOString(),
|
|
331
|
-
};
|
|
332
|
-
return this.replaceProject(normalizedProjectPath, current);
|
|
333
|
-
}
|
|
334
|
-
readStore() {
|
|
335
|
-
if (!fs_1.default.existsSync(this.stateFilePath)) {
|
|
336
|
-
return { version: 1, projects: {} };
|
|
337
|
-
}
|
|
338
|
-
try {
|
|
339
|
-
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
340
|
-
if (raw.version !== 1 || !raw.projects || typeof raw.projects !== 'object') {
|
|
341
|
-
return { version: 1, projects: {} };
|
|
342
|
-
}
|
|
343
|
-
// Issue #708: fold legacy manager/company-invoked runs into scope buckets on read, then
|
|
344
|
-
// relocate any project-scoped record mis-filed under the wrong project bucket back to its
|
|
345
|
-
// own project (see docs/rca/hub-conversation-cross-project-leak.md).
|
|
346
|
-
return migrateProjectBuckets(migrateScopeBuckets({ version: 1, projects: raw.projects }));
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
return { version: 1, projects: {} };
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
writeStore(store) {
|
|
353
|
-
fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
|
|
354
|
-
const tempPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
355
|
-
fs_1.default.writeFileSync(tempPath, JSON.stringify(store, null, 2), 'utf8');
|
|
356
|
-
fs_1.default.renameSync(tempPath, this.stateFilePath);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
exports.AiHubConversationStore = AiHubConversationStore;
|