fraim-framework 2.0.171 → 2.0.174

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.
Files changed (32) hide show
  1. package/dist/src/ai-hub/hosts.js +227 -6
  2. package/dist/src/ai-hub/server.js +1014 -35
  3. package/dist/src/cli/commands/add-ide.js +2 -0
  4. package/dist/src/cli/commands/cleanup-artifacts.js +39 -0
  5. package/dist/src/cli/commands/init-project.js +12 -5
  6. package/dist/src/cli/commands/sync.js +74 -7
  7. package/dist/src/cli/fraim.js +2 -0
  8. package/dist/src/cli/setup/ide-detector.js +6 -0
  9. package/dist/src/cli/utils/agent-adapters.js +40 -18
  10. package/dist/src/cli/utils/fraim-gitignore.js +13 -0
  11. package/dist/src/cli/utils/remote-sync.js +129 -53
  12. package/dist/src/cli/utils/user-config.js +12 -0
  13. package/dist/src/config/ai-manager-hiring.js +121 -0
  14. package/dist/src/config/compat.js +16 -0
  15. package/dist/src/config/feature-flags.js +25 -0
  16. package/dist/src/config/persona-capability-bundles.js +273 -0
  17. package/dist/src/config/persona-hiring.js +270 -0
  18. package/dist/src/config/portfolio-slug-overrides.js +17 -0
  19. package/dist/src/config/pricing.js +37 -0
  20. package/dist/src/config/stripe.js +43 -0
  21. package/dist/src/core/fraim-config-schema.generated.js +8 -2
  22. package/dist/src/core/utils/local-registry-resolver.js +26 -0
  23. package/dist/src/core/utils/project-fraim-paths.js +89 -2
  24. package/dist/src/first-run/session-service.js +9 -0
  25. package/dist/src/local-mcp-server/artifact-retention-cleanup.js +298 -0
  26. package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
  27. package/dist/src/local-mcp-server/stdio-server.js +42 -7
  28. package/package.json +5 -1
  29. package/public/ai-hub/index.html +205 -89
  30. package/public/ai-hub/review.css +12 -0
  31. package/public/ai-hub/script.js +1720 -240
  32. package/public/ai-hub/styles.css +473 -6
@@ -51,6 +51,7 @@ const mcp_server_registry_1 = require("../mcp/mcp-server-registry");
51
51
  const get_provider_client_1 = require("../api/get-provider-client");
52
52
  const provider_prompts_1 = require("../setup/provider-prompts");
53
53
  const provider_registry_1 = require("../providers/provider-registry");
54
+ const user_config_1 = require("../utils/user-config");
54
55
  const loadGlobalConfig = async () => {
55
56
  const globalConfigPath = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'config.json');
