fraim-framework 2.0.160 → 2.0.162
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/dist/src/ai-hub/conversation-store.js +164 -0
- package/dist/src/ai-hub/desktop-main.js +34 -12
- package/dist/src/ai-hub/hosts.js +383 -27
- package/dist/src/ai-hub/managed-browser.js +269 -0
- package/dist/src/ai-hub/manager-turns.js +13 -0
- package/dist/src/ai-hub/office-sideload.js +21 -3
- package/dist/src/ai-hub/preferences.js +10 -1
- package/dist/src/ai-hub/server.js +1243 -65
- package/dist/src/cli/commands/init-project.js +7 -1
- package/dist/src/cli/utils/agent-adapters.js +1 -1
- package/dist/src/core/fraim-config-schema.generated.js +50 -13
- package/dist/src/core/quality-evidence.js +4 -1
- package/dist/src/local-mcp-server/agent-token-prices.js +23 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +438 -2
- package/dist/src/local-mcp-server/stdio-server.js +73 -15
- package/package.json +5 -4
- package/public/ai-hub/index.html +456 -7
- package/public/ai-hub/powerpoint-taskpane/index.html +2 -1
- package/public/ai-hub/review.css +354 -0
- package/public/ai-hub/script.js +5945 -1279
- package/public/ai-hub/styles.css +1805 -16
|
@@ -0,0 +1,164 @@
|
|
|
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 = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
10
|
+
const emptyProjectState = () => ({
|
|
11
|
+
activeId: null,
|
|
12
|
+
conversations: [],
|
|
13
|
+
});
|
|
14
|
+
function timestampValue(value) {
|
|
15
|
+
if (typeof value === 'number')
|
|
16
|
+
return Number.isFinite(value) ? value : 0;
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
const parsed = Date.parse(value);
|
|
19
|
+
if (Number.isFinite(parsed))
|
|
20
|
+
return parsed;
|
|
21
|
+
const numeric = Number(value);
|
|
22
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
function normalizeProjectPath(projectPath) {
|
|
27
|
+
return path_1.default.resolve(projectPath || process.cwd());
|
|
28
|
+
}
|
|
29
|
+
function normalizeConversation(projectPath, raw) {
|
|
30
|
+
if (!raw || typeof raw !== 'object')
|
|
31
|
+
return null;
|
|
32
|
+
const value = raw;
|
|
33
|
+
if (typeof value.id !== 'string' || value.id.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof value.title !== 'string')
|
|
36
|
+
return null;
|
|
37
|
+
if (typeof value.jobId !== 'string')
|
|
38
|
+
return null;
|
|
39
|
+
if (value.agentName !== 'codex' && value.agentName !== 'claude' && value.agentName !== 'gemini')
|
|
40
|
+
return null;
|
|
41
|
+
if (value.status !== 'running' && value.status !== 'completed' && value.status !== 'failed')
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
...value,
|
|
45
|
+
id: value.id,
|
|
46
|
+
projectPath: normalizeProjectPath(typeof value.projectPath === 'string' ? value.projectPath : projectPath),
|
|
47
|
+
title: value.title,
|
|
48
|
+
jobId: value.jobId,
|
|
49
|
+
agentName: value.agentName,
|
|
50
|
+
status: value.status,
|
|
51
|
+
createdAt: value.createdAt ?? new Date().toISOString(),
|
|
52
|
+
lastUpdatedAt: value.lastUpdatedAt ?? value.createdAt ?? new Date().toISOString(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function normalizeProjectState(projectPath, raw) {
|
|
56
|
+
if (!raw || typeof raw !== 'object')
|
|
57
|
+
return emptyProjectState();
|
|
58
|
+
const value = raw;
|
|
59
|
+
const conversations = Array.isArray(value.conversations)
|
|
60
|
+
? value.conversations.map((entry) => normalizeConversation(projectPath, entry)).filter((entry) => Boolean(entry))
|
|
61
|
+
: [];
|
|
62
|
+
const activeId = typeof value.activeId === 'string' && conversations.some((entry) => entry.id === value.activeId)
|
|
63
|
+
? value.activeId
|
|
64
|
+
: null;
|
|
65
|
+
return { activeId, conversations };
|
|
66
|
+
}
|
|
67
|
+
class AiHubConversationStore {
|
|
68
|
+
constructor(stateFilePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-conversations.json')) {
|
|
69
|
+
this.stateFilePath = stateFilePath;
|
|
70
|
+
}
|
|
71
|
+
loadProject(projectPath) {
|
|
72
|
+
const state = this.readStore();
|
|
73
|
+
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
74
|
+
return normalizeProjectState(normalizedProjectPath, state.projects[normalizedProjectPath]);
|
|
75
|
+
}
|
|
76
|
+
replaceProject(projectPath, next) {
|
|
77
|
+
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
78
|
+
const normalized = normalizeProjectState(normalizedProjectPath, next);
|
|
79
|
+
const state = this.readStore();
|
|
80
|
+
state.projects[normalizedProjectPath] = normalized;
|
|
81
|
+
this.writeStore(state);
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
upsertConversation(projectPath, conversation, activeId) {
|
|
85
|
+
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
86
|
+
const normalizedConversation = normalizeConversation(normalizedProjectPath, conversation);
|
|
87
|
+
if (!normalizedConversation) {
|
|
88
|
+
return this.loadProject(normalizedProjectPath);
|
|
89
|
+
}
|
|
90
|
+
const current = this.loadProject(normalizedProjectPath);
|
|
91
|
+
const idx = current.conversations.findIndex((entry) => entry.id === normalizedConversation.id);
|
|
92
|
+
if (idx >= 0) {
|
|
93
|
+
const existing = current.conversations[idx];
|
|
94
|
+
if (timestampValue(normalizedConversation.lastUpdatedAt) < timestampValue(existing.lastUpdatedAt)) {
|
|
95
|
+
return current;
|
|
96
|
+
}
|
|
97
|
+
current.conversations[idx] = normalizedConversation;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
current.conversations.unshift(normalizedConversation);
|
|
101
|
+
}
|
|
102
|
+
current.activeId = activeId === undefined ? current.activeId : activeId;
|
|
103
|
+
if (current.activeId && !current.conversations.some((entry) => entry.id === current.activeId)) {
|
|
104
|
+
current.activeId = null;
|
|
105
|
+
}
|
|
106
|
+
return this.replaceProject(normalizedProjectPath, current);
|
|
107
|
+
}
|
|
108
|
+
patchConversation(projectPath, conversationId, patch) {
|
|
109
|
+
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
110
|
+
const current = this.loadProject(normalizedProjectPath);
|
|
111
|
+
const idx = current.conversations.findIndex((entry) => entry.id === conversationId);
|
|
112
|
+
if (idx < 0) {
|
|
113
|
+
const candidate = normalizeConversation(normalizedProjectPath, {
|
|
114
|
+
...patch,
|
|
115
|
+
id: conversationId,
|
|
116
|
+
projectPath: normalizedProjectPath,
|
|
117
|
+
});
|
|
118
|
+
if (candidate) {
|
|
119
|
+
current.conversations.unshift(candidate);
|
|
120
|
+
return this.replaceProject(normalizedProjectPath, current);
|
|
121
|
+
}
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
const existing = current.conversations[idx];
|
|
125
|
+
if (patch.lastUpdatedAt !== undefined
|
|
126
|
+
&& timestampValue(patch.lastUpdatedAt) < timestampValue(existing.lastUpdatedAt)) {
|
|
127
|
+
return current;
|
|
128
|
+
}
|
|
129
|
+
current.conversations[idx] = {
|
|
130
|
+
...existing,
|
|
131
|
+
...patch,
|
|
132
|
+
id: existing.id,
|
|
133
|
+
projectPath: normalizedProjectPath,
|
|
134
|
+
agentName: patch.agentName || existing.agentName,
|
|
135
|
+
lastUpdatedAt: patch.lastUpdatedAt ?? new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
return this.replaceProject(normalizedProjectPath, current);
|
|
138
|
+
}
|
|
139
|
+
readStore() {
|
|
140
|
+
if (!fs_1.default.existsSync(this.stateFilePath)) {
|
|
141
|
+
return { version: 1, projects: {} };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
145
|
+
if (raw.version !== 1 || !raw.projects || typeof raw.projects !== 'object') {
|
|
146
|
+
return { version: 1, projects: {} };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
version: 1,
|
|
150
|
+
projects: raw.projects,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { version: 1, projects: {} };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
writeStore(store) {
|
|
158
|
+
fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
|
|
159
|
+
const tempPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
160
|
+
fs_1.default.writeFileSync(tempPath, JSON.stringify(store, null, 2), 'utf8');
|
|
161
|
+
fs_1.default.renameSync(tempPath, this.stateFilePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.AiHubConversationStore = AiHubConversationStore;
|
|
@@ -82,22 +82,23 @@ function ensureLoginItem() {
|
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
83
|
// Word manifest sideload (runs once on first launch)
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
|
-
function ensureWordSideload(projectPath) {
|
|
85
|
+
function ensureWordSideload(projectPath, httpPort) {
|
|
86
86
|
// Flag version bump: bump this string when new manifests are added so all
|
|
87
87
|
// users get re-sideloaded on their next launch.
|
|
88
|
-
const FLAG_VERSION = '
|
|
88
|
+
const FLAG_VERSION = 'v4-dynamic-port';
|
|
89
|
+
const expectedFlag = `${FLAG_VERSION}:${httpPort}`;
|
|
89
90
|
const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'word-sideloaded.flag');
|
|
90
|
-
if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() ===
|
|
91
|
+
if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === expectedFlag)
|
|
91
92
|
return;
|
|
92
|
-
if (
|
|
93
|
-
const result = (0, office_sideload_1.sideloadManifest)(projectPath);
|
|
93
|
+
if (httpPort > 0) {
|
|
94
|
+
const result = (0, office_sideload_1.sideloadManifest)(projectPath, { httpPort, userDataDir: electron_1.app.getPath('userData') });
|
|
94
95
|
if (!result.ok) {
|
|
95
96
|
console.warn('[fraim] Office sideload skipped:', result.reason);
|
|
96
97
|
return; // don't write flag — retry next launch
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
|
|
100
|
-
fs_1.default.writeFileSync(flagPath,
|
|
101
|
+
fs_1.default.writeFileSync(flagPath, expectedFlag);
|
|
101
102
|
}
|
|
102
103
|
// ---------------------------------------------------------------------------
|
|
103
104
|
// Tray setup
|
|
@@ -173,7 +174,12 @@ async function createWindow(url) {
|
|
|
173
174
|
backgroundColor: (isMac || isWin) ? '#00000000' : '#ECECEC',
|
|
174
175
|
titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
|
|
175
176
|
...(isWin && {
|
|
176
|
-
titleBarOverlay: {
|
|
177
|
+
titleBarOverlay: {
|
|
178
|
+
color: 'rgba(0,0,0,0)',
|
|
179
|
+
// Glyphs must contrast the Mica material: light in dark mode, dark in light mode.
|
|
180
|
+
symbolColor: electron_1.nativeTheme.shouldUseDarkColors ? '#E8E8E8' : '#1A1A1A',
|
|
181
|
+
height: 36,
|
|
182
|
+
},
|
|
177
183
|
}),
|
|
178
184
|
...(isWin && { backgroundMaterial: 'mica' }),
|
|
179
185
|
...(isMac && { vibrancy: 'sidebar' }),
|
|
@@ -181,6 +187,24 @@ async function createWindow(url) {
|
|
|
181
187
|
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
182
188
|
});
|
|
183
189
|
electron_1.Menu.setApplicationMenu(null);
|
|
190
|
+
// Keep the Windows titlebar control glyphs readable when the OS theme flips
|
|
191
|
+
// between light and dark while the window is open.
|
|
192
|
+
if (isWin) {
|
|
193
|
+
const applyOverlay = () => {
|
|
194
|
+
if (!mainWindow)
|
|
195
|
+
return;
|
|
196
|
+
try {
|
|
197
|
+
mainWindow.setTitleBarOverlay({
|
|
198
|
+
color: 'rgba(0,0,0,0)',
|
|
199
|
+
symbolColor: electron_1.nativeTheme.shouldUseDarkColors ? '#E8E8E8' : '#1A1A1A',
|
|
200
|
+
height: 36,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch { /* overlay unsupported on this Windows build */ }
|
|
204
|
+
};
|
|
205
|
+
electron_1.nativeTheme.on('updated', applyOverlay);
|
|
206
|
+
mainWindow.on('closed', () => electron_1.nativeTheme.removeListener('updated', applyOverlay));
|
|
207
|
+
}
|
|
184
208
|
mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
|
|
185
209
|
void electron_1.shell.openExternal(targetUrl);
|
|
186
210
|
return { action: 'deny' };
|
|
@@ -217,10 +241,8 @@ function stopServerOnce() {
|
|
|
217
241
|
// Launch
|
|
218
242
|
// ---------------------------------------------------------------------------
|
|
219
243
|
async function launchDesktopShell(options) {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
(0, server_1.findAvailablePort)(43092),
|
|
223
|
-
]);
|
|
244
|
+
const httpPort = await (0, server_1.findAvailablePort)(options.preferredPort);
|
|
245
|
+
const httpsPort = await (0, server_1.findAvailablePortExcluding)(43092, new Set([httpPort]));
|
|
224
246
|
// Generate (or load cached) self-signed cert for HTTPS.
|
|
225
247
|
// Fast on subsequent launches (file read); ~200ms on first launch (key gen).
|
|
226
248
|
const certBundle = await (0, cert_store_1.loadOrCreateCert)();
|
|
@@ -239,7 +261,7 @@ async function launchDesktopShell(options) {
|
|
|
239
261
|
},
|
|
240
262
|
});
|
|
241
263
|
await server.start(httpPort);
|
|
242
|
-
ensureWordSideload(options.projectPath);
|
|
264
|
+
ensureWordSideload(options.projectPath, httpPort);
|
|
243
265
|
const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
|
|
244
266
|
createTray(hubUrl);
|
|
245
267
|
await createWindow(hubUrl);
|