fraim-hub 2.0.325 → 2.0.327
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/conversation-store.js +30 -28
- package/dist/src/ai-hub/desktop-installer-setup.js +79 -0
- package/dist/src/ai-hub/desktop-launcher.js +4 -0
- package/dist/src/ai-hub/desktop-main.js +18 -9
- package/dist/src/ai-hub/hosts.js +5 -2
- package/dist/src/ai-hub/hub-app-materializer.js +10 -20
- package/dist/src/ai-hub/raw-event-log-store.js +130 -20
- package/dist/src/ai-hub/server.js +145 -140
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +31 -130
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -6
- package/dist/src/cli/utils/managed-agent-paths.js +14 -3
- package/dist/src/cli/utils/managed-node-runtime.js +22 -19
- package/dist/src/first-run/session-service.js +17 -11
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +106 -6
- package/package.json +4 -3
- package/public/ai-hub/index.html +6 -2
- package/public/ai-hub/script.js +193 -87
- package/public/ai-hub/styles.css +26 -11
|
@@ -17,22 +17,19 @@ const conversation_search_projection_1 = require("./conversation-search-projecti
|
|
|
17
17
|
// They are NOT filesystem paths and must never be passed through path.resolve.
|
|
18
18
|
exports.MANAGER_SCOPE_KEY = '@manager';
|
|
19
19
|
exports.COMPANY_SCOPE_KEY = '@company';
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'_stopping',
|
|
32
|
-
'_priorEvents',
|
|
33
|
-
'_priorMessages',
|
|
20
|
+
// Issue #1765: public list metadata is opt-in. These fields feed the shared Projects,
|
|
21
|
+
// Manager and Company rail identity, ordering, status, trigger and delegation controls
|
|
22
|
+
// (buildAreaRunButton / conversationUiState in public/ai-hub/script.js). New body fields
|
|
23
|
+
// must never silently enlarge every list response or short-circuit lazy hydration.
|
|
24
|
+
const HEADER_FIELDS = [
|
|
25
|
+
'id', 'projectPath', 'scope', 'invokedArea', 'title', 'jobId', 'jobTitle',
|
|
26
|
+
'agentName', 'configuredAgentId', 'configuredAgentLabel', 'baseHostId', 'personaKey',
|
|
27
|
+
'runId', 'status', 'pauseReason', 'createdAt', 'lastUpdatedAt', 'issueNumber',
|
|
28
|
+
'sourceTrigger', 'blocked', 'stopped', 'reviewApproved', 'compareMode', 'compareRunId',
|
|
29
|
+
'managedByRunId', 'managedByPersonaKey', 'managedReviewStatus', 'delegationTaskId',
|
|
30
|
+
'humanCoachingDisabled', 'personaSnapshot', 'reviewHandoff',
|
|
34
31
|
];
|
|
35
|
-
const
|
|
32
|
+
const HEADER_FIELD_SET = new Set([...HEADER_FIELDS, 'personaSnapshot', 'delegation', 'reviewHandoff']);
|
|
36
33
|
/**
|
|
37
34
|
* Issue #708: resolve the conversation store bucket key for a given scope.
|
|
38
35
|
* - 'manager'/'company' → a stable sentinel key (project-independent home).
|
|
@@ -340,7 +337,7 @@ function normalizeConversation(projectPath, raw) {
|
|
|
340
337
|
status: value.status,
|
|
341
338
|
// Issue #904: carry pauseReason explicitly so it survives the ...value spread round-trip.
|
|
342
339
|
...(value.pauseReason !== undefined && { pauseReason: value.pauseReason }),
|
|
343
|
-
createdAt: value.createdAt ?? new Date().toISOString(),
|
|
340
|
+
createdAt: value.createdAt ?? value.lastUpdatedAt ?? new Date().toISOString(),
|
|
344
341
|
lastUpdatedAt: value.lastUpdatedAt ?? value.createdAt ?? new Date().toISOString(),
|
|
345
342
|
};
|
|
346
343
|
}
|
|
@@ -361,18 +358,23 @@ function newestFirst(a, b) {
|
|
|
361
358
|
return timestampValue(b.lastUpdatedAt) - timestampValue(a.lastUpdatedAt);
|
|
362
359
|
}
|
|
363
360
|
function toHeader(conv) {
|
|
364
|
-
const header =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
delete header[field];
|
|
368
|
-
if (delegation)
|
|
369
|
-
header.delegation = delegation;
|
|
370
|
-
for (const field of Object.keys(header)) {
|
|
371
|
-
if (field.startsWith('_'))
|
|
372
|
-
delete header[field];
|
|
361
|
+
const header = pickHeaderFields(conv, HEADER_FIELDS);
|
|
362
|
+
if (conv.delegation !== undefined) {
|
|
363
|
+
header.delegation = compactDelegationForHeader(conv.delegation) ?? null;
|
|
373
364
|
}
|
|
374
365
|
return header;
|
|
375
366
|
}
|
|
367
|
+
function pickHeaderFields(raw, fields) {
|
|
368
|
+
const result = {};
|
|
369
|
+
if (!raw || typeof raw !== 'object')
|
|
370
|
+
return result;
|
|
371
|
+
const value = raw;
|
|
372
|
+
for (const field of fields) {
|
|
373
|
+
if (Object.prototype.hasOwnProperty.call(value, field))
|
|
374
|
+
result[field] = value[field];
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
376
378
|
function compactDelegationForHeader(raw) {
|
|
377
379
|
if (!raw || typeof raw !== 'object')
|
|
378
380
|
return undefined;
|
|
@@ -402,15 +404,15 @@ function compactDelegationForHeader(raw) {
|
|
|
402
404
|
};
|
|
403
405
|
}
|
|
404
406
|
function headerNeedsSanitization(header) {
|
|
405
|
-
|
|
406
|
-
return Object.keys(value).some((field) => HEADER_OMITTED_FIELD_SET.has(field) || field.startsWith('_'));
|
|
407
|
+
return Object.keys(header).some((field) => !HEADER_FIELD_SET.has(field));
|
|
407
408
|
}
|
|
408
409
|
function sanitizeBucketIndex(index) {
|
|
409
410
|
let changed = false;
|
|
410
411
|
const headers = index.headers.map((header) => {
|
|
412
|
+
const compact = toHeader(header);
|
|
411
413
|
if (headerNeedsSanitization(header))
|
|
412
414
|
changed = true;
|
|
413
|
-
return
|
|
415
|
+
return compact;
|
|
414
416
|
}).sort(newestFirst);
|
|
415
417
|
const activeId = index.activeId && headers.some((header) => header.id === index.activeId) ? index.activeId : null;
|
|
416
418
|
if (activeId !== index.activeId)
|
|
@@ -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
|
|
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;
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -603,7 +603,7 @@ function stringifyMentorCallArgs(rawArgs) {
|
|
|
603
603
|
}
|
|
604
604
|
function resultTextFromValue(value) {
|
|
605
605
|
if (typeof value === 'string')
|
|
606
|
-
return value.trim()
|
|
606
|
+
return value.trim() ? value : null;
|
|
607
607
|
if (Array.isArray(value)) {
|
|
608
608
|
const parts = value
|
|
609
609
|
.map((entry) => resultTextFromValue(entry))
|
|
@@ -693,6 +693,9 @@ function parseMicroEventsSignal(hostId, line) {
|
|
|
693
693
|
}
|
|
694
694
|
else if (itemType !== 'agent_message' && itemType !== 'error') {
|
|
695
695
|
events.push(microEventFromNonMentorToolCall(item) || { kind: 'tool_call', text: itemType });
|
|
696
|
+
const output = resultTextFromValue(item.result ?? item.output ?? item.aggregated_output);
|
|
697
|
+
if (output)
|
|
698
|
+
events.push({ kind: 'tool_result', text: output });
|
|
696
699
|
}
|
|
697
700
|
}
|
|
698
701
|
if (item && item.type === 'mcp_tool_call' && isFraimTool(readToolName(item), 'seekMentoring')) {
|
|
@@ -724,7 +727,7 @@ function parseMicroEventsSignal(hostId, line) {
|
|
|
724
727
|
}
|
|
725
728
|
else if (root.type === 'tool.execution_complete') {
|
|
726
729
|
const resultText = resultTextFromValue(data.result ?? data.output ?? data.content);
|
|
727
|
-
events.push({ kind: '
|
|
730
|
+
events.push({ kind: 'tool_result', text: resultText || rootType });
|
|
728
731
|
rootTypeHandled = true;
|
|
729
732
|
}
|
|
730
733
|
}
|
|
@@ -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
|
|
182
|
-
*
|
|
183
|
-
*
|
|
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 });
|
|
@@ -6,16 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.AiHubRawEventLogStore = void 0;
|
|
7
7
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
10
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const types_1 = require("./types");
|
|
11
11
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
12
12
|
function defaultRawEventLogRoot() {
|
|
13
|
-
|
|
14
|
-
return node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-event-logs');
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return node_path_1.default.join(node_os_1.default.homedir(), '.fraim', 'ai-hub-event-logs');
|
|
18
|
-
}
|
|
13
|
+
return node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-event-logs');
|
|
19
14
|
}
|
|
20
15
|
function hashSegment(value, length = 24) {
|
|
21
16
|
return node_crypto_1.default.createHash('sha256').update(value || 'unknown').digest('hex').slice(0, length);
|
|
@@ -23,20 +18,39 @@ function hashSegment(value, length = 24) {
|
|
|
23
18
|
function safeRunFileName(runId) {
|
|
24
19
|
return `${hashSegment(runId, 40)}.jsonl`;
|
|
25
20
|
}
|
|
21
|
+
function parseEntry(line) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(line);
|
|
24
|
+
if (typeof value.text !== 'string' || !['stdout', 'stderr', 'system'].includes(value.channel))
|
|
25
|
+
return null;
|
|
26
|
+
return { createdAt: typeof value.createdAt === 'string' ? value.createdAt : new Date(0).toISOString(), channel: value.channel, text: value.text,
|
|
27
|
+
...(types_1.AI_HUB_MICRO_EVENT_KINDS.includes(value.hubKind) ? { hubKind: value.hubKind } : {}),
|
|
28
|
+
...(typeof value.hostId === 'string' ? { hostId: value.hostId } : {}),
|
|
29
|
+
...(Array.isArray(value.microEvents) ? { microEvents: value.microEvents.filter((event) => event && types_1.AI_HUB_MICRO_EVENT_KINDS.includes(event.kind) && typeof event.text === 'string') } : {}) };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
26
35
|
class AiHubRawEventLogStore {
|
|
27
36
|
constructor(rootDir = defaultRawEventLogRoot()) {
|
|
28
37
|
this.rootDir = node_path_1.default.resolve(rootDir);
|
|
29
38
|
}
|
|
30
39
|
append(bucketKey, conversationId, runId, entry) {
|
|
31
40
|
const dir = node_path_1.default.join(this.rootDir, hashSegment(bucketKey), hashSegment(conversationId));
|
|
32
|
-
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
33
41
|
const logPath = node_path_1.default.join(dir, safeRunFileName(runId));
|
|
34
42
|
const metaPath = `${logPath}.meta.json`;
|
|
43
|
+
if (!this.resolveReadableLogPath(logPath) || !this.resolveReadableLogPath(metaPath))
|
|
44
|
+
throw new Error('Event archive path is outside its root');
|
|
45
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
35
46
|
const createdAt = entry.createdAt || new Date().toISOString();
|
|
36
47
|
const line = {
|
|
37
48
|
createdAt,
|
|
38
49
|
channel: entry.channel,
|
|
39
50
|
text: entry.text,
|
|
51
|
+
...(entry.hubKind ? { hubKind: entry.hubKind } : {}),
|
|
52
|
+
...(entry.hostId ? { hostId: entry.hostId } : {}),
|
|
53
|
+
...(entry.microEvents ? { microEvents: entry.microEvents } : {}),
|
|
40
54
|
};
|
|
41
55
|
node_fs_1.default.appendFileSync(logPath, `${JSON.stringify(line)}\n`, 'utf8');
|
|
42
56
|
const prior = this.readMeta(metaPath, runId, logPath);
|
|
@@ -48,6 +62,8 @@ class AiHubRawEventLogStore {
|
|
|
48
62
|
eventCount: prior.eventCount + 1,
|
|
49
63
|
createdAt: prior.createdAt || createdAt,
|
|
50
64
|
updatedAt: createdAt,
|
|
65
|
+
ownerPid: process.pid,
|
|
66
|
+
...(entry.hostId || prior.hostId ? { hostId: entry.hostId || prior.hostId } : {}),
|
|
51
67
|
...(prior.truncated ? { truncated: true } : {}),
|
|
52
68
|
};
|
|
53
69
|
node_fs_1.default.writeFileSync(metaPath, `${JSON.stringify(ref, null, 2)}\n`, 'utf8');
|
|
@@ -60,22 +76,90 @@ class AiHubRawEventLogStore {
|
|
|
60
76
|
const lines = node_fs_1.default.readFileSync(logPath, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
61
77
|
const entries = [];
|
|
62
78
|
for (const line of lines) {
|
|
79
|
+
const entry = parseEntry(line);
|
|
80
|
+
if (entry)
|
|
81
|
+
entries.push(entry);
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
markEnded(ref, endedAt) {
|
|
86
|
+
const logPath = this.resolveReadableLogPath(ref.path);
|
|
87
|
+
if (!logPath || !node_fs_1.default.existsSync(logPath))
|
|
88
|
+
return ref;
|
|
89
|
+
if (!this.resolveReadableLogPath(`${logPath}.meta.json`))
|
|
90
|
+
throw new Error('Event archive metadata is outside its root');
|
|
91
|
+
const next = { ...this.readMeta(`${logPath}.meta.json`, ref.runId, logPath), endedAt };
|
|
92
|
+
delete next.ownerPid;
|
|
93
|
+
node_fs_1.default.writeFileSync(`${logPath}.meta.json`, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
/** Read display-ready entries in chronological order without reconstructing host output. */
|
|
97
|
+
async readMicroEvents(refs) {
|
|
98
|
+
const events = [];
|
|
99
|
+
let missingArchives = 0;
|
|
100
|
+
let legacyArchives = 0;
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
const logPath = this.resolveReadableLogPath(ref.path);
|
|
103
|
+
if (!logPath) {
|
|
104
|
+
missingArchives++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
let size;
|
|
108
|
+
try {
|
|
109
|
+
size = (await node_fs_1.default.promises.stat(logPath)).size;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === 'ENOENT') {
|
|
113
|
+
missingArchives++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
if (!size)
|
|
119
|
+
continue;
|
|
120
|
+
let hasLegacyEntries = false;
|
|
121
|
+
// Snapshot the size; only newline-terminated records are committed. The
|
|
122
|
+
// stream owns its descriptor and decodes UTF-8 across chunk boundaries.
|
|
123
|
+
const stream = node_fs_1.default.createReadStream(logPath, { encoding: 'utf8', end: size - 1 });
|
|
124
|
+
let pending = '';
|
|
125
|
+
let lineNumber = 0;
|
|
63
126
|
try {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
127
|
+
for await (const chunk of stream) {
|
|
128
|
+
const lines = (pending + chunk).split('\n');
|
|
129
|
+
pending = lines.pop();
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const entry = parseEntry(line);
|
|
132
|
+
const id = `${ref.runId}:${lineNumber++}`;
|
|
133
|
+
if (!entry)
|
|
134
|
+
continue;
|
|
135
|
+
const displayed = entry.hubKind ? [{ kind: entry.hubKind, text: entry.text }] : entry.microEvents;
|
|
136
|
+
if (!displayed) {
|
|
137
|
+
hasLegacyEntries = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
displayed.forEach((event, index) => events.push({
|
|
141
|
+
id: `${id}:${index}`, kind: event.kind, text: event.text,
|
|
142
|
+
channel: event.kind === 'manager_input' ? 'manager' : entry.channel, createdAt: entry.createdAt,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
72
146
|
}
|
|
73
147
|
}
|
|
74
|
-
catch {
|
|
75
|
-
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code === 'ENOENT') {
|
|
150
|
+
missingArchives++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
76
154
|
}
|
|
155
|
+
finally {
|
|
156
|
+
stream.destroy();
|
|
157
|
+
}
|
|
158
|
+
if (hasLegacyEntries)
|
|
159
|
+
legacyArchives++;
|
|
77
160
|
}
|
|
78
|
-
|
|
161
|
+
events.sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
162
|
+
return { events, archiveAvailable: refs.length > missingArchives, missingArchives, legacyArchives };
|
|
79
163
|
}
|
|
80
164
|
readMeta(metaPath, runId, logPath) {
|
|
81
165
|
if (!node_fs_1.default.existsSync(metaPath)) {
|
|
@@ -90,6 +174,9 @@ class AiHubRawEventLogStore {
|
|
|
90
174
|
eventCount: typeof parsed.eventCount === 'number' ? parsed.eventCount : 0,
|
|
91
175
|
createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : '',
|
|
92
176
|
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
|
|
177
|
+
...(typeof parsed.endedAt === 'string' ? { endedAt: parsed.endedAt } : {}),
|
|
178
|
+
...(typeof parsed.ownerPid === 'number' ? { ownerPid: parsed.ownerPid } : {}),
|
|
179
|
+
...(typeof parsed.hostId === 'string' ? { hostId: parsed.hostId } : {}),
|
|
93
180
|
...(parsed.truncated ? { truncated: true } : {}),
|
|
94
181
|
};
|
|
95
182
|
}
|
|
@@ -104,7 +191,30 @@ class AiHubRawEventLogStore {
|
|
|
104
191
|
const rootPrefix = `${this.rootDir}${node_path_1.default.sep}`;
|
|
105
192
|
const comparableResolved = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
106
193
|
const comparableRootPrefix = process.platform === 'win32' ? rootPrefix.toLowerCase() : rootPrefix;
|
|
107
|
-
|
|
194
|
+
if (!comparableResolved.startsWith(comparableRootPrefix))
|
|
195
|
+
return null;
|
|
196
|
+
// A symlink inside the archive root must not grant access outside it.
|
|
197
|
+
try {
|
|
198
|
+
const real = node_fs_1.default.realpathSync(resolved);
|
|
199
|
+
const root = node_fs_1.default.realpathSync(this.rootDir) + node_path_1.default.sep;
|
|
200
|
+
return (process.platform === 'win32' ? real.toLowerCase().startsWith(root.toLowerCase()) : real.startsWith(root)) ? real : null;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code === 'ENOENT') {
|
|
204
|
+
// Missing leaves still inherit the confinement of their nearest real
|
|
205
|
+
// parent, including Windows junctions inside a hashed bucket directory.
|
|
206
|
+
let parent = node_path_1.default.dirname(resolved);
|
|
207
|
+
while (!node_fs_1.default.existsSync(parent) && parent !== node_path_1.default.dirname(parent))
|
|
208
|
+
parent = node_path_1.default.dirname(parent);
|
|
209
|
+
if (!node_fs_1.default.existsSync(this.rootDir))
|
|
210
|
+
return resolved;
|
|
211
|
+
const realParent = node_fs_1.default.realpathSync(parent);
|
|
212
|
+
const realRoot = node_fs_1.default.realpathSync(this.rootDir);
|
|
213
|
+
const relative = node_path_1.default.relative(realRoot, realParent);
|
|
214
|
+
return relative === '' || (!relative.startsWith('..') && !node_path_1.default.isAbsolute(relative)) ? resolved : null;
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
108
218
|
}
|
|
109
219
|
}
|
|
110
220
|
exports.AiHubRawEventLogStore = AiHubRawEventLogStore;
|