fraim-hub 2.0.283 → 2.0.285

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.
@@ -2156,7 +2156,8 @@ class FakeHostRuntime {
2156
2156
  }
2157
2157
  startRun(hostId, _projectPath, message, handlers, _sessionId) {
2158
2158
  this.lastStartMessage = message;
2159
- return this.fakeProcess(hostId, this.fakeEmployeeReply('start', message), handlers);
2159
+ const reply = this.startReply ?? this.fakeEmployeeReply('start', message);
2160
+ return this.fakeProcess(hostId, reply, handlers);
2160
2161
  }
2161
2162
  continueRun(hostId, _projectPath, sessionId, message, handlers) {
2162
2163
  this.lastContinueMessage = message;
@@ -9,9 +9,16 @@ exports.refreshLatestPublishedVersionInBackground = refreshLatestPublishedVersio
9
9
  exports.__resetLatestVersionCache = __resetLatestVersionCache;
10
10
  const https_1 = __importDefault(require("https"));
11
11
  // Issue #755: best-effort "latest published fraim version" for the Hub's
12
- // update-available nudge. Cached in-process (1h TTL) and never throws — returns
12
+ // update-available nudge. Cached in-process (5m TTL) and never throws — returns
13
13
  // null when offline or the registry is unreachable, so the nudge simply hides.
14
- const TTL_MS = 60 * 60 * 1000;
14
+ // Issue #1379: TTL_MS was 1 hour, so a real npm publish could take up to an hour to
15
+ // ever show up in a running Hub server's own cache, confirmed live (one process still
16
+ // reporting a stale "latest" long after a newer version had been on the registry for
17
+ // several minutes, while a freshly-started process reported the correct one). The
18
+ // client only calls GET /api/ai-hub/version on page load, bootstrap refresh, and
19
+ // persona-identity retry (not a fast poll loop), so 5 minutes cuts worst-case
20
+ // staleness by 12x with no meaningful increase in registry request volume.
21
+ const TTL_MS = 5 * 60 * 1000;
15
22
  const FAILED_RETRY_MS = 5 * 60 * 1000;
16
23
  const REGISTRY_URL = 'https://registry.npmjs.org/fraim/latest';
17
24
  let cache = { value: null, at: 0 };
@@ -18,6 +18,7 @@ exports.writeMacosAppBundle = writeMacosAppBundle;
18
18
  // `fraim-hub install` from the npm package.
19
19
  const fs_1 = __importDefault(require("fs"));
20
20
  const path_1 = __importDefault(require("path"));
21
+ const windows_shim_path_search_1 = require("../core/utils/windows-shim-path-search");
21
22
  /**
22
23
  * What the launcher runs.
23
24
  *
@@ -94,13 +95,16 @@ function renderWindowsLauncher(nodeDir) {
94
95
  'rem re-running `npx fraim-hub@latest install` recreates it.',
95
96
  'setlocal',
96
97
  `set "PATH=%PATH%;${batchEscape(nodeDir)}"`,
97
- 'where npx >nul 2>nul',
98
- 'if errorlevel 1 (',
98
+ // Issue #1375: `where npx` depends on `where.exe` (C:\Windows\System32), which is
99
+ // unresolvable when the launching process's PATH omits System32. The PATH-search below
100
+ // needs no external binary and resolves an absolute path we can invoke directly.
101
+ ...(0, windows_shim_path_search_1.windowsPathSearchLines)('NPX_CMD', 'npx'),
102
+ 'if not defined NPX_CMD (',
99
103
  ` echo ${MISSING_NODE_MESSAGE}`,
100
104
  ' pause',
101
105
  ' exit /b 1',
102
106
  ')',
103
- `call npx ${NPX_ARGS} %*`,
107
+ `call "%NPX_CMD%" ${NPX_ARGS} %*`,
104
108
  '',
105
109
  ].join('\r\n');
106
110
  }
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.findAvailablePortExcluding = exports.findAvailablePort = exports.AiHubServer = exports.HostConfigStore = exports.DeploymentStore = void 0;
39
+ exports.findAvailablePortExcluding = exports.findAvailablePort = exports.AiHubServer = exports.HubConnectorStatusStore = exports.HostConfigStore = exports.DeploymentStore = void 0;
40
40
  exports.configureFraimForHubAgent = configureFraimForHubAgent;
41
41
  exports.hubCommandVersion = hubCommandVersion;
42
42
  exports.buildOpenFileInvocation = buildOpenFileInvocation;
@@ -48,6 +48,7 @@ const crypto_1 = require("crypto");
48
48
  const child_process_1 = require("child_process");
49
49
  const https_1 = __importDefault(require("https"));
50
50
  const types_1 = require("../first-run/types");
51
+ const local_provider_registry_1 = require("../cli/providers/local-provider-registry");
51
52
  const learning_context_builder_1 = require("../local-mcp-server/learning-context-builder");
52
53
  const brand_store_1 = require("../core/brand-store");
53
54
  const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
@@ -604,6 +605,227 @@ class HostConfigStore {
604
605
  }
605
606
  }
606
607
  exports.HostConfigStore = HostConfigStore;
608
+ class HubConnectorStatusStore {
609
+ constructor(filePath) {
610
+ this.filePath = filePath ?? path_1.default.join(getUserHubDir(), 'hub-connectors.json');
611
+ }
612
+ load() {
613
+ try {
614
+ if (!fs_1.default.existsSync(this.filePath))
615
+ return [];
616
+ const parsed = JSON.parse(fs_1.default.readFileSync(this.filePath, 'utf8'));
617
+ if (!Array.isArray(parsed))
618
+ return [];
619
+ return parsed.map((entry) => normalizeStoredConnectorStatus(entry)).filter(Boolean);
620
+ }
621
+ catch {
622
+ return [];
623
+ }
624
+ }
625
+ saveStatus(status) {
626
+ const list = this.load();
627
+ const next = list.filter((entry) => entry.id !== status.id);
628
+ next.push(status);
629
+ fs_1.default.mkdirSync(path_1.default.dirname(this.filePath), { recursive: true });
630
+ fs_1.default.writeFileSync(this.filePath, JSON.stringify(next.sort((a, b) => a.id.localeCompare(b.id)), null, 2));
631
+ return status;
632
+ }
633
+ }
634
+ exports.HubConnectorStatusStore = HubConnectorStatusStore;
635
+ function normalizeConnectorId(raw) {
636
+ if (typeof raw !== 'string')
637
+ return null;
638
+ const id = raw.trim().toLowerCase();
639
+ return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(id) ? id : null;
640
+ }
641
+ function connectorReadiness(configured, authenticated) {
642
+ if (!configured)
643
+ return 'missing_config';
644
+ if (!authenticated)
645
+ return 'needs_auth';
646
+ return 'ready';
647
+ }
648
+ function normalizeStoredConnectorStatus(raw) {
649
+ if (!raw || typeof raw !== 'object')
650
+ return null;
651
+ const value = raw;
652
+ const id = normalizeConnectorId(value.id);
653
+ if (!id)
654
+ return null;
655
+ const configured = Boolean(value.configured);
656
+ const authenticated = Boolean(value.authenticated);
657
+ const label = typeof value.label === 'string' && value.label.trim() ? value.label.trim() : id;
658
+ const transport = value.transport === 'stdio' || value.transport === 'http' ? value.transport : undefined;
659
+ const authMode = value.authMode === 'none' ||
660
+ value.authMode === 'env' ||
661
+ value.authMode === 'oauth' ||
662
+ value.authMode === 'api-token' ||
663
+ value.authMode === 'host-managed' ||
664
+ value.authMode === 'unknown'
665
+ ? value.authMode
666
+ : undefined;
667
+ return {
668
+ id,
669
+ label,
670
+ configured,
671
+ authenticated,
672
+ readiness: connectorReadiness(configured, authenticated),
673
+ source: 'configured',
674
+ ...(transport ? { transport } : {}),
675
+ ...(authMode ? { authMode } : {}),
676
+ ...(typeof value.detail === 'string' && value.detail.trim() ? { detail: value.detail.trim() } : {}),
677
+ ...(typeof value.updatedAt === 'string' && value.updatedAt.trim() ? { updatedAt: value.updatedAt.trim() } : {}),
678
+ };
679
+ }
680
+ function hasSecretLikeField(raw) {
681
+ if (!raw || typeof raw !== 'object')
682
+ return false;
683
+ if (Array.isArray(raw))
684
+ return raw.some((entry) => hasSecretLikeField(entry));
685
+ return Object.entries(raw).some(([key, value]) => /token|secret|password|apikey|api_key|credential/i.test(key) || hasSecretLikeField(value));
686
+ }
687
+ function readFraimApiKeyConfigured() {
688
+ if (process.env.FRAIM_API_KEY && process.env.FRAIM_API_KEY.trim())
689
+ return true;
690
+ try {
691
+ const configPath = path_1.default.join(os_1.default.homedir(), '.fraim', 'config.json');
692
+ if (!fs_1.default.existsSync(configPath))
693
+ return false;
694
+ const parsed = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
695
+ return typeof parsed.apiKey === 'string' && parsed.apiKey.trim().length > 0;
696
+ }
697
+ catch {
698
+ return false;
699
+ }
700
+ }
701
+ function providerConnectorStatus(provider) {
702
+ if (!provider.mcpServer)
703
+ return null;
704
+ const mcpServer = provider.mcpServer;
705
+ const needsAuth = Boolean(mcpServer.authHeaderTemplate || mcpServer.envTemplate);
706
+ return {
707
+ id: provider.id,
708
+ label: provider.displayName,
709
+ configured: false,
710
+ authenticated: false,
711
+ readiness: 'missing_config',
712
+ source: 'derived',
713
+ transport: mcpServer.type,
714
+ authMode: needsAuth ? 'unknown' : 'host-managed',
715
+ detail: 'External provider readiness is supplied by an agent-guided setup on this Hub machine, then recorded with non-secret connector status.',
716
+ };
717
+ }
718
+ function genericConnectorStatus(id) {
719
+ const provider = (0, local_provider_registry_1.getAllLocalProviders)().find((entry) => entry.id === id);
720
+ const providerStatus = provider ? providerConnectorStatus(provider) : null;
721
+ if (providerStatus)
722
+ return providerStatus;
723
+ return {
724
+ id,
725
+ label: id,
726
+ configured: false,
727
+ authenticated: false,
728
+ readiness: 'missing_config',
729
+ source: 'derived',
730
+ authMode: 'unknown',
731
+ detail: 'No connector status has been configured for this machine.',
732
+ };
733
+ }
734
+ function buildStoredConnectorStatusFromDerived(status) {
735
+ return normalizeStoredConnectorStatus({
736
+ ...status,
737
+ source: 'configured',
738
+ updatedAt: new Date().toISOString(),
739
+ });
740
+ }
741
+ function connectorSetupResponseForStatus(status, setupStatus, message, action) {
742
+ return {
743
+ connectorId: status.id,
744
+ status: setupStatus,
745
+ connector: status,
746
+ ...(action ? { action } : {}),
747
+ checkUrl: `/api/ai-hub/connectors/${status.id}/setup/check`,
748
+ message,
749
+ };
750
+ }
751
+ function buildDerivedConnectorStatuses() {
752
+ const fraimReady = readFraimApiKeyConfigured();
753
+ const base = [
754
+ {
755
+ id: 'fraim',
756
+ label: 'FRAIM',
757
+ configured: fraimReady,
758
+ authenticated: fraimReady,
759
+ readiness: connectorReadiness(fraimReady, fraimReady),
760
+ source: 'derived',
761
+ transport: 'stdio',
762
+ authMode: 'api-token',
763
+ detail: fraimReady ? 'FRAIM credential is available on this machine.' : 'Run fraim setup on this machine.',
764
+ },
765
+ {
766
+ id: 'git',
767
+ label: 'Git',
768
+ configured: true,
769
+ authenticated: true,
770
+ readiness: 'ready',
771
+ source: 'derived',
772
+ transport: 'stdio',
773
+ authMode: 'none',
774
+ detail: 'Base local Git MCP server is available in the generated FRAIM MCP set.',
775
+ },
776
+ {
777
+ id: 'playwright',
778
+ label: 'Playwright',
779
+ configured: true,
780
+ authenticated: true,
781
+ readiness: 'ready',
782
+ source: 'derived',
783
+ transport: 'stdio',
784
+ authMode: 'none',
785
+ detail: 'Base local Playwright MCP server is available in the generated FRAIM MCP set.',
786
+ },
787
+ ];
788
+ const providers = (0, local_provider_registry_1.getAllLocalProviders)()
789
+ .map((provider) => providerConnectorStatus(provider))
790
+ .filter(Boolean);
791
+ return [...base, ...providers];
792
+ }
793
+ function mergeConnectorStatuses(stored) {
794
+ const merged = new Map();
795
+ for (const status of buildDerivedConnectorStatuses())
796
+ merged.set(status.id, status);
797
+ for (const status of stored) {
798
+ const derived = merged.get(status.id);
799
+ if (derived?.readiness === 'ready' && status.readiness !== 'ready')
800
+ continue;
801
+ merged.set(status.id, status);
802
+ }
803
+ return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
804
+ }
805
+ function missingRequiredConnectors(required, connectors) {
806
+ if (!Array.isArray(required) || required.length === 0)
807
+ return [];
808
+ const byId = new Map(connectors.map((connector) => [connector.id, connector]));
809
+ const missing = [];
810
+ for (const raw of required) {
811
+ const id = normalizeConnectorId(raw);
812
+ if (!id)
813
+ continue;
814
+ const connector = byId.get(id) || {
815
+ id,
816
+ label: id,
817
+ configured: false,
818
+ authenticated: false,
819
+ readiness: 'missing_config',
820
+ source: 'derived',
821
+ authMode: 'unknown',
822
+ detail: 'No connector status has been configured for this machine.',
823
+ };
824
+ if (connector.readiness !== 'ready')
825
+ missing.push(connector);
826
+ }
827
+ return missing;
828
+ }
607
829
  async function pingHost(host) {
608
830
  const start = Date.now();
609
831
  try {
@@ -1530,6 +1752,31 @@ function hubCommandVersion(command, extraBinDirs, basePath) {
1530
1752
  const raw = (result.stdout || result.stderr || '').trim();
1531
1753
  return raw || null;
1532
1754
  }
1755
+ function hubRunSync(command, args, env) {
1756
+ const resolvedCommand = (0, command_resolution_1.getSystemCommandPath)(command, process.env.PATH) || command;
1757
+ const executable = process.platform === 'win32' ? 'cmd.exe' : resolvedCommand;
1758
+ const realArgs = process.platform === 'win32'
1759
+ ? ['/d', '/s', '/c', [resolvedCommand, ...args].map(hosts_1.escapeWindowsArg).join(' ')]
1760
+ : args;
1761
+ const childEnv = { ...process.env };
1762
+ for (const [key, value] of Object.entries(env || {})) {
1763
+ if (value === undefined)
1764
+ delete childEnv[key];
1765
+ else
1766
+ childEnv[key] = value;
1767
+ }
1768
+ const result = (0, child_process_1.spawnSync)(executable, realArgs, {
1769
+ encoding: 'utf8',
1770
+ timeout: 10_000,
1771
+ env: childEnv,
1772
+ });
1773
+ return {
1774
+ status: result.status,
1775
+ stdout: result.stdout || '',
1776
+ stderr: result.stderr || '',
1777
+ ...(result.error ? { error: result.error } : {}),
1778
+ };
1779
+ }
1533
1780
  function hubRunProcess(command, args, env) {
1534
1781
  if (process.env.NODE_ENV === 'test' && command === 'npm' && process.env.FRAIM_TEST_HUB_NPM_ERROR) {
1535
1782
  return Promise.reject(new Error(process.env.FRAIM_TEST_HUB_NPM_ERROR));
@@ -2200,6 +2447,7 @@ class AiHubServer {
2200
2447
  this.deploymentStoreProvided = Boolean(options.deploymentStore);
2201
2448
  this.deploymentStore = options.deploymentStore ?? new DeploymentStore();
2202
2449
  this.hostConfigStore = options.hostConfigStore ?? new HostConfigStore();
2450
+ this.connectorStatusStore = options.connectorStatusStore ?? new HubConnectorStatusStore();
2203
2451
  this.restartRecoveryPolicy = new restart_recovery_policy_1.RestartRecoveryPolicy({ machineLevelJobIds: MACHINE_LEVEL_JOB_IDS });
2204
2452
  this.app.use(express_1.default.json({ limit: '10mb' }));
2205
2453
  // CORS + Chrome Private Network Access for browser extensions and Office add-in task panes
@@ -5446,6 +5694,16 @@ class AiHubServer {
5446
5694
  if (!instructions && !legacyMessage) {
5447
5695
  throw new Error('Coach your employee before starting the run.');
5448
5696
  }
5697
+ const missingConnectors = missingRequiredConnectors(req.body.requiredConnectors, mergeConnectorStatuses(this.connectorStatusStore.load()));
5698
+ if (missingConnectors.length > 0) {
5699
+ return res.status(409).json({
5700
+ error: 'Remote Hub connector preflight failed.',
5701
+ code: 'missing_remote_connectors',
5702
+ missingConnectors,
5703
+ capabilitiesUrl: '/api/ai-hub/capabilities',
5704
+ setupHint: 'Configure the connector on this Hub machine through the agent-guided setup workflow, then update non-secret connector status and retry.',
5705
+ });
5706
+ }
5449
5707
  const employees = this.hostRuntime.detectEmployees();
5450
5708
  const { hostId, agent: configuredAgent, launchContext } = this.resolveLaunchAgent(req.body.configuredAgentId, requestedHostId, employees);
5451
5709
  const prepared = instructions
@@ -6551,6 +6809,120 @@ class AiHubServer {
6551
6809
  });
6552
6810
  // ─── End Issue #945 ───────────────────────────────────────────────────────
6553
6811
  // GET /api/ai-hub/hosts — list registered remote hosts with health status.
6812
+ this.app.get('/api/ai-hub/capabilities', (_req, res) => {
6813
+ const employees = this.hostRuntime.detectEmployees();
6814
+ const response = {
6815
+ host: {
6816
+ id: 'local',
6817
+ label: os_1.default.hostname(),
6818
+ url: this.hubBase,
6819
+ },
6820
+ generatedAt: new Date().toISOString(),
6821
+ agents: this.configuredAgentsForCurrentMachine(employees).map((agent) => (0, configured_agents_1.projectConfiguredAgent)(agent, employees)),
6822
+ connectors: mergeConnectorStatuses(this.connectorStatusStore.load()),
6823
+ };
6824
+ return res.json(response);
6825
+ });
6826
+ this.app.put('/api/ai-hub/connectors/:id/status', (req, res) => {
6827
+ if (!this.requireTrustedHubOrigin(req, res))
6828
+ return;
6829
+ const id = normalizeConnectorId(req.params.id);
6830
+ if (!id)
6831
+ return res.status(400).json({ error: 'Invalid connector id.' });
6832
+ if (hasSecretLikeField(req.body)) {
6833
+ console.warn('[ai-hub] connector status rejected secret-shaped payload', { connectorId: id });
6834
+ return res.status(400).json({ error: 'Connector status must not include tokens, passwords, API keys, or credentials.' });
6835
+ }
6836
+ const configured = Boolean(req.body?.configured);
6837
+ const authenticated = Boolean(req.body?.authenticated);
6838
+ const candidate = normalizeStoredConnectorStatus({
6839
+ id,
6840
+ label: typeof req.body?.label === 'string' ? req.body.label : id,
6841
+ configured,
6842
+ authenticated,
6843
+ transport: req.body?.transport,
6844
+ authMode: req.body?.authMode,
6845
+ detail: req.body?.detail,
6846
+ updatedAt: new Date().toISOString(),
6847
+ });
6848
+ if (!candidate)
6849
+ return res.status(400).json({ error: 'Invalid connector status.' });
6850
+ const saved = this.connectorStatusStore.saveStatus(candidate);
6851
+ console.log('[ai-hub] connector status updated', {
6852
+ connectorId: saved.id,
6853
+ readiness: saved.readiness,
6854
+ configured: saved.configured,
6855
+ authenticated: saved.authenticated,
6856
+ });
6857
+ return res.json(saved);
6858
+ });
6859
+ this.app.post('/api/ai-hub/connectors/:id/setup/start', (req, res) => {
6860
+ if (!this.requireTrustedHubOrigin(req, res))
6861
+ return;
6862
+ const id = normalizeConnectorId(req.params.id);
6863
+ if (!id)
6864
+ return res.status(400).json({ error: 'Invalid connector id.' });
6865
+ if (hasSecretLikeField(req.body)) {
6866
+ console.warn('[ai-hub] connector setup rejected secret-shaped payload', { connectorId: id, action: 'start' });
6867
+ return res.status(400).json({ error: 'Connector setup must not include tokens, passwords, API keys, or credentials.' });
6868
+ }
6869
+ const current = mergeConnectorStatuses(this.connectorStatusStore.load()).find((status) => status.id === id) || genericConnectorStatus(id);
6870
+ if (current.readiness === 'ready') {
6871
+ const saved = this.connectorStatusStore.saveStatus(buildStoredConnectorStatusFromDerived(current));
6872
+ console.log('[ai-hub] connector setup status updated', {
6873
+ connectorId: saved.id,
6874
+ action: 'start',
6875
+ setupStatus: 'verified',
6876
+ readiness: saved.readiness,
6877
+ });
6878
+ return res.json(connectorSetupResponseForStatus(saved, 'verified', `${saved.label} is verified on this Hub machine.`));
6879
+ }
6880
+ const saved = this.connectorStatusStore.saveStatus(buildStoredConnectorStatusFromDerived(current));
6881
+ console.log('[ai-hub] connector setup status updated', {
6882
+ connectorId: saved.id,
6883
+ action: 'start',
6884
+ setupStatus: 'action_required',
6885
+ readiness: saved.readiness,
6886
+ });
6887
+ return res.json(connectorSetupResponseForStatus(saved, 'action_required', `${saved.label} must be configured on this Hub machine before the remote run can start.`, {
6888
+ type: 'manual',
6889
+ label: `Configure ${saved.label} on the remote Hub machine`,
6890
+ detail: 'Use the appropriate provider or MCP setup flow for the selected remote machine. Do not assume a CLI dependency from the Hub API; after verification, record non-secret readiness with PUT /api/ai-hub/connectors/{id}/status.',
6891
+ }));
6892
+ });
6893
+ this.app.post('/api/ai-hub/connectors/:id/setup/check', (req, res) => {
6894
+ if (!this.requireTrustedHubOrigin(req, res))
6895
+ return;
6896
+ const id = normalizeConnectorId(req.params.id);
6897
+ if (!id)
6898
+ return res.status(400).json({ error: 'Invalid connector id.' });
6899
+ if (hasSecretLikeField(req.body)) {
6900
+ console.warn('[ai-hub] connector setup rejected secret-shaped payload', { connectorId: id, action: 'check' });
6901
+ return res.status(400).json({ error: 'Connector setup check must not include tokens, passwords, API keys, or credentials.' });
6902
+ }
6903
+ const current = mergeConnectorStatuses(this.connectorStatusStore.load()).find((status) => status.id === id) || genericConnectorStatus(id);
6904
+ const saved = this.connectorStatusStore.saveStatus(buildStoredConnectorStatusFromDerived(current));
6905
+ if (saved.readiness === 'ready') {
6906
+ console.log('[ai-hub] connector setup status updated', {
6907
+ connectorId: saved.id,
6908
+ action: 'check',
6909
+ setupStatus: 'verified',
6910
+ readiness: saved.readiness,
6911
+ });
6912
+ return res.json(connectorSetupResponseForStatus(saved, 'verified', `${saved.label} is verified on this Hub machine.`));
6913
+ }
6914
+ console.log('[ai-hub] connector setup status updated', {
6915
+ connectorId: saved.id,
6916
+ action: 'check',
6917
+ setupStatus: 'action_required',
6918
+ readiness: saved.readiness,
6919
+ });
6920
+ return res.json(connectorSetupResponseForStatus(saved, 'action_required', `${saved.label} is not ready on this Hub machine.`, {
6921
+ type: 'manual',
6922
+ label: `Verify ${saved.label} on the remote Hub machine`,
6923
+ detail: 'The agent-guided setup must verify the provider on the remote machine and then PUT a non-secret ready status. The Hub does not infer provider-specific auth state.',
6924
+ }));
6925
+ });
6554
6926
  this.app.get('/api/ai-hub/hosts', async (_req, res) => {
6555
6927
  const hosts = this.hostConfigStore.load();
6556
6928
  const healthResults = await Promise.allSettled(hosts.map((h) => pingHost(h)));
@@ -15,11 +15,12 @@ const child_process_1 = require("child_process");
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const managed_agent_paths_1 = require("../../utils/managed-agent-paths");
17
17
  const command_resolution_1 = require("../../mcp/command-resolution");
18
- // Codex is FRAIM's first Hub-compatible CLI with a managed-install fallback
19
- // (npm install -g into FRAIM's portable Node when no system install is
20
- // found). Extend this list as claude/gemini/copilot gain the same fallback.
18
+ // CLIs with a managed-install fallback (npm install -g into FRAIM's portable
19
+ // Node when no system install is found). Add gemini/copilot here when their
20
+ // installManagedAgent wiring lands.
21
21
  const MANAGED_CLIS = [
22
22
  { id: 'codex', label: 'Codex', command: 'codex' },
23
+ { id: 'claude-code', label: 'Claude Code', command: 'claude' },
23
24
  ];
24
25
  // Windows cannot CreateProcess a `.cmd`/`.bat` file directly (spawnSync on a
25
26
  // resolved absolute `.cmd` path throws EINVAL) — it must go through cmd.exe,
@@ -14,6 +14,7 @@ exports.ensureFraimMcpLatestLauncher = ensureFraimMcpLatestLauncher;
14
14
  const fs_1 = __importDefault(require("fs"));
15
15
  const os_1 = __importDefault(require("os"));
16
16
  const path_1 = __importDefault(require("path"));
17
+ const windows_shim_path_search_1 = require("../../core/utils/windows-shim-path-search");
17
18
  const LAUNCHER_VERSION = 1;
18
19
  const PACKAGED_RUNTIME_VERSION = 1;
19
20
  const launcherSource = `#!/usr/bin/env node
@@ -194,9 +195,9 @@ function packagedFraimShimSource(command, runtime) {
194
195
  const windowsFraimMcpShimSource = `@echo off
195
196
  setlocal
196
197
  for %%I in ("%~dp0..") do set "FRAIM_USER_DIR_VALUE=%%~fI"
197
- where node >nul 2>nul
198
- if not errorlevel 1 (
199
- node "%FRAIM_USER_DIR_VALUE%\\bin\\fraim-mcp-latest.js" %*
198
+ ${(0, windows_shim_path_search_1.windowsPathSearchLines)('NODE_EXE', 'node').join('\n')}
199
+ if defined NODE_EXE (
200
+ "%NODE_EXE%" "%FRAIM_USER_DIR_VALUE%\\bin\\fraim-mcp-latest.js" %*
200
201
  exit /b %ERRORLEVEL%
201
202
  )
202
203
  if exist "%FRAIM_USER_DIR_VALUE%\\node\\node.exe" (
@@ -217,9 +218,9 @@ exit /b 1
217
218
  const windowsFraimNpxShimSource = `@echo off
218
219
  setlocal
219
220
  for %%I in ("%~dp0..") do set "FRAIM_USER_DIR_VALUE=%%~fI"
220
- where npx >nul 2>nul
221
- if not errorlevel 1 (
222
- npx %*
221
+ ${(0, windows_shim_path_search_1.windowsPathSearchLines)('NPX_CMD', 'npx').join('\n')}
222
+ if defined NPX_CMD (
223
+ "%NPX_CMD%" %*
223
224
  exit /b %ERRORLEVEL%
224
225
  )
225
226
  if exist "%FRAIM_USER_DIR_VALUE%\\node\\npx.cmd" (
@@ -83,6 +83,13 @@ function cleanupOrphanedManagedShims() {
83
83
  const removed = [];
84
84
  for (const basename of basenames) {
85
85
  const candidate = path_1.default.join(nodeRoot, basename);
86
+ // Guard: only delete the flat-dir shim once the versioned dir already has
87
+ // an equivalent. Without this, upgrading users lose their shims before a
88
+ // reinstall has had a chance to place them in the versioned dir — the
89
+ // agent CLI disappears entirely until the next explicit reinstall.
90
+ const versionedEquiv = path_1.default.join(versionedDir, basename);
91
+ if (!fs_1.default.existsSync(versionedEquiv))
92
+ continue;
86
93
  try {
87
94
  if (fs_1.default.statSync(candidate).isFile()) {
88
95
  fs_1.default.unlinkSync(candidate);
@@ -63,7 +63,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
63
63
  personaKey: 'swen',
64
64
  bundleId: 'persona-swen-core',
65
65
  catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'technical-design', 'code-refactoring']),
66
- protectedJobs: ['feature-implementation', 'technical-design', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
66
+ protectedJobs: ['feature-implementation', 'technical-design', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'application-replication-workflow'],
67
67
  protectedAliases: ['software-engineering', 'implementation'],
68
68
  defaultHireMode: 'job',
69
69
  lockCopy: 'Hire SWEn to unlock software-engineering delivery for this request.'
@@ -262,6 +262,23 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
262
262
  protectedAliases: ['banking-audit', 'kyc-audit', 'audit'],
263
263
  defaultHireMode: 'job',
264
264
  lockCopy: 'Hire AUDITya to unlock banking evidence audit work for this request.'
265
+ },
266
+ aida: {
267
+ personaKey: 'aida',
268
+ bundleId: 'persona-aida-core',
269
+ catalogMetadata: buildCatalogMetadata('aida', ['create-ai-agent', 'author-ai-evals', 'enable-web-mcp']),
270
+ protectedJobs: [
271
+ 'create-ai-agent',
272
+ 'author-ai-evals',
273
+ 'enable-web-mcp',
274
+ 'evaluate-ai-agent',
275
+ 'mcp-server-creation',
276
+ 'publish-mcp-app',
277
+ 'create-hub-configured-agent',
278
+ ],
279
+ protectedAliases: ['ai-engineering', 'agent-engineering', 'ai-agents'],
280
+ defaultHireMode: 'job',
281
+ lockCopy: 'Hire AIda to unlock AI agent design, MCP enablement, eval authoring, and agent evaluation work for this request.'
265
282
  }
266
283
  };
267
284
  const PROTECTED_JOB_TO_PERSONA = new Map();
@@ -275,7 +292,6 @@ for (const bundle of Object.values(exports.PERSONA_CAPABILITY_BUNDLES)) {
275
292
  // as "free") so the Hub attributes them to FRAIMworker, but they are never
276
293
  // hire-gated because FRAIMworker is not a purchasable persona.
277
294
  const GENERIC_WORKER_OWNED_JOBS = new Set([
278
- 'create-hub-configured-agent',
279
295
  'contribute-to-fraim',
280
296
  'file-fraim-issue',
281
297
  'praise-fraim',