fraim-framework 2.0.162 → 2.0.164
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/desktop-main.js +4 -1
- package/dist/src/ai-hub/hosts.js +97 -12
- package/dist/src/ai-hub/preferences.js +1 -1
- package/dist/src/ai-hub/server.js +49 -123
- package/dist/src/cli/commands/init-project.js +15 -14
- package/dist/src/cli/commands/sync.js +38 -0
- package/dist/src/cli/doctor/check-runner.js +3 -1
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +261 -2
- package/dist/src/cli/utils/git-org-sync.js +56 -0
- package/dist/src/cli/utils/org-migration.js +50 -0
- package/dist/src/cli/utils/org-pack-sync.js +208 -0
- package/dist/src/cli/utils/project-bootstrap.js +20 -7
- package/dist/src/cli/utils/user-config.js +68 -0
- package/dist/src/core/fraim-config-schema.generated.js +10 -0
- package/dist/src/first-run/types.js +8 -0
- package/dist/src/local-mcp-server/agent-token-prices.js +30 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +78 -29
- package/dist/src/local-mcp-server/stdio-server.js +30 -0
- package/index.js +1 -1
- package/package.json +2 -3
- package/public/ai-hub/index.html +5 -5
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
- package/public/ai-hub/review.css +15 -15
- package/public/ai-hub/script.js +254 -195
- package/public/ai-hub/styles.css +206 -16
- package/public/first-run/styles.css +73 -73
- package/dist/src/ai-hub/word-sideload.js +0 -95
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
|
@@ -273,7 +273,10 @@ async function bootstrap() {
|
|
|
273
273
|
const options = parseArgs(process.argv.slice(2));
|
|
274
274
|
// Single-instance lock — if another instance is already running, focus it
|
|
275
275
|
// and exit rather than spawning a second server + window.
|
|
276
|
-
|
|
276
|
+
// Skip when FRAIM_AI_HUB_FAKE_HOST=1 (test mode) so Playwright can launch
|
|
277
|
+
// a test instance alongside the real desktop app without the lock killing it.
|
|
278
|
+
const skipSingleInstance = process.env.FRAIM_AI_HUB_FAKE_HOST === '1';
|
|
279
|
+
const gotLock = skipSingleInstance || electron_1.app.requestSingleInstanceLock();
|
|
277
280
|
if (!gotLock) {
|
|
278
281
|
electron_1.app.quit();
|
|
279
282
|
return;
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -319,7 +319,13 @@ const EMPLOYEE_LABELS = {
|
|
|
319
319
|
codex: 'Codex',
|
|
320
320
|
claude: 'Claude Code',
|
|
321
321
|
gemini: 'Gemini CLI',
|
|
322
|
+
copilot: 'GitHub Copilot CLI',
|
|
322
323
|
};
|
|
324
|
+
// GitHub Copilot CLI binary name after `npm install -g @github/copilot`.
|
|
325
|
+
// The @github/copilot package installs a binary named `copilot` on PATH.
|
|
326
|
+
// Note: the package name is @github/copilot (NOT @github/copilot-cli which
|
|
327
|
+
// does not exist on npm). The binary is `copilot` (NOT `github-copilot-cli`).
|
|
328
|
+
const COPILOT_BINARY = 'copilot';
|
|
323
329
|
const executableName = (command) => command;
|
|
324
330
|
function quoteWindowsArg(value) {
|
|
325
331
|
if (value.length === 0) {
|
|
@@ -348,9 +354,15 @@ const availableByVersionProbe = (command) => {
|
|
|
348
354
|
});
|
|
349
355
|
return result.status === 0;
|
|
350
356
|
};
|
|
357
|
+
// Resolve the binary name for each agent tool.
|
|
358
|
+
function agentBinaryName(id) {
|
|
359
|
+
if (id === 'copilot')
|
|
360
|
+
return COPILOT_BINARY;
|
|
361
|
+
return executableName(id);
|
|
362
|
+
}
|
|
351
363
|
function detectEmployees() {
|
|
352
364
|
return Object.keys(EMPLOYEE_LABELS).map((id) => {
|
|
353
|
-
const available = availableByVersionProbe(
|
|
365
|
+
const available = availableByVersionProbe(agentBinaryName(id));
|
|
354
366
|
return {
|
|
355
367
|
id,
|
|
356
368
|
label: EMPLOYEE_LABELS[id],
|
|
@@ -424,17 +436,10 @@ function machineLevelStorageGuard(jobId) {
|
|
|
424
436
|
'- If the exact machine-level paths cannot be written, fail the phase and report the concrete filesystem error.',
|
|
425
437
|
].join('\n');
|
|
426
438
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
'Storage scope guardrail:',
|
|
432
|
-
'- Organization onboarding artifacts are machine-level, not repo-level.',
|
|
433
|
-
`- Required write targets: ${orgContext} and ${orgRules}.`,
|
|
434
|
-
'- Do not write, validate, call canonical, commit, or open a PR for repo-local fraim/personalized-employee/context/org_context.md or fraim/personalized-employee/rules/org_rules.md as substitutes.',
|
|
435
|
-
'- If the exact machine-level paths cannot be written, fail the phase and report the concrete filesystem error.',
|
|
436
|
-
].join('\n');
|
|
437
|
-
}
|
|
439
|
+
// organization-onboarding intentionally has no guardrail here: the job's
|
|
440
|
+
// submit phase is backend-aware and owns the write-path procedure (git PR /
|
|
441
|
+
// cloud publish / machine-level fallback), so duplicating it in runtime
|
|
442
|
+
// prompt text would be redundant (issue #563 review).
|
|
438
443
|
return null;
|
|
439
444
|
}
|
|
440
445
|
// If ~/.gemini/settings.json has a wrong/test FRAIM_API_KEY, patch it with the
|
|
@@ -560,6 +565,15 @@ function sharedBrowserHostConfig(hostId, env = process.env) {
|
|
|
560
565
|
}
|
|
561
566
|
return { args: [], env: { GEMINI_CLI_SYSTEM_SETTINGS_PATH: file } };
|
|
562
567
|
}
|
|
568
|
+
if (hostId === 'copilot') {
|
|
569
|
+
// GitHub Copilot CLI does not yet publish a documented per-invocation
|
|
570
|
+
// settings-file env var analogous to GEMINI_CLI_SYSTEM_SETTINGS_PATH.
|
|
571
|
+
// If one is discovered in a future release, write the ephemeral file here
|
|
572
|
+
// and return { args: [], env: { <COPILOT_SETTINGS_ENV_VAR>: file } }.
|
|
573
|
+
// Until then, return the Option-B no-op per spec R5.2 — the Hub's
|
|
574
|
+
// start-payload builder will inject a browser-guidance note instead.
|
|
575
|
+
return { args: [] };
|
|
576
|
+
}
|
|
563
577
|
return { args: [] };
|
|
564
578
|
}
|
|
565
579
|
function buildStartPlan(hostId, message, sessionId) {
|
|
@@ -586,6 +600,22 @@ function buildStartPlan(hostId, message, sessionId) {
|
|
|
586
600
|
env: browser.env,
|
|
587
601
|
};
|
|
588
602
|
}
|
|
603
|
+
if (hostId === 'copilot') {
|
|
604
|
+
// GitHub Copilot CLI headless invocation.
|
|
605
|
+
// --yolo auto-approves all tool permissions (analogous to
|
|
606
|
+
// --dangerously-skip-permissions for Claude Code). The task is provided
|
|
607
|
+
// via stdin; -p/--prompt requires inline text which is cumbersome for
|
|
608
|
+
// multi-line FRAIM instructions. The session id is self-assigned by the
|
|
609
|
+
// binary on first run; Hub captures it from the stream output
|
|
610
|
+
// (parseHostLine 'copilot' branch).
|
|
611
|
+
const browser = sharedBrowserHostConfig('copilot');
|
|
612
|
+
return {
|
|
613
|
+
command: COPILOT_BINARY,
|
|
614
|
+
args: ['--yolo', ...browser.args],
|
|
615
|
+
stdin: transformHeadlessFraimMessage(message, 'start'),
|
|
616
|
+
env: browser.env,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
589
619
|
const browser = sharedBrowserHostConfig('claude');
|
|
590
620
|
return {
|
|
591
621
|
command: executableName('claude'),
|
|
@@ -615,6 +645,17 @@ function buildContinuePlan(hostId, sessionId, message) {
|
|
|
615
645
|
env: browser.env,
|
|
616
646
|
};
|
|
617
647
|
}
|
|
648
|
+
if (hostId === 'copilot') {
|
|
649
|
+
// Resume an existing GitHub Copilot CLI session.
|
|
650
|
+
// --resume <sessionId> accepts the session id returned on the first run.
|
|
651
|
+
const browser = sharedBrowserHostConfig('copilot');
|
|
652
|
+
return {
|
|
653
|
+
command: COPILOT_BINARY,
|
|
654
|
+
args: ['--yolo', '--resume', sessionId, ...browser.args],
|
|
655
|
+
stdin: transformHeadlessFraimMessage(message, 'continue'),
|
|
656
|
+
env: browser.env,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
618
659
|
const browser = sharedBrowserHostConfig('claude');
|
|
619
660
|
return {
|
|
620
661
|
command: executableName('claude'),
|
|
@@ -666,6 +707,14 @@ function buildDirectStartPlan(hostId, message, sessionId) {
|
|
|
666
707
|
stdin: DIRECT_PREAMBLE + message,
|
|
667
708
|
};
|
|
668
709
|
}
|
|
710
|
+
if (hostId === 'copilot') {
|
|
711
|
+
// Direct (A/B) mode for Copilot: headless, no FRAIM MCP wiring.
|
|
712
|
+
return {
|
|
713
|
+
command: COPILOT_BINARY,
|
|
714
|
+
args: ['--yolo'],
|
|
715
|
+
stdin: DIRECT_PREAMBLE + message,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
669
718
|
return {
|
|
670
719
|
command: executableName('claude'),
|
|
671
720
|
args: [
|
|
@@ -696,6 +745,14 @@ function buildDirectContinuePlan(hostId, sessionId, message) {
|
|
|
696
745
|
stdin: DIRECT_PREAMBLE + message,
|
|
697
746
|
};
|
|
698
747
|
}
|
|
748
|
+
if (hostId === 'copilot') {
|
|
749
|
+
// Direct continue mode for Copilot: resume session, no FRAIM MCP wiring.
|
|
750
|
+
return {
|
|
751
|
+
command: COPILOT_BINARY,
|
|
752
|
+
args: ['--yolo', '--resume', sessionId],
|
|
753
|
+
stdin: DIRECT_PREAMBLE + message,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
699
756
|
return {
|
|
700
757
|
command: executableName('claude'),
|
|
701
758
|
args: [
|
|
@@ -764,6 +821,32 @@ function parseHostLine(hostId, line) {
|
|
|
764
821
|
return withSignal({ message: trimmed, raw: trimmed });
|
|
765
822
|
}
|
|
766
823
|
}
|
|
824
|
+
// GitHub Copilot CLI output: JSON stream where each event carries a `type`
|
|
825
|
+
// field. Known event shapes (from the agentic CLI stream):
|
|
826
|
+
// { "type": "session.started", "session_id": "..." } — session id
|
|
827
|
+
// { "type": "message", "role": "assistant", "content": "..." } — reply text
|
|
828
|
+
// { "type": "turn.completed", "usage": { ... } } — token usage (same shape as Codex)
|
|
829
|
+
// For any JSON event not matching the above, signal scanning (seekMentoring,
|
|
830
|
+
// agent identity) still runs because withSignal is applied to every parsed result.
|
|
831
|
+
// Non-JSON lines from Copilot are treated as plain-text employee messages.
|
|
832
|
+
if (hostId === 'copilot') {
|
|
833
|
+
try {
|
|
834
|
+
const parsed = JSON.parse(trimmed);
|
|
835
|
+
if (parsed.type === 'session.started' && typeof parsed.session_id === 'string' && parsed.session_id.length > 0) {
|
|
836
|
+
return withSignal({ sessionId: parsed.session_id, raw: trimmed });
|
|
837
|
+
}
|
|
838
|
+
if (parsed.type === 'message' && parsed.role === 'assistant' && typeof parsed.content === 'string') {
|
|
839
|
+
return withSignal({ message: parsed.content, raw: trimmed });
|
|
840
|
+
}
|
|
841
|
+
// All other JSON events: apply signal scanning and surface as raw.
|
|
842
|
+
return withSignal({ raw: trimmed });
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Non-JSON line from Copilot: treat as a plain-text employee message,
|
|
846
|
+
// same pattern as Gemini CLI's non-JSON output.
|
|
847
|
+
return withSignal({ message: trimmed, raw: trimmed });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
767
850
|
try {
|
|
768
851
|
const parsed = JSON.parse(trimmed);
|
|
769
852
|
if (parsed.type === 'system' && parsed.session_id) {
|
|
@@ -950,6 +1033,7 @@ class FakeHostRuntime {
|
|
|
950
1033
|
{ id: 'codex', label: 'Codex', available: true, detail: 'Test double employee.', supportsRaw: true },
|
|
951
1034
|
{ id: 'claude', label: 'Claude Code', available: true, detail: 'Test double employee.', supportsRaw: true },
|
|
952
1035
|
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Test double employee.', supportsRaw: true },
|
|
1036
|
+
{ id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Test double agent tool.', supportsRaw: true },
|
|
953
1037
|
];
|
|
954
1038
|
}
|
|
955
1039
|
detectEmployees() {
|
|
@@ -1032,6 +1116,7 @@ class ScriptedHostRuntime {
|
|
|
1032
1116
|
{ id: 'codex', label: 'Codex', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1033
1117
|
{ id: 'claude', label: 'Claude Code', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1034
1118
|
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1119
|
+
{ id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Scripted test double.', supportsRaw: true },
|
|
1035
1120
|
];
|
|
1036
1121
|
// Track each active run so the test can emit signals at it. Key is the
|
|
1037
1122
|
// sessionId we hand back on startRun; mapping sessionId → handlers
|
|
@@ -29,7 +29,7 @@ class AiHubPreferencesStore {
|
|
|
29
29
|
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
30
30
|
return {
|
|
31
31
|
projectPath: raw.projectPath || projectPath,
|
|
32
|
-
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
32
|
+
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
33
33
|
categoryId: typeof raw.categoryId === 'string' && raw.categoryId.length > 0 ? raw.categoryId : DEFAULT_CATEGORY,
|
|
34
34
|
recentJobIds: Array.isArray(raw.recentJobIds) ? raw.recentJobIds.filter((value) => typeof value === 'string') : [],
|
|
35
35
|
recentJobInstructions: (typeof raw.recentJobInstructions === 'object' && raw.recentJobInstructions !== null && !Array.isArray(raw.recentJobInstructions))
|
|
@@ -50,30 +50,7 @@ const https_1 = __importDefault(require("https"));
|
|
|
50
50
|
const types_1 = require("../first-run/types");
|
|
51
51
|
const learning_context_builder_1 = require("../local-mcp-server/learning-context-builder");
|
|
52
52
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
53
|
-
const
|
|
54
|
-
maestro: { seed: 'MAESTRO-founder-mode', bg: 'fde68a' },
|
|
55
|
-
beza: { seed: 'BEZA-strategist', bg: 'c7d2fe' },
|
|
56
|
-
pam: { seed: 'PAM-product', bg: 'ddd6fe' },
|
|
57
|
-
swen: { seed: 'SWEN-engineer', bg: 'bfdbfe' },
|
|
58
|
-
qasm: { seed: 'QASM-quality', bg: 'a7f3d0' },
|
|
59
|
-
huxley: { seed: 'HUXLEY-design', bg: 'fbcfe8' },
|
|
60
|
-
gautam: { seed: 'Gautam-marketing', bg: 'fed7aa' },
|
|
61
|
-
cela: { seed: 'CELA-legal', bg: 'cbd5e1' },
|
|
62
|
-
sekhar: { seed: 'SEKHAR-security', bg: 'fecaca' },
|
|
63
|
-
ashley: { seed: 'Ashley-assistant', bg: 'fde68a' },
|
|
64
|
-
mandy: { seed: 'MANDY-manager', bg: 'ede9fe' },
|
|
65
|
-
hari: { seed: 'HARI-hr', bg: 'ccfbf1' },
|
|
66
|
-
careena: { seed: 'CAREENA-career-coach', bg: 'e0f2fe' },
|
|
67
|
-
ricardo: { seed: 'RICARDO-recruiter', bg: 'ede9fe' },
|
|
68
|
-
sade: { seed: 'SADE-salesforce-dev', bg: 'bae6fd' },
|
|
69
|
-
sam: { seed: 'SAM-sales-manager', bg: 'bbf7d0' },
|
|
70
|
-
casey: { seed: 'CASEY-customer-cx', bg: 'fecdd3' },
|
|
71
|
-
};
|
|
72
|
-
function buildPersonaAvatarUrl(personaKey) {
|
|
73
|
-
const data = PERSONA_AVATAR_SEEDS[personaKey] ?? { seed: personaKey, bg: 'f1f5f9' };
|
|
74
|
-
const params = new URLSearchParams({ seed: data.seed, backgroundColor: data.bg, radius: '50' });
|
|
75
|
-
return `https://api.dicebear.com/9.x/notionists/svg?${params.toString()}`;
|
|
76
|
-
}
|
|
53
|
+
const persona_hiring_1 = require("../config/persona-hiring");
|
|
77
54
|
const catalog_1 = require("./catalog");
|
|
78
55
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
79
56
|
const hosts_1 = require("./hosts");
|
|
@@ -516,6 +493,7 @@ const HUB_TO_FIRST_RUN_ID = {
|
|
|
516
493
|
claude: 'claude-code',
|
|
517
494
|
codex: 'codex',
|
|
518
495
|
gemini: 'gemini-cli',
|
|
496
|
+
copilot: 'copilot-cli',
|
|
519
497
|
};
|
|
520
498
|
function hubAgentOption(hubId) {
|
|
521
499
|
const frId = HUB_TO_FIRST_RUN_ID[hubId];
|
|
@@ -601,19 +579,41 @@ function resolveSafeArtifactPath(rawPath, projectPath) {
|
|
|
601
579
|
return safeRoots.some((root) => pathWithin(root, resolved)) ? resolved : null;
|
|
602
580
|
}
|
|
603
581
|
function hubOpenFile(filePath) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
582
|
+
return new Promise((resolve, reject) => {
|
|
583
|
+
const args = process.platform === 'win32'
|
|
584
|
+
? [
|
|
585
|
+
'-NoProfile',
|
|
586
|
+
'-ExecutionPolicy',
|
|
587
|
+
'Bypass',
|
|
588
|
+
'-Command',
|
|
589
|
+
'& { param($p) try { Invoke-Item -LiteralPath $p; exit 0 } catch { Write-Error $_; exit 1 } }',
|
|
590
|
+
filePath,
|
|
591
|
+
]
|
|
592
|
+
: process.platform === 'darwin'
|
|
593
|
+
? [filePath]
|
|
594
|
+
: [filePath];
|
|
595
|
+
const command = process.platform === 'win32'
|
|
596
|
+
? 'powershell.exe'
|
|
597
|
+
: process.platform === 'darwin'
|
|
598
|
+
? 'open'
|
|
599
|
+
: 'xdg-open';
|
|
600
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
601
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
602
|
+
windowsHide: process.platform === 'win32',
|
|
603
|
+
});
|
|
604
|
+
let stderr = '';
|
|
605
|
+
child.stderr?.on('data', (chunk) => {
|
|
606
|
+
stderr += String(chunk);
|
|
607
|
+
});
|
|
608
|
+
child.on('error', reject);
|
|
609
|
+
child.on('close', (code) => {
|
|
610
|
+
if (code === 0) {
|
|
611
|
+
resolve();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
reject(new Error(stderr.trim() || `Open command failed with exit code ${code ?? 'unknown'}.`));
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
617
|
}
|
|
618
618
|
function buildManagedLoginCommand(command) {
|
|
619
619
|
const managedPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
|
|
@@ -1113,36 +1113,6 @@ class AiHubServer {
|
|
|
1113
1113
|
// Lightweight markdown → .docx. Shared by the GET (file path) and POST (inline
|
|
1114
1114
|
// content) export routes so a conversational deliverable with no on-disk file
|
|
1115
1115
|
// can still be downloaded for Word annotation.
|
|
1116
|
-
async markdownToDocxBuffer(md) {
|
|
1117
|
-
const html = md
|
|
1118
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
1119
|
-
// headings
|
|
1120
|
-
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
|
1121
|
-
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
1122
|
-
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
1123
|
-
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
1124
|
-
// bold, italic, code
|
|
1125
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
1126
|
-
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
1127
|
-
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
1128
|
-
// unordered lists
|
|
1129
|
-
.replace(/^[-*] (.+)$/gm, '<li>$1</li>')
|
|
1130
|
-
// horizontal rules
|
|
1131
|
-
.replace(/^---+$/gm, '<hr/>')
|
|
1132
|
-
// paragraphs (double newlines)
|
|
1133
|
-
.replace(/\n{2,}/g, '</p><p>')
|
|
1134
|
-
.replace(/^/, '<p>')
|
|
1135
|
-
.replace(/$/, '</p>')
|
|
1136
|
-
// clean up list items into a ul
|
|
1137
|
-
.replace(/(<li>.+<\/li>\n?)+/g, (m) => `<ul>${m}</ul>`);
|
|
1138
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1139
|
-
const htmlToDocx = require('html-to-docx');
|
|
1140
|
-
return await htmlToDocx(`<html><body>${html}</body></html>`, null, {
|
|
1141
|
-
table: { row: { cantSplit: true } },
|
|
1142
|
-
footer: false,
|
|
1143
|
-
pageNumber: false,
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
1116
|
prepareStartPayload(projectPath, hostId, selectedJobId, instructions) {
|
|
1147
1117
|
const explicit = (0, manager_turns_1.extractExplicitFraimInvocation)(instructions);
|
|
1148
1118
|
const resolvedJobId = explicit?.jobId || selectedJobId;
|
|
@@ -1201,7 +1171,7 @@ class AiHubServer {
|
|
|
1201
1171
|
key: bundle.personaKey,
|
|
1202
1172
|
displayName: bundle.catalogMetadata.displayName,
|
|
1203
1173
|
role: bundle.catalogMetadata.role,
|
|
1204
|
-
avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
|
|
1174
|
+
avatarUrl: (0, persona_hiring_1.buildPersonaAvatarUrl)(bundle.personaKey),
|
|
1205
1175
|
pricingLabel: bundle.catalogMetadata.pricingLabel,
|
|
1206
1176
|
status: 'locked',
|
|
1207
1177
|
hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
@@ -1239,7 +1209,7 @@ class AiHubServer {
|
|
|
1239
1209
|
key: bundle.personaKey,
|
|
1240
1210
|
displayName: bundle.catalogMetadata.displayName,
|
|
1241
1211
|
role: bundle.catalogMetadata.role,
|
|
1242
|
-
avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
|
|
1212
|
+
avatarUrl: (0, persona_hiring_1.buildPersonaAvatarUrl)(bundle.personaKey),
|
|
1243
1213
|
pricingLabel: hiredKeys.has(bundle.personaKey) ? '' : bundle.catalogMetadata.pricingLabel,
|
|
1244
1214
|
status: (hiredKeys.has(bundle.personaKey) ? 'hired' : 'locked'),
|
|
1245
1215
|
hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
|
|
@@ -1582,6 +1552,14 @@ class AiHubServer {
|
|
|
1582
1552
|
? path_1.default.resolve(body.projectPath)
|
|
1583
1553
|
: this.projectPath;
|
|
1584
1554
|
const loc = (0, learning_context_builder_1.resolveTeamContextFile)(projectPath, body.key);
|
|
1555
|
+
if (loc.managedByOrgSync || !loc.writePath) {
|
|
1556
|
+
// Enforcement only: block editing a synced org file (it would be
|
|
1557
|
+
// overwritten on next sync). The how-to-change procedure lives in the
|
|
1558
|
+
// organization-onboarding job, not in this error body (issue #563 review).
|
|
1559
|
+
return res.status(409).json({
|
|
1560
|
+
error: 'This organization file is managed by org sync and is read-only here.'
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1585
1563
|
const dest = path_1.default.resolve(loc.writePath);
|
|
1586
1564
|
// Path-traversal guard: the resolved destination must live under a
|
|
1587
1565
|
// personalized-employee directory (covers both ~/.fraim/... and repo-local).
|
|
@@ -1599,59 +1577,7 @@ class AiHubServer {
|
|
|
1599
1577
|
// Re-read so the client gets the canonical post-write state (present flips).
|
|
1600
1578
|
return res.json(readContextFile(projectPath, body.key));
|
|
1601
1579
|
});
|
|
1602
|
-
|
|
1603
|
-
// GET /api/ai-hub/artifact/export-docx?path=<abs-path-to-md>
|
|
1604
|
-
// Converts a markdown file to .docx using html-to-docx and streams the result.
|
|
1605
|
-
// The manager downloads it, annotates in Word, and saves it in place on disk
|
|
1606
|
-
// (no upload). The agent then reads the comments + tracked changes via the
|
|
1607
|
-
// `apply-docx-changes-to-md` skill (registry script extract-docx-edits.js)
|
|
1608
|
-
// during the address-feedback phase.
|
|
1609
|
-
this.app.get('/api/ai-hub/artifact/export-docx', async (req, res) => {
|
|
1610
|
-
const rawPath = typeof req.query.path === 'string' ? req.query.path : '';
|
|
1611
|
-
if (!rawPath)
|
|
1612
|
-
return res.status(400).json({ error: 'path is required.' });
|
|
1613
|
-
// Safety: path must be under the current workspace or a known safe root.
|
|
1614
|
-
const resolved = resolveSafeArtifactPath(rawPath, this.projectPath);
|
|
1615
|
-
if (!resolved)
|
|
1616
|
-
return res.status(403).json({ error: 'Path outside allowed roots.' });
|
|
1617
|
-
if (!fs_1.default.existsSync(resolved))
|
|
1618
|
-
return res.status(404).json({ error: 'File not found.' });
|
|
1619
|
-
try {
|
|
1620
|
-
const md = fs_1.default.readFileSync(resolved, 'utf8');
|
|
1621
|
-
const docxBuf = await this.markdownToDocxBuffer(md);
|
|
1622
|
-
const basename = path_1.default.basename(resolved, path_1.default.extname(resolved));
|
|
1623
|
-
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
|
1624
|
-
res.setHeader('Content-Disposition', `attachment; filename="${basename}.docx"`);
|
|
1625
|
-
res.send(docxBuf);
|
|
1626
|
-
}
|
|
1627
|
-
catch (err) {
|
|
1628
|
-
res.status(500).json({ error: err instanceof Error ? err.message : 'Export failed.' });
|
|
1629
|
-
}
|
|
1630
|
-
});
|
|
1631
|
-
// POST /api/ai-hub/artifact/export-docx { content, filename? }
|
|
1632
|
-
// Converts inline markdown (the employee's conversational deliverable) to .docx
|
|
1633
|
-
// when the run produced no on-disk file — e.g. an onboarding answer to an empty
|
|
1634
|
-
// repo. Keeps the "annotate in Word" review flow working instead of erroring
|
|
1635
|
-
// with "no local artifact path available".
|
|
1636
|
-
this.app.post('/api/ai-hub/artifact/export-docx', async (req, res) => {
|
|
1637
|
-
const content = typeof req.body?.content === 'string' ? req.body.content : '';
|
|
1638
|
-
if (!content.trim())
|
|
1639
|
-
return res.status(400).json({ error: 'content is required.' });
|
|
1640
|
-
const rawName = typeof req.body?.filename === 'string' && req.body.filename.trim()
|
|
1641
|
-
? req.body.filename.trim()
|
|
1642
|
-
: 'deliverable';
|
|
1643
|
-
const safeName = rawName.replace(/[^a-zA-Z0-9._ -]/g, '').replace(/\.docx?$/i, '').slice(0, 80) || 'deliverable';
|
|
1644
|
-
try {
|
|
1645
|
-
const docxBuf = await this.markdownToDocxBuffer(content);
|
|
1646
|
-
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
|
1647
|
-
res.setHeader('Content-Disposition', `attachment; filename="${safeName}.docx"`);
|
|
1648
|
-
res.send(docxBuf);
|
|
1649
|
-
}
|
|
1650
|
-
catch (err) {
|
|
1651
|
-
res.status(500).json({ error: err instanceof Error ? err.message : 'Export failed.' });
|
|
1652
|
-
}
|
|
1653
|
-
});
|
|
1654
|
-
this.app.post('/api/ai-hub/artifact/open', (req, res) => {
|
|
1580
|
+
this.app.post('/api/ai-hub/artifact/open', async (req, res) => {
|
|
1655
1581
|
const rawPath = typeof req.body?.path === 'string' ? req.body.path : '';
|
|
1656
1582
|
const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.length > 0
|
|
1657
1583
|
? path_1.default.resolve(req.body.projectPath)
|
|
@@ -1664,7 +1590,7 @@ class AiHubServer {
|
|
|
1664
1590
|
if (!fs_1.default.existsSync(resolved))
|
|
1665
1591
|
return res.status(404).json({ error: 'File not found.' });
|
|
1666
1592
|
try {
|
|
1667
|
-
hubOpenFile(resolved);
|
|
1593
|
+
await hubOpenFile(resolved);
|
|
1668
1594
|
return res.json({ ok: true, path: resolved });
|
|
1669
1595
|
}
|
|
1670
1596
|
catch (err) {
|
|
@@ -210,22 +210,23 @@ const runInitProject = async (options = {}) => {
|
|
|
210
210
|
if (!isMinimalConversationMode(preferredMode)) {
|
|
211
211
|
console.log(chalk_1.default.blue(` Platform: ${formatPlatformLabel(detection.provider)}`));
|
|
212
212
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const repo = detection.repository;
|
|
216
|
-
if (repo.owner && repo.name) {
|
|
217
|
-
console.log(chalk_1.default.gray(` Repository: ${repo.owner}/${repo.name}`));
|
|
218
|
-
}
|
|
219
|
-
else if (repo.organization && repo.project && repo.name) {
|
|
220
|
-
console.log(chalk_1.default.gray(` Organization: ${repo.organization}`));
|
|
221
|
-
console.log(chalk_1.default.gray(` Project: ${repo.project}`));
|
|
222
|
-
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
223
|
-
}
|
|
224
|
-
else if (repo.namespace && repo.name) {
|
|
225
|
-
console.log(chalk_1.default.gray(` Namespace: ${repo.namespace || '(none)'}`));
|
|
226
|
-
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
213
|
+
else {
|
|
214
|
+
console.log(chalk_1.default.blue(` Repository-backed project: ${formatPlatformLabel(detection.provider)}`));
|
|
227
215
|
}
|
|
228
216
|
}
|
|
217
|
+
const repo = detection.repository;
|
|
218
|
+
if (repo.owner && repo.name) {
|
|
219
|
+
console.log(chalk_1.default.gray(` Repository: ${repo.owner}/${repo.name}`));
|
|
220
|
+
}
|
|
221
|
+
else if (repo.organization && repo.project && repo.name) {
|
|
222
|
+
console.log(chalk_1.default.gray(` Organization: ${repo.organization}`));
|
|
223
|
+
console.log(chalk_1.default.gray(` Project: ${repo.project}`));
|
|
224
|
+
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
225
|
+
}
|
|
226
|
+
else if (repo.namespace && repo.name) {
|
|
227
|
+
console.log(chalk_1.default.gray(` Namespace: ${repo.namespace || '(none)'}`));
|
|
228
|
+
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
229
|
+
}
|
|
229
230
|
}
|
|
230
231
|
else {
|
|
231
232
|
result.mode = 'conversational';
|
|
@@ -144,6 +144,42 @@ const runSync = async (options) => {
|
|
|
144
144
|
console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
|
+
// Issue #563: refresh the machine-level org cache (~/.fraim/org/) from the
|
|
148
|
+
// configured org backend. Failures never fail the sync: an existing cache is
|
|
149
|
+
// served stale with its age (R2.3, R4.3).
|
|
150
|
+
const refreshOrgCache = async (remoteUrl, apiKey) => {
|
|
151
|
+
// Org sync is a network round-trip to the org backend. In automated
|
|
152
|
+
// tests there is no org configured and no server to reach, so skip it
|
|
153
|
+
// entirely rather than emit warnings or attempt a real request. Local
|
|
154
|
+
// dev (--local / FRAIM_LOCAL_SYNC) passes the loopback URL + 'local-dev'
|
|
155
|
+
// key below, which syncOrgCache treats as "no cloud org" unless a git
|
|
156
|
+
// backend is configured, so it degrades cleanly without this guard.
|
|
157
|
+
if (process.env.TEST_MODE === 'true')
|
|
158
|
+
return;
|
|
159
|
+
const { syncOrgCache } = await Promise.resolve().then(() => __importStar(require('../utils/org-pack-sync')));
|
|
160
|
+
const outcome = await syncOrgCache({ remoteUrl, apiKey });
|
|
161
|
+
if (outcome.status === 'synced') {
|
|
162
|
+
console.log(chalk_1.default.green(`Org context synced (${outcome.metadata.backend}, version ${outcome.metadata.version.slice(0, 12)})`));
|
|
163
|
+
}
|
|
164
|
+
else if (outcome.status === 'stale') {
|
|
165
|
+
console.log(chalk_1.default.yellow(`Org source unreachable. Using cached org context from ${Math.round(outcome.ageHours)}h ago. Will refresh when reachable.`));
|
|
166
|
+
}
|
|
167
|
+
else if (outcome.status === 'absent') {
|
|
168
|
+
console.log(chalk_1.default.yellow(`Org context not synced: ${outcome.error}`));
|
|
169
|
+
}
|
|
170
|
+
// 'disabled' (no org configured) stays silent.
|
|
171
|
+
// R8.1: one-time publish offer for legacy machine-local org files. The
|
|
172
|
+
// publish itself runs through organization-onboarding (propose-and-approve);
|
|
173
|
+
// sync only surfaces the offer and never moves files on its own (R8.2).
|
|
174
|
+
if (outcome.status !== 'disabled') {
|
|
175
|
+
const { detectLegacyOrgArtifacts } = await Promise.resolve().then(() => __importStar(require('../utils/org-migration')));
|
|
176
|
+
const legacy = detectLegacyOrgArtifacts();
|
|
177
|
+
if (legacy.length > 0) {
|
|
178
|
+
console.log(chalk_1.default.yellow(`Found ${legacy.length} legacy machine-local org file(s) at ~/.fraim/personalized-employee/.`));
|
|
179
|
+
console.log(chalk_1.default.yellow('Run the organization-onboarding job to publish them to your shared org backend; the originals are archived to ~/.fraim/backups/.'));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
147
183
|
const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
|
|
148
184
|
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
149
185
|
if (isGlobal && !options.skipUpdates) {
|
|
@@ -188,6 +224,7 @@ const runSync = async (options) => {
|
|
|
188
224
|
console.log(chalk_1.default.green(line));
|
|
189
225
|
}
|
|
190
226
|
}
|
|
227
|
+
await refreshOrgCache(localUrl, 'local-dev');
|
|
191
228
|
return;
|
|
192
229
|
}
|
|
193
230
|
console.error(chalk_1.default.red(`Local sync failed: ${result.error}`));
|
|
@@ -241,6 +278,7 @@ const runSync = async (options) => {
|
|
|
241
278
|
if (adapterUpdates.length > 0) {
|
|
242
279
|
console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
|
|
243
280
|
}
|
|
281
|
+
await refreshOrgCache(config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me', apiKey);
|
|
244
282
|
};
|
|
245
283
|
exports.runSync = runSync;
|
|
246
284
|
exports.syncCommand = new commander_1.Command('sync')
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.runChecks = runChecks;
|
|
9
9
|
const CHECK_TIMEOUT = 2000; // 2 seconds per check
|
|
10
|
-
|
|
10
|
+
// Issue #532: stdio MCP servers need up to 15s for handshake + npm version resolution
|
|
11
|
+
// on first call via fraim-mcp-latest-launcher; 20s gives a 5s buffer.
|
|
12
|
+
const MCP_CHECK_TIMEOUT = 20000; // 20 seconds for MCP connectivity checks
|
|
11
13
|
const TOTAL_TIMEOUT = 30000; // 30 seconds total
|
|
12
14
|
// Simple logger for doctor command (optional, falls back to no-op)
|
|
13
15
|
const logger = {
|