fraim-hub 2.0.326 → 2.0.328

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.
@@ -337,7 +337,7 @@ function normalizeConversation(projectPath, raw) {
337
337
  status: value.status,
338
338
  // Issue #904: carry pauseReason explicitly so it survives the ...value spread round-trip.
339
339
  ...(value.pauseReason !== undefined && { pauseReason: value.pauseReason }),
340
- createdAt: value.createdAt ?? new Date().toISOString(),
340
+ createdAt: value.createdAt ?? value.lastUpdatedAt ?? new Date().toISOString(),
341
341
  lastUpdatedAt: value.lastUpdatedAt ?? value.createdAt ?? new Date().toISOString(),
342
342
  };
343
343
  }
@@ -0,0 +1,79 @@
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.readInstallerSetupApiKey = readInstallerSetupApiKey;
7
+ exports.repairInstallerIdeMcpEntries = repairInstallerIdeMcpEntries;
8
+ exports.runDesktopInstallerSetup = runDesktopInstallerSetup;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
12
+ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
13
+ const ide_detector_1 = require("../cli/setup/ide-detector");
14
+ const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
15
+ const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
16
+ const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
17
+ const win32_cmd_spawn_1 = require("../cli/utils/win32-cmd-spawn");
18
+ function readInstallerSetupApiKey(configPath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'config.json')) {
19
+ if (!fs_1.default.existsSync(configPath))
20
+ return null;
21
+ try {
22
+ const parsed = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
23
+ return typeof parsed.apiKey === 'string' && parsed.apiKey.trim() ? parsed.apiKey.trim() : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ async function repairInstallerIdeMcpEntries(options = {}) {
30
+ const logger = options.logger ?? console;
31
+ const fraimKey = readInstallerSetupApiKey(options.configPath);
32
+ if (!fraimKey) {
33
+ logger.log('[fraim-installer-setup] no existing config.json apiKey; IDE MCP repair will run from first-run');
34
+ return { status: 'skipped', ideCount: 0, reason: 'missing-config-api-key' };
35
+ }
36
+ const detectedIDEs = (options.detectInstalledIDEs ?? ide_detector_1.detectInstalledIDEs)();
37
+ const ideNames = detectedIDEs.map((ide) => ide.name).filter(Boolean);
38
+ if (ideNames.length === 0) {
39
+ logger.log('[fraim-installer-setup] no supported IDEs detected for MCP repair');
40
+ return { status: 'skipped', ideCount: 0, reason: 'no-detected-ides' };
41
+ }
42
+ const previousNonInteractive = process.env.FRAIM_NON_INTERACTIVE;
43
+ process.env.FRAIM_NON_INTERACTIVE = '1';
44
+ try {
45
+ await (options.autoConfigureMCP ?? auto_mcp_setup_1.autoConfigureMCP)(fraimKey, ideNames);
46
+ logger.log(`[fraim-installer-setup] repaired MCP config for ${ideNames.length} detected IDE(s)`);
47
+ return { status: 'repaired', ideCount: ideNames.length };
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ logger.warn(`[fraim-installer-setup] IDE MCP repair skipped after error: ${message}`);
52
+ return { status: 'failed', ideCount: ideNames.length, reason: message };
53
+ }
54
+ finally {
55
+ if (previousNonInteractive === undefined)
56
+ delete process.env.FRAIM_NON_INTERACTIVE;
57
+ else
58
+ process.env.FRAIM_NON_INTERACTIVE = previousNonInteractive;
59
+ }
60
+ }
61
+ function warmInstallerCliCache() {
62
+ const cliShim = (0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)();
63
+ const options = { env: process.env, encoding: 'utf8', stdio: 'inherit' };
64
+ const result = process.platform === 'win32'
65
+ ? (0, win32_cmd_spawn_1.spawnViaCmdExe)(cliShim, ['--version'], options)
66
+ : (0, child_process_1.spawnSync)(cliShim, ['--version'], options);
67
+ if (result.status !== 0) {
68
+ throw new Error(`FRAIM CLI bootstrap exited with code ${result.status ?? 'unknown'}.`);
69
+ }
70
+ }
71
+ async function runDesktopInstallerSetup(logger = console) {
72
+ await (0, managed_node_runtime_1.ensureManagedNpm)({
73
+ onProgress: (message) => logger.log(`[fraim-installer-setup] ${message}`),
74
+ });
75
+ (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
76
+ warmInstallerCliCache();
77
+ await repairInstallerIdeMcpEntries({ logger });
78
+ logger.log('[fraim-installer-setup] stable CLI and MCP launchers are ready');
79
+ }
@@ -20,6 +20,10 @@ async function bootstrapFrom(entryPath) {
20
20
  await entry.bootstrap();
21
21
  }
22
22
  async function launch() {
23
+ if (process.argv.includes('--fraim-installer-setup')) {
24
+ await bootstrapFrom(BUNDLED_ENTRY_PATH);
25
+ return;
26
+ }
23
27
  // Issue #1415: deterministic escape hatch for integration testing — skips the real npm
24
28
  // resolution (and its dependence on whatever happens to be published at test time) so a test can
25
29
  // prove the Electron main-process boot chain itself (resolveHubAppEntry -> require -> bootstrap
@@ -20,7 +20,7 @@ const window_open_decision_1 = require("./window-open-decision");
20
20
  const bundled_asset_resolver_1 = require("./bundled-asset-resolver");
21
21
  const server_2 = require("../first-run/server");
22
22
  const session_service_1 = require("../first-run/session-service");
23
- const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
23
+ const desktop_installer_setup_1 = require("./desktop-installer-setup");
24
24
  const desktop_auto_updater_1 = require("./desktop-auto-updater");
25
25
  const hub_app_materializer_1 = require("./hub-app-materializer");
26
26
  const hub_instance_reconciliation_1 = require("./hub-instance-reconciliation");
@@ -89,13 +89,6 @@ function applyUserDataOverride() {
89
89
  fs_1.default.mkdirSync(userDataDir, { recursive: true });
90
90
  electron_1.app.setPath('userData', userDataDir);
91
91
  }
92
- function configurePackagedFraimRuntime() {
93
- if (!electron_1.app.isPackaged && process.env.FRAIM_DESKTOP_FIRST_RUN !== '1')
94
- return;
95
- process.env.FRAIM_PACKAGED_CLI_EXECUTABLE = process.execPath;
96
- process.env.FRAIM_PACKAGED_CLI_SCRIPT = (0, hub_app_materializer_1.resolvePackagedCliScriptPath)(__dirname);
97
- (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
98
- }
99
92
  function shouldShowEmbeddedFirstRun() {
100
93
  if (!electron_1.app.isPackaged && process.env.FRAIM_DESKTOP_FIRST_RUN !== '1')
101
94
  return false;
@@ -153,6 +146,16 @@ async function configureAutoUpdater() {
153
146
  });
154
147
  return result.action === 'installing';
155
148
  }
149
+ async function runPackagedInstallerRepair() {
150
+ if (!electron_1.app.isPackaged || process.env.FRAIM_INSTALLER_LIFECYCLE_TEST === '1')
151
+ return;
152
+ try {
153
+ await (0, desktop_installer_setup_1.runDesktopInstallerSetup)(console);
154
+ }
155
+ catch (error) {
156
+ console.warn('[fraim] packaged installer repair failed:', error);
157
+ }
158
+ }
156
159
  // Issue #1415: shared by every path that needs to know "is a newer FRAIM Hub published than the
157
160
  // one currently running" — the automatic second-instance/activate recheck below, and the
158
161
  // manually-triggered restart from the web UI's update badge (see restartToLatestNow). Compares
@@ -655,6 +658,12 @@ async function bootstrap() {
655
658
  preferredPort: options.preferredPort,
656
659
  hasProjectPath: Boolean(options.projectPath),
657
660
  });
661
+ if (process.argv.includes('--fraim-installer-setup')) {
662
+ await electron_1.app.whenReady();
663
+ await (0, desktop_installer_setup_1.runDesktopInstallerSetup)(console);
664
+ electron_1.app.exit(0);
665
+ return;
666
+ }
658
667
  electron_1.app.on('child-process-gone', (_event, details) => {
659
668
  (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('app.child_process_gone', details);
660
669
  });
@@ -679,7 +688,6 @@ async function bootstrap() {
679
688
  });
680
689
  await electron_1.app.whenReady();
681
690
  electron_1.app.setName(displayName(options.runtimeId));
682
- configurePackagedFraimRuntime();
683
691
  // Issue #1415 (follow-up): app.requestSingleInstanceLock() above only sees another instance of
684
692
  // *this same identity* — it cannot see a Hub already running via a different launch path (e.g.
685
693
  // `npx fraim-hub`'s raw electron.exe, confirmed live to end up under a different userData scope
@@ -710,6 +718,7 @@ async function bootstrap() {
710
718
  // First-launch housekeeping (idempotent, fast on subsequent runs)
711
719
  if (process.env.FRAIM_INSTALLER_LIFECYCLE_TEST !== '1') {
712
720
  ensureLoginItem();
721
+ await runPackagedInstallerRepair();
713
722
  const installingUpdate = await configureAutoUpdater();
714
723
  if (installingUpdate)
715
724
  return;
@@ -5,11 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.HUB_APP_ENTRY_RELATIVE = exports.HUB_APP_PACKAGE_NAME = void 0;
7
7
  exports.redactCredentialLikeSubstrings = redactCredentialLikeSubstrings;
8
- exports.resolvePackagedCliScriptPath = resolvePackagedCliScriptPath;
9
8
  exports.planHubAppMaterialization = planHubAppMaterialization;
10
9
  exports.resolveHubAppCacheDir = resolveHubAppCacheDir;
11
10
  exports.isHubAppCacheComplete = isHubAppCacheComplete;
12
11
  exports.listCachedHubAppVersions = listCachedHubAppVersions;
12
+ exports.runNpmInstall = runNpmInstall;
13
13
  exports.ensureMaterializedHubApp = ensureMaterializedHubApp;
14
14
  exports.resolveHubAppEntry = resolveHubAppEntry;
15
15
  const fs_1 = __importDefault(require("fs"));
@@ -55,19 +55,6 @@ function redactCredentialLikeSubstrings(text) {
55
55
  exports.HUB_APP_PACKAGE_NAME = 'fraim-hub';
56
56
  /** Relative path, from a materialized (or bundled) package root, to the Electron main entry. */
57
57
  exports.HUB_APP_ENTRY_RELATIVE = path_1.default.join('dist', 'src', 'ai-hub', 'desktop-main.js');
58
- /**
59
- * Where the packaged CLI/MCP shim (`ensureFraimMcpLatestLauncher()`) should find `fraim`'s CLI
60
- * script, derived from `desktop-main.ts`'s own directory (`<packageRoot>/dist/src/ai-hub/`) rather
61
- * than Electron's `app.getAppPath()`. `desktop-launcher.ts` may have loaded `desktop-main.js` from a
62
- * materialized `fraim-hub@<latest>` copy under `~/.fraim/bin/hub-app/<version>/` rather than from
63
- * the original app.asar, and `app.getAppPath()` always names the latter regardless of which copy is
64
- * actually running. `moduleDir` tracks whichever copy is really executing. Kept in this
65
- * Electron-free module (rather than inlined in `desktop-main.ts`) so it is unit-testable without an
66
- * Electron `app` object.
67
- */
68
- function resolvePackagedCliScriptPath(moduleDir) {
69
- return path_1.default.join(moduleDir, '..', '..', '..', 'node_modules', 'fraim', 'bin', 'fraim.js');
70
- }
71
58
  function pickNewestComplete(cachedVersions) {
72
59
  const versions = cachedVersions.filter((entry) => entry.complete).map((entry) => entry.version);
73
60
  if (versions.length === 0)
@@ -178,16 +165,19 @@ function listCachedHubAppVersions(fraimDir = (0, project_fraim_paths_1.getUserFr
178
165
  * every dependency to that top level; nothing here relies on nested-dependency resolution.
179
166
  *
180
167
  * The `npm` executable itself is resolved via `ensureManagedNpm()` (system PATH, an already-managed
181
- * runtime, or a freshly downloaded one) rather than assumed present on PATH — a genuinely GUI-only
182
- * install (native installer only, no CLI ever run) has neither system npm nor FRAIM's own managed
183
- * one, since first-run's embedded-desktop path only needs Electron's own bundled Node for its own
184
- * purposes and never downloads a standalone npm (see managed-node-runtime.ts's own header comment).
168
+ * runtime, or a freshly downloaded one) rather than assumed present on PATH. Installer setup
169
+ * prepares that shared runtime, while this fallback also supports older installs and recovery
170
+ * when installer setup did not finish.
185
171
  */
186
172
  function runNpmInstall(npmPath, args) {
173
+ // POSIX npm uses /usr/bin/env node. A GUI launch may have no system Node
174
+ // on PATH even after ensureManagedNpm has downloaded a complete runtime.
175
+ const npmPathValue = [path_1.default.dirname(npmPath), process.env.PATH].filter(Boolean).join(path_1.default.delimiter);
176
+ const env = { ...process.env, PATH: npmPathValue, ...(process.platform === 'win32' ? { Path: npmPathValue } : {}) };
187
177
  if (process.platform !== 'win32') {
188
- return (0, child_process_1.spawnSync)(npmPath, args, { encoding: 'utf8' });
178
+ return (0, child_process_1.spawnSync)(npmPath, args, { encoding: 'utf8', env });
189
179
  }
190
- return (0, win32_cmd_spawn_1.spawnViaCmdExe)(npmPath, args, { encoding: 'utf8' });
180
+ return (0, win32_cmd_spawn_1.spawnViaCmdExe)(npmPath, args, { encoding: 'utf8', env });
191
181
  }
192
182
  const defaultInstallPackage = async (version, destDir) => {
193
183
  fs_1.default.mkdirSync(destDir, { recursive: true });
@@ -3826,8 +3826,9 @@ class AiHubServer {
3826
3826
  try {
3827
3827
  const ref = this.rawEventLogStore.append((0, conversation_store_1.conversationScopeKey)(run.scope, run.projectPath), run.conversationId || run.id, run.id, { channel: 'system', text, hubKind: kind, hostId: run.baseHostId || run.hostId, createdAt });
3828
3828
  run.eventLogRefs = upsertEventLogRef(run.eventLogRefs, ref);
3829
- if (ref.eventCount === 1)
3829
+ if (ref.eventCount === 1 && this.runRegistry.get(run.id)) {
3830
3830
  this.persistRunConversationNow(run, run.managedByRunId ? undefined : (run.conversationId || run.id));
3831
+ }
3831
3832
  }
3832
3833
  catch (error) {
3833
3834
  run.events.push((0, hosts_1.createHubEvent)('system', 'Event archive write failed; some Micro-manage entries are unavailable.'));
@@ -11,6 +11,7 @@ exports.resolveSubscriptionNextBillingDateWithInvoiceFallback = resolveSubscript
11
11
  exports.buildCurrentPeriodEndUpdate = buildCurrentPeriodEndUpdate;
12
12
  exports.resolveInvoicePaidNextBillingDate = resolveInvoicePaidNextBillingDate;
13
13
  const stripe_1 = require("../../config/stripe");
14
+ const redact_secrets_1 = require("../../core/utils/redact-secrets");
14
15
  const feature_flags_1 = require("../../config/feature-flags");
15
16
  const email_service_1 = require("../../services/email-service");
16
17
  const dashboard_access_1 = require("../../services/dashboard-access");
@@ -273,7 +274,7 @@ async function handleSubscriptionCreatedOrUpdated(subscription, dbService) {
273
274
  expiresAt: null, // Subscriptions don't have fixed expiration
274
275
  cancelAt: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) : null
275
276
  });
276
- console.log(`✅ Upgraded trial key to paid subscription: ${apiKey.key}`);
277
+ console.log(`✅ Upgraded trial key to paid subscription: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
277
278
  }
278
279
  else {
279
280
  // Create new API key for subscription (shouldn't happen in normal flow)
@@ -295,7 +296,7 @@ async function handleSubscriptionCreatedOrUpdated(subscription, dbService) {
295
296
  apiCallCount: 0,
296
297
  personaSystemActive: (0, feature_flags_1.isPersonaEntitlementsEnabled)() || undefined
297
298
  });
298
- console.log(`✅ Created new API key for subscription: ${newKey}`);
299
+ console.log(`✅ Created new API key for subscription: ${(0, redact_secrets_1.redactApiKey)(newKey)}`);
299
300
  apiKey = { key: newKey, userId: email };
300
301
  }
301
302
  }
@@ -358,7 +359,7 @@ async function handleSubscriptionDeleted(subscription, dbService) {
358
359
  // Send cancellation email
359
360
  const emailService = new email_service_1.EmailService();
360
361
  await emailService.sendSubscriptionCancelled(apiKey.userId, new Date());
361
- console.log(`✅ API key expired due to subscription cancellation: ${apiKey.key}`);
362
+ console.log(`✅ API key expired due to subscription cancellation: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
362
363
  }
363
364
  async function handleInvoicePaid(invoice, dbService) {
364
365
  console.log(`✅ Invoice paid: ${invoice.id}`);
@@ -438,7 +439,7 @@ async function handleInvoicePaid(invoice, dbService) {
438
439
  suspendedAt: null,
439
440
  suspensionReason: null
440
441
  });
441
- console.log(`✅ API key restored after payment: ${apiKey.key}`);
442
+ console.log(`✅ API key restored after payment: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
442
443
  // Send restoration email
443
444
  const emailService = new email_service_1.EmailService();
444
445
  const restorationBillingDate = nextBillingDate || new Date();
@@ -467,7 +468,7 @@ async function handleInvoicePaid(invoice, dbService) {
467
468
  }
468
469
  // Regular renewal
469
470
  {
470
- console.log(`✅ Subscription renewed: ${apiKey.key}`);
471
+ console.log(`✅ Subscription renewed: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
471
472
  if (nextBillingDate) {
472
473
  await dbService.updateApiKey(apiKey.key, buildCurrentPeriodEndUpdate(nextBillingDate));
473
474
  }
@@ -532,7 +533,7 @@ async function handleInvoicePaymentFailed(invoice, dbService) {
532
533
  suspendedAt: new Date(),
533
534
  suspensionReason: 'payment_failed'
534
535
  });
535
- console.log(`✅ API key suspended due to payment failure: ${apiKey.key}`);
536
+ console.log(`✅ API key suspended due to payment failure: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
536
537
  // Send payment failure email with 7-day grace period
537
538
  const emailService = new email_service_1.EmailService();
538
539
  const gracePeriodEndsAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days