56
57
  if (!fs_1.default.existsSync(globalConfigPath)) {
@@ -410,6 +411,7 @@ const runAddIDE = async (options) => {
410
411
  try {
411
412
  await configureIDEMCP(ide, globalConfig.fraimKey, platformTokens, globalConfig.providerConfigs);
412
413
  results.successful.push(ide.name);
414
+ (0, user_config_1.addInstalledIde)((0, ide_detector_1.getAdapterConfigType)(ide));
413
415
  }
414
416
  catch (error) {
415
417
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@@ -0,0 +1,39 @@
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.cleanupArtifactsCommand = void 0;
7
+ exports.runCleanupArtifactsCommand = runCleanupArtifactsCommand;
8
+ const commander_1 = require("commander");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const artifact_retention_cleanup_1 = require("../../local-mcp-server/artifact-retention-cleanup");
11
+ function runCleanupArtifactsCommand(options) {
12
+ const activeUserEmail = options.user || process.env.FRAIM_ACTIVE_USER_EMAIL || '';
13
+ if (!activeUserEmail.trim()) {
14
+ throw new Error('Missing active user email. Pass --user <email> or set FRAIM_ACTIVE_USER_EMAIL.');
15
+ }
16
+ const workspaceRoot = node_path_1.default.resolve(options.workspaceRoot || options.root || process.cwd());
17
+ const report = (0, artifact_retention_cleanup_1.runArtifactCleanup)(workspaceRoot, {
18
+ activeUserEmail: activeUserEmail.trim(),
19
+ apply: Boolean(options.apply),
20
+ });
21
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
22
+ }
23
+ exports.cleanupArtifactsCommand = new commander_1.Command('cleanup-artifacts')
24
+ .alias('cleanup-artifact')
25
+ .description('Evaluate and optionally apply artifact retention cleanup for the current FRAIM workspace')
26
+ .option('--user <email>', 'Active user email used only for user-scoped cleanup categories')
27
+ .option('--workspace-root <path>', 'Workspace root containing fraim/config.json')
28
+ .option('--root <path>', 'Alias for --workspace-root')
29
+ .option('--apply', 'Delete eligible files. Without this flag the command performs a dry run.')
30
+ .action((options) => {
31
+ try {
32
+ runCleanupArtifactsCommand(options);
33
+ }
34
+ catch (err) {
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ process.stderr.write(`artifact cleanup failed: ${message}\n`);
37
+ process.exit(1);
38
+ }
39
+ });
@@ -18,6 +18,7 @@ const fraim_gitignore_1 = require("../utils/fraim-gitignore");
18
18
  const agent_adapters_1 = require("../utils/agent-adapters");
19
19
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
20
20
  const project_bootstrap_1 = require("../utils/project-bootstrap");
21
+ const user_config_1 = require("../utils/user-config");
21
22
  const checkGlobalSetup = () => {
22
23
  const fraimUserDir = process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim');
23
24
  const globalConfigPath = path_1.default.join(fraimUserDir, 'config.json');
@@ -178,20 +179,26 @@ const runInitProject = async (options = {}) => {
178
179
  else {
179
180
  result.warnings.push('Sync was skipped for this run.');
180
181
  }
181
- const codexAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'codex') ||
182
- (0, ide_detector_1.detectInstalledIDEs)('cli-runnable').some((ide) => ide.configType === 'codex');
182
+ const detectedIdes = [
183
+ ...(0, ide_detector_1.detectInstalledIDEs)(),
184
+ ...(0, ide_detector_1.detectInstalledIDEs)('cli-runnable')
185
+ ];
186
+ const detectedConfigTypes = [...new Set(detectedIdes.map((ide) => (0, ide_detector_1.getAdapterConfigType)(ide)))];
187
+ if (detectedConfigTypes.length > 0) {
188
+ (0, user_config_1.writeUserFraimConfig)({ installedIdes: detectedConfigTypes });
189
+ }
190
+ const codexAvailable = detectedIdes.some((ide) => ide.configType === 'codex');
183
191
  if (codexAvailable) {
184
192
  const codexLocalResult = (0, codex_local_config_1.ensureCodexLocalConfig)(projectRoot);
185
193
  const status = codexLocalResult.created ? 'Created' : codexLocalResult.updated ? 'Updated' : 'Verified';
186
194
  console.log(chalk_1.default.green(`${status} project Codex config at ${codexLocalResult.path}`));
187
195
  }
188
196
  // Enable token telemetry for Claude Code (user-level, applies to all projects)
189
- const claudeCodeAvailable = (0, ide_detector_1.detectInstalledIDEs)().some((ide) => ide.configType === 'claude-code') ||
190
- (0, ide_detector_1.detectInstalledIDEs)('cli-runnable').some((ide) => ide.configType === 'claude-code');
197
+ const claudeCodeAvailable = detectedIdes.some((ide) => ide.configType === 'claude-code');
191
198
  if (claudeCodeAvailable) {
192
199
  (0, claude_code_telemetry_1.ensureClaudeCodeTelemetryEnv)();
193
200
  }
194
- const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
201
+ const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, detectedConfigTypes.length > 0 ? detectedConfigTypes : null);
195
202
  if (adapterUpdates.length > 0) {
196
203
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
197
204
  }
@@ -47,6 +47,9 @@ const script_sync_utils_1 = require("../utils/script-sync-utils");
47
47
  const git_utils_1 = require("../../core/utils/git-utils");
48
48
  const agent_adapters_1 = require("../utils/agent-adapters");
49
49
  const fraim_gitignore_1 = require("../utils/fraim-gitignore");
50
+ const user_config_1 = require("../utils/user-config");
51
+ const ide_detector_1 = require("../setup/ide-detector");
52
+ const remote_sync_1 = require("../utils/remote-sync");
50
53
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
51
54
  function resolveExplicitLocalSyncUrl() {
52
55
  const candidates = [
@@ -83,9 +86,10 @@ function loadUserApiKey() {
83
86
  return undefined;
84
87
  }
85
88
  }
86
- function writeSyncMetadata(fraimDir, mode, remoteUrl) {
87
- fs_1.default.mkdirSync(fraimDir, { recursive: true });
88
- fs_1.default.writeFileSync(path_1.default.join(fraimDir, '.sync-metadata.json'), JSON.stringify({
89
+ function writeSyncMetadata(mode, remoteUrl) {
90
+ const userDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
91
+ fs_1.default.mkdirSync(userDir, { recursive: true });
92
+ fs_1.default.writeFileSync(path_1.default.join(userDir, '.sync-metadata.json'), JSON.stringify({
89
93
  localVersion: (0, version_utils_1.getFraimVersion)(),
90
94
  mode,
91
95
  remoteUrl,
@@ -109,6 +113,65 @@ function removeLegacyVersionFromConfig(fraimDir) {
109
113
  console.warn(chalk_1.default.yellow('Could not remove legacy version from config.json.'));
110
114
  }
111
115
  }
116
+ function resolveAllowedConfigTypes() {
117
+ const persisted = (0, user_config_1.getInstalledIdes)();
118
+ const liveDetected = [
119
+ ...(0, ide_detector_1.detectInstalledIDEs)(),
120
+ ...(0, ide_detector_1.detectInstalledIDEs)('cli-runnable')
121
+ ].map((ide) => (0, ide_detector_1.getAdapterConfigType)(ide));
122
+ const liveSet = [...new Set(liveDetected)];
123
+ if (!persisted) {
124
+ return liveSet.length > 0 ? liveSet : null;
125
+ }
126
+ const merged = [...new Set([...persisted, ...liveSet])];
127
+ return merged.length > 0 ? merged : null;
128
+ }
129
+ async function cleanupStaleAdapterFiles(projectRoot, allowedConfigTypes) {
130
+ if (!allowedConfigTypes) {
131
+ return;
132
+ }
133
+ (0, project_fraim_paths_1.assertWorkspaceFraimRoot)(projectRoot);
134
+ const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
135
+ const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
136
+ const execFileAsync = promisify(execFile);
137
+ for (const [relPath, configType] of Object.entries((0, agent_adapters_1.getAdapterConfigTypes)())) {
138
+ if (configType === 'standard' || allowedConfigTypes.includes(configType)) {
139
+ continue;
140
+ }
141
+ const fullPath = path_1.default.join(projectRoot, relPath);
142
+ if (!fs_1.default.existsSync(fullPath)) {
143
+ continue;
144
+ }
145
+ try {
146
+ const content = fs_1.default.readFileSync(fullPath, 'utf8');
147
+ if (!content.includes(remote_sync_1.SYNCED_CONTENT_BANNER_MARKER)) {
148
+ continue;
149
+ }
150
+ }
151
+ catch {
152
+ continue;
153
+ }
154
+ try {
155
+ await execFileAsync('git', ['rm', '--cached', relPath], { cwd: projectRoot });
156
+ }
157
+ catch {
158
+ // Not tracked or git unavailable — still clean up from disk and gitignore.
159
+ }
160
+ try {
161
+ (0, fraim_gitignore_1.addToFraimGitignore)(projectRoot, relPath);
162
+ }
163
+ catch {
164
+ // Best-effort.
165
+ }
166
+ try {
167
+ fs_1.default.unlinkSync(fullPath);
168
+ console.log(chalk_1.default.green(`Removed stale FRAIM adapter file: ${relPath}`));
169
+ }
170
+ catch {
171
+ // Best-effort.
172
+ }
173
+ }
174
+ }
112
175
  function failSync(mode, message) {
113
176
  if (mode === 'throw') {
114
177
  throw new Error(message);
@@ -222,9 +285,11 @@ const runSync = async (options) => {
222
285
  if (result.success) {
223
286
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
224
287
  removeLegacyVersionFromConfig(fraimDir);
225
- writeSyncMetadata(fraimDir, 'local', localUrl);
288
+ writeSyncMetadata('local', localUrl);
226
289
  refreshLocalIgnoreConfig();
227
- const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
290
+ const allowedTypes = resolveAllowedConfigTypes();
291
+ await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
292
+ const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, allowedTypes);
228
293
  if (adapterUpdates.length > 0) {
229
294
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
230
295
  }
@@ -267,9 +332,11 @@ const runSync = async (options) => {
267
332
  }
268
333
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
269
334
  removeLegacyVersionFromConfig(fraimDir);
270
- writeSyncMetadata(fraimDir, 'remote', config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
335
+ writeSyncMetadata('remote', config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
271
336
  refreshLocalIgnoreConfig();
272
- const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
337
+ const allowedTypes = resolveAllowedConfigTypes();
338
+ await cleanupStaleAdapterFiles(projectRoot, allowedTypes);
339
+ const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot, allowedTypes);
273
340
  if (adapterUpdates.length > 0) {
274
341
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
275
342
  }
@@ -55,6 +55,7 @@ const first_run_1 = require("./commands/first-run");
55
55
  const workspace_config_1 = require("./commands/workspace-config");
56
56
  const org_1 = require("./commands/org");
57
57
  const manager_1 = require("./commands/manager");
58
+ const cleanup_artifacts_1 = require("./commands/cleanup-artifacts");
58
59
  const fs_1 = __importDefault(require("fs"));
59
60
  const path_1 = __importDefault(require("path"));
60
61
  const program = new commander_1.Command();
@@ -99,6 +100,7 @@ program.addCommand(first_run_1.firstRunCommand);
99
100
  program.addCommand(workspace_config_1.workspaceConfigCommand);
100
101
  program.addCommand(org_1.orgCommand);
101
102
  program.addCommand(manager_1.managerCommand);
103
+ program.addCommand(cleanup_artifacts_1.cleanupArtifactsCommand);
102
104
  // Wait for async command initialization before parsing
103
105
  (async () => {
104
106
  // Import the initialization promise from setup command
@@ -4,10 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = exports.detectInstalledIDEs = exports.IDE_CONFIGS = void 0;
7
+ exports.getAdapterConfigType = getAdapterConfigType;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const os_1 = __importDefault(require("os"));
10
11
  const child_process_1 = require("child_process");
12
+ /** Returns the configType to use for adapter file filtering. Falls back to configType when adapterConfigType is not set. */
13
+ function getAdapterConfigType(ide) {
14
+ return ide.adapterConfigType ?? ide.configType;
15
+ }
11
16
  const expandPath = (filePath) => {
12
17
  if (filePath.startsWith('~/')) {
13
18
  return path_1.default.join(os_1.default.homedir(), filePath.slice(2));
@@ -151,6 +156,7 @@ exports.IDE_CONFIGS = [
151
156
  configPath: '~/.cursor/mcp.json',
152
157
  configFormat: 'json',
153
158
  configType: 'kiro',
159
+ adapterConfigType: 'cursor',
154
160
  invocationProfile: 'cursor-mention',
155
161
  detectMethod: detectCursor,
156
162
  alternativePaths: [
@@ -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.getAdapterConfigTypes = getAdapterConfigTypes;
6
7
  exports.ensureAgentAdapterFiles = ensureAgentAdapterFiles;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -47,7 +48,7 @@ function mergeCursorRule(existingContent, managedSection) {
47
48
  const mergedBody = mergeManagedSection(bodyWithoutLeadingFrontmatter, managedSection).trim();
48
49
  return `${ide_invocation_surfaces_1.CURSOR_MDC_FRONTMATTER}\n\n${mergedBody}\n`;
49
50
  }
50
- function getAdapterFiles() {
51
+ function getAdapterFiles(allowedConfigTypes = null) {
51
52
  const fraimRoot = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)().replace(/\/$/, '');
52
53
  const employeeJobsPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('ai-employee/jobs');
53
54
  const managerJobsPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('ai-manager/jobs');
@@ -109,27 +110,48 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
109
110
 
110
111
  @../AGENTS.md
111
112
  `;
112
- return [
113
- { path: 'AGENTS.md', content: markdownBody },
114
- { path: 'CLAUDE.md', content: markdownBody },
115
- { path: path_1.default.join('.github', 'copilot-instructions.md'), content: copilotBody },
116
- { path: CURSOR_RULE_PATH, content: cursorManagedBody },
117
- { path: VSCODE_FRAIM_PROMPT_PATH, content: vscodePrompt },
118
- { path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme },
119
- { path: CLAUDE_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSkillContent)() },
120
- { path: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeCommandShimContent)() },
121
- { path: CODEX_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildCodexSkillContent)() },
122
- { path: GROK_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildGrokSkillContent)() },
123
- { path: GEMINI_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildGeminiCommandContent)() },
124
- { path: GEMINI_PROJECT_INSTRUCTIONS_PATH, content: geminiProjectInstructions },
125
- { path: WINDSURF_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)() },
126
- { path: KIRO_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildKiroCommandContent)() }
113
+ const all = [
114
+ { path: 'AGENTS.md', content: markdownBody, configType: 'standard' },
115
+ { path: 'CLAUDE.md', content: markdownBody, configType: 'standard' },
116
+ { path: path_1.default.join('.github', 'copilot-instructions.md'), content: copilotBody, configType: 'vscode' },
117
+ { path: CURSOR_RULE_PATH, content: cursorManagedBody, configType: 'cursor' },
118
+ { path: VSCODE_FRAIM_PROMPT_PATH, content: vscodePrompt, configType: 'vscode' },
119
+ { path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme, configType: 'standard' },
120
+ { path: CLAUDE_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSkillContent)(), configType: 'claude-code' },
121
+ { path: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeCommandShimContent)(), configType: 'claude-code' },
122
+ { path: CODEX_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildCodexSkillContent)(), configType: 'codex' },
123
+ { path: GROK_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildGrokSkillContent)(), configType: 'grok' },
124
+ { path: GEMINI_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildGeminiCommandContent)(), configType: 'gemini-cli' },
125
+ { path: GEMINI_PROJECT_INSTRUCTIONS_PATH, content: geminiProjectInstructions, configType: 'gemini-cli' },
126
+ { path: WINDSURF_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)(), configType: 'windsurf' },
127
+ { path: KIRO_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildKiroCommandContent)(), configType: 'kiro' }
127
128
  ];
129
+ if (allowedConfigTypes === null) {
130
+ return all;
131
+ }
132
+ return all.filter(f => f.configType === 'standard' || allowedConfigTypes.includes(f.configType));
133
+ }
134
+ function getAdapterConfigTypes() {
135
+ const result = {};
136
+ for (const f of getAdapterFiles()) {
137
+ result[f.path.replace(/\\/g, '/')] = f.configType;
138
+ }
139
+ return result;
128
140
  }
129
- function ensureAgentAdapterFiles(projectRoot) {
141
+ function ensureAgentAdapterFiles(projectRoot, allowedConfigTypes = null) {
142
+ (0, project_fraim_paths_1.assertWorkspaceFraimRoot)(projectRoot);
130
143
  const updatedPaths = [];
131
- for (const file of getAdapterFiles()) {
144
+ const resolvedProjectRoot = path_1.default.resolve(projectRoot);
145
+ for (const file of getAdapterFiles(allowedConfigTypes)) {
132
146
  const fullPath = path_1.default.join(projectRoot, file.path);
147
+ const resolvedFullPath = path_1.default.resolve(fullPath);
148
+ const relativeToProject = path_1.default.relative(resolvedProjectRoot, resolvedFullPath);
149
+ if (relativeToProject === '' || relativeToProject.startsWith('..') || path_1.default.isAbsolute(relativeToProject)) {
150
+ throw new Error(`Refusing to write FRAIM adapter outside project root: ${resolvedFullPath}`);
151
+ }
152
+ if (file.path.replace(/\\/g, '/').startsWith(`${project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME}/`)) {
153
+ (0, project_fraim_paths_1.assertWorkspaceFraimPath)(projectRoot, fullPath);
154
+ }
133
155
  const dir = path_1.default.dirname(fullPath);
134
156
  if (!fs_1.default.existsSync(dir)) {
135
157
  fs_1.default.mkdirSync(dir, { recursive: true });
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ensureFraimSyncedContentLocallyExcluded = exports.removeFraimSyncedContentGitignoreBlock = exports.FRAIM_SYNC_GITIGNORE_ENTRIES = exports.FRAIM_SYNC_GITIGNORE_END = exports.FRAIM_SYNC_GITIGNORE_START = void 0;
7
+ exports.addToFraimGitignore = addToFraimGitignore;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  exports.FRAIM_SYNC_GITIGNORE_START = '# BEGIN FRAIM SYNCED CONTENT';
@@ -73,6 +74,18 @@ const removeFraimSyncedContentGitignoreBlock = (projectRoot) => {
73
74
  return true;
74
75
  };
75
76
  exports.removeFraimSyncedContentGitignoreBlock = removeFraimSyncedContentGitignoreBlock;
77
+ function addToFraimGitignore(projectRoot, entry) {
78
+ const gitignorePath = path_1.default.join(projectRoot, '.gitignore');
79
+ const existing = fs_1.default.existsSync(gitignorePath) ? fs_1.default.readFileSync(gitignorePath, 'utf8') : '';
80
+ const lines = existing.replace(/\r\n/g, '\n').split('\n');
81
+ if (lines.includes(entry)) {
82
+ return;
83
+ }
84
+ const appended = existing.trimEnd().length > 0
85
+ ? `${existing.trimEnd()}\n${entry}\n`
86
+ : `${entry}\n`;
87
+ fs_1.default.writeFileSync(gitignorePath, appended, 'utf8');
88
+ }
76
89
  const ensureFraimSyncedContentLocallyExcluded = (projectRoot) => {
77
90
  const gitignoreUpdated = (0, exports.removeFraimSyncedContentGitignoreBlock)(projectRoot);
78
91
  const excludePath = resolveGitInfoExcludePath(projectRoot);