fraim-framework 2.0.163 → 2.0.165

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.
@@ -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
- const gotLock = electron_1.app.requestSingleInstanceLock();
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;
@@ -436,17 +436,10 @@ function machineLevelStorageGuard(jobId) {
436
436
  '- If the exact machine-level paths cannot be written, fail the phase and report the concrete filesystem error.',
437
437
  ].join('\n');
438
438
  }
439
- if (normalized === 'organization-onboarding') {
440
- const orgContext = path_1.default.join(userFraim, 'personalized-employee', 'context', 'org_context.md');
441
- const orgRules = path_1.default.join(userFraim, 'personalized-employee', 'rules', 'org_rules.md');
442
- return [
443
- 'Storage scope guardrail:',
444
- '- Organization onboarding artifacts are machine-level, not repo-level.',
445
- `- Required write targets: ${orgContext} and ${orgRules}.`,
446
- '- 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.',
447
- '- If the exact machine-level paths cannot be written, fail the phase and report the concrete filesystem error.',
448
- ].join('\n');
449
- }
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).
450
443
  return null;
451
444
  }
452
445
  // If ~/.gemini/settings.json has a wrong/test FRAIM_API_KEY, patch it with the
@@ -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 PERSONA_AVATAR_SEEDS = {
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");
@@ -602,19 +579,41 @@ function resolveSafeArtifactPath(rawPath, projectPath) {
602
579
  return safeRoots.some((root) => pathWithin(root, resolved)) ? resolved : null;
603
580
  }
604
581
  function hubOpenFile(filePath) {
605
- if (process.platform === 'win32') {
606
- (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-Command', 'Start-Process -LiteralPath $args[0]', filePath], {
607
- detached: true,
608
- stdio: 'ignore',
609
- windowsHide: true,
610
- }).unref();
611
- return;
612
- }
613
- if (process.platform === 'darwin') {
614
- (0, child_process_1.spawn)('open', [filePath], { detached: true, stdio: 'ignore' }).unref();
615
- return;
616
- }
617
- (0, child_process_1.spawn)('xdg-open', [filePath], { detached: true, stdio: 'ignore' }).unref();
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
+ });
618
617
  }
