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.
- package/dist/src/ai-hub/hosts.js +2 -1
- package/dist/src/ai-hub/hub-latest-version.js +9 -2
- package/dist/src/ai-hub/hub-launcher.js +7 -3
- package/dist/src/ai-hub/server.js +373 -1
- package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +4 -3
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +7 -6
- package/dist/src/cli/utils/managed-agent-paths.js +7 -0
- package/dist/src/config/persona-capability-bundles.js +18 -2
- package/dist/src/config/persona-hiring.js +123 -113
- package/dist/src/core/utils/windows-shim-path-search.js +56 -0
- package/dist/src/first-run/session-service.js +42 -8
- package/package.json +3 -2
- package/public/ai-hub/excel-taskpane/index.html +47 -11
- package/public/ai-hub/index.html +4 -0
- package/public/ai-hub/script.js +29 -5
- package/public/ai-hub/styles.css +22 -0
- package/public/portfolio/aida.html +243 -0
- package/public/portfolio/index.html +198 -177
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -2156,7 +2156,8 @@ class FakeHostRuntime {
|
|
|
2156
2156
|
}
|
|
2157
2157
|
startRun(hostId, _projectPath, message, handlers, _sessionId) {
|
|
2158
2158
|
this.lastStartMessage = message;
|
|
2159
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
'
|
|
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
|
|
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
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
-
|
|
198
|
-
if
|
|
199
|
-
|
|
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
|
-
|
|
221
|
-
if
|
|
222
|
-
|
|
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', '
|
|
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',
|