fraim 2.0.163 → 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 +4 -11
- package/dist/src/ai-hub/server.js +48 -123
- package/dist/src/cli/commands/init-project.js +15 -14
- package/dist/src/cli/commands/sync.js +38 -0
- 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/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 +1 -2
- package/public/ai-hub/index.html +2 -2
- 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 +70 -78
- package/public/ai-hub/styles.css +173 -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
|
@@ -0,0 +1,208 @@
|
|
|
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.ORG_CACHE_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_CACHE_DIRNAME = void 0;
|
|
7
|
+
exports.getOrgCacheDir = getOrgCacheDir;
|
|
8
|
+
exports.readOrgCacheMetadata = readOrgCacheMetadata;
|
|
9
|
+
exports.getOrgCacheAgeHours = getOrgCacheAgeHours;
|
|
10
|
+
exports.syncOrgCache = syncOrgCache;
|
|
11
|
+
/**
|
|
12
|
+
* Org cache sync (issue #563).
|
|
13
|
+
*
|
|
14
|
+
* Materializes the organization's shared context/rules/learnings into the
|
|
15
|
+
* managed read-only cache at ~/.fraim/org/, from either backend:
|
|
16
|
+
* - git: shallow snapshot of the customer-owned org repo (R7)
|
|
17
|
+
* - fraim-cloud: GET /api/org/pack from the FRAIM server (R6)
|
|
18
|
+
*
|
|
19
|
+
* Cache files are managed content: marked, overwritten on every sync, and
|
|
20
|
+
* never a write target for agents (R2.2). A failed refresh never throws:
|
|
21
|
+
* an existing cache is served stale with its age (R2.3, R4.3).
|
|
22
|
+
*/
|
|
23
|
+
const axios_1 = __importDefault(require("axios"));
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
27
|
+
const user_config_1 = require("./user-config");
|
|
28
|
+
const git_org_sync_1 = require("./git-org-sync");
|
|
29
|
+
exports.ORG_CACHE_DIRNAME = 'org';
|
|
30
|
+
exports.ORG_SYNC_METADATA_FILE = '.org-sync-metadata.json';
|
|
31
|
+
exports.ORG_CACHE_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
|
|
32
|
+
/** Subdirectories of the org pack that sync into the cache (spec R7.1). */
|
|
33
|
+
const ORG_PACK_DIRS = ['context', 'rules', 'learnings'];
|
|
34
|
+
/**
|
|
35
|
+
* Pack files must stay inside the three org pack directories. Applied to
|
|
36
|
+
* every relativePath before it touches the filesystem, so a compromised
|
|
37
|
+
* backend response can never write outside the cache directory.
|
|
38
|
+
*/
|
|
39
|
+
const SAFE_PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
|
|
40
|
+
function getOrgCacheDir() {
|
|
41
|
+
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.ORG_CACHE_DIRNAME);
|
|
42
|
+
}
|
|
43
|
+
function readOrgCacheMetadata() {
|
|
44
|
+
try {
|
|
45
|
+
const metadataPath = path_1.default.join(getOrgCacheDir(), exports.ORG_SYNC_METADATA_FILE);
|
|
46
|
+
if (!fs_1.default.existsSync(metadataPath))
|
|
47
|
+
return null;
|
|
48
|
+
return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function getOrgCacheAgeHours() {
|
|
55
|
+
const metadata = readOrgCacheMetadata();
|
|
56
|
+
if (!metadata?.syncedAt)
|
|
57
|
+
return null;
|
|
58
|
+
const syncedAt = Date.parse(metadata.syncedAt);
|
|
59
|
+
if (Number.isNaN(syncedAt))
|
|
60
|
+
return null;
|
|
61
|
+
return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
|
|
62
|
+
}
|
|
63
|
+
function decorateManagedOrgFile(content, backend) {
|
|
64
|
+
const normalized = content.replace(/^/, '');
|
|
65
|
+
if (normalized.startsWith(exports.ORG_CACHE_MANAGED_HEADER))
|
|
66
|
+
return normalized;
|
|
67
|
+
const writePath = backend === 'git'
|
|
68
|
+
? 'open a pull request against your organization repo'
|
|
69
|
+
: 'update it through the organization-onboarding flow';
|
|
70
|
+
const marker = [
|
|
71
|
+
exports.ORG_CACHE_MANAGED_HEADER,
|
|
72
|
+
'> [!IMPORTANT]',
|
|
73
|
+
'> Synced from your organization\'s shared context. Local edits are overwritten on the next sync.',
|
|
74
|
+
`> To change this content, ${writePath}.`,
|
|
75
|
+
''
|
|
76
|
+
].join('\n');
|
|
77
|
+
return `${marker}\n${normalized}`;
|
|
78
|
+
}
|
|
79
|
+
function collectGitPackFiles(snapshotDir) {
|
|
80
|
+
const files = [];
|
|
81
|
+
for (const dirName of ORG_PACK_DIRS) {
|
|
82
|
+
const dirPath = path_1.default.join(snapshotDir, dirName);
|
|
83
|
+
if (!fs_1.default.existsSync(dirPath))
|
|
84
|
+
continue;
|
|
85
|
+
for (const entry of fs_1.default.readdirSync(dirPath, { withFileTypes: true })) {
|
|
86
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
87
|
+
continue;
|
|
88
|
+
files.push({
|
|
89
|
+
relativePath: `${dirName}/${entry.name}`,
|
|
90
|
+
content: fs_1.default.readFileSync(path_1.default.join(dirPath, entry.name), 'utf8')
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
96
|
+
async function fetchCloudPack(remoteUrl, apiKey) {
|
|
97
|
+
const response = await axios_1.default.get(`${remoteUrl.replace(/\/$/, '')}/api/org/pack`, {
|
|
98
|
+
headers: { 'x-api-key': apiKey },
|
|
99
|
+
timeout: 30_000
|
|
100
|
+
});
|
|
101
|
+
const files = Array.isArray(response.data?.files) ? response.data.files : [];
|
|
102
|
+
return {
|
|
103
|
+
files: files.filter((f) => typeof f?.relativePath === 'string' &&
|
|
104
|
+
SAFE_PACK_RELATIVE_PATH.test(f.relativePath) &&
|
|
105
|
+
typeof f?.content === 'string'),
|
|
106
|
+
version: String(response.data?.version ?? '0')
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Replace the cache contents atomically: stage fully, then swap. */
|
|
110
|
+
function materializeCache(files, metadata) {
|
|
111
|
+
const cacheDir = getOrgCacheDir();
|
|
112
|
+
const stagingDir = `${cacheDir}.staging-${process.pid}`;
|
|
113
|
+
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
114
|
+
fs_1.default.mkdirSync(stagingDir, { recursive: true });
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
if (!SAFE_PACK_RELATIVE_PATH.test(file.relativePath))
|
|
117
|
+
continue;
|
|
118
|
+
const destination = path_1.default.join(stagingDir, file.relativePath);
|
|
119
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
120
|
+
fs_1.default.writeFileSync(destination, decorateManagedOrgFile(file.content, metadata.backend));
|
|
121
|
+
}
|
|
122
|
+
fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.ORG_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
|
|
123
|
+
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
124
|
+
fs_1.default.renameSync(stagingDir, cacheDir);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* New-machine onboarding (R1.3, AC1): when this machine has an API key but no
|
|
128
|
+
* organization block yet, probe the FRAIM server. If the account owns org
|
|
129
|
+
* artifacts, persist the fraim-cloud organization block so `fraim setup` +
|
|
130
|
+
* `fraim sync` is the complete second-machine flow. Quietly resolves null on
|
|
131
|
+
* any failure or when the account has no org.
|
|
132
|
+
*/
|
|
133
|
+
async function discoverCloudOrganization(options) {
|
|
134
|
+
try {
|
|
135
|
+
const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
|
|
136
|
+
if (!apiKey || apiKey === 'local-dev' || apiKey === 'test-mode-key')
|
|
137
|
+
return null;
|
|
138
|
+
const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
139
|
+
const pack = await fetchCloudPack(remoteUrl, apiKey);
|
|
140
|
+
if (pack.files.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
(0, user_config_1.writeUserFraimConfig)({ organization: { backend: 'fraim-cloud' } });
|
|
143
|
+
return (0, user_config_1.getOrganizationConfig)();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function failureOutcome(error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
const existing = readOrgCacheMetadata();
|
|
152
|
+
if (existing) {
|
|
153
|
+
return {
|
|
154
|
+
status: 'stale',
|
|
155
|
+
metadata: existing,
|
|
156
|
+
ageHours: getOrgCacheAgeHours() ?? 0,
|
|
157
|
+
error: message
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { status: 'absent', error: message };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Refresh the org cache from the configured backend (R4.1). Never throws:
|
|
164
|
+
* returns 'disabled' (no org configured), 'synced', 'stale' (refresh failed,
|
|
165
|
+
* existing cache served with age), or 'absent' (refresh failed, no cache).
|
|
166
|
+
*/
|
|
167
|
+
async function syncOrgCache(options) {
|
|
168
|
+
let organization = (0, user_config_1.getOrganizationConfig)();
|
|
169
|
+
if (!organization) {
|
|
170
|
+
organization = await discoverCloudOrganization(options);
|
|
171
|
+
if (!organization)
|
|
172
|
+
return { status: 'disabled' };
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
if (organization.backend === 'git') {
|
|
176
|
+
const snapshot = (0, git_org_sync_1.fetchOrgRepoSnapshot)(organization.gitUrl);
|
|
177
|
+
try {
|
|
178
|
+
const metadata = {
|
|
179
|
+
version: snapshot.sha,
|
|
180
|
+
backend: 'git',
|
|
181
|
+
source: organization.gitUrl,
|
|
182
|
+
syncedAt: new Date().toISOString()
|
|
183
|
+
};
|
|
184
|
+
materializeCache(collectGitPackFiles(snapshot.dir), metadata);
|
|
185
|
+
return { status: 'synced', metadata };
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
snapshot.cleanup();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
192
|
+
const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
|
|
193
|
+
if (!apiKey)
|
|
194
|
+
throw new Error('No FRAIM API key available for the fraim-cloud org backend.');
|
|
195
|
+
const pack = await fetchCloudPack(remoteUrl, apiKey);
|
|
196
|
+
const metadata = {
|
|
197
|
+
version: pack.version,
|
|
198
|
+
backend: 'fraim-cloud',
|
|
199
|
+
source: remoteUrl,
|
|
200
|
+
syncedAt: new Date().toISOString()
|
|
201
|
+
};
|
|
202
|
+
materializeCache(pack.files, metadata);
|
|
203
|
+
return { status: 'synced', metadata };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return failureOutcome(error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -9,7 +9,11 @@ exports.buildInitProjectSummary = buildInitProjectSummary;
|
|
|
9
9
|
exports.printInitProjectSummary = printInitProjectSummary;
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
11
|
const ONBOARDING_VIDEO_PLAYLIST_URL = 'https://fraimworks.ai/resources.html#videos';
|
|
12
|
-
function formatModeLabel(
|
|
12
|
+
function formatModeLabel(result) {
|
|
13
|
+
if (result.mode === 'conversational' && result.repositoryDetected) {
|
|
14
|
+
return 'Pending project onboarding';
|
|
15
|
+
}
|
|
16
|
+
const mode = result.mode;
|
|
13
17
|
switch (mode) {
|
|
14
18
|
case 'conversational':
|
|
15
19
|
return 'Conversational';
|
|
@@ -19,7 +23,11 @@ function formatModeLabel(mode) {
|
|
|
19
23
|
return 'Integrated';
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
|
-
function getModeSpecificNextStep(
|
|
26
|
+
function getModeSpecificNextStep(result) {
|
|
27
|
+
if (result.mode === 'conversational' && result.repositoryDetected) {
|
|
28
|
+
return `The agent will create fraim/config.json during onboarding, preserve the detected repository identity, then focus on project context, validation commands, durable project rules, and any optional automation choices. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
29
|
+
}
|
|
30
|
+
const mode = result.mode;
|
|
23
31
|
switch (mode) {
|
|
24
32
|
case 'conversational':
|
|
25
33
|
return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable project rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
@@ -53,7 +61,8 @@ function buildInitProjectSummary(result) {
|
|
|
53
61
|
return {
|
|
54
62
|
status: 'FRAIM project initialized.',
|
|
55
63
|
fields: {
|
|
56
|
-
mode: formatModeLabel(result
|
|
64
|
+
mode: formatModeLabel(result),
|
|
65
|
+
projectContext: result.repositoryDetected ? 'repository-backed' : 'local-folder',
|
|
57
66
|
project: result.projectName,
|
|
58
67
|
repositoryDetection: result.repositoryDetected ? 'detected' : 'not detected',
|
|
59
68
|
issueTracking: result.issueTrackingDetected ? 'detected' : 'not detected',
|
|
@@ -64,19 +73,23 @@ function buildInitProjectSummary(result) {
|
|
|
64
73
|
warnings: [...result.warnings],
|
|
65
74
|
nextStep: {
|
|
66
75
|
prompt: 'Tell your AI agent "Onboard this project".',
|
|
67
|
-
explanation: getModeSpecificNextStep(result
|
|
76
|
+
explanation: getModeSpecificNextStep(result)
|
|
68
77
|
}
|
|
69
78
|
};
|
|
70
79
|
}
|
|
71
80
|
function printInitProjectSummary(result) {
|
|
72
81
|
const summary = buildInitProjectSummary(result);
|
|
73
|
-
const
|
|
82
|
+
const showRepositoryDetection = result.mode !== 'conversational' || result.repositoryDetected;
|
|
83
|
+
const showIssueTrackingDetails = result.mode !== 'conversational';
|
|
74
84
|
console.log(chalk_1.default.green(`\n${summary.status}`));
|
|
75
85
|
console.log(chalk_1.default.blue('Project summary:'));
|
|
76
|
-
console.log(chalk_1.default.gray(`
|
|
86
|
+
console.log(chalk_1.default.gray(` Automation mode: ${summary.fields.mode}`));
|
|
87
|
+
console.log(chalk_1.default.gray(` Project context: ${summary.fields.projectContext}`));
|
|
77
88
|
console.log(chalk_1.default.gray(` Project: ${summary.fields.project}`));
|
|
78
|
-
if (
|
|
89
|
+
if (showRepositoryDetection) {
|
|
79
90
|
console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
|
|
91
|
+
}
|
|
92
|
+
if (showIssueTrackingDetails) {
|
|
80
93
|
console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
|
|
81
94
|
}
|
|
82
95
|
console.log(chalk_1.default.gray(` Sync: ${summary.fields.sync}`));
|
|
@@ -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.readUserFraimConfig = readUserFraimConfig;
|
|
7
|
+
exports.writeUserFraimConfig = writeUserFraimConfig;
|
|
8
|
+
exports.getOrganizationConfig = getOrganizationConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Typed read/write helpers for the user-level FRAIM config (~/.fraim/config.json),
|
|
11
|
+
* including the organization block introduced by issue #563.
|
|
12
|
+
*
|
|
13
|
+
* The user-level config is machine-scoped and distinct from the workspace
|
|
14
|
+
* config (fraim/config.json). Honors the FRAIM_USER_DIR override used by tests.
|
|
15
|
+
*/
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
19
|
+
function getUserConfigPath() {
|
|
20
|
+
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'config.json');
|
|
21
|
+
}
|
|
22
|
+
function readUserFraimConfig() {
|
|
23
|
+
try {
|
|
24
|
+
const configPath = getUserConfigPath();
|
|
25
|
+
if (!fs_1.default.existsSync(configPath))
|
|
26
|
+
return {};
|
|
27
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
28
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Shallow-merge a patch into the user config and persist it. Existing keys
|
|
36
|
+
* not named in the patch are preserved. Setting a key to undefined removes it.
|
|
37
|
+
*/
|
|
38
|
+
function writeUserFraimConfig(patch) {
|
|
39
|
+
const current = readUserFraimConfig();
|
|
40
|
+
const merged = { ...current, ...patch };
|
|
41
|
+
for (const key of Object.keys(patch)) {
|
|
42
|
+
if (patch[key] === undefined)
|
|
43
|
+
delete merged[key];
|
|
44
|
+
}
|
|
45
|
+
const dir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
46
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
47
|
+
fs_1.default.writeFileSync(getUserConfigPath(), JSON.stringify(merged, null, 2));
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the validated organization configuration, or null when no valid
|
|
52
|
+
* organization is configured on this machine (R1.1).
|
|
53
|
+
*/
|
|
54
|
+
function getOrganizationConfig() {
|
|
55
|
+
const raw = readUserFraimConfig().organization;
|
|
56
|
+
if (!raw || typeof raw !== 'object')
|
|
57
|
+
return null;
|
|
58
|
+
if (raw.backend === 'git') {
|
|
59
|
+
const gitUrl = typeof raw.gitUrl === 'string' ? raw.gitUrl.trim() : '';
|
|
60
|
+
if (!gitUrl)
|
|
61
|
+
return null;
|
|
62
|
+
return { backend: 'git', gitUrl, id: raw.id };
|
|
63
|
+
}
|
|
64
|
+
if (raw.backend === 'fraim-cloud') {
|
|
65
|
+
return { backend: 'fraim-cloud', id: raw.id };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
@@ -296,6 +296,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
296
296
|
},
|
|
297
297
|
"instanceUrl": {
|
|
298
298
|
"kind": "string"
|
|
299
|
+
},
|
|
300
|
+
"accessScript": {
|
|
301
|
+
"kind": "string"
|
|
299
302
|
}
|
|
300
303
|
}
|
|
301
304
|
},
|
|
@@ -390,6 +393,12 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
390
393
|
},
|
|
391
394
|
"instructions": {
|
|
392
395
|
"kind": "string"
|
|
396
|
+
},
|
|
397
|
+
"accessScript": {
|
|
398
|
+
"kind": "string"
|
|
399
|
+
},
|
|
400
|
+
"purpose": {
|
|
401
|
+
"kind": "string"
|
|
393
402
|
}
|
|
394
403
|
}
|
|
395
404
|
}
|
|
@@ -588,6 +597,7 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
588
597
|
"integrations.itsm",
|
|
589
598
|
"integrations.itsm.provider",
|
|
590
599
|
"integrations.itsm.instanceUrl",
|
|
600
|
+
"integrations.itsm.accessScript",
|
|
591
601
|
"integrations.identity",
|
|
592
602
|
"integrations.identity.provider",
|
|
593
603
|
"automation",
|
|
@@ -341,20 +341,40 @@ function computeOldestL0AgeDays(workspaceRoot, userId) {
|
|
|
341
341
|
}
|
|
342
342
|
return oldest;
|
|
343
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Resolve an L2 org-scope learning file (issue #563): a repo-local override
|
|
346
|
+
* (`fraim/personalized-employee/learnings/`) wins, then the synced org cache
|
|
347
|
+
* (`~/.fraim/org/learnings/`). Org learnings are part of the shared org pack,
|
|
348
|
+
* so a synced copy must be readable by the agent even with no repo-local copy.
|
|
349
|
+
* Mirrors the org context/rules resolution order. Returns `present:false` with
|
|
350
|
+
* the repo path/display when neither tier has the file.
|
|
351
|
+
*/
|
|
352
|
+
function resolveOrgLearningFile(repoLearningsBase, fileName) {
|
|
353
|
+
const repoPath = (0, path_1.join)(repoLearningsBase, fileName);
|
|
354
|
+
if ((0, fs_1.existsSync)(repoPath)) {
|
|
355
|
+
return { present: true, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
|
|
356
|
+
}
|
|
357
|
+
const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', 'learnings', fileName);
|
|
358
|
+
if ((0, fs_1.existsSync)(cachePath)) {
|
|
359
|
+
return { present: true, path: cachePath, displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/learnings/${fileName}`) };
|
|
360
|
+
}
|
|
361
|
+
return { present: false, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
|
|
362
|
+
}
|
|
344
363
|
function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
345
364
|
const roots = getLearningRoots(workspaceRoot);
|
|
346
365
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
347
366
|
const threshold = getScoreThreshold(workspaceRoot);
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const
|
|
367
|
+
// L2 org learnings resolve repo-local first, then the synced org cache (#563).
|
|
368
|
+
const l2Mistake = resolveOrgLearningFile(roots.repoLearningsBase, 'org-mistake-patterns.md');
|
|
369
|
+
const l2Pref = resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md');
|
|
370
|
+
const l2Coach = resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md');
|
|
371
|
+
const l2Validated = resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md');
|
|
372
|
+
const l2MistakePresent = l2Mistake.present;
|
|
373
|
+
const l2PrefPresent = l2Pref.present;
|
|
374
|
+
const l2CoachPresent = l2Coach.present;
|
|
375
|
+
const l2ValidatedPresent = l2Validated.present;
|
|
376
|
+
const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2Mistake.path, threshold, 'mistake-patterns') : null;
|
|
377
|
+
const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2Validated.path, threshold, 'validated-patterns') : null;
|
|
358
378
|
const l1Mistake = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-mistake-patterns.md`);
|
|
359
379
|
const l1Pref = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-preferences.md`);
|
|
360
380
|
const l1Coach = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-manager-coaching.md`);
|
|
@@ -394,13 +414,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
394
414
|
if (hasL2) {
|
|
395
415
|
section += '### L2 - Org patterns\n';
|
|
396
416
|
if (l2MistakePresent)
|
|
397
|
-
section += `\`${
|
|
417
|
+
section += `\`${l2Mistake.displayPath}\` (entries above score threshold)\n`;
|
|
398
418
|
if (l2PrefPresent)
|
|
399
|
-
section += `\`${
|
|
419
|
+
section += `\`${l2Pref.displayPath}\` (all entries)\n`;
|
|
400
420
|
if (l2CoachPresent)
|
|
401
|
-
section += `\`${
|
|
421
|
+
section += `\`${l2Coach.displayPath}\` (manager-facing; all entries)\n`;
|
|
402
422
|
if (l2ValidatedPresent)
|
|
403
|
-
section += `\`${
|
|
423
|
+
section += `\`${l2Validated.displayPath}\` (entries above score threshold)\n`;
|
|
404
424
|
const l2DormantTotal = (l2MistakeStats?.dormant || 0) + (l2ValidatedStats?.dormant || 0);
|
|
405
425
|
if (l2DormantTotal > 0) {
|
|
406
426
|
section += `Dormant: ${l2DormantTotal} org pattern${l2DormantTotal !== 1 ? 's' : ''} below threshold\n`;
|
|
@@ -465,12 +485,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
465
485
|
return section;
|
|
466
486
|
}
|
|
467
487
|
/**
|
|
468
|
-
* Resolve an organization/manager-scope context file
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
488
|
+
* Resolve an organization/manager-scope context file (issue #563 order, R3.1):
|
|
489
|
+
* 1. repo-local override (`fraim/personalized-employee/…`) — wins when present
|
|
490
|
+
* 2. managed org cache (`~/.fraim/org/…`) — the synced shared org layer;
|
|
491
|
+
* eligible for org files only, never manager files (R3.3)
|
|
492
|
+
* 3. legacy machine-local (`~/.fraim/personalized-employee/…`)
|
|
472
493
|
*/
|
|
473
|
-
function resolveOrgContextFile(workspaceRoot, relativePath) {
|
|
494
|
+
function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = true) {
|
|
474
495
|
try {
|
|
475
496
|
const repoPath = (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', relativePath);
|
|
476
497
|
if ((0, fs_1.existsSync)(repoPath)) {
|
|
@@ -479,6 +500,15 @@ function resolveOrgContextFile(workspaceRoot, relativePath) {
|
|
|
479
500
|
displayPath: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/${relativePath}`)
|
|
480
501
|
};
|
|
481
502
|
}
|
|
503
|
+
if (orgCacheEligible) {
|
|
504
|
+
const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', relativePath);
|
|
505
|
+
if ((0, fs_1.existsSync)(cachePath)) {
|
|
506
|
+
return {
|
|
507
|
+
present: true,
|
|
508
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
482
512
|
const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
|
|
483
513
|
if ((0, fs_1.existsSync)(userPath)) {
|
|
484
514
|
return {
|
|
@@ -518,11 +548,12 @@ function resolveProjectContextFile(workspaceRoot, relativePath) {
|
|
|
518
548
|
* lists only present files, and returns '' when none exist.
|
|
519
549
|
*/
|
|
520
550
|
function buildTeamContextSection(workspaceRoot, forJob) {
|
|
521
|
-
// Organization layer
|
|
551
|
+
// Organization layer: repo override, then org cache, then legacy machine-local.
|
|
552
|
+
// Manager files are personal and never resolve from the shared org cache (R3.3).
|
|
522
553
|
const orgContext = resolveOrgContextFile(workspaceRoot, 'context/org_context.md');
|
|
523
|
-
const managerContext = resolveOrgContextFile(workspaceRoot, 'context/manager_context.md');
|
|
554
|
+
const managerContext = resolveOrgContextFile(workspaceRoot, 'context/manager_context.md', false);
|
|
524
555
|
const orgRules = resolveOrgContextFile(workspaceRoot, 'rules/org_rules.md');
|
|
525
|
-
const managerRules = resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md');
|
|
556
|
+
const managerRules = resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md', false);
|
|
526
557
|
// Project layer (repo-local only).
|
|
527
558
|
const projectContext = resolveProjectContextFile(workspaceRoot, 'context/project_context.md');
|
|
528
559
|
const projectBrief = resolveProjectContextFile(workspaceRoot, 'context/project_brief.md');
|
|
@@ -571,9 +602,9 @@ function buildTeamContextSection(workspaceRoot, forJob) {
|
|
|
571
602
|
function resolveTeamContextFiles(workspaceRoot) {
|
|
572
603
|
return {
|
|
573
604
|
orgContext: resolveOrgContextFile(workspaceRoot, 'context/org_context.md'),
|
|
574
|
-
managerContext: resolveOrgContextFile(workspaceRoot, 'context/manager_context.md'),
|
|
605
|
+
managerContext: resolveOrgContextFile(workspaceRoot, 'context/manager_context.md', false),
|
|
575
606
|
orgRules: resolveOrgContextFile(workspaceRoot, 'rules/org_rules.md'),
|
|
576
|
-
managerRules: resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md'),
|
|
607
|
+
managerRules: resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md', false),
|
|
577
608
|
projectContext: resolveProjectContextFile(workspaceRoot, 'context/project_context.md'),
|
|
578
609
|
projectBrief: resolveProjectContextFile(workspaceRoot, 'context/project_brief.md'),
|
|
579
610
|
projectQa: resolveProjectContextFile(workspaceRoot, 'context/project_qa.md'),
|
|
@@ -627,6 +658,23 @@ function resolveTeamContextFile(workspaceRoot, key) {
|
|
|
627
658
|
scope
|
|
628
659
|
};
|
|
629
660
|
}
|
|
661
|
+
// Synced org cache (org files only, never manager files — R3.3). Reads
|
|
662
|
+
// resolve here, but the cache is managed content: it is never a write
|
|
663
|
+
// destination (R5.1), so writePath stays empty and the caller must route
|
|
664
|
+
// edits through the org backend's propose-and-approve flow.
|
|
665
|
+
if (key === 'org' || key === 'orgRules') {
|
|
666
|
+
const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', relativePath);
|
|
667
|
+
if ((0, fs_1.existsSync)(cachePath)) {
|
|
668
|
+
return {
|
|
669
|
+
present: true,
|
|
670
|
+
readPath: cachePath,
|
|
671
|
+
writePath: '',
|
|
672
|
+
displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`),
|
|
673
|
+
scope,
|
|
674
|
+
managedByOrgSync: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
630
678
|
const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
|
|
631
679
|
const userDisplay = (0, project_fraim_paths_1.getUserFraimDisplayPath)(`personalized-employee/${relativePath}`);
|
|
632
680
|
return {
|
|
@@ -674,11 +722,12 @@ function countLearningEntries(filePath) {
|
|
|
674
722
|
function countPreservedLearnings(workspaceRoot, userId) {
|
|
675
723
|
const roots = getLearningRoots(workspaceRoot);
|
|
676
724
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
677
|
-
// L2 organization-scope preserved files
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
countLearningEntries((
|
|
681
|
-
countLearningEntries((
|
|
725
|
+
// L2 organization-scope preserved files: repo-local override, then the
|
|
726
|
+
// synced org cache (#563), matching what buildLearningContextSection injects.
|
|
727
|
+
const organization = countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-mistake-patterns.md').path) +
|
|
728
|
+
countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md').path) +
|
|
729
|
+
countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md').path) +
|
|
730
|
+
countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md').path);
|
|
682
731
|
const resolve = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
|
|
683
732
|
// L1 manager-facing reverse-mentoring file.
|
|
684
733
|
const manager = countLearningEntries(resolve(`${resolvedUserId}-manager-coaching.md`).path);
|
|
@@ -415,6 +415,7 @@ class FraimLocalMCPServer {
|
|
|
415
415
|
this.mentoringResponseCache = null;
|
|
416
416
|
this.connectSyncInFlight = null;
|
|
417
417
|
this.latestConnectSyncWarning = null;
|
|
418
|
+
this.orgCacheRefreshInFlight = false;
|
|
418
419
|
this.writer = writer || process.stdout.write.bind(process.stdout);
|
|
419
420
|
this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
420
421
|
this.apiKey = this.loadApiKey();
|
|
@@ -748,6 +749,35 @@ class FraimLocalMCPServer {
|
|
|
748
749
|
finally {
|
|
749
750
|
this.connectSyncInFlight = null;
|
|
750
751
|
}
|
|
752
|
+
this.maybeRefreshOrgCache(String(requestId));
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Issue #563 (R4.2): opportunistically refresh the managed org cache
|
|
756
|
+
* (~/.fraim/org/) at session bootstrap when it is missing or older than
|
|
757
|
+
* 24 hours. Fire-and-forget: session start never blocks on the org
|
|
758
|
+
* backend, and a failed refresh leaves the existing cache serving stale
|
|
759
|
+
* with its age (R4.3).
|
|
760
|
+
*/
|
|
761
|
+
maybeRefreshOrgCache(requestId) {
|
|
762
|
+
if (this.orgCacheRefreshInFlight)
|
|
763
|
+
return;
|
|
764
|
+
this.orgCacheRefreshInFlight = true;
|
|
765
|
+
void Promise.resolve().then(() => __importStar(require('../cli/utils/org-pack-sync'))).then(async ({ getOrgCacheAgeHours, syncOrgCache }) => {
|
|
766
|
+
const ageHours = getOrgCacheAgeHours();
|
|
767
|
+
if (ageHours !== null && ageHours < 24)
|
|
768
|
+
return;
|
|
769
|
+
const outcome = await syncOrgCache();
|
|
770
|
+
if (outcome.status !== 'disabled') {
|
|
771
|
+
const version = outcome.metadata ? ` (version ${outcome.metadata.version.slice(0, 12)})` : '';
|
|
772
|
+
this.log(`[req:${requestId}] Org cache refresh at connect: ${outcome.status}${version}`);
|
|
773
|
+
}
|
|
774
|
+
})
|
|
775
|
+
.catch((error) => {
|
|
776
|
+
this.logError(`[req:${requestId}] Org cache refresh failed: ${error?.message || error}`);
|
|
777
|
+
})
|
|
778
|
+
.finally(() => {
|
|
779
|
+
this.orgCacheRefreshInFlight = false;
|
|
780
|
+
});
|
|
751
781
|
}
|
|
752
782
|
/**
|
|
753
783
|
* Automatically detect machine information
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.164",
|
|
4
4
|
"description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -104,7 +104,6 @@
|
|
|
104
104
|
"@types/prompts": "^2.4.9",
|
|
105
105
|
"@types/semver": "^7.7.1",
|
|
106
106
|
"fast-glob": "^3.3.3",
|
|
107
|
-
"html-to-docx": "^1.8.0",
|
|
108
107
|
"markdown-it": "^14.1.1",
|
|
109
108
|
"markdown-it-highlightjs": "^4.3.0",
|
|
110
109
|
"playwright": "^1.58.2",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<!-- Detect Electron + host platform before CSS paints so html.electron / html.mac /
|
|
10
10
|
html.win / html.linux rules apply on first render (no flash, correct native chrome). -->
|
|
11
11
|
<script>(function(){var h=document.documentElement,ua=navigator.userAgent;if(/Electron/.test(ua))h.classList.add('electron');var p=(navigator.userAgentData&&navigator.userAgentData.platform)||navigator.platform||'';if(/mac/i.test(p)||/Mac|iPhone|iPad|iPod/.test(ua))h.classList.add('mac');else if(/win/i.test(p)||/Windows/.test(ua))h.classList.add('win');else if(/linux/i.test(p)||/Linux|X11/.test(ua))h.classList.add('linux');})();</script>
|
|
12
|
-
<link rel="stylesheet" href="./styles.css">
|
|
12
|
+
<link rel="stylesheet" href="./styles.css?v=conv-panels-20260611b">
|
|
13
13
|
<link rel="stylesheet" href="./review.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
@@ -363,7 +363,7 @@
|
|
|
363
363
|
<summary>
|
|
364
364
|
<span class="panel-summary-copy">
|
|
365
365
|
<span class="panel-kicker">Micro-manage</span>
|
|
366
|
-
<span class="panel-summary-text">Raw host
|
|
366
|
+
<span class="panel-summary-text">Raw host events and tool calls</span>
|
|
367
367
|
</span>
|
|
368
368
|
</summary>
|
|
369
369
|
<pre class="micro-log" id="micro-log"></pre>
|