619
618
  function buildManagedLoginCommand(command) {
620
619
  const managedPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
@@ -1114,36 +1113,6 @@ class AiHubServer {
1114
1113
  // Lightweight markdown → .docx. Shared by the GET (file path) and POST (inline
1115
1114
  // content) export routes so a conversational deliverable with no on-disk file
1116
1115
  // can still be downloaded for Word annotation.
1117
- async markdownToDocxBuffer(md) {
1118
- const html = md
1119
- .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1120
- // headings
1121
- .replace(/^#### (.+)$/gm, '<h4>$1</h4>')
1122
- .replace(/^### (.+)$/gm, '<h3>$1</h3>')
1123
- .replace(/^## (.+)$/gm, '<h2>$1</h2>')
1124
- .replace(/^# (.+)$/gm, '<h1>$1</h1>')
1125
- // bold, italic, code
1126
- .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
1127
- .replace(/\*(.+?)\*/g, '<em>$1</em>')
1128
- .replace(/`(.+?)`/g, '<code>$1</code>')
1129
- // unordered lists
1130
- .replace(/^[-*] (.+)$/gm, '<li>$1</li>')
1131
- // horizontal rules
1132
- .replace(/^---+$/gm, '<hr/>')
1133
- // paragraphs (double newlines)
1134
- .replace(/\n{2,}/g, '</p><p>')
1135
- .replace(/^/, '<p>')
1136
- .replace(/$/, '</p>')
1137
- // clean up list items into a ul
1138
- .replace(/(<li>.+<\/li>\n?)+/g, (m) => `<ul>${m}</ul>`);
1139
- // eslint-disable-next-line @typescript-eslint/no-var-requires
1140
- const htmlToDocx = require('html-to-docx');
1141
- return await htmlToDocx(`<html><body>${html}</body></html>`, null, {
1142
- table: { row: { cantSplit: true } },
1143
- footer: false,
1144
- pageNumber: false,
1145
- });
1146
- }
1147
1116
  prepareStartPayload(projectPath, hostId, selectedJobId, instructions) {
1148
1117
  const explicit = (0, manager_turns_1.extractExplicitFraimInvocation)(instructions);
1149
1118
  const resolvedJobId = explicit?.jobId || selectedJobId;
@@ -1202,7 +1171,7 @@ class AiHubServer {
1202
1171
  key: bundle.personaKey,
1203
1172
  displayName: bundle.catalogMetadata.displayName,
1204
1173
  role: bundle.catalogMetadata.role,
1205
- avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
1174
+ avatarUrl: (0, persona_hiring_1.buildPersonaAvatarUrl)(bundle.personaKey),
1206
1175
  pricingLabel: bundle.catalogMetadata.pricingLabel,
1207
1176
  status: 'locked',
1208
1177
  hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
@@ -1240,7 +1209,7 @@ class AiHubServer {
1240
1209
  key: bundle.personaKey,
1241
1210
  displayName: bundle.catalogMetadata.displayName,
1242
1211
  role: bundle.catalogMetadata.role,
1243
- avatarUrl: buildPersonaAvatarUrl(bundle.personaKey),
1212
+ avatarUrl: (0, persona_hiring_1.buildPersonaAvatarUrl)(bundle.personaKey),
1244
1213
  pricingLabel: hiredKeys.has(bundle.personaKey) ? '' : bundle.catalogMetadata.pricingLabel,
1245
1214
  status: (hiredKeys.has(bundle.personaKey) ? 'hired' : 'locked'),
1246
1215
  hireUrl: buildHubPersonaHireUrl(bundle.personaKey, bundle.defaultHireMode),
@@ -1583,6 +1552,14 @@ class AiHubServer {
1583
1552
  ? path_1.default.resolve(body.projectPath)
1584
1553
  : this.projectPath;
1585
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
+ }
1586
1563
  const dest = path_1.default.resolve(loc.writePath);
1587
1564
  // Path-traversal guard: the resolved destination must live under a
1588
1565
  // personalized-employee directory (covers both ~/.fraim/... and repo-local).
@@ -1600,59 +1577,7 @@ class AiHubServer {
1600
1577
  // Re-read so the client gets the canonical post-write state (present flips).
1601
1578
  return res.json(readContextFile(projectPath, body.key));
1602
1579
  });
1603
- // ── Issue #512 R7: Artifact export (md → docx) ──────────────────────────
1604
- // GET /api/ai-hub/artifact/export-docx?path=<abs-path-to-md>
1605
- // Converts a markdown file to .docx using html-to-docx and streams the result.
1606
- // The manager downloads it, annotates in Word, and saves it in place on disk
1607
- // (no upload). The agent then reads the comments + tracked changes via the
1608
- // `apply-docx-changes-to-md` skill (registry script extract-docx-edits.js)
1609
- // during the address-feedback phase.
1610
- this.app.get('/api/ai-hub/artifact/export-docx', async (req, res) => {
1611
- const rawPath = typeof req.query.path === 'string' ? req.query.path : '';
1612
- if (!rawPath)
1613
- return res.status(400).json({ error: 'path is required.' });
1614
- // Safety: path must be under the current workspace or a known safe root.
1615
- const resolved = resolveSafeArtifactPath(rawPath, this.projectPath);
1616
- if (!resolved)
1617
- return res.status(403).json({ error: 'Path outside allowed roots.' });
1618
- if (!fs_1.default.existsSync(resolved))
1619
- return res.status(404).json({ error: 'File not found.' });
1620
- try {
1621
- const md = fs_1.default.readFileSync(resolved, 'utf8');
1622
- const docxBuf = await this.markdownToDocxBuffer(md);
1623
- const basename = path_1.default.basename(resolved, path_1.default.extname(resolved));
1624
- res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
1625
- res.setHeader('Content-Disposition', `attachment; filename="${basename}.docx"`);
1626
- res.send(docxBuf);
1627
- }
1628
- catch (err) {
1629
- res.status(500).json({ error: err instanceof Error ? err.message : 'Export failed.' });
1630
- }
1631
- });
1632
- // POST /api/ai-hub/artifact/export-docx { content, filename? }
1633
- // Converts inline markdown (the employee's conversational deliverable) to .docx
1634
- // when the run produced no on-disk file — e.g. an onboarding answer to an empty
1635
- // repo. Keeps the "annotate in Word" review flow working instead of erroring
1636
- // with "no local artifact path available".
1637
- this.app.post('/api/ai-hub/artifact/export-docx', async (req, res) => {
1638
- const content = typeof req.body?.content === 'string' ? req.body.content : '';
1639
- if (!content.trim())
1640
- return res.status(400).json({ error: 'content is required.' });
1641
- const rawName = typeof req.body?.filename === 'string' && req.body.filename.trim()
1642
- ? req.body.filename.trim()
1643
- : 'deliverable';
1644
- const safeName = rawName.replace(/[^a-zA-Z0-9._ -]/g, '').replace(/\.docx?$/i, '').slice(0, 80) || 'deliverable';
1645
- try {
1646
- const docxBuf = await this.markdownToDocxBuffer(content);
1647
- res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
1648
- res.setHeader('Content-Disposition', `attachment; filename="${safeName}.docx"`);
1649
- res.send(docxBuf);
1650
- }
1651
- catch (err) {
1652
- res.status(500).json({ error: err instanceof Error ? err.message : 'Export failed.' });
1653
- }
1654
- });
1655
- this.app.post('/api/ai-hub/artifact/open', (req, res) => {
1580
+ this.app.post('/api/ai-hub/artifact/open', async (req, res) => {
1656
1581
  const rawPath = typeof req.body?.path === 'string' ? req.body.path : '';
1657
1582
  const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.length > 0
1658
1583
  ? path_1.default.resolve(req.body.projectPath)
@@ -1665,7 +1590,7 @@ class AiHubServer {
1665
1590
  if (!fs_1.default.existsSync(resolved))
1666
1591
  return res.status(404).json({ error: 'File not found.' });
1667
1592
  try {
1668
- hubOpenFile(resolved);
1593
+ await hubOpenFile(resolved);
1669
1594
  return res.json({ ok: true, path: resolved });
1670
1595
  }
1671
1596
  catch (err) {
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -9,7 +42,6 @@ const chalk_1 = __importDefault(require("chalk"));
9
42
  const child_process_1 = require("child_process");
10
43
  const server_1 = require("../../first-run/server");
11
44
  const session_service_1 = require("../../first-run/session-service");
12
- const server_2 = require("../../ai-hub/server");
13
45
  function openBrowser(url) {
14
46
  try {
15
47
  if (process.platform === 'win32') {
@@ -45,7 +77,10 @@ const runFirstRun = async (options) => {
45
77
  projectRoot: options.projectRoot,
46
78
  });
47
79
  const server = new server_1.FirstRunServer({ sessionService });
48
- const port = await (0, server_2.findAvailablePort)(43120);
80
+ // Lazy import: ai-hub/server pulls server-only code not shipped in the client
81
+ // package; keep it out of the CLI startup graph so the packed client loads (#422).
82
+ const { findAvailablePort } = await Promise.resolve().then(() => __importStar(require('../../ai-hub/server')));
83
+ const port = await findAvailablePort(43120);
49
84
  const url = `http://127.0.0.1:${port}/first-run/`;
50
85
  await server.start(port);
51
86
  console.log(chalk_1.default.blue('Starting FRAIM first-run...'));
@@ -1,11 +1,46 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.hubCommand = void 0;
7
40
  const commander_1 = require("commander");
8
- const server_1 = require("../../ai-hub/server");
41
+ // ai-hub/server pulls in server-only code (persona-hiring etc.) that is not
42
+ // shipped in the client package; it is imported lazily inside the action below
43
+ // to keep it out of the CLI startup graph so the packed client loads (issue #422).
9
44
  const git_utils_1 = require("../../core/utils/git-utils");
10
45
  const path_1 = __importDefault(require("path"));
11
46
  const child_process_1 = require("child_process");
@@ -69,13 +104,14 @@ exports.hubCommand = new commander_1.Command('hub')
69
104
  .option('--no-open', 'Do not open the hub after startup')
70
105
  .option('--browser', 'Open in the default browser instead of the desktop shell')
71
106
  .action(async (options) => {
107
+ const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('../../ai-hub/server')));
72
108
  const preferredPort = options.port || (0, git_utils_1.getPort)() + 100;
73
109
  const projectPath = path_1.default.resolve(options.projectPath || process.cwd());
74
110
  if (options.open) {
75
111
  const openedDesktop = !options.browser && openDesktopWindow(projectPath, preferredPort);
76
112
  if (!openedDesktop) {
77
- const port = await (0, server_1.findAvailablePort)(preferredPort);
78
- const server = new server_1.AiHubServer({ projectPath });
113
+ const port = await findAvailablePort(preferredPort);
114
+ const server = new AiHubServer({ projectPath });
79
115
  await server.start(port);
80
116
  const url = `http://127.0.0.1:${port}/ai-hub/`;
81
117
  console.log(`AI Hub running at ${url}`);
@@ -87,8 +123,8 @@ exports.hubCommand = new commander_1.Command('hub')
87
123
  console.log(`Project path: ${projectPath}`);
88
124
  return;
89
125
  }
90
- const port = await (0, server_1.findAvailablePort)(preferredPort);
91
- const server = new server_1.AiHubServer({ projectPath });
126
+ const port = await findAvailablePort(preferredPort);
127
+ const server = new AiHubServer({ projectPath });
92
128
  await server.start(port);
93
129
  const url = `http://127.0.0.1:${port}/ai-hub/`;
94
130
  console.log(`AI Hub running at ${url}`);
@@ -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
- if (!isMinimalConversationMode(preferredMode)) {
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')
@@ -0,0 +1,56 @@
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.fetchOrgRepoSnapshot = fetchOrgRepoSnapshot;
7
+ /**
8
+ * Shallow snapshot of a customer-owned org repo (issue #563, git backend).
9
+ *
10
+ * Uses execSync git like the rest of the CLI (see core/utils/git-utils.ts);
11
+ * no new git dependency. The snapshot is a depth-1 clone of the default
12
+ * branch into a temp directory the caller copies from and then cleans up.
13
+ */
14
+ const child_process_1 = require("child_process");
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const path_1 = __importDefault(require("path"));
18
+ /**
19
+ * Org repo URLs are restricted to real transport schemes plus scp-style
20
+ * SSH shorthand. This blocks git's command-executing transports
21
+ * (`ext::`, `fd::`) and option injection via URLs starting with `-`.
22
+ */
23
+ const ALLOWED_GIT_URL = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[\w.-]+@[\w.-]+:)/;
24
+ /**
25
+ * Shallow-clone the org repo's default branch (R7.4). Throws on any git
26
+ * failure; callers translate failures into stale/absent outcomes.
27
+ */
28
+ function fetchOrgRepoSnapshot(gitUrl) {
29
+ if (!ALLOWED_GIT_URL.test(gitUrl)) {
30
+ throw new Error(`Org repo URL has an unsupported scheme: ${gitUrl}`);
31
+ }
32
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-org-snap-'));
33
+ try {
34
+ // Arg-array exec (no shell) and `--` so the URL can never be parsed
35
+ // as a git option or shell metacharacters.
36
+ (0, child_process_1.execFileSync)('git', ['clone', '--depth=1', '--quiet', '--', gitUrl, '.'], {
37
+ cwd: dir,
38
+ stdio: ['ignore', 'pipe', 'pipe'],
39
+ timeout: 60_000
40
+ });
41
+ const sha = (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], {
42
+ cwd: dir,
43
+ encoding: 'utf8',
44
+ stdio: ['ignore', 'pipe', 'pipe']
45
+ }).trim();
46
+ return {
47
+ dir,
48
+ sha,
49
+ cleanup: () => fs_1.default.rmSync(dir, { recursive: true, force: true })
50
+ };
51
+ }
52
+ catch (error) {
53
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
54
+ throw error;
55
+ }
56
+ }
@@ -0,0 +1,50 @@
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.detectLegacyOrgArtifacts = detectLegacyOrgArtifacts;
7
+ exports.archiveLegacyOrgArtifacts = archiveLegacyOrgArtifacts;
8
+ /**
9
+ * Legacy org artifact migration (issue #563, R8).
10
+ *
11
+ * Before #563, organization-onboarding wrote org_context.md and org_rules.md
12
+ * machine-locally under ~/.fraim/personalized-employee/. These helpers detect
13
+ * those files for a one-time publish offer and archive them (never delete)
14
+ * once the user accepts. Declining is safe: detection is read-only and the
15
+ * legacy resolution tier keeps working (AC8).
16
+ *
17
+ * Manager files are personal by design and are never migrated (R3.3).
18
+ */
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
22
+ const LEGACY_ORG_RELATIVE_PATHS = [
23
+ 'context/org_context.md',
24
+ 'rules/org_rules.md'
25
+ ];
26
+ function detectLegacyOrgArtifacts() {
27
+ const root = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee');
28
+ const detected = [];
29
+ for (const relativePath of LEGACY_ORG_RELATIVE_PATHS) {
30
+ const absolutePath = path_1.default.join(root, relativePath);
31
+ if (fs_1.default.existsSync(absolutePath)) {
32
+ detected.push({ absolutePath, relativePath });
33
+ }
34
+ }
35
+ return detected;
36
+ }
37
+ /**
38
+ * Move legacy org artifacts into a timestamped directory under
39
+ * ~/.fraim/backups/ (R8.1). Returns the backup directory path.
40
+ */
41
+ function archiveLegacyOrgArtifacts(artifacts) {
42
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
43
+ const backupDir = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'backups', `org-migration-${stamp}`);
44
+ for (const artifact of artifacts) {
45
+ const destination = path_1.default.join(backupDir, artifact.relativePath);
46
+ fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
47
+ fs_1.default.renameSync(artifact.absolutePath, destination);
48
+ }
49
+ return backupDir;
50
+ }