fraim 2.0.288 → 2.0.290
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/cli/utils/managed-node-runtime.js +269 -0
- package/dist/src/config/ai-manager-hiring.js +6 -2
- package/dist/src/config/persona-capability-bundles.js +1 -1
- package/dist/src/config/persona-hiring.js +26 -26
- package/dist/src/first-run/session-service.js +12 -2
- package/package.json +3 -1
- package/public/first-run/script.js +10 -1
|
@@ -0,0 +1,269 @@
|
|
|
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.MANAGED_NODE_VERSION = void 0;
|
|
7
|
+
exports.npmExecutableName = npmExecutableName;
|
|
8
|
+
exports.nodeExecutableName = nodeExecutableName;
|
|
9
|
+
exports.findExecutableOnPath = findExecutableOnPath;
|
|
10
|
+
exports.hasWorkingNodeAndNpm = hasWorkingNodeAndNpm;
|
|
11
|
+
exports.isManagedNodeVersionComplete = isManagedNodeVersionComplete;
|
|
12
|
+
exports.nodeVersionedDirName = nodeVersionedDirName;
|
|
13
|
+
exports.ensureManagedNpm = ensureManagedNpm;
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const https_1 = __importDefault(require("https"));
|
|
18
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
19
|
+
const managed_agent_paths_1 = require("./managed-agent-paths");
|
|
20
|
+
// Issue #1415 (extended): a genuinely GUI-only FRAIM Hub install — the native installer
|
|
21
|
+
// downloaded from the website, never a CLI — has no working `npm`/`npx` anywhere. First-run's
|
|
22
|
+
// `embeddedDesktop` branch (session-service.ts) deliberately skips installing one (it only needs
|
|
23
|
+
// Electron's own bundled Node for FRAIM's own CLI/MCP shim, via ELECTRON_RUN_AS_NODE), and the
|
|
24
|
+
// separate CLI installer's portable-Node bootstrap (scripts/installer/fraim-install-win.template.cmd)
|
|
25
|
+
// never runs for a desktop-only user. That left two features silently unable to do their real job
|
|
26
|
+
// on such a machine: `hub-app-materializer.ts`'s npm install (this issue), and agent-CLI
|
|
27
|
+
// installation (`managed-agent-install.ts`), which also shells out to a bare `npm`.
|
|
28
|
+
//
|
|
29
|
+
// This module is the one place that guarantees a real, usable `npm`/`node` exists, for both
|
|
30
|
+
// consumers: prefer whatever is already on system PATH (common for technical users), then an
|
|
31
|
+
// already-downloaded managed runtime (shared with the CLI installer's `~/.fraim/node/` convention
|
|
32
|
+
// via `managed-agent-paths.ts` — reused, not duplicated, so a user who has ever run the `fraim` CLI
|
|
33
|
+
// never pays for a second download), and only download a portable Node.js distribution from
|
|
34
|
+
// nodejs.org as a last resort.
|
|
35
|
+
//
|
|
36
|
+
// `~/.fraim/node/` is a *shared* directory — it already holds agent-CLI shims and their own
|
|
37
|
+
// `node_modules/` (managed-agent-install.ts) — not a directory this module exclusively owns like
|
|
38
|
+
// `electron-dist.ts` owns its versioned Electron dist. So the extract-once-reuse-forever,
|
|
39
|
+
// staging-then-atomic-rename transaction that pattern established is applied here to just the one
|
|
40
|
+
// versioned subfolder a Node.js archive naturally extracts as (`node-v<version>-<platform>-<arch>/`,
|
|
41
|
+
// exactly what `getPortableNodeBinPath()` already knows how to find), never to the shared root
|
|
42
|
+
// itself — so a concurrent or failed download can never disturb sibling content already there.
|
|
43
|
+
exports.MANAGED_NODE_VERSION = '20.11.1'; // matches NODE_VERSION in fraim-install-win.template.cmd
|
|
44
|
+
function npmExecutableName(platform = process.platform) {
|
|
45
|
+
return platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
46
|
+
}
|
|
47
|
+
function nodeExecutableName(platform = process.platform) {
|
|
48
|
+
return platform === 'win32' ? 'node.exe' : 'node';
|
|
49
|
+
}
|
|
50
|
+
/** First executable named `name` found by walking `pathValue`'s directories, or null. */
|
|
51
|
+
function findExecutableOnPath(name, pathValue = process.env.PATH) {
|
|
52
|
+
for (const dir of (pathValue ?? '').split(path_1.default.delimiter).filter(Boolean)) {
|
|
53
|
+
const candidate = path_1.default.join(dir, name);
|
|
54
|
+
try {
|
|
55
|
+
if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile())
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Unreadable entry — treat as absent, not fatal.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** Whether `dir` already has a real, runnable node+npm — any version; this function's job is "a working npm", not "this exact one". */
|
|
65
|
+
function hasWorkingNodeAndNpm(dir, platform = process.platform) {
|
|
66
|
+
return fs_1.default.existsSync(path_1.default.join(dir, npmExecutableName(platform))) && fs_1.default.existsSync(path_1.default.join(dir, nodeExecutableName(platform)));
|
|
67
|
+
}
|
|
68
|
+
/** Whether `dir` (a specific versioned bin dir) holds a complete extraction for exactly `version`. */
|
|
69
|
+
function isManagedNodeVersionComplete(dir, version, platform = process.platform) {
|
|
70
|
+
try {
|
|
71
|
+
const recorded = fs_1.default.readFileSync(path_1.default.join(dir, 'fraim-managed-node-version'), 'utf8').trim();
|
|
72
|
+
return recorded === version && hasWorkingNodeAndNpm(dir, platform);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function nodePlatformName(platform) {
|
|
79
|
+
if (platform === 'darwin')
|
|
80
|
+
return 'darwin';
|
|
81
|
+
if (platform === 'win32')
|
|
82
|
+
return 'win';
|
|
83
|
+
return 'linux';
|
|
84
|
+
}
|
|
85
|
+
function nodeArchiveExtension(platform) {
|
|
86
|
+
return platform === 'win32' ? 'zip' : 'tar.gz';
|
|
87
|
+
}
|
|
88
|
+
// `request.version` is always `MANAGED_NODE_VERSION` today, but it is a public option on
|
|
89
|
+
// `ensureManagedNpm`, and this string is joined into a filesystem path below (`nodeRoot/<this>`)
|
|
90
|
+
// that a failed extraction's cleanup recursively deletes. Anchoring on an exact-release shape
|
|
91
|
+
// before it ever reaches a path, same guard shape as `hub-app-materializer.ts`'s
|
|
92
|
+
// `assertPathSafeVersion`, so a hostile or malformed override can never escape `nodeRoot`.
|
|
93
|
+
const SAFE_NODE_VERSION = /^\d+\.\d+\.\d+$/;
|
|
94
|
+
function assertPathSafeNodeVersion(version) {
|
|
95
|
+
if (!SAFE_NODE_VERSION.test(version)) {
|
|
96
|
+
throw new Error(`Refusing to use "${version}" as a managed Node.js version: expected an exact release such as 20.11.1.`);
|
|
97
|
+
}
|
|
98
|
+
return version;
|
|
99
|
+
}
|
|
100
|
+
/** The exact folder name Node's own official archive extracts as — also this module's versioned subfolder name under the shared `~/.fraim/node/` root. */
|
|
101
|
+
function nodeVersionedDirName(request) {
|
|
102
|
+
return `node-v${assertPathSafeNodeVersion(request.version)}-${nodePlatformName(request.platform)}-${request.arch}`;
|
|
103
|
+
}
|
|
104
|
+
function nodeArchiveFileName(request) {
|
|
105
|
+
return `${nodeVersionedDirName(request)}.${nodeArchiveExtension(request.platform)}`;
|
|
106
|
+
}
|
|
107
|
+
function nodeDistBaseUrl(version) {
|
|
108
|
+
return `https://nodejs.org/dist/v${version}`;
|
|
109
|
+
}
|
|
110
|
+
function httpsGetBuffer(url) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
https_1.default.get(url, { headers: { 'user-agent': 'fraim-hub-managed-node-runtime' } }, (res) => {
|
|
113
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
114
|
+
httpsGetBuffer(res.headers.location).then(resolve, reject);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (res.statusCode !== 200) {
|
|
118
|
+
res.resume();
|
|
119
|
+
reject(new Error(`GET ${url} failed: HTTP ${res.statusCode}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const chunks = [];
|
|
123
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
124
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
125
|
+
res.on('error', reject);
|
|
126
|
+
}).on('error', reject);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function httpsDownloadToFile(url, destPath) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const file = fs_1.default.createWriteStream(destPath);
|
|
132
|
+
https_1.default.get(url, { headers: { 'user-agent': 'fraim-hub-managed-node-runtime' } }, (res) => {
|
|
133
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
134
|
+
file.close();
|
|
135
|
+
httpsDownloadToFile(res.headers.location, destPath).then(resolve, reject);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (res.statusCode !== 200) {
|
|
139
|
+
res.resume();
|
|
140
|
+
file.close();
|
|
141
|
+
reject(new Error(`GET ${url} failed: HTTP ${res.statusCode}`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
res.pipe(file);
|
|
145
|
+
file.on('finish', () => file.close(() => resolve()));
|
|
146
|
+
}).on('error', (err) => {
|
|
147
|
+
file.close();
|
|
148
|
+
reject(err);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Verify `filePath` against the sha256 line naming `fileName` in the release's own
|
|
154
|
+
* `SHASUMS256.txt` — the same integrity check `@electron/get` performs for Electron zips
|
|
155
|
+
* (`electron-dist.ts`), applied to Node's own equivalent publishing convention.
|
|
156
|
+
*/
|
|
157
|
+
async function verifyNodeArchiveChecksum(filePath, fileName, version) {
|
|
158
|
+
const shasums = (await httpsGetBuffer(`${nodeDistBaseUrl(version)}/SHASUMS256.txt`)).toString('utf8');
|
|
159
|
+
const line = shasums.split('\n').find((entry) => entry.trim().endsWith(fileName));
|
|
160
|
+
if (!line) {
|
|
161
|
+
throw new Error(`SHASUMS256.txt for Node v${version} has no entry for ${fileName}; refusing to trust an unverified download`);
|
|
162
|
+
}
|
|
163
|
+
const expected = line.trim().split(/\s+/)[0];
|
|
164
|
+
const actual = crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(filePath)).digest('hex');
|
|
165
|
+
if (actual !== expected) {
|
|
166
|
+
throw new Error(`checksum mismatch for ${fileName}: expected ${expected}, got ${actual}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const defaultDownloadNodeArchive = async (request) => {
|
|
170
|
+
const fileName = nodeArchiveFileName(request);
|
|
171
|
+
const url = `${nodeDistBaseUrl(request.version)}/${fileName}`;
|
|
172
|
+
const tempPath = path_1.default.join(os_1.default.tmpdir(), `fraim-managed-node-${process.pid}-${Date.now()}-${fileName}`);
|
|
173
|
+
await httpsDownloadToFile(url, tempPath);
|
|
174
|
+
try {
|
|
175
|
+
await verifyNodeArchiveChecksum(tempPath, fileName, request.version);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
fs_1.default.rmSync(tempPath, { force: true });
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
return tempPath;
|
|
182
|
+
};
|
|
183
|
+
const defaultExtractNodeArchive = async (archivePath, destDir, request) => {
|
|
184
|
+
if (request.platform === 'win32') {
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
186
|
+
const extract = require('extract-zip');
|
|
187
|
+
await extract(archivePath, { dir: destDir });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
191
|
+
const tar = require('tar');
|
|
192
|
+
await tar.x({ file: archivePath, cwd: destDir });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Download and extract Node v`request.version` into `nodeRoot/<node-vX.Y.Z-platform-arch>/`,
|
|
197
|
+
* touching nothing else in `nodeRoot`. Staging-then-atomic-rename, scoped to just that one
|
|
198
|
+
* versioned subfolder (mirrors `electron-dist.ts`'s transaction, applied to a shared parent
|
|
199
|
+
* directory instead of one this module exclusively owns): a concurrent download that already
|
|
200
|
+
* finished is kept rather than clobbered, and a failed extraction never leaves a versioned
|
|
201
|
+
* subfolder behind that a later launch could mistake for complete.
|
|
202
|
+
*/
|
|
203
|
+
async function downloadAndPromoteVersionedNodeDir(args) {
|
|
204
|
+
const { nodeRoot, request, downloadArchive, extractArchive } = args;
|
|
205
|
+
const destDir = path_1.default.join(nodeRoot, nodeVersionedDirName(request));
|
|
206
|
+
const stagingDir = `${destDir}.staging-${process.pid}`;
|
|
207
|
+
fs_1.default.mkdirSync(nodeRoot, { recursive: true });
|
|
208
|
+
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
209
|
+
const archivePath = await downloadArchive(request);
|
|
210
|
+
try {
|
|
211
|
+
await extractArchive(archivePath, stagingDir, request);
|
|
212
|
+
// The archive's own top-level folder (matching nodeVersionedDirName) may land either as the
|
|
213
|
+
// extraction root itself (tar) or as one nested level inside it (some zip extractors preserve
|
|
214
|
+
// the archive's internal folder) — handle both without assuming either.
|
|
215
|
+
const nestedCandidate = path_1.default.join(stagingDir, nodeVersionedDirName(request));
|
|
216
|
+
const extractedRoot = fs_1.default.existsSync(nestedCandidate) ? nestedCandidate : stagingDir;
|
|
217
|
+
fs_1.default.writeFileSync(path_1.default.join(extractedRoot, 'fraim-managed-node-version'), request.version, 'utf8');
|
|
218
|
+
if (!isManagedNodeVersionComplete(extractedRoot, request.version, request.platform)) {
|
|
219
|
+
throw new Error(`extracted Node v${request.version} is missing ${npmExecutableName(request.platform)}/${nodeExecutableName(request.platform)}; refusing to promote an incomplete install`);
|
|
220
|
+
}
|
|
221
|
+
// A concurrent launch may have finished downloading the same version already; the winner
|
|
222
|
+
// keeps its copy rather than being clobbered by this one.
|
|
223
|
+
if (!isManagedNodeVersionComplete(destDir, request.version, request.platform)) {
|
|
224
|
+
fs_1.default.rmSync(destDir, { recursive: true, force: true });
|
|
225
|
+
try {
|
|
226
|
+
fs_1.default.renameSync(extractedRoot, destDir);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!isManagedNodeVersionComplete(destDir, request.version, request.platform))
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
fs_1.default.rmSync(archivePath, { force: true });
|
|
236
|
+
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
return destDir;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Resolve a working `npm` executable, preferring (in order): system PATH, an already-downloaded
|
|
242
|
+
* managed runtime (shared with the CLI installer's convention, any version — this function's job
|
|
243
|
+
* is "a working npm", not "this exact one"), or a freshly downloaded one. Only throws for a
|
|
244
|
+
* genuine download/extraction failure; callers should treat that as "materialization unavailable
|
|
245
|
+
* this launch," not a hard failure — never for "nothing found," which falls through to download.
|
|
246
|
+
*/
|
|
247
|
+
async function ensureManagedNpm(options = {}) {
|
|
248
|
+
const version = options.version ?? exports.MANAGED_NODE_VERSION;
|
|
249
|
+
const platform = options.platform ?? process.platform;
|
|
250
|
+
const arch = options.arch ?? process.arch;
|
|
251
|
+
const systemNpm = findExecutableOnPath(npmExecutableName(platform), options.systemPath ?? process.env.PATH);
|
|
252
|
+
if (systemNpm) {
|
|
253
|
+
return { npmPath: systemNpm, binDir: path_1.default.dirname(systemNpm), source: 'system' };
|
|
254
|
+
}
|
|
255
|
+
const existingBinDir = (0, managed_agent_paths_1.getPortableNodeBinPath)();
|
|
256
|
+
if (hasWorkingNodeAndNpm(existingBinDir, platform)) {
|
|
257
|
+
return { npmPath: path_1.default.join(existingBinDir, npmExecutableName(platform)), binDir: existingBinDir, source: 'managed-existing' };
|
|
258
|
+
}
|
|
259
|
+
const request = { version, platform, arch };
|
|
260
|
+
options.onProgress?.('Preparing FRAIM\'s bundled Node.js runtime (one-time, ~30MB)...');
|
|
261
|
+
const binDir = await downloadAndPromoteVersionedNodeDir({
|
|
262
|
+
nodeRoot: (0, managed_agent_paths_1.getManagedNodeRoot)(),
|
|
263
|
+
request,
|
|
264
|
+
downloadArchive: options.downloadArchive ?? defaultDownloadNodeArchive,
|
|
265
|
+
extractArchive: options.extractArchive ?? defaultExtractNodeArchive,
|
|
266
|
+
});
|
|
267
|
+
options.onProgress?.('FRAIM Node.js runtime ready.');
|
|
268
|
+
return { npmPath: path_1.default.join(binDir, npmExecutableName(platform)), binDir, source: 'managed-downloaded' };
|
|
269
|
+
}
|
|
@@ -64,13 +64,17 @@ function buildSearchQuery(roleKey) {
|
|
|
64
64
|
const profile = getHumanManagerProfile(roleKey);
|
|
65
65
|
return `${profile.keywords.join(' OR ')} AND ${AI_MANAGER_QUERY_TERMS}`;
|
|
66
66
|
}
|
|
67
|
+
function indefiniteArticle(phrase) {
|
|
68
|
+
return /^[aeiou]/i.test(phrase.trim()) ? 'an' : 'a';
|
|
69
|
+
}
|
|
67
70
|
function buildJobDescription(roleKey) {
|
|
68
71
|
const profile = getHumanManagerProfile(roleKey);
|
|
69
72
|
const role = persona_hiring_1.PERSONA_HIRE_CATALOG[roleKey]?.role ?? 'AI Employee';
|
|
73
|
+
const article = indefiniteArticle(role);
|
|
70
74
|
return [
|
|
71
|
-
`${profile.humanTitle} — manager for
|
|
75
|
+
`${profile.humanTitle} — manager for ${article} ${role}`,
|
|
72
76
|
'',
|
|
73
|
-
`You will manage
|
|
77
|
+
`You will manage ${article} ${role} (an autonomous AI agent) and the humans around it, owning the outcomes it ships.`,
|
|
74
78
|
'',
|
|
75
79
|
"What you'll do:",
|
|
76
80
|
`- Write crisp specifications and acceptance criteria the ${role} can execute against.`,
|
|
@@ -278,7 +278,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
278
278
|
],
|
|
279
279
|
protectedAliases: ['ai-engineering', 'agent-engineering', 'ai-agents'],
|
|
280
280
|
defaultHireMode: 'job',
|
|
281
|
-
lockCopy: 'Hire
|
|
281
|
+
lockCopy: 'Hire AIDa to unlock AI agent design, MCP enablement, eval authoring, and agent evaluation work for this request.'
|
|
282
282
|
}
|
|
283
283
|
};
|
|
284
284
|
const PROTECTED_JOB_TO_PERSONA = new Map();
|
|
@@ -20,8 +20,8 @@ exports.getPersonaHireAmountCents = getPersonaHireAmountCents;
|
|
|
20
20
|
*/
|
|
21
21
|
exports.PERSONA_HIRE_CATALOG = {
|
|
22
22
|
aida: {
|
|
23
|
-
displayName: '
|
|
24
|
-
role: 'AI
|
|
23
|
+
displayName: 'AIDa',
|
|
24
|
+
role: 'AI Developer',
|
|
25
25
|
emoji: '\u{1F9E0}',
|
|
26
26
|
gradient: 'linear-gradient(135deg, #4f46e5 0%, #06b6d4 50%, #10b981 100%)',
|
|
27
27
|
blurb: 'Designs production AI agents, connects them to tools and data, and proves their behavior with evals before deployment.',
|
|
@@ -30,7 +30,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
30
30
|
},
|
|
31
31
|
swen: {
|
|
32
32
|
displayName: 'SWEn',
|
|
33
|
-
role: '
|
|
33
|
+
role: 'Software Engineer',
|
|
34
34
|
emoji: '💻',
|
|
35
35
|
gradient: 'linear-gradient(135deg, #2563eb 0%, #06b6d4 100%)',
|
|
36
36
|
blurb: 'Implements features, refactors code, and drives PR iteration to merge, reviewing design and code as a senior would.',
|
|
@@ -39,7 +39,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
39
39
|
},
|
|
40
40
|
qasm: {
|
|
41
41
|
displayName: 'QAsm',
|
|
42
|
-
role: '
|
|
42
|
+
role: 'QA Engineer',
|
|
43
43
|
emoji: '🛡️',
|
|
44
44
|
gradient: 'linear-gradient(135deg, #10b981 0%, #14b8a6 100%)',
|
|
45
45
|
blurb: 'Runs tests, assesses code and test quality, polishes UI, and drives bug bashes with evidence.',
|
|
@@ -48,7 +48,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
48
48
|
},
|
|
49
49
|
sekhar: {
|
|
50
50
|
displayName: 'SEChar',
|
|
51
|
-
role: '
|
|
51
|
+
role: 'Security Engineer',
|
|
52
52
|
emoji: '🔒',
|
|
53
53
|
gradient: 'linear-gradient(135deg, #ef4444 0%, #f43f5e 100%)',
|
|
54
54
|
blurb: 'Sets up AI-native security baselines, runs the findings command center, reviews changes for risk, and drives remediation to closure.',
|
|
@@ -57,7 +57,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
57
57
|
},
|
|
58
58
|
sreya: {
|
|
59
59
|
displayName: 'SREya',
|
|
60
|
-
role: '
|
|
60
|
+
role: 'Site Reliability Engineer',
|
|
61
61
|
emoji: '☁️',
|
|
62
62
|
gradient: 'linear-gradient(135deg, #2563eb 0%, #10b981 100%)',
|
|
63
63
|
blurb: 'Manages deployments, monitors uptime, optimizes cloud cost, and keeps infrastructure resilient and observable.',
|
|
@@ -66,7 +66,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
66
66
|
},
|
|
67
67
|
sade: {
|
|
68
68
|
displayName: 'SADE',
|
|
69
|
-
role: '
|
|
69
|
+
role: 'Salesforce Developer',
|
|
70
70
|
emoji: '☁️',
|
|
71
71
|
gradient: 'linear-gradient(135deg, #0284c7 0%, #0369a1 100%)',
|
|
72
72
|
blurb: 'Deploys Salesforce configuration from ServiceNow tickets, builds reports and dashboards, creates Flows from business requirements, audits org health, and manages users and data at scale.',
|
|
@@ -75,7 +75,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
75
75
|
},
|
|
76
76
|
pam: {
|
|
77
77
|
displayName: 'PaM',
|
|
78
|
-
role: '
|
|
78
|
+
role: 'Product Manager',
|
|
79
79
|
emoji: '📋',
|
|
80
80
|
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #d946ef 100%)',
|
|
81
81
|
blurb: 'Owns specs, PRDs, technical design, issue prep, and the path from idea to shippable artifact.',
|
|
@@ -84,7 +84,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
84
84
|
},
|
|
85
85
|
huxley: {
|
|
86
86
|
displayName: 'hUXley',
|
|
87
|
-
role: '
|
|
87
|
+
role: 'UX / Brand Designer',
|
|
88
88
|
emoji: '🎨',
|
|
89
89
|
gradient: 'linear-gradient(135deg, #ec4899 0%, #f472b6 100%)',
|
|
90
90
|
blurb: 'Builds design systems, prototypes polished user-facing surfaces, and carries brand decisions into shipped product experiences.',
|
|
@@ -93,7 +93,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
93
93
|
},
|
|
94
94
|
gautam: {
|
|
95
95
|
displayName: 'GauTaM',
|
|
96
|
-
role: '
|
|
96
|
+
role: 'GTM & Marketing Manager',
|
|
97
97
|
emoji: '📣',
|
|
98
98
|
gradient: 'linear-gradient(135deg, #f97316 0%, #f59e0b 100%)',
|
|
99
99
|
blurb: 'Defines marketing strategy, ships content, runs launches, and owns the brand voice in market.',
|
|
@@ -102,7 +102,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
102
102
|
},
|
|
103
103
|
sam: {
|
|
104
104
|
displayName: 'SAM',
|
|
105
|
-
role: '
|
|
105
|
+
role: 'Sales Account Manager',
|
|
106
106
|
emoji: '📈',
|
|
107
107
|
gradient: 'linear-gradient(135deg, #059669 0%, #0d9488 100%)',
|
|
108
108
|
blurb: 'Surfaces stalled deals, computes pipeline health, and drafts account-specific re-engagement proposals for your top at-risk opportunities.',
|
|
@@ -111,7 +111,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
111
111
|
},
|
|
112
112
|
casey: {
|
|
113
113
|
displayName: 'CaSey',
|
|
114
|
-
role: '
|
|
114
|
+
role: 'Customer Success + Support',
|
|
115
115
|
emoji: '💬',
|
|
116
116
|
gradient: 'linear-gradient(135deg, #db2777 0%, #9333ea 100%)',
|
|
117
117
|
blurb: 'Scores account churn risk and expansion potential, generates prioritized action plans for CSMs, triages support cases, and routes L1 cases through automated resolution.',
|
|
@@ -120,7 +120,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
120
120
|
},
|
|
121
121
|
mona: {
|
|
122
122
|
displayName: 'MONa',
|
|
123
|
-
role: '
|
|
123
|
+
role: 'Finance Manager',
|
|
124
124
|
emoji: '💰',
|
|
125
125
|
gradient: 'linear-gradient(135deg, #10b981 0%, #f59e0b 100%)',
|
|
126
126
|
blurb: 'Models revenue, tracks unit economics, builds financial forecasts, and owns the metrics that drive growth decisions.',
|
|
@@ -129,7 +129,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
129
129
|
},
|
|
130
130
|
hari: {
|
|
131
131
|
displayName: 'HaRi',
|
|
132
|
-
role: '
|
|
132
|
+
role: 'HR Manager',
|
|
133
133
|
emoji: '👥',
|
|
134
134
|
gradient: 'linear-gradient(135deg, #0d9488 0%, #059669 100%)',
|
|
135
135
|
blurb: 'Manages onboarding, performance reviews, benefits analysis, payroll coordination, and HR business-partner advisory.',
|
|
@@ -138,7 +138,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
138
138
|
},
|
|
139
139
|
ricardo: {
|
|
140
140
|
displayName: 'RECardo',
|
|
141
|
-
role: '
|
|
141
|
+
role: 'Recruiter',
|
|
142
142
|
emoji: '🤝',
|
|
143
143
|
gradient: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
|
|
144
144
|
blurb: 'Sources candidates, writes job descriptions, screens pipelines, and manages the hiring loop end-to-end.',
|
|
@@ -147,7 +147,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
147
147
|
},
|
|
148
148
|
cela: {
|
|
149
149
|
displayName: 'CELiA',
|
|
150
|
-
role: '
|
|
150
|
+
role: 'Legal Counsel',
|
|
151
151
|
emoji: '⚖️',
|
|
152
152
|
gradient: 'linear-gradient(135deg, #475569 0%, #6366f1 100%)',
|
|
153
153
|
blurb: 'Drafts and reviews contracts, NDAs, patents, trademarks, and the SaaS legal stack.',
|
|
@@ -156,7 +156,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
156
156
|
},
|
|
157
157
|
procella: {
|
|
158
158
|
displayName: 'PROCella',
|
|
159
|
-
role: '
|
|
159
|
+
role: 'Procurement Manager',
|
|
160
160
|
emoji: '📦',
|
|
161
161
|
gradient: 'linear-gradient(135deg, #0f766e 0%, #7c3aed 100%)',
|
|
162
162
|
blurb: 'Frames procurement strategy, sources suppliers, runs RFx packages, evaluates responses, and keeps purchases acceptance-gated.',
|
|
@@ -165,7 +165,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
165
165
|
},
|
|
166
166
|
banke: {
|
|
167
167
|
displayName: 'BANKe',
|
|
168
|
-
role: '
|
|
168
|
+
role: 'Banking KYC Employee',
|
|
169
169
|
emoji: '\u{1F3E6}',
|
|
170
170
|
gradient: 'linear-gradient(135deg, #0f766e 0%, #2563eb 100%)',
|
|
171
171
|
blurb: 'Runs banking KYC cases with consent-aware evidence capture, decision receipts, and stable audit handoff artifacts.',
|
|
@@ -174,7 +174,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
174
174
|
},
|
|
175
175
|
auditya: {
|
|
176
176
|
displayName: 'AUDITya',
|
|
177
|
-
role: '
|
|
177
|
+
role: 'Banking Auditor',
|
|
178
178
|
emoji: '\u{1F50E}',
|
|
179
179
|
gradient: 'linear-gradient(135deg, #7c3aed 0%, #0f172a 100%)',
|
|
180
180
|
blurb: 'Audits AI banking work by tracing decisions back to evidence, receipts, exceptions, and reusable audit reports.',
|
|
@@ -183,7 +183,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
183
183
|
},
|
|
184
184
|
deidre: {
|
|
185
185
|
displayName: 'DEIdre',
|
|
186
|
-
role: '
|
|
186
|
+
role: 'Inclusion Leader',
|
|
187
187
|
emoji: '🌍',
|
|
188
188
|
gradient: 'linear-gradient(135deg, #9333ea 0%, #d946ef 100%)',
|
|
189
189
|
blurb: 'Audits equity gaps, designs bias-aware AI governance, builds ERG toolkits, and creates inclusion-fluency programs.',
|
|
@@ -192,7 +192,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
192
192
|
},
|
|
193
193
|
careena: {
|
|
194
194
|
displayName: 'CAREEna',
|
|
195
|
-
role: '
|
|
195
|
+
role: 'Career Coach',
|
|
196
196
|
emoji: '🎓',
|
|
197
197
|
gradient: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)',
|
|
198
198
|
blurb: 'Runs the candidate-side search loop: role sourcing, application execution, networking, interview prep, and close-stage offer strategy.',
|
|
@@ -201,7 +201,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
201
201
|
},
|
|
202
202
|
ashley: {
|
|
203
203
|
displayName: 'AshLey',
|
|
204
|
-
role: '
|
|
204
|
+
role: 'Executive Assistant',
|
|
205
205
|
emoji: '📅',
|
|
206
206
|
gradient: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)',
|
|
207
207
|
blurb: 'Owns executive coordination, weekly operating reviews, and portfolio reporting across the workforce.',
|
|
@@ -210,7 +210,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
210
210
|
},
|
|
211
211
|
mandy: {
|
|
212
212
|
displayName: 'MANdy',
|
|
213
|
-
role: '
|
|
213
|
+
role: 'Manager',
|
|
214
214
|
emoji: '🎯',
|
|
215
215
|
gradient: 'linear-gradient(135deg, #7c3aed 0%, #4338ca 100%)',
|
|
216
216
|
blurb: 'Plans the job sequence, runs sub-agents in parallel, coaches them through verification loops, and hands back a synthesized DRAFT for your approval.',
|
|
@@ -219,7 +219,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
219
219
|
},
|
|
220
220
|
beza: {
|
|
221
221
|
displayName: 'BeZa',
|
|
222
|
-
role: '
|
|
222
|
+
role: 'Business Strategist',
|
|
223
223
|
emoji: '🧭',
|
|
224
224
|
gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
225
225
|
blurb: 'Turns ideas into structured business plans, validates founder-market fit, and pressure-tests strategy.',
|
|
@@ -228,7 +228,7 @@ exports.PERSONA_HIRE_CATALOG = {
|
|
|
228
228
|
},
|
|
229
229
|
maestro: {
|
|
230
230
|
displayName: 'MAESTRO',
|
|
231
|
-
role: 'Full-Brained
|
|
231
|
+
role: 'Full-Brained Employee',
|
|
232
232
|
emoji: '★',
|
|
233
233
|
gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%)',
|
|
234
234
|
blurb: 'One AI employee who can take a job from any function and ship it back with evidence. You set the direction. You sign off on what ships. Maestro does the work.',
|
|
@@ -260,7 +260,7 @@ exports.PERSONA_AVATAR_CATALOG = {
|
|
|
260
260
|
procella: { seed: 'PROCELLA-procurement', bg: 'ccfbf1', style: 'notionists' },
|
|
261
261
|
banke: { seed: 'BANKe-banking-kyc', bg: 'ccfbf1', style: 'notionists' },
|
|
262
262
|
auditya: { seed: 'AUDITya-banking-audit', bg: 'e9d5ff', style: 'notionists' },
|
|
263
|
-
aida: { seed: '
|
|
263
|
+
aida: { seed: 'AIDa-ai-developer', bg: 'c7d2fe', style: 'notionists' },
|
|
264
264
|
};
|
|
265
265
|
function buildPersonaAvatarUrl(personaKey) {
|
|
266
266
|
const avatar = exports.PERSONA_AVATAR_CATALOG[personaKey];
|
|
@@ -52,6 +52,7 @@ const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launche
|
|
|
52
52
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
53
53
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
54
54
|
const managed_agent_install_1 = require("../cli/utils/managed-agent-install");
|
|
55
|
+
const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
|
|
55
56
|
const types_1 = require("./types");
|
|
56
57
|
Object.defineProperty(exports, "FIRST_RUN_ROW_IDS", { enumerable: true, get: function () { return types_1.FIRST_RUN_ROW_IDS; } });
|
|
57
58
|
const install_state_1 = require("./install-state");
|
|
@@ -856,9 +857,18 @@ class FirstRunSessionService {
|
|
|
856
857
|
loginHint: `Sign in to ${option.label} to activate it. A terminal window will open with the sign-in command — complete sign-in there, then return here and click "Check if Ready".`,
|
|
857
858
|
};
|
|
858
859
|
}
|
|
859
|
-
|
|
860
|
+
// Issue #1415 (extended): a genuinely GUI-only install (native installer only, no CLI ever
|
|
861
|
+
// run) has no working npm on `systemPath` at all — `installManagedAgent`'s "standard" attempt
|
|
862
|
+
// needs one to run `npm install -g <agent>` in the first place. Ensure one exists (system,
|
|
863
|
+
// already-managed, or freshly downloaded) and fold its directory into the PATH passed down,
|
|
864
|
+
// so the standard attempt can actually succeed instead of silently falling through.
|
|
865
|
+
const npm = await (0, managed_node_runtime_1.ensureManagedNpm)({
|
|
866
|
+
onProgress: (message) => appendInstallLog(`managed-node: ${message}`),
|
|
867
|
+
});
|
|
868
|
+
const pathWithNpm = (0, managed_agent_paths_1.appendBinDirsToPath)(systemPath, [npm.binDir]);
|
|
869
|
+
const outcome = await (0, managed_agent_install_1.installManagedAgent)({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, pathWithNpm, { runProcess, commandVersion });
|
|
860
870
|
if (outcome.outcome === 'standard' && outcome.npmGlobalBinDirs.length > 0) {
|
|
861
|
-
process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(
|
|
871
|
+
process.env.PATH = (0, managed_agent_paths_1.appendBinDirsToPath)(pathWithNpm, outcome.npmGlobalBinDirs);
|
|
862
872
|
}
|
|
863
873
|
this.setAgentInstallStatus(agentId, 'needs-sign-in', `Sign in to ${option.label} to activate it.`);
|
|
864
874
|
appendInstallLog(outcome.outcome === 'standard' ? `agent-installed-standard ${agentId}` : `agent-installed-managed ${agentId}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.290",
|
|
4
4
|
"description": "FRAIM core CLI and MCP package.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"cors": "^2.8.5",
|
|
47
47
|
"dotenv": "^16.4.7",
|
|
48
48
|
"express": "^5.2.1",
|
|
49
|
+
"extract-zip": "^2.0.1",
|
|
49
50
|
"mongodb": "^7.0.0",
|
|
50
51
|
"node-cron": "4.2.1",
|
|
51
52
|
"node-edge-tts": "^1.2.10",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"selfsigned": "^5.5.0",
|
|
56
57
|
"semver": "^7.7.4",
|
|
57
58
|
"stripe": "^20.3.1",
|
|
59
|
+
"tar": "^7.4.3",
|
|
58
60
|
"toml": "^3.0.0",
|
|
59
61
|
"tree-kill": "^1.2.2",
|
|
60
62
|
"xml2js": "^0.6.2"
|
|
@@ -717,7 +717,16 @@
|
|
|
717
717
|
}
|
|
718
718
|
};
|
|
719
719
|
submit.addEventListener('click', onSubmit);
|
|
720
|
-
input.addEventListener('keydown', (e) => {
|
|
720
|
+
input.addEventListener('keydown', (e) => {
|
|
721
|
+
if (e.key === 'Enter') {
|
|
722
|
+
onSubmit();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (e.key === 'Tab' && !e.shiftKey) {
|
|
726
|
+
e.preventDefault();
|
|
727
|
+
submit.focus();
|
|
728
|
+
}
|
|
729
|
+
});
|
|
721
730
|
card.appendChild(submit);
|
|
722
731
|
|
|
723
732
|
CHECKLIST_EL.appendChild(card);
|