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.
@@ -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 = 'v3-filepath';
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() === FLAG_VERSION)
91
+ if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === expectedFlag)
91
92
  return;
92
- if (!(0, office_sideload_1.isSideloaded)()) {
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, FLAG_VERSION);
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: { color: 'rgba(0,0,0,0)', symbolColor: '#1A1A1A', height: 36 },
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 [httpPort, httpsPort] = await Promise.all([
221
- (0, server_1.findAvailablePort)(options.preferredPort),
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);