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.
@@ -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 an ${role}`,
75
+ `${profile.humanTitle} — manager for ${article} ${role}`,
72
76
  '',
73
- `You will manage an ${role} (an autonomous AI agent) and the humans around it, owning the outcomes it ships.`,
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 AIda to unlock AI agent design, MCP enablement, eval authoring, and agent evaluation work for this request.'
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: 'AIda',
24
- role: 'AI Engineer',
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: 'AI Software Engineer',
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: 'AI QA Engineer',
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: 'AI Security Engineer',
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: 'AI Site Reliability Engineer',
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: 'AI Salesforce Developer',
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: 'AI Product Manager',
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: 'AI UX / Brand Designer',
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: 'AI GTM & Marketing Manager',
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: 'AI Sales Account Manager',
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: 'AI Customer Success + Support',
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: 'AI Finance Manager',
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: 'AI HR Manager',
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: 'AI Recruiter',
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: 'AI Legal Counsel',
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: 'AI Procurement Manager',
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: 'AI Banking KYC Employee',
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: 'AI Banking Auditor',
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: 'AI Inclusion Leader',
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: 'AI Career Coach',
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: 'AI Executive Assistant',
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: 'AI Manager',
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: 'AI Business Strategist',
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 AI Employee',
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: 'AIda-ai-engineer', bg: 'c7d2fe', style: 'notionists' },
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
- const outcome = await (0, managed_agent_install_1.installManagedAgent)({ label: option.label, installPackage: option.installPackage, launchCommand: option.launchCommand }, systemPath, { runProcess, commandVersion });
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)(systemPath, outcome.npmGlobalBinDirs);
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.288",
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) => { if (e.key === 'Enter') onSubmit(); });
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);