fraim-hub 2.0.297 → 2.0.299
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/configured-agent-command.js +3 -3
- package/dist/src/ai-hub/configured-agents.js +13 -20
- package/dist/src/ai-hub/desktop-main.js +17 -5
- package/dist/src/ai-hub/hosts.js +23 -8
- package/dist/src/ai-hub/hub-latest-version.js +11 -0
- package/dist/src/ai-hub/manager-turns.js +1 -1
- package/dist/src/ai-hub/run-working-directory.js +68 -0
- package/dist/src/ai-hub/server.js +489 -94
- package/dist/src/cli/utils/user-config.js +20 -0
- package/dist/src/config/persona-capability-bundles.js +10 -1
- package/dist/src/config/persona-hiring.js +10 -0
- package/dist/src/core/resolve-phase-edge.js +3 -1
- package/dist/src/fraim/db-service.js +64 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +11 -7
- package/package.json +2 -2
- package/public/ai-hub/index.html +16 -2
- package/public/ai-hub/script.js +137 -20
- package/public/ai-hub/styles.css +21 -0
- package/public/portfolio/index.html +22 -0
- package/public/portfolio/travis.html +106 -0
|
@@ -42,12 +42,12 @@ function parseConfiguredCommand(runner, commandText) {
|
|
|
42
42
|
// the only way to catch this class of error is to check, at readiness time,
|
|
43
43
|
// that what the user typed there actually was that same base binary — not
|
|
44
44
|
// to attempt to execute or otherwise interpret it.
|
|
45
|
-
function describeConfiguredCommandInvocationProblem(parsed, baseHostId) {
|
|
45
|
+
function describeConfiguredCommandInvocationProblem(parsed, baseHostId, agentLabel = 'Configured agent') {
|
|
46
46
|
if (!parsed.invocationHead) {
|
|
47
|
-
return
|
|
47
|
+
return `${agentLabel} command has no invocation after the environment prefix. Run the create-hub-configured-agent job to repair this profile with an environment prefix followed by "${baseHostId}".`;
|
|
48
48
|
}
|
|
49
49
|
if (parsed.invocationHead !== baseHostId) {
|
|
50
|
-
return `
|
|
50
|
+
return `Configured agent "${agentLabel}" invokes "${parsed.invocationHead}", but Hub launches the "${baseHostId}" base CLI and cannot apply that wrapper's environment. Run the create-hub-configured-agent job to repair this profile with an environment prefix followed by "${baseHostId}".`;
|
|
51
51
|
}
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
@@ -178,6 +178,17 @@ function checkConfiguredAgentAvailability(agent, employees, env = process.env) {
|
|
|
178
178
|
if (!agent.command.command.trim()) {
|
|
179
179
|
reasons.push('Command is empty.');
|
|
180
180
|
}
|
|
181
|
+
else {
|
|
182
|
+
try {
|
|
183
|
+
const parsed = (0, configured_agent_command_1.parseConfiguredCommand)(agent.command.runner, agent.command.command);
|
|
184
|
+
const problem = (0, configured_agent_command_1.describeConfiguredCommandInvocationProblem)(parsed, agent.baseHostId, agent.label);
|
|
185
|
+
if (problem)
|
|
186
|
+
reasons.push(problem);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
reasons.push(error instanceof Error ? error.message : 'Configured agent command could not be parsed.');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
181
192
|
}
|
|
182
193
|
else if (agent.setupScript) {
|
|
183
194
|
const command = agent.setupScript.command;
|
|
@@ -195,26 +206,8 @@ function checkConfiguredAgentReadiness(agent, employees, env = process.env) {
|
|
|
195
206
|
const check = checkConfiguredAgentAvailability(agent, employees, env);
|
|
196
207
|
if (!check.available)
|
|
197
208
|
return check;
|
|
198
|
-
if (agent.command)
|
|
199
|
-
|
|
200
|
-
const parsed = (0, configured_agent_command_1.parseConfiguredCommand)(agent.command.runner, agent.command.command);
|
|
201
|
-
const problem = (0, configured_agent_command_1.describeConfiguredCommandInvocationProblem)(parsed, agent.baseHostId);
|
|
202
|
-
if (problem) {
|
|
203
|
-
return { ...check, available: false, reasons: [...check.reasons, problem] };
|
|
204
|
-
}
|
|
205
|
-
return check;
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
return {
|
|
209
|
-
...check,
|
|
210
|
-
available: false,
|
|
211
|
-
reasons: [
|
|
212
|
-
...check.reasons,
|
|
213
|
-
error instanceof Error ? error.message : 'Configured agent command could not be parsed.',
|
|
214
|
-
],
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
}
|
|
209
|
+
if (agent.command)
|
|
210
|
+
return check;
|
|
218
211
|
if (!agent.setupScript)
|
|
219
212
|
return check;
|
|
220
213
|
try {
|
|
@@ -630,12 +630,24 @@ async function bootstrap() {
|
|
|
630
630
|
});
|
|
631
631
|
await launchDesktopShell(options);
|
|
632
632
|
}
|
|
633
|
-
|
|
633
|
+
function isDirectElectronEntry() {
|
|
634
|
+
if (require.main === module)
|
|
635
|
+
return true;
|
|
636
|
+
const currentFile = path_1.default.resolve(__filename).toLowerCase();
|
|
637
|
+
return process.argv.some((arg) => {
|
|
638
|
+
try {
|
|
639
|
+
return path_1.default.resolve(arg).toLowerCase() === currentFile;
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
// Issue #1415: self-execution is gated on a direct Electron entry check in addition to the
|
|
634
647
|
// existing Electron-main-process check, so `desktop-launcher.ts` requiring this file (bundled or
|
|
635
|
-
// materialized) does not double-run bootstrap
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
if (require.main === module && process.versions.electron && process.type !== 'renderer') {
|
|
648
|
+
// materialized) does not double-run bootstrap. Electron's loader can own `require.main`, so
|
|
649
|
+
// direct test launches also match the resolved script path in argv.
|
|
650
|
+
if (isDirectElectronEntry() && process.versions.electron && process.type !== 'renderer') {
|
|
639
651
|
bootstrap().catch(async (error) => {
|
|
640
652
|
console.error(error instanceof Error ? error.message : error);
|
|
641
653
|
await stopServerOnce();
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -43,6 +43,7 @@ const command_resolution_1 = require("../cli/mcp/command-resolution");
|
|
|
43
43
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
44
44
|
const configured_agents_1 = require("./configured-agents");
|
|
45
45
|
const pack_home_1 = require("../cli/utils/pack-home");
|
|
46
|
+
const run_working_directory_1 = require("./run-working-directory");
|
|
46
47
|
// Parse a single line of host stdout looking for a seekMentoring tool-use
|
|
47
48
|
// signal. Returns null if the line does not contain one. Supports both
|
|
48
49
|
// hosts FRAIM ships against today:
|
|
@@ -1916,7 +1917,7 @@ function detectCodexCompactTaskFailure(line) {
|
|
|
1916
1917
|
}
|
|
1917
1918
|
return null;
|
|
1918
1919
|
}
|
|
1919
|
-
function wireHostProcess(hostId, child, handlers) {
|
|
1920
|
+
function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
1920
1921
|
const wire = (buffer, channel) => {
|
|
1921
1922
|
let pending = '';
|
|
1922
1923
|
let suppressCodexModelRefreshStderr = false;
|
|
@@ -1971,12 +1972,23 @@ function wireHostProcess(hostId, child, handlers) {
|
|
|
1971
1972
|
// message names the cause. 'close' still fires after 'error' for spawn
|
|
1972
1973
|
// failures, so handlers.onExit is called by the close listener above.
|
|
1973
1974
|
child.on('error', (err) => {
|
|
1974
|
-
|
|
1975
|
+
const spawnError = err;
|
|
1976
|
+
console.warn('[ai-hub] hub.host_spawn_error', {
|
|
1977
|
+
hostId,
|
|
1978
|
+
cwd: spawnCwd,
|
|
1979
|
+
cwdExists: spawnCwd ? (0, run_working_directory_1.workingDirectoryExists)(spawnCwd) : undefined,
|
|
1980
|
+
errorCode: spawnError.code,
|
|
1981
|
+
path: spawnError.path,
|
|
1982
|
+
});
|
|
1983
|
+
handlers.onEvent({ hostError: describeSpawnError(spawnError, spawnCwd) }, 'system');
|
|
1975
1984
|
});
|
|
1976
1985
|
return child;
|
|
1977
1986
|
}
|
|
1978
|
-
function describeSpawnError(err) {
|
|
1987
|
+
function describeSpawnError(err, cwd) {
|
|
1979
1988
|
if (err.code === 'ENOENT') {
|
|
1989
|
+
if (cwd && !(0, run_working_directory_1.workingDirectoryExists)(cwd)) {
|
|
1990
|
+
return `Agent working directory not found (${cwd}). The conversation scope may have been used as a path.`;
|
|
1991
|
+
}
|
|
1980
1992
|
return `Agent binary not found${err.path ? ` (${err.path})` : ''}. Ensure the agent CLI is installed and on PATH.`;
|
|
1981
1993
|
}
|
|
1982
1994
|
if (err.code === 'EACCES') {
|
|
@@ -2003,9 +2015,10 @@ function buildHostSpawnEnv(plan) {
|
|
|
2003
2015
|
}
|
|
2004
2016
|
function spawnHostProcess(hostId, plan, projectPath, handlers) {
|
|
2005
2017
|
const invocation = resolveHostInvocation(plan);
|
|
2018
|
+
const spawnCwd = (0, run_working_directory_1.resolveHostWorkingDirectory)(projectPath);
|
|
2006
2019
|
const startedAtMs = Date.now();
|
|
2007
2020
|
const child = (0, child_process_1.spawn)(invocation.command, invocation.args, {
|
|
2008
|
-
cwd:
|
|
2021
|
+
cwd: spawnCwd,
|
|
2009
2022
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
2010
2023
|
env: buildHostSpawnEnv(plan),
|
|
2011
2024
|
});
|
|
@@ -2015,13 +2028,13 @@ function spawnHostProcess(hostId, plan, projectPath, handlers) {
|
|
|
2015
2028
|
child.stdin.end();
|
|
2016
2029
|
if (typeof plan.stdin === 'string' && !plan.args.includes('--resume')) {
|
|
2017
2030
|
child.once('close', () => {
|
|
2018
|
-
const sessionId = discoverSessionIdAfterStart(hostId,
|
|
2031
|
+
const sessionId = discoverSessionIdAfterStart(hostId, spawnCwd, plan.stdin || '', startedAtMs);
|
|
2019
2032
|
if (sessionId) {
|
|
2020
2033
|
handlers.onEvent({ sessionId, raw: `${hostId}-session:${sessionId}` }, 'system');
|
|
2021
2034
|
}
|
|
2022
2035
|
});
|
|
2023
2036
|
}
|
|
2024
|
-
return wireHostProcess(hostId, child, handlers);
|
|
2037
|
+
return wireHostProcess(hostId, child, handlers, spawnCwd);
|
|
2025
2038
|
}
|
|
2026
2039
|
function discoverSessionIdAfterStart(hostId, projectPath, prompt, startedAtMs) {
|
|
2027
2040
|
if (hostId !== 'gemini')
|
|
@@ -2257,13 +2270,15 @@ class FakeHostRuntime {
|
|
|
2257
2270
|
detectEmployees() {
|
|
2258
2271
|
return this.employees;
|
|
2259
2272
|
}
|
|
2260
|
-
startRun(hostId, _projectPath, message, handlers, _sessionId) {
|
|
2273
|
+
startRun(hostId, _projectPath, message, handlers, _sessionId, launchContext) {
|
|
2261
2274
|
this.lastStartMessage = message;
|
|
2275
|
+
this.lastLaunchContext = launchContext;
|
|
2262
2276
|
const reply = this.startReply ?? this.fakeEmployeeReply('start', message);
|
|
2263
2277
|
return this.fakeProcess(hostId, reply, handlers);
|
|
2264
2278
|
}
|
|
2265
|
-
continueRun(hostId, _projectPath, sessionId, message, handlers) {
|
|
2279
|
+
continueRun(hostId, _projectPath, sessionId, message, handlers, launchContext) {
|
|
2266
2280
|
this.lastContinueMessage = message;
|
|
2281
|
+
this.lastLaunchContext = launchContext;
|
|
2267
2282
|
return this.fakeProcess(hostId, this.fakeEmployeeReply('continue', message), handlers);
|
|
2268
2283
|
}
|
|
2269
2284
|
startDirectRun(hostId, _message, _projectPath, handlers, _sessionId) {
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BADGE_POLL_INTERVAL_MS = void 0;
|
|
6
7
|
exports.getLatestPublishedVersion = getLatestPublishedVersion;
|
|
7
8
|
exports.getCachedLatestPublishedVersion = getCachedLatestPublishedVersion;
|
|
8
9
|
exports.refreshLatestPublishedVersionInBackground = refreshLatestPublishedVersionInBackground;
|
|
@@ -21,6 +22,16 @@ const https_1 = __importDefault(require("https"));
|
|
|
21
22
|
const TTL_MS = 5 * 60 * 1000;
|
|
22
23
|
const FAILED_RETRY_MS = 5 * 60 * 1000;
|
|
23
24
|
const REGISTRY_URL = 'https://registry.npmjs.org/fraim/latest';
|
|
25
|
+
// A resident Hub (hidden in the tray, login-item autostart) can go days without any of
|
|
26
|
+
// this module's other callers ever firing: they're all reactive (a page load, a
|
|
27
|
+
// persona-identity retry, a tray-click relaunch decision), and none of them are timer-driven.
|
|
28
|
+
// If nothing calls in, this cache never refreshes and the badge never learns about a new
|
|
29
|
+
// release, no matter how far behind it drifts. AiHubServer.start() below runs this on its
|
|
30
|
+
// own interval, independent of any request or window, so the cache — and therefore the
|
|
31
|
+
// badge, on its next client-side poll — stays within 15 minutes of the truth even when the
|
|
32
|
+
// Hub is otherwise fully idle. Lives here (not in the Electron shell) so it ships to
|
|
33
|
+
// existing installs via a normal npm publish, not a new signed installer.
|
|
34
|
+
exports.BADGE_POLL_INTERVAL_MS = 15 * 60 * 1000;
|
|
24
35
|
let cache = { value: null, at: 0 };
|
|
25
36
|
let lastAttemptAt = 0;
|
|
26
37
|
let inFlight = null;
|
|
@@ -50,7 +50,7 @@ function buildCommunicationStyleNote() {
|
|
|
50
50
|
function buildBackgroundTaskPolicyNote() {
|
|
51
51
|
return [
|
|
52
52
|
'',
|
|
53
|
-
'[Background task policy] You are running in Hub headless mode. Never end your turn while background tasks (bash commands started with run_in_background=true, or sub-agents) are still active. Instead: monitor them, poll for results, and provide periodic progress commentary to the manager. Only end your turn when all background tasks have finished, or when you genuinely need a user decision or input to continue. If you exit while a background task is active, the Hub loses the link to that task permanently.',
|
|
53
|
+
'[Background task policy] You are running in Hub headless mode. Never end your turn while background tasks (bash commands started with run_in_background=true, or sub-agents) are still active. Instead: monitor them, poll for results, and provide periodic progress commentary to the manager. Only end your turn when all background tasks have finished, or when you genuinely need a user decision or input to continue. If you exit while a background task is active, the Hub loses the link to that task permanently. Background tasks and Monitor watchers cannot survive this turn ending: the next turn is a new process with no link to the old one (Windows: hard-killed; Unix: orphaned). The two things that reliably work: run the command synchronously in the foreground within this turn, or supervise it with `npx tsx ~/.fraim/scripts/exec-with-timeout.ts --supervise <name> --timeout <n> -- <command>` and poll `--status`/`--wait` (or `--stop` to cancel) without ending your turn.',
|
|
54
54
|
].join('\n');
|
|
55
55
|
}
|
|
56
56
|
// Issue #732: a plain continue of the SAME active job must not re-load the job.
|
|
@@ -0,0 +1,68 @@
|
|
|
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.resolveRunWorkingDirectory = resolveRunWorkingDirectory;
|
|
7
|
+
exports.executionScopeFromConversation = executionScopeFromConversation;
|
|
8
|
+
exports.resolveHostWorkingDirectory = resolveHostWorkingDirectory;
|
|
9
|
+
exports.workingDirectoryExists = workingDirectoryExists;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const pack_home_1 = require("../cli/utils/pack-home");
|
|
13
|
+
const conversation_store_1 = require("./conversation-store");
|
|
14
|
+
function ensureExistingDirectory(directoryPath, label) {
|
|
15
|
+
const trimmed = (directoryPath || '').trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
throw new Error(label === 'Project' ? 'Project path is required.' : `${label} working directory is required.`);
|
|
18
|
+
}
|
|
19
|
+
const resolved = path_1.default.resolve(trimmed);
|
|
20
|
+
const stat = fs_1.default.existsSync(resolved) ? fs_1.default.statSync(resolved) : null;
|
|
21
|
+
if (!stat || !stat.isDirectory()) {
|
|
22
|
+
throw new Error(label === 'Project'
|
|
23
|
+
? 'Project path must point to an existing directory.'
|
|
24
|
+
: `${label} working directory is not available: ${resolved}`);
|
|
25
|
+
}
|
|
26
|
+
return resolved;
|
|
27
|
+
}
|
|
28
|
+
function scopeHome(scope) {
|
|
29
|
+
return scope === 'manager'
|
|
30
|
+
? (0, pack_home_1.resolvePackHome)('manager').contentRoot
|
|
31
|
+
: (0, pack_home_1.resolvePackHome)('org').contentRoot;
|
|
32
|
+
}
|
|
33
|
+
function resolveRunWorkingDirectory(scope, candidateProjectPath, defaultProjectPath) {
|
|
34
|
+
if (scope === 'manager') {
|
|
35
|
+
return ensureExistingDirectory(scopeHome('manager'), 'Manager');
|
|
36
|
+
}
|
|
37
|
+
if (scope === 'company') {
|
|
38
|
+
return ensureExistingDirectory(scopeHome('company'), 'Company');
|
|
39
|
+
}
|
|
40
|
+
return ensureExistingDirectory(candidateProjectPath || defaultProjectPath || '', 'Project');
|
|
41
|
+
}
|
|
42
|
+
function executionScopeFromConversation(scope, projectPath) {
|
|
43
|
+
if (scope === 'manager' || projectPath === conversation_store_1.MANAGER_SCOPE_KEY)
|
|
44
|
+
return 'manager';
|
|
45
|
+
if (scope === 'company' || projectPath === conversation_store_1.COMPANY_SCOPE_KEY)
|
|
46
|
+
return 'company';
|
|
47
|
+
return 'project';
|
|
48
|
+
}
|
|
49
|
+
function resolveHostWorkingDirectory(candidateProjectPath) {
|
|
50
|
+
const trimmed = (candidateProjectPath || '').trim();
|
|
51
|
+
if (trimmed === conversation_store_1.MANAGER_SCOPE_KEY) {
|
|
52
|
+
return ensureExistingDirectory(scopeHome('manager'), 'Manager');
|
|
53
|
+
}
|
|
54
|
+
if (trimmed === conversation_store_1.COMPANY_SCOPE_KEY) {
|
|
55
|
+
return ensureExistingDirectory(scopeHome('company'), 'Company');
|
|
56
|
+
}
|
|
57
|
+
return ensureExistingDirectory(trimmed, 'Agent');
|
|
58
|
+
}
|
|
59
|
+
function workingDirectoryExists(directoryPath) {
|
|
60
|
+
if (!directoryPath)
|
|
61
|
+
return false;
|
|
62
|
+
try {
|
|
63
|
+
return fs_1.default.statSync(path_1.default.resolve(directoryPath)).isDirectory();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|