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,52 +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.getLatestPublishedVersion = getLatestPublishedVersion;
|
|
7
|
-
exports.__resetLatestVersionCache = __resetLatestVersionCache;
|
|
8
|
-
const https_1 = __importDefault(require("https"));
|
|
9
|
-
// Issue #755: best-effort "latest published fraim version" for the Hub's
|
|
10
|
-
// update-available nudge. Cached in-process (1h TTL) and never throws — returns
|
|
11
|
-
// null when offline or the registry is unreachable, so the nudge simply hides.
|
|
12
|
-
const TTL_MS = 60 * 60 * 1000;
|
|
13
|
-
const REGISTRY_URL = 'https://registry.npmjs.org/fraim/latest';
|
|
14
|
-
let cache = { value: null, at: 0 };
|
|
15
|
-
async function getLatestPublishedVersion() {
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
if (cache.at !== 0 && now - cache.at < TTL_MS)
|
|
18
|
-
return cache.value;
|
|
19
|
-
const value = await fetchNpmLatest();
|
|
20
|
-
// Only cache successful lookups; a failed fetch retries next call rather than
|
|
21
|
-
// caching a null for an hour.
|
|
22
|
-
if (value !== null)
|
|
23
|
-
cache = { value, at: now };
|
|
24
|
-
return value ?? cache.value;
|
|
25
|
-
}
|
|
26
|
-
/** Test-only: reset the module cache. */
|
|
27
|
-
function __resetLatestVersionCache() {
|
|
28
|
-
cache = { value: null, at: 0 };
|
|
29
|
-
}
|
|
30
|
-
function fetchNpmLatest() {
|
|
31
|
-
return new Promise((resolve) => {
|
|
32
|
-
const req = https_1.default.get(REGISTRY_URL, { timeout: 4000 }, (res) => {
|
|
33
|
-
if (res.statusCode !== 200) {
|
|
34
|
-
res.resume();
|
|
35
|
-
return resolve(null);
|
|
36
|
-
}
|
|
37
|
-
let body = '';
|
|
38
|
-
res.on('data', (chunk) => { body += chunk; });
|
|
39
|
-
res.on('end', () => {
|
|
40
|
-
try {
|
|
41
|
-
const version = JSON.parse(body).version;
|
|
42
|
-
resolve(typeof version === 'string' ? version : null);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
resolve(null);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
req.on('error', () => resolve(null));
|
|
50
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
51
|
-
});
|
|
52
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.decideHubLaunch = decideHubLaunch;
|
|
37
|
-
const semver = __importStar(require("semver"));
|
|
38
|
-
function decideHubLaunch(input) {
|
|
39
|
-
const { running, pidAlive, cliVersion, restart, noRestart } = input;
|
|
40
|
-
if (!running || !pidAlive) {
|
|
41
|
-
return { action: 'launch-fresh', reason: 'no live Hub instance is running' };
|
|
42
|
-
}
|
|
43
|
-
if (noRestart) {
|
|
44
|
-
return { action: 'focus-existing', reason: '--no-restart: keeping the running instance' };
|
|
45
|
-
}
|
|
46
|
-
if (restart) {
|
|
47
|
-
return { action: 'replace', reason: '--restart: replacing the running instance' };
|
|
48
|
-
}
|
|
49
|
-
// Only treat as stale when BOTH versions are valid semver and the running one is
|
|
50
|
-
// strictly older. On any uncertainty (unknown/unparseable version) do not kill —
|
|
51
|
-
// conservative default is to focus the existing instance.
|
|
52
|
-
const comparable = !!semver.valid(running.version) && !!semver.valid(cliVersion);
|
|
53
|
-
if (comparable && semver.lt(running.version, cliVersion)) {
|
|
54
|
-
return { action: 'replace', reason: `running ${running.version} is older than ${cliVersion}; replacing with the newer build` };
|
|
55
|
-
}
|
|
56
|
-
return { action: 'focus-existing', reason: `running ${running.version} is current or not comparable to ${cliVersion}` };
|
|
57
|
-
}
|
|
@@ -1,43 +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.hubRuntimeFilePath = hubRuntimeFilePath;
|
|
7
|
-
exports.writeHubRuntimeFile = writeHubRuntimeFile;
|
|
8
|
-
exports.readHubRuntimeFile = readHubRuntimeFile;
|
|
9
|
-
exports.removeHubRuntimeFile = removeHubRuntimeFile;
|
|
10
|
-
const fs_1 = __importDefault(require("fs"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
// Issue #755: the running desktop Hub writes ~/.fraim/hub-runtime.json so the
|
|
13
|
-
// `fraim hub` CLI can discover a live instance (pid/port/version) before deciding
|
|
14
|
-
// whether to replace a stale build. `fraimDir` is injected (never hard-coded to
|
|
15
|
-
// the real ~/.fraim) so tests isolate via a temp directory.
|
|
16
|
-
const RUNTIME_FILE = 'hub-runtime.json';
|
|
17
|
-
function hubRuntimeFilePath(fraimDir) {
|
|
18
|
-
return path_1.default.join(fraimDir, RUNTIME_FILE);
|
|
19
|
-
}
|
|
20
|
-
function writeHubRuntimeFile(fraimDir, info) {
|
|
21
|
-
fs_1.default.mkdirSync(fraimDir, { recursive: true });
|
|
22
|
-
fs_1.default.writeFileSync(hubRuntimeFilePath(fraimDir), JSON.stringify(info, null, 2));
|
|
23
|
-
}
|
|
24
|
-
function readHubRuntimeFile(fraimDir) {
|
|
25
|
-
try {
|
|
26
|
-
const parsed = JSON.parse(fs_1.default.readFileSync(hubRuntimeFilePath(fraimDir), 'utf8'));
|
|
27
|
-
if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number' || typeof parsed.version !== 'string') {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
return { pid: parsed.pid, port: parsed.port, version: parsed.version, startedAt: String(parsed.startedAt ?? '') };
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function removeHubRuntimeFile(fraimDir) {
|
|
37
|
-
try {
|
|
38
|
-
fs_1.default.rmSync(hubRuntimeFilePath(fraimDir), { force: true });
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
/* no-op: absent file is fine */
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,269 +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.ManagedBrowser = void 0;
|
|
7
|
-
exports.browserExecutableCandidates = browserExecutableCandidates;
|
|
8
|
-
exports.resolveBrowserExecutable = resolveBrowserExecutable;
|
|
9
|
-
exports.buildBrowserLaunchArgs = buildBrowserLaunchArgs;
|
|
10
|
-
exports.buildBrowserContextNote = buildBrowserContextNote;
|
|
11
|
-
exports.defaultBrowserProfileDir = defaultBrowserProfileDir;
|
|
12
|
-
exports.probeCdpEndpoint = probeCdpEndpoint;
|
|
13
|
-
/**
|
|
14
|
-
* #521 — FRAIM-managed persistent browser.
|
|
15
|
-
*
|
|
16
|
-
* Problem: the Hub runs each agent turn as a one-shot process (`claude -p …`).
|
|
17
|
-
* When a turn ends, that process exits and any browser it launched (a child of
|
|
18
|
-
* the agent) is torn down — so "open a browser, I'll log in, then continue"
|
|
19
|
-
* never works: the window dies at the turn boundary before the manager can log in.
|
|
20
|
-
*
|
|
21
|
-
* Fix: FRAIM owns ONE long-lived Chromium browser (Chrome or Edge) launched with
|
|
22
|
-
* a remote-debugging port and a persistent profile. It outlives agent turns.
|
|
23
|
-
* Agents `connectOverCDP` to it instead of launching their own, so the session —
|
|
24
|
-
* including a manual login — survives across turns. One shared browser, one
|
|
25
|
-
* persistent profile; concurrency isolation is intentionally NOT solved here
|
|
26
|
-
* (see persistent-browser-session skill) to keep this simple.
|
|
27
|
-
*/
|
|
28
|
-
const child_process_1 = require("child_process");
|
|
29
|
-
const http_1 = __importDefault(require("http"));
|
|
30
|
-
const os_1 = __importDefault(require("os"));
|
|
31
|
-
const path_1 = __importDefault(require("path"));
|
|
32
|
-
const fs_1 = __importDefault(require("fs"));
|
|
33
|
-
/**
|
|
34
|
-
* Known install locations for Chrome and Edge per platform. Pure + injectable so
|
|
35
|
-
* resolution can be unit-tested without the browsers actually being installed.
|
|
36
|
-
*/
|
|
37
|
-
function browserExecutableCandidates(platform = process.platform, env = process.env) {
|
|
38
|
-
if (platform === 'win32') {
|
|
39
|
-
const pf = env['ProgramFiles'] || 'C:\\Program Files';
|
|
40
|
-
const pf86 = env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
41
|
-
const local = env['LOCALAPPDATA']
|
|
42
|
-
|| path_1.default.join(env['USERPROFILE'] || 'C:\\Users\\Default', 'AppData', 'Local');
|
|
43
|
-
return {
|
|
44
|
-
chrome: [
|
|
45
|
-
path_1.default.join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
46
|
-
path_1.default.join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
47
|
-
path_1.default.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
48
|
-
],
|
|
49
|
-
msedge: [
|
|
50
|
-
path_1.default.join(pf86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
51
|
-
path_1.default.join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
if (platform === 'darwin') {
|
|
56
|
-
return {
|
|
57
|
-
chrome: ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],
|
|
58
|
-
msedge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// linux + anything else
|
|
62
|
-
return {
|
|
63
|
-
chrome: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser'],
|
|
64
|
-
msedge: ['/usr/bin/microsoft-edge', '/usr/bin/microsoft-edge-stable'],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Resolve a Chromium executable. Returns null when neither Chrome nor Edge is
|
|
69
|
-
* found (caller surfaces a clear "install Chrome or Edge" error).
|
|
70
|
-
*/
|
|
71
|
-
function resolveBrowserExecutable(opts = {}) {
|
|
72
|
-
const exists = opts.exists || ((p) => fs_1.default.existsSync(p));
|
|
73
|
-
if (opts.explicitPath) {
|
|
74
|
-
if (exists(opts.explicitPath)) {
|
|
75
|
-
const channel = /edge|msedge/i.test(opts.explicitPath) ? 'msedge' : 'chrome';
|
|
76
|
-
return { channel, path: opts.explicitPath };
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const candidates = browserExecutableCandidates(opts.platform, opts.env);
|
|
81
|
-
const order = opts.channel === 'msedge'
|
|
82
|
-
? ['msedge', 'chrome']
|
|
83
|
-
: ['chrome', 'msedge']; // 'chrome' and 'auto' both prefer Chrome first
|
|
84
|
-
for (const channel of order) {
|
|
85
|
-
for (const candidate of candidates[channel]) {
|
|
86
|
-
if (exists(candidate))
|
|
87
|
-
return { channel, path: candidate };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Args that put Chrome/Edge into a debuggable, prompt-free, persistent-profile
|
|
94
|
-
* state. Headed (no --headless) so the manager can log in by hand.
|
|
95
|
-
*/
|
|
96
|
-
function buildBrowserLaunchArgs(opts) {
|
|
97
|
-
return [
|
|
98
|
-
`--remote-debugging-port=${opts.port}`,
|
|
99
|
-
`--user-data-dir=${opts.userDataDir}`,
|
|
100
|
-
'--no-first-run',
|
|
101
|
-
'--no-default-browser-check',
|
|
102
|
-
// Required so Playwright's CDP WebSocket handshake is accepted by recent Chromium.
|
|
103
|
-
'--remote-allow-origins=*',
|
|
104
|
-
'about:blank',
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* The runtime guidance the Hub injects into an agent's start message so it uses
|
|
109
|
-
* the shared browser. This is intentionally a UI/Hub-layer injection — it is NOT
|
|
110
|
-
* baked into any registry job or skill. Returns '' when the shared browser is not
|
|
111
|
-
* available (so the note only appears when there is something to connect to).
|
|
112
|
-
*/
|
|
113
|
-
function buildBrowserContextNote(cdpEndpoint, hubBaseUrl) {
|
|
114
|
-
if (!cdpEndpoint || !hubBaseUrl)
|
|
115
|
-
return '';
|
|
116
|
-
return [
|
|
117
|
-
'',
|
|
118
|
-
'[FRAIM shared browser] A persistent shared browser is available — use it instead of launching your own.',
|
|
119
|
-
`Before using a browser, ensure it is running: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` (idempotent — it reuses the running browser, or relaunches it if it was closed).`,
|
|
120
|
-
`Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and reuse its existing context/tab.`,
|
|
121
|
-
'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in — the session persists to your next turn.',
|
|
122
|
-
].join('\n');
|
|
123
|
-
}
|
|
124
|
-
/** Default persistent profile directory — sandboxed from the user's daily browser. */
|
|
125
|
-
function defaultBrowserProfileDir(env = process.env) {
|
|
126
|
-
const home = env['FRAIM_HOME'] || os_1.default.homedir();
|
|
127
|
-
return path_1.default.join(home, '.fraim', 'browser-profile');
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Probe a CDP endpoint's /json/version. Resolves true only when it answers with a
|
|
131
|
-
* Chromium DevTools payload. Uses node http (no fetch dependency).
|
|
132
|
-
*/
|
|
133
|
-
function probeCdpEndpoint(endpoint, timeoutMs = 1500) {
|
|
134
|
-
return new Promise((resolve) => {
|
|
135
|
-
let url;
|
|
136
|
-
try {
|
|
137
|
-
url = new URL('/json/version', endpoint);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
resolve(false);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const req = http_1.default.get(url, { timeout: timeoutMs }, (res) => {
|
|
144
|
-
let body = '';
|
|
145
|
-
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
146
|
-
res.on('end', () => {
|
|
147
|
-
try {
|
|
148
|
-
const data = JSON.parse(body);
|
|
149
|
-
resolve(!!(data && (data.webSocketDebuggerUrl || data.Browser)));
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
resolve(false);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
req.on('error', () => resolve(false));
|
|
157
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
const DEFAULT_PORT = 9222;
|
|
161
|
-
/**
|
|
162
|
-
* Owns the lifecycle of the single shared browser. start() is idempotent — it
|
|
163
|
-
* reuses an already-running browser on the port — so multiple callers (and Hub
|
|
164
|
-
* restarts) converge on one instance.
|
|
165
|
-
*/
|
|
166
|
-
class ManagedBrowser {
|
|
167
|
-
constructor(opts = {}) {
|
|
168
|
-
this.child = null;
|
|
169
|
-
this.ownsProcess = false;
|
|
170
|
-
this.resolved = null;
|
|
171
|
-
this.opts = opts;
|
|
172
|
-
this.channel = opts.channel || 'auto';
|
|
173
|
-
this.port = opts.port || DEFAULT_PORT;
|
|
174
|
-
this.host = opts.host || '127.0.0.1';
|
|
175
|
-
this.userDataDir = opts.userDataDir || defaultBrowserProfileDir(opts.env);
|
|
176
|
-
}
|
|
177
|
-
cdpEndpoint() {
|
|
178
|
-
return `http://${this.host}:${this.port}`;
|
|
179
|
-
}
|
|
180
|
-
probe() {
|
|
181
|
-
return this.opts.probeFn ? this.opts.probeFn(this.cdpEndpoint()) : probeCdpEndpoint(this.cdpEndpoint());
|
|
182
|
-
}
|
|
183
|
-
/** Live check — does a Chromium CDP endpoint answer on the port right now. */
|
|
184
|
-
isRunning() {
|
|
185
|
-
return this.probe();
|
|
186
|
-
}
|
|
187
|
-
async start() {
|
|
188
|
-
// 1. Reuse a browser already listening on the port (idempotent across callers
|
|
189
|
-
// and Hub restarts; also lets the manager pre-launch their own).
|
|
190
|
-
if (await this.probe()) {
|
|
191
|
-
return { endpoint: this.cdpEndpoint(), reused: true, executable: this.resolved?.path || null, channel: this.resolved?.channel || null };
|
|
192
|
-
}
|
|
193
|
-
// Already spawned by us but not yet reachable → just wait for it.
|
|
194
|
-
if (this.child && this.ownsProcess) {
|
|
195
|
-
await this.waitForRunning();
|
|
196
|
-
return { endpoint: this.cdpEndpoint(), reused: false, executable: this.resolved?.path || null, channel: this.resolved?.channel || null };
|
|
197
|
-
}
|
|
198
|
-
// 2. Resolve the executable.
|
|
199
|
-
const exe = resolveBrowserExecutable({
|
|
200
|
-
channel: this.channel,
|
|
201
|
-
explicitPath: this.opts.explicitPath,
|
|
202
|
-
platform: this.opts.platform,
|
|
203
|
-
env: this.opts.env,
|
|
204
|
-
exists: this.opts.exists,
|
|
205
|
-
});
|
|
206
|
-
if (!exe) {
|
|
207
|
-
throw new Error('No Chrome or Edge found. Install Google Chrome or Microsoft Edge, or set FRAIM_BROWSER_PATH to a Chromium executable.');
|
|
208
|
-
}
|
|
209
|
-
this.resolved = exe;
|
|
210
|
-
// 3. Ensure the profile dir exists, then spawn the browser long-lived. It is a
|
|
211
|
-
// child of the Hub server (so it dies with the Hub) but NOT of any agent
|
|
212
|
-
// turn — that is the whole point.
|
|
213
|
-
try {
|
|
214
|
-
if (!this.opts.exists)
|
|
215
|
-
fs_1.default.mkdirSync(this.userDataDir, { recursive: true });
|
|
216
|
-
}
|
|
217
|
-
catch { /* best-effort; browser will create it */ }
|
|
218
|
-
const args = buildBrowserLaunchArgs({ port: this.port, userDataDir: this.userDataDir });
|
|
219
|
-
const spawnFn = this.opts.spawnFn || ((c, a, o) => (0, child_process_1.spawn)(c, a, o));
|
|
220
|
-
const child = spawnFn(exe.path, args, { stdio: 'ignore', windowsHide: false });
|
|
221
|
-
this.child = child;
|
|
222
|
-
this.ownsProcess = true;
|
|
223
|
-
if (typeof child.on === 'function') {
|
|
224
|
-
child.on('exit', () => {
|
|
225
|
-
if (this.child === child) {
|
|
226
|
-
this.child = null;
|
|
227
|
-
this.ownsProcess = false;
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
// 4. Wait until the CDP endpoint is reachable before returning the endpoint.
|
|
232
|
-
await this.waitForRunning();
|
|
233
|
-
return { endpoint: this.cdpEndpoint(), reused: false, executable: exe.path, channel: exe.channel };
|
|
234
|
-
}
|
|
235
|
-
async waitForRunning() {
|
|
236
|
-
const timeoutMs = this.opts.waitTimeoutMs ?? 15000;
|
|
237
|
-
const intervalMs = this.opts.waitIntervalMs ?? 250;
|
|
238
|
-
const startedAt = Date.now();
|
|
239
|
-
for (;;) {
|
|
240
|
-
if (await this.probe())
|
|
241
|
-
return;
|
|
242
|
-
if (Date.now() - startedAt > timeoutMs) {
|
|
243
|
-
throw new Error(`Browser did not become reachable on ${this.cdpEndpoint()} within ${timeoutMs}ms.`);
|
|
244
|
-
}
|
|
245
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
/** Kill the browser only if WE launched it — never a browser the manager owns. */
|
|
249
|
-
stop() {
|
|
250
|
-
if (this.child && this.ownsProcess && typeof this.child.kill === 'function') {
|
|
251
|
-
try {
|
|
252
|
-
this.child.kill();
|
|
253
|
-
}
|
|
254
|
-
catch { /* ignore */ }
|
|
255
|
-
}
|
|
256
|
-
this.child = null;
|
|
257
|
-
this.ownsProcess = false;
|
|
258
|
-
}
|
|
259
|
-
status() {
|
|
260
|
-
return {
|
|
261
|
-
ownsProcess: this.ownsProcess,
|
|
262
|
-
endpoint: this.cdpEndpoint(),
|
|
263
|
-
channel: this.resolved?.channel || this.channel,
|
|
264
|
-
userDataDir: this.userDataDir,
|
|
265
|
-
executable: this.resolved?.path || null,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
exports.ManagedBrowser = ManagedBrowser;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractExplicitFraimInvocation = extractExplicitFraimInvocation;
|
|
4
|
-
exports.fraimInvocationFor = fraimInvocationFor;
|
|
5
|
-
exports.buildCommunicationStyleNote = buildCommunicationStyleNote;
|
|
6
|
-
exports.buildSameJobContinueMessage = buildSameJobContinueMessage;
|
|
7
|
-
exports.buildManagerMessage = buildManagerMessage;
|
|
8
|
-
function extractExplicitFraimInvocation(text) {
|
|
9
|
-
const raw = String(text || '');
|
|
10
|
-
const match = raw.match(/(?:^|\n|\s)([$/@]fraim)\s+([a-z0-9][a-z0-9-]*)(?=\s|$)/i);
|
|
11
|
-
if (!match || match.index == null)
|
|
12
|
-
return null;
|
|
13
|
-
const before = raw.slice(0, match.index).trim();
|
|
14
|
-
const after = raw.slice(match.index + match[0].length).trim();
|
|
15
|
-
const remainder = [before, after].filter(Boolean).join('\n\n').trim();
|
|
16
|
-
return {
|
|
17
|
-
symbol: match[1].toLowerCase(),
|
|
18
|
-
jobId: match[2].toLowerCase(),
|
|
19
|
-
remainder,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function fraimInvocationFor(employeeId, jobId) {
|
|
23
|
-
if (jobId === '__freeform__')
|
|
24
|
-
return null;
|
|
25
|
-
const symbol = employeeId === 'codex' ? '$fraim' : '/fraim';
|
|
26
|
-
return `${symbol} ${jobId}`;
|
|
27
|
-
}
|
|
28
|
-
// #521: a Hub-injected communication-style note so the employee's messages to the
|
|
29
|
-
// manager stay about the JOB and its outcomes — not the FRAIM machinery (seeking
|
|
30
|
-
// mentoring, tool calls, git, phase switches). The structured signals (the
|
|
31
|
-
// tracker's seekMentoring events, the review_handoff) still flow as tool calls
|
|
32
|
-
// behind the scenes, so this only shapes the human-facing prose. Injected at the
|
|
33
|
-
// Hub layer — it never edits the jobs.
|
|
34
|
-
function buildCommunicationStyleNote() {
|
|
35
|
-
return [
|
|
36
|
-
'',
|
|
37
|
-
'[How to talk to me] In your messages to me, report ONLY on the job and its outcome — what you found, what you changed, the decisions you made, blockers, and what you need from me. Do NOT narrate the FRAIM machinery: don\'t announce that you are talking to FRAIM, asking your mentor, following the process, moving between phases, or calling tools (git, playwright, etc.). I can see the raw tool activity separately if I want it. Keep your updates short and about the work, not the process.',
|
|
38
|
-
].join('\n');
|
|
39
|
-
}
|
|
40
|
-
// Issue #732: a plain continue of the SAME active job must not re-load the job.
|
|
41
|
-
// Continue turns resume the existing agent session, which already has the job
|
|
42
|
-
// loaded, so this message carries NO `/fraim <job>` invocation — the headless
|
|
43
|
-
// transform therefore does not re-instruct get_fraim_job (which is slow and
|
|
44
|
-
// mints a fresh UUID job id every call). The manager coaching still reaches the
|
|
45
|
-
// agent, which advances the workflow via seekMentoring using its resumed context.
|
|
46
|
-
function buildSameJobContinueMessage(coaching) {
|
|
47
|
-
const remainder = String(coaching || '').trim();
|
|
48
|
-
return remainder
|
|
49
|
-
? `Continue the active FRAIM job with this manager coaching:\n\n${remainder}`
|
|
50
|
-
: 'Continue the active FRAIM job using the current session context.';
|
|
51
|
-
}
|
|
52
|
-
function buildManagerMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
53
|
-
const trimmed = String(instructions || '').trim();
|
|
54
|
-
const explicit = extractExplicitFraimInvocation(trimmed);
|
|
55
|
-
const effectiveJobId = explicit?.jobId || jobId;
|
|
56
|
-
const invocation = fraimInvocationFor(employeeId, effectiveJobId);
|
|
57
|
-
if (!invocation)
|
|
58
|
-
return explicit?.remainder || trimmed;
|
|
59
|
-
const remainder = explicit ? explicit.remainder : trimmed;
|
|
60
|
-
const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
|
|
61
|
-
if (!remainder)
|
|
62
|
-
return `${invocation}${stub}`;
|
|
63
|
-
return `${invocation}${stub}\n\n${remainder}`;
|
|
64
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
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.manifestXmlForPort = manifestXmlForPort;
|
|
26
|
-
exports.isSideloaded = isSideloaded;
|
|
27
|
-
exports.sideloadManifest = sideloadManifest;
|
|
28
|
-
exports.removeSideload = removeSideload;
|
|
29
|
-
const fs_1 = __importDefault(require("fs"));
|
|
30
|
-
const path_1 = __importDefault(require("path"));
|
|
31
|
-
const os_1 = __importDefault(require("os"));
|
|
32
|
-
const child_process_1 = require("child_process");
|
|
33
|
-
const WEF_DEVELOPER_KEY = 'HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer';
|
|
34
|
-
const MANIFESTS = [
|
|
35
|
-
{
|
|
36
|
-
guid: 'd1090951-50cf-4cf2-9d12-b0f8541d265c',
|
|
37
|
-
candidates: (base) => [
|
|
38
|
-
path_1.default.resolve(base, 'extensions/office-word/manifest.xml'),
|
|
39
|
-
path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/manifest.xml'),
|
|
40
|
-
path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/manifest.xml'),
|
|
41
|
-
],
|
|
42
|
-
macContainer: 'com.microsoft.Word',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
guid: 'e7a3c812-91fd-4b2e-8c15-d4f6a903b71e',
|
|
46
|
-
candidates: (base) => [
|
|
47
|
-
path_1.default.resolve(base, 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
|
|
48
|
-
path_1.default.resolve(__dirname, '..', '..', 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
|
|
49
|
-
path_1.default.resolve(__dirname, '..', '..', '..', 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
|
|
50
|
-
],
|
|
51
|
-
macContainer: 'com.microsoft.Powerpoint',
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
function resolveManifestPath(entry, projectPath) {
|
|
55
|
-
return entry.candidates(projectPath).find(c => fs_1.default.existsSync(c)) ?? null;
|
|
56
|
-
}
|
|
57
|
-
function macWefPath(container, guid) {
|
|
58
|
-
return path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', container, 'Data', 'Documents', 'wef', `${guid}.xml`);
|
|
59
|
-
}
|
|
60
|
-
function generatedManifestPath(entry, userDataDir) {
|
|
61
|
-
return path_1.default.join(userDataDir, 'office-manifests', entry.guid, 'manifest.xml');
|
|
62
|
-
}
|
|
63
|
-
function manifestXmlForPort(xml, httpPort) {
|
|
64
|
-
return xml.replace(/(https?:\/\/(?:localhost|127\.0\.0\.1)):43091/g, `$1:${httpPort}`);
|
|
65
|
-
}
|
|
66
|
-
function prepareManifestForSideload(entry, sourcePath, options) {
|
|
67
|
-
if (!options.httpPort)
|
|
68
|
-
return sourcePath;
|
|
69
|
-
const userDataDir = options.userDataDir || path_1.default.join(os_1.default.homedir(), '.fraim');
|
|
70
|
-
const dest = generatedManifestPath(entry, userDataDir);
|
|
71
|
-
const rendered = manifestXmlForPort(fs_1.default.readFileSync(sourcePath, 'utf8'), options.httpPort);
|
|
72
|
-
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
73
|
-
fs_1.default.writeFileSync(dest, rendered, { encoding: 'utf8' });
|
|
74
|
-
return dest;
|
|
75
|
-
}
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Windows registry helpers
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
function winRegisteredValue(guid) {
|
|
80
|
-
const r = (0, child_process_1.spawnSync)('reg', ['query', WEF_DEVELOPER_KEY, '/v', guid], { encoding: 'utf8' });
|
|
81
|
-
if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
|
|
82
|
-
return null;
|
|
83
|
-
// Output line looks like: " <guid> REG_SZ C:\path\to\manifest.xml"
|
|
84
|
-
const line = r.stdout.split(/\r?\n/).find(l => l.includes('REG_SZ'));
|
|
85
|
-
if (!line)
|
|
86
|
-
return null;
|
|
87
|
-
const idx = line.indexOf('REG_SZ');
|
|
88
|
-
return line.slice(idx + 'REG_SZ'.length).trim() || null;
|
|
89
|
-
}
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// Public API
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
/** Returns true when ALL manifests are sideloaded with the correct value. */
|
|
94
|
-
function isSideloaded() {
|
|
95
|
-
return MANIFESTS.every(m => isManifestSideloaded(m));
|
|
96
|
-
}
|
|
97
|
-
function isManifestSideloaded(m) {
|
|
98
|
-
if (process.platform === 'win32') {
|
|
99
|
-
const value = winRegisteredValue(m.guid);
|
|
100
|
-
// Must be a real file path that exists — a URL or a stale/missing path
|
|
101
|
-
// means we have NOT correctly sideloaded and should re-register.
|
|
102
|
-
return !!value && !/^https?:\/\//i.test(value) && fs_1.default.existsSync(value);
|
|
103
|
-
}
|
|
104
|
-
if (process.platform === 'darwin')
|
|
105
|
-
return fs_1.default.existsSync(macWefPath(m.macContainer, m.guid));
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Sideloads all manifests. Returns ok:true if every manifest was registered.
|
|
110
|
-
* Returns ok:false with the first failure reason.
|
|
111
|
-
*/
|
|
112
|
-
function sideloadManifest(projectPath, options = {}) {
|
|
113
|
-
for (const entry of MANIFESTS) {
|
|
114
|
-
const manifestPath = resolveManifestPath(entry, projectPath);
|
|
115
|
-
if (!manifestPath) {
|
|
116
|
-
return { ok: false, reason: `Manifest not found for GUID ${entry.guid} — check extensions/office-word/ and public/ai-hub/powerpoint-taskpane/` };
|
|
117
|
-
}
|
|
118
|
-
const sideloadPath = prepareManifestForSideload(entry, manifestPath, options);
|
|
119
|
-
if (process.platform === 'win32') {
|
|
120
|
-
// Developer-key value = absolute file path to manifest.xml (NOT a URL).
|
|
121
|
-
const r = (0, child_process_1.spawnSync)('reg', [
|
|
122
|
-
'add', WEF_DEVELOPER_KEY,
|
|
123
|
-
'/v', entry.guid, '/t', 'REG_SZ', '/d', sideloadPath, '/f',
|
|
124
|
-
], { encoding: 'utf8' });
|
|
125
|
-
if (r.status !== 0)
|
|
126
|
-
return { ok: false, reason: r.stderr || `reg add failed for ${entry.guid}` };
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (process.platform === 'darwin') {
|
|
130
|
-
const dest = macWefPath(entry.macContainer, entry.guid);
|
|
131
|
-
try {
|
|
132
|
-
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
133
|
-
fs_1.default.copyFileSync(sideloadPath, dest);
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
return { ok: false, reason: String(err) };
|
|
137
|
-
}
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
return { ok: false, reason: `Unsupported platform: ${process.platform}` };
|
|
141
|
-
}
|
|
142
|
-
return { ok: true };
|
|
143
|
-
}
|
|
144
|
-
/** Removes all sideloaded manifests. */
|
|
145
|
-
function removeSideload() {
|
|
146
|
-
for (const entry of MANIFESTS) {
|
|
147
|
-
if (process.platform === 'win32') {
|
|
148
|
-
(0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', entry.guid, '/f'], { encoding: 'utf8' });
|
|
149
|
-
}
|
|
150
|
-
else if (process.platform === 'darwin') {
|
|
151
|
-
const target = macWefPath(entry.macContainer, entry.guid);
|
|
152
|
-
if (fs_1.default.existsSync(target))
|
|
153
|
-
fs_1.default.unlinkSync(target);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|