fraim 2.0.159 → 2.0.160
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 +1 -1
- package/dist/src/ai-hub/cert-store.js +70 -0
- package/dist/src/ai-hub/desktop-main.js +225 -50
- package/dist/src/ai-hub/manager-turns.js +38 -0
- package/dist/src/ai-hub/office-sideload.js +138 -0
- package/dist/src/ai-hub/openclaw-bridge.js +239 -0
- package/dist/src/ai-hub/server.js +346 -115
- package/dist/src/ai-hub/word-sideload.js +95 -0
- package/dist/src/cli/commands/add-ide.js +9 -0
- package/dist/src/cli/commands/login.js +1 -2
- package/dist/src/cli/commands/setup.js +0 -2
- package/dist/src/cli/commands/sync.js +19 -10
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +66 -2
- package/dist/src/cli/doctor/checks/workflow-checks.js +1 -65
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +14 -10
- package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
- package/dist/src/cli/utils/fraim-gitignore.js +11 -0
- package/dist/src/cli/utils/remote-sync.js +1 -1
- package/dist/src/core/config-loader.js +1 -2
- package/dist/src/core/fraim-config-schema.generated.js +0 -5
- package/dist/src/core/types.js +0 -1
- package/dist/src/first-run/session-service.js +3 -3
- package/package.json +2 -1
- package/public/ai-hub/index.html +20 -2
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +235 -0
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
- package/public/ai-hub/script.js +337 -120
- package/public/ai-hub/styles.css +456 -135
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ R - Retrospectives: Continuous learning from experience
|
|
|
231
231
|
|
|
232
232
|
**Recommended: Use npx (no installation needed)**
|
|
233
233
|
```bash
|
|
234
|
-
npx fraim
|
|
234
|
+
npx fraim@latest setup --key=<your-fraim-key>
|
|
235
235
|
|
|
236
236
|
# Optional: Create alias for convenience
|
|
237
237
|
echo 'alias fraim="npx fraim-framework"' >> ~/.bashrc
|
|
@@ -0,0 +1,70 @@
|
|
|
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.loadOrCreateCert = loadOrCreateCert;
|
|
7
|
+
exports.trustCert = trustCert;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
function certDir() {
|
|
13
|
+
const base = process.env.APPDATA ||
|
|
14
|
+
(process.platform === 'darwin'
|
|
15
|
+
? path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support')
|
|
16
|
+
: path_1.default.join(os_1.default.homedir(), '.config'));
|
|
17
|
+
return path_1.default.join(base, 'FRAIM', 'certs');
|
|
18
|
+
}
|
|
19
|
+
function certPaths() {
|
|
20
|
+
const dir = certDir();
|
|
21
|
+
return { keyPath: path_1.default.join(dir, 'localhost.key'), certPath: path_1.default.join(dir, 'localhost.crt') };
|
|
22
|
+
}
|
|
23
|
+
async function loadOrCreateCert() {
|
|
24
|
+
const { keyPath, certPath } = certPaths();
|
|
25
|
+
if (fs_1.default.existsSync(keyPath) && fs_1.default.existsSync(certPath)) {
|
|
26
|
+
return { key: fs_1.default.readFileSync(keyPath, 'utf8'), cert: fs_1.default.readFileSync(certPath, 'utf8') };
|
|
27
|
+
}
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
const selfsigned = require('selfsigned');
|
|
30
|
+
const pem = await selfsigned.generate([{ name: 'commonName', value: 'localhost' }], {
|
|
31
|
+
keySize: 2048,
|
|
32
|
+
days: 3650,
|
|
33
|
+
algorithm: 'sha256',
|
|
34
|
+
extensions: [
|
|
35
|
+
{ name: 'subjectAltName', altNames: [{ type: 2, value: 'localhost' }, { type: 7, ip: '127.0.0.1' }] },
|
|
36
|
+
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true },
|
|
37
|
+
{ name: 'extKeyUsage', serverAuth: true },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
fs_1.default.mkdirSync(path_1.default.dirname(keyPath), { recursive: true });
|
|
41
|
+
fs_1.default.writeFileSync(keyPath, pem.private, { mode: 0o600 });
|
|
42
|
+
fs_1.default.writeFileSync(certPath, pem.cert, { mode: 0o644 });
|
|
43
|
+
// NOTE: we deliberately do NOT install this into the OS trusted-root store here.
|
|
44
|
+
// Desktop Word/PowerPoint reach the pane over plain HTTP loopback and need no cert.
|
|
45
|
+
// Trusting a root CA triggers a Windows security popup on every add/remove and is
|
|
46
|
+
// invasive, so it is reserved for explicit Word-Online opt-in via trustCert() below.
|
|
47
|
+
return { key: pem.private, cert: pem.cert };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Installs the cert as a trusted root CA in the user's OS certificate store.
|
|
51
|
+
* ONLY call this for explicit Word-Online support — it prompts a Windows security
|
|
52
|
+
* dialog. It is idempotent: it no-ops if a matching localhost cert is already
|
|
53
|
+
* trusted, so it never produces the delete/add popup churn.
|
|
54
|
+
* Windows: certutil -addstore -user Root (skipped if already present)
|
|
55
|
+
* macOS: security add-trusted-cert -r trustRoot -k login.keychain
|
|
56
|
+
*/
|
|
57
|
+
function trustCert(certPath) {
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
// Skip if a localhost cert is already trusted — avoids the confirmation popup.
|
|
60
|
+
const check = (0, child_process_1.spawnSync)('certutil', ['-user', '-store', 'Root', 'localhost'], { encoding: 'utf8' });
|
|
61
|
+
if (check.status === 0 && /localhost/i.test(check.stdout))
|
|
62
|
+
return;
|
|
63
|
+
(0, child_process_1.spawnSync)('certutil', ['-addstore', '-user', 'Root', certPath], { stdio: 'ignore' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (process.platform === 'darwin') {
|
|
67
|
+
const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
|
|
68
|
+
(0, child_process_1.spawnSync)('security', ['add-trusted-cert', '-r', 'trustRoot', '-k', keychain, certPath], { stdio: 'ignore' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.launchDesktopShell = launchDesktopShell;
|
|
4
7
|
const electron_1 = require("electron");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
5
10
|
const server_1 = require("./server");
|
|
6
11
|
const db_service_1 = require("../fraim/db-service");
|
|
12
|
+
const cert_store_1 = require("./cert-store");
|
|
13
|
+
const office_sideload_1 = require("./office-sideload");
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// State
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
let server = null;
|
|
18
|
+
let mainWindow = null;
|
|
19
|
+
let tray = null;
|
|
20
|
+
let stopping = null;
|
|
21
|
+
let isQuitting = false; // distinguishes window-close (→ tray) from app-quit
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
7
25
|
function tryCreateDbService() {
|
|
8
26
|
try {
|
|
9
27
|
return new db_service_1.FraimDbService();
|
|
@@ -12,102 +30,259 @@ function tryCreateDbService() {
|
|
|
12
30
|
return undefined;
|
|
13
31
|
}
|
|
14
32
|
}
|
|
15
|
-
let server = null;
|
|
16
|
-
let mainWindow = null;
|
|
17
|
-
let stopping = null;
|
|
18
33
|
function preferredWindowSize() {
|
|
19
|
-
const
|
|
20
|
-
const workArea = primaryDisplay.workAreaSize;
|
|
34
|
+
const { workAreaSize } = electron_1.screen.getPrimaryDisplay();
|
|
21
35
|
return {
|
|
22
|
-
width: Math.max(1440, Math.min(1680,
|
|
23
|
-
height: Math.max(980, Math.min(1180,
|
|
36
|
+
width: Math.max(1440, Math.min(1680, workAreaSize.width)),
|
|
37
|
+
height: Math.max(980, Math.min(1180, workAreaSize.height)),
|
|
24
38
|
};
|
|
25
39
|
}
|
|
26
40
|
function parseArgs(argv) {
|
|
27
41
|
let projectPath = process.cwd();
|
|
28
42
|
let preferredPort = 43091;
|
|
29
|
-
for (let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
index += 1;
|
|
34
|
-
continue;
|
|
43
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
44
|
+
if (argv[i] === '--project-path' && argv[i + 1]) {
|
|
45
|
+
projectPath = argv[i + 1];
|
|
46
|
+
i += 1;
|
|
35
47
|
}
|
|
36
|
-
if (
|
|
37
|
-
preferredPort = Number(argv[
|
|
38
|
-
|
|
48
|
+
if (argv[i] === '--port' && argv[i + 1]) {
|
|
49
|
+
preferredPort = Number(argv[i + 1]) || preferredPort;
|
|
50
|
+
i += 1;
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
return { projectPath, preferredPort };
|
|
42
54
|
}
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Tray icon resolution — prefers bundled icon, falls back to a 1×1 empty image
|
|
57
|
+
// so the app never crashes if assets aren't present.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
function resolveTrayIcon() {
|
|
60
|
+
const candidates = [
|
|
61
|
+
path_1.default.resolve(process.cwd(), 'extensions/office-word/icon-64.png'),
|
|
62
|
+
path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/icon-64.png'),
|
|
63
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/icon-64.png'),
|
|
64
|
+
];
|
|
65
|
+
for (const c of candidates) {
|
|
66
|
+
if (fs_1.default.existsSync(c))
|
|
67
|
+
return electron_1.nativeImage.createFromPath(c).resize({ width: 16, height: 16 });
|
|
68
|
+
}
|
|
69
|
+
return electron_1.nativeImage.createEmpty();
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Login-item (auto-start on boot)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
function ensureLoginItem() {
|
|
75
|
+
const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'login-item-set.flag');
|
|
76
|
+
if (fs_1.default.existsSync(flagPath))
|
|
45
77
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
electron_1.app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true });
|
|
79
|
+
fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
|
|
80
|
+
fs_1.default.writeFileSync(flagPath, '1');
|
|
48
81
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Word manifest sideload (runs once on first launch)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function ensureWordSideload(projectPath) {
|
|
86
|
+
// Flag version bump: bump this string when new manifests are added so all
|
|
87
|
+
// users get re-sideloaded on their next launch.
|
|
88
|
+
const FLAG_VERSION = 'v3-filepath';
|
|
89
|
+
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
|
+
return;
|
|
92
|
+
if (!(0, office_sideload_1.isSideloaded)()) {
|
|
93
|
+
const result = (0, office_sideload_1.sideloadManifest)(projectPath);
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
console.warn('[fraim] Office sideload skipped:', result.reason);
|
|
96
|
+
return; // don't write flag — retry next launch
|
|
97
|
+
}
|
|
54
98
|
}
|
|
55
|
-
|
|
99
|
+
fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
|
|
100
|
+
fs_1.default.writeFileSync(flagPath, FLAG_VERSION);
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Tray setup
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
function buildTrayMenu(hubUrl) {
|
|
106
|
+
return electron_1.Menu.buildFromTemplate([
|
|
107
|
+
{
|
|
108
|
+
label: 'Open FRAIM Hub',
|
|
109
|
+
click: () => {
|
|
110
|
+
if (mainWindow) {
|
|
111
|
+
mainWindow.show();
|
|
112
|
+
mainWindow.focus();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
void createWindow(hubUrl);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{ type: 'separator' },
|
|
120
|
+
{
|
|
121
|
+
label: 'Word Add-in',
|
|
122
|
+
enabled: false,
|
|
123
|
+
sublabel: (0, office_sideload_1.isSideloaded)() ? 'Active in Word' : 'Not sideloaded',
|
|
124
|
+
},
|
|
125
|
+
{ type: 'separator' },
|
|
126
|
+
{
|
|
127
|
+
label: 'Start at Login',
|
|
128
|
+
type: 'checkbox',
|
|
129
|
+
checked: electron_1.app.getLoginItemSettings().openAtLogin,
|
|
130
|
+
click: (item) => {
|
|
131
|
+
electron_1.app.setLoginItemSettings({ openAtLogin: item.checked, openAsHidden: true });
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ type: 'separator' },
|
|
135
|
+
{
|
|
136
|
+
label: 'Quit FRAIM',
|
|
137
|
+
click: () => {
|
|
138
|
+
isQuitting = true;
|
|
139
|
+
electron_1.app.quit();
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
56
143
|
}
|
|
144
|
+
function createTray(hubUrl) {
|
|
145
|
+
tray = new electron_1.Tray(resolveTrayIcon());
|
|
146
|
+
tray.setToolTip('FRAIM AI Hub');
|
|
147
|
+
tray.setContextMenu(buildTrayMenu(hubUrl));
|
|
148
|
+
tray.on('double-click', () => {
|
|
149
|
+
if (mainWindow) {
|
|
150
|
+
mainWindow.show();
|
|
151
|
+
mainWindow.focus();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
void createWindow(hubUrl);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// BrowserWindow
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
57
161
|
async function createWindow(url) {
|
|
58
162
|
const { width, height } = preferredWindowSize();
|
|
163
|
+
const isMac = process.platform === 'darwin';
|
|
164
|
+
const isWin = process.platform === 'win32';
|
|
59
165
|
mainWindow = new electron_1.BrowserWindow({
|
|
60
|
-
title: 'AI Hub',
|
|
166
|
+
title: 'FRAIM AI Hub',
|
|
61
167
|
width,
|
|
62
168
|
height,
|
|
63
|
-
minWidth:
|
|
64
|
-
minHeight:
|
|
169
|
+
minWidth: 1200,
|
|
170
|
+
minHeight: 800,
|
|
65
171
|
useContentSize: true,
|
|
66
172
|
autoHideMenuBar: true,
|
|
67
|
-
backgroundColor: '#
|
|
173
|
+
backgroundColor: (isMac || isWin) ? '#00000000' : '#ECECEC',
|
|
174
|
+
titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
|
|
175
|
+
...(isWin && {
|
|
176
|
+
titleBarOverlay: { color: 'rgba(0,0,0,0)', symbolColor: '#1A1A1A', height: 36 },
|
|
177
|
+
}),
|
|
178
|
+
...(isWin && { backgroundMaterial: 'mica' }),
|
|
179
|
+
...(isMac && { vibrancy: 'sidebar' }),
|
|
68
180
|
show: false,
|
|
69
|
-
webPreferences: {
|
|
70
|
-
contextIsolation: true,
|
|
71
|
-
nodeIntegration: false,
|
|
72
|
-
sandbox: true,
|
|
73
|
-
},
|
|
181
|
+
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
74
182
|
});
|
|
75
183
|
electron_1.Menu.setApplicationMenu(null);
|
|
76
184
|
mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
|
|
77
185
|
void electron_1.shell.openExternal(targetUrl);
|
|
78
186
|
return { action: 'deny' };
|
|
79
187
|
});
|
|
80
|
-
mainWindow.once('ready-to-show', () =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
mainWindow.on('
|
|
84
|
-
|
|
188
|
+
mainWindow.once('ready-to-show', () => mainWindow?.show());
|
|
189
|
+
// Closing the window hides it to the tray rather than quitting the app.
|
|
190
|
+
// The server keeps running so Word can still reach the task pane.
|
|
191
|
+
mainWindow.on('close', (event) => {
|
|
192
|
+
if (!isQuitting) {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
mainWindow?.hide();
|
|
195
|
+
}
|
|
85
196
|
});
|
|
197
|
+
mainWindow.on('closed', () => { mainWindow = null; });
|
|
86
198
|
await mainWindow.loadURL(url);
|
|
87
199
|
mainWindow.webContents.setZoomFactor(1);
|
|
88
200
|
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Server lifecycle
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
async function stopServer() {
|
|
205
|
+
if (!server)
|
|
206
|
+
return;
|
|
207
|
+
await server.stop();
|
|
208
|
+
server = null;
|
|
209
|
+
}
|
|
210
|
+
function stopServerOnce() {
|
|
211
|
+
if (!stopping) {
|
|
212
|
+
stopping = stopServer().finally(() => { stopping = null; });
|
|
213
|
+
}
|
|
214
|
+
return stopping;
|
|
215
|
+
}
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Launch
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
89
219
|
async function launchDesktopShell(options) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
220
|
+
const [httpPort, httpsPort] = await Promise.all([
|
|
221
|
+
(0, server_1.findAvailablePort)(options.preferredPort),
|
|
222
|
+
(0, server_1.findAvailablePort)(43092),
|
|
223
|
+
]);
|
|
224
|
+
// Generate (or load cached) self-signed cert for HTTPS.
|
|
225
|
+
// Fast on subsequent launches (file read); ~200ms on first launch (key gen).
|
|
226
|
+
const certBundle = await (0, cert_store_1.loadOrCreateCert)();
|
|
227
|
+
server = new server_1.AiHubServer({
|
|
228
|
+
projectPath: options.projectPath,
|
|
229
|
+
dbService: tryCreateDbService(),
|
|
230
|
+
httpsPort,
|
|
231
|
+
certBundle,
|
|
232
|
+
folderPicker: async () => {
|
|
233
|
+
const result = await electron_1.dialog.showOpenDialog(mainWindow, {
|
|
234
|
+
title: 'Select a FRAIM project folder',
|
|
235
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
236
|
+
buttonLabel: 'Select Folder',
|
|
237
|
+
});
|
|
238
|
+
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
await server.start(httpPort);
|
|
242
|
+
ensureWordSideload(options.projectPath);
|
|
243
|
+
const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
|
|
244
|
+
createTray(hubUrl);
|
|
245
|
+
await createWindow(hubUrl);
|
|
94
246
|
}
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Bootstrap
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
95
250
|
async function bootstrap() {
|
|
96
251
|
const options = parseArgs(process.argv.slice(2));
|
|
252
|
+
// Single-instance lock — if another instance is already running, focus it
|
|
253
|
+
// and exit rather than spawning a second server + window.
|
|
254
|
+
const gotLock = electron_1.app.requestSingleInstanceLock();
|
|
255
|
+
if (!gotLock) {
|
|
256
|
+
electron_1.app.quit();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
electron_1.app.on('second-instance', () => {
|
|
260
|
+
if (mainWindow) {
|
|
261
|
+
mainWindow.show();
|
|
262
|
+
mainWindow.focus();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
97
265
|
await electron_1.app.whenReady();
|
|
98
|
-
electron_1.app.setName('
|
|
266
|
+
electron_1.app.setName('FRAIM Hub');
|
|
267
|
+
// First-launch housekeeping (idempotent, fast on subsequent runs)
|
|
268
|
+
ensureLoginItem();
|
|
99
269
|
electron_1.app.on('activate', () => {
|
|
100
|
-
|
|
101
|
-
|
|
270
|
+
// macOS: clicking dock icon re-shows the window
|
|
271
|
+
if (mainWindow) {
|
|
272
|
+
mainWindow.show();
|
|
273
|
+
mainWindow.focus();
|
|
102
274
|
}
|
|
103
275
|
});
|
|
104
276
|
electron_1.app.on('before-quit', () => {
|
|
277
|
+
isQuitting = true;
|
|
105
278
|
void stopServerOnce();
|
|
106
279
|
});
|
|
280
|
+
// Keep app alive when all windows are closed — server must keep serving
|
|
281
|
+
// for Word to reach the task pane. Only quit on explicit tray → Quit.
|
|
107
282
|
electron_1.app.on('window-all-closed', () => {
|
|
108
|
-
|
|
109
|
-
electron_1.app.quit();
|
|
110
|
-
}
|
|
283
|
+
if (process.platform !== 'darwin' && isQuitting) {
|
|
284
|
+
void stopServerOnce().finally(() => electron_1.app.quit());
|
|
285
|
+
}
|
|
111
286
|
});
|
|
112
287
|
await launchDesktopShell(options);
|
|
113
288
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractExplicitFraimInvocation = extractExplicitFraimInvocation;
|
|
4
|
+
exports.fraimInvocationFor = fraimInvocationFor;
|
|
5
|
+
exports.buildManagerMessage = buildManagerMessage;
|
|
6
|
+
function extractExplicitFraimInvocation(text) {
|
|
7
|
+
const raw = String(text || '');
|
|
8
|
+
const match = raw.match(/(?:^|\n|\s)([$/]fraim)\s+([a-z0-9][a-z0-9-]*)(?=\s|$)/i);
|
|
9
|
+
if (!match || match.index == null)
|
|
10
|
+
return null;
|
|
11
|
+
const before = raw.slice(0, match.index).trim();
|
|
12
|
+
const after = raw.slice(match.index + match[0].length).trim();
|
|
13
|
+
const remainder = [before, after].filter(Boolean).join('\n\n').trim();
|
|
14
|
+
return {
|
|
15
|
+
symbol: match[1].toLowerCase() === '$fraim' ? '$fraim' : '/fraim',
|
|
16
|
+
jobId: match[2].toLowerCase(),
|
|
17
|
+
remainder,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function fraimInvocationFor(employeeId, jobId) {
|
|
21
|
+
if (jobId === '__freeform__')
|
|
22
|
+
return null;
|
|
23
|
+
const symbol = employeeId === 'codex' ? '$fraim' : '/fraim';
|
|
24
|
+
return `${symbol} ${jobId}`;
|
|
25
|
+
}
|
|
26
|
+
function buildManagerMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
27
|
+
const trimmed = String(instructions || '').trim();
|
|
28
|
+
const explicit = extractExplicitFraimInvocation(trimmed);
|
|
29
|
+
const effectiveJobId = explicit?.jobId || jobId;
|
|
30
|
+
const invocation = fraimInvocationFor(employeeId, effectiveJobId);
|
|
31
|
+
if (!invocation)
|
|
32
|
+
return explicit?.remainder || trimmed;
|
|
33
|
+
const remainder = explicit ? explicit.remainder : trimmed;
|
|
34
|
+
const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
|
|
35
|
+
if (!remainder)
|
|
36
|
+
return `${invocation}${stub}`;
|
|
37
|
+
return `${invocation}${stub}\n\n${remainder}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sideloads Office add-in manifests (Word + PowerPoint) so they appear under
|
|
4
|
+
* Insert > My Add-ins > Developer Add-ins without admin rights or AppSource.
|
|
5
|
+
*
|
|
6
|
+
* Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer\<guid> =
|
|
7
|
+
* ABSOLUTE FILE PATH to manifest.xml. This is exactly what Microsoft's
|
|
8
|
+
* `office-addin-dev-settings register` writes. A URL value is for
|
|
9
|
+
* SharePoint/network-share catalogs and yields "catalog access denied"
|
|
10
|
+
* for a developer sideload — do NOT use a URL here.
|
|
11
|
+
* macOS: copies the manifest into each app's
|
|
12
|
+
* ~/Library/Containers/<bundle>/Data/Documents/wef/<guid>.xml
|
|
13
|
+
*
|
|
14
|
+
* NOTE: the manifest XML itself must be ASCII-clean (no smart quotes, em/en
|
|
15
|
+
* dashes, or BOM) or Office's parser hard-fails with -1072894428 and the pane
|
|
16
|
+
* silently never appears. See project_rules.md.
|
|
17
|
+
*
|
|
18
|
+
* Both paths are non-admin and survive app updates (keyed by manifest GUID).
|
|
19
|
+
* Safe to call multiple times — idempotent.
|
|
20
|
+
*/
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.isSideloaded = isSideloaded;
|
|
26
|
+
exports.sideloadManifest = sideloadManifest;
|
|
27
|
+
exports.removeSideload = removeSideload;
|
|
28
|
+
const fs_1 = __importDefault(require("fs"));
|
|
29
|
+
const path_1 = __importDefault(require("path"));
|
|
30
|
+
const os_1 = __importDefault(require("os"));
|
|
31
|
+
const child_process_1 = require("child_process");
|
|
32
|
+
const WEF_DEVELOPER_KEY = 'HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer';
|
|
33
|
+
const MANIFESTS = [
|
|
34
|
+
{
|
|
35
|
+
guid: 'd1090951-50cf-4cf2-9d12-b0f8541d265c',
|
|
36
|
+
candidates: (base) => [
|
|
37
|
+
path_1.default.resolve(base, 'extensions/office-word/manifest.xml'),
|
|
38
|
+
path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/manifest.xml'),
|
|
39
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/manifest.xml'),
|
|
40
|
+
],
|
|
41
|
+
macContainer: 'com.microsoft.Word',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
guid: 'e7a3c812-91fd-4b2e-8c15-d4f6a903b71e',
|
|
45
|
+
candidates: (base) => [
|
|
46
|
+
path_1.default.resolve(base, 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
|
|
47
|
+
path_1.default.resolve(__dirname, '..', '..', 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
|
|
48
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
|
|
49
|
+
],
|
|
50
|
+
macContainer: 'com.microsoft.Powerpoint',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
function resolveManifestPath(entry, projectPath) {
|
|
54
|
+
return entry.candidates(projectPath).find(c => fs_1.default.existsSync(c)) ?? null;
|
|
55
|
+
}
|
|
56
|
+
function macWefPath(container, guid) {
|
|
57
|
+
return path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', container, 'Data', 'Documents', 'wef', `${guid}.xml`);
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Windows registry helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function winRegisteredValue(guid) {
|
|
63
|
+
const r = (0, child_process_1.spawnSync)('reg', ['query', WEF_DEVELOPER_KEY, '/v', guid], { encoding: 'utf8' });
|
|
64
|
+
if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
|
|
65
|
+
return null;
|
|
66
|
+
// Output line looks like: " <guid> REG_SZ C:\path\to\manifest.xml"
|
|
67
|
+
const line = r.stdout.split(/\r?\n/).find(l => l.includes('REG_SZ'));
|
|
68
|
+
if (!line)
|
|
69
|
+
return null;
|
|
70
|
+
const idx = line.indexOf('REG_SZ');
|
|
71
|
+
return line.slice(idx + 'REG_SZ'.length).trim() || null;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Public API
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/** Returns true when ALL manifests are sideloaded with the correct value. */
|
|
77
|
+
function isSideloaded() {
|
|
78
|
+
return MANIFESTS.every(m => isManifestSideloaded(m));
|
|
79
|
+
}
|
|
80
|
+
function isManifestSideloaded(m) {
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
const value = winRegisteredValue(m.guid);
|
|
83
|
+
// Must be a real file path that exists — a URL or a stale/missing path
|
|
84
|
+
// means we have NOT correctly sideloaded and should re-register.
|
|
85
|
+
return !!value && !/^https?:\/\//i.test(value) && fs_1.default.existsSync(value);
|
|
86
|
+
}
|
|
87
|
+
if (process.platform === 'darwin')
|
|
88
|
+
return fs_1.default.existsSync(macWefPath(m.macContainer, m.guid));
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Sideloads all manifests. Returns ok:true if every manifest was registered.
|
|
93
|
+
* Returns ok:false with the first failure reason.
|
|
94
|
+
*/
|
|
95
|
+
function sideloadManifest(projectPath) {
|
|
96
|
+
for (const entry of MANIFESTS) {
|
|
97
|
+
const manifestPath = resolveManifestPath(entry, projectPath);
|
|
98
|
+
if (!manifestPath) {
|
|
99
|
+
return { ok: false, reason: `Manifest not found for GUID ${entry.guid} — check extensions/office-word/ and public/ai-hub/powerpoint-taskpane/` };
|
|
100
|
+
}
|
|
101
|
+
if (process.platform === 'win32') {
|
|
102
|
+
// Developer-key value = absolute file path to manifest.xml (NOT a URL).
|
|
103
|
+
const r = (0, child_process_1.spawnSync)('reg', [
|
|
104
|
+
'add', WEF_DEVELOPER_KEY,
|
|
105
|
+
'/v', entry.guid, '/t', 'REG_SZ', '/d', manifestPath, '/f',
|
|
106
|
+
], { encoding: 'utf8' });
|
|
107
|
+
if (r.status !== 0)
|
|
108
|
+
return { ok: false, reason: r.stderr || `reg add failed for ${entry.guid}` };
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (process.platform === 'darwin') {
|
|
112
|
+
const dest = macWefPath(entry.macContainer, entry.guid);
|
|
113
|
+
try {
|
|
114
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
115
|
+
fs_1.default.copyFileSync(manifestPath, dest);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return { ok: false, reason: String(err) };
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
return { ok: false, reason: `Unsupported platform: ${process.platform}` };
|
|
123
|
+
}
|
|
124
|
+
return { ok: true };
|
|
125
|
+
}
|
|
126
|
+
/** Removes all sideloaded manifests. */
|
|
127
|
+
function removeSideload() {
|
|
128
|
+
for (const entry of MANIFESTS) {
|
|
129
|
+
if (process.platform === 'win32') {
|
|
130
|
+
(0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', entry.guid, '/f'], { encoding: 'utf8' });
|
|
131
|
+
}
|
|
132
|
+
else if (process.platform === 'darwin') {
|
|
133
|
+
const target = macWefPath(entry.macContainer, entry.guid);
|
|
134
|
+
if (fs_1.default.existsSync(target))
|
|
135
|
+
fs_1.default.unlinkSync(target);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|