fraim 2.0.357-next.1 → 2.0.357-next.10
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/registry/scripts/learning/run-application.js +2 -2
- package/dist/registry/scripts/learning-usage/learning-source-record.js +3 -10
- package/dist/src/cli/api/provider-client.js +4 -1
- package/dist/src/cli/commands/override.js +1 -1
- package/dist/src/cli/commands/sync.js +1 -1
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +3 -3
- package/dist/src/cli/mcp/mcp-server-registry.js +4 -1
- package/dist/src/cli/utils/manager-pack-sync.js +7 -29
- package/dist/src/cli/utils/org-pack-sync.js +5 -29
- package/dist/src/core/capability-pack.js +0 -9
- package/dist/src/fraim/db-service.js +12 -7
- package/dist/src/local-mcp-server/learning-source-record.js +3 -10
- package/dist/src/local-mcp-server/stdio-server.js +3 -1
- package/package.json +1 -1
|
@@ -780,9 +780,9 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
780
780
|
const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.path, (0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
|
|
781
781
|
if (parsed.status !== 'valid' || parsed.record.kind !== source.kind || !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, home.owner) || parsed.record.state !== 'pending' || parsed.record.synthesized)
|
|
782
782
|
throw new Error('invalid-learning-source');
|
|
783
|
-
// References may use immutable identity, home-relative
|
|
783
|
+
// References may use immutable identity, content-relative or home-relative path, or
|
|
784
784
|
// the selected absolute path. None may archive an unfinished dependency.
|
|
785
|
-
const refs = new Set([parsed.record.usageIdentity, parsed.record.sourceId,
|
|
785
|
+
const refs = new Set([parsed.record.usageIdentity, parsed.record.sourceId,
|
|
786
786
|
node_path_1.default.relative(home.contentRoot, source.path).replace(/\\/g, '/'), node_path_1.default.relative((0, owner_identity_1.ownerArtifactRoot)(home), source.path).replace(/\\/g, '/')]);
|
|
787
787
|
if (actions.some(action => action.sourceRefs.some(ref => refs.has(ref.replace(/\\/g, '/')) || (node_path_1.default.isAbsolute(ref) && (0, completion_state_2.sourcePathKey)(ref) === (0, completion_state_2.sourcePathKey)(source.path)))))
|
|
788
788
|
throw new Error('action-processing-pending');
|
|
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
49
49
|
for (const line of fm[1].split(/\r?\n/)) {
|
|
50
50
|
const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
|
|
51
51
|
if (!match) {
|
|
52
|
-
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|
|
|
52
|
+
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|synthesized)["']?\s*:/.test(line))
|
|
53
53
|
malformedField = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
76
76
|
return invalid('Unsupported metadata key spelling');
|
|
77
77
|
if (duplicates.size)
|
|
78
78
|
return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
|
|
79
|
-
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', '
|
|
79
|
+
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'synthesized']) {
|
|
80
80
|
if (!Object.prototype.hasOwnProperty.call(fields, field))
|
|
81
81
|
return invalid(`Missing metadata: ${field}`);
|
|
82
82
|
}
|
|
@@ -110,21 +110,14 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
110
110
|
return invalid('Source is outside its configured learning location');
|
|
111
111
|
if (state === 'archived' && !fields.synthesized)
|
|
112
112
|
return invalid('Archive has no successful processing date');
|
|
113
|
-
const reference = fields.usageSourceRef;
|
|
114
113
|
const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
|
|
115
114
|
if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
|
|
116
115
|
return invalid('Source escapes its owning root');
|
|
117
|
-
if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
|
|
118
|
-
return invalid('Invalid usageSourceRef');
|
|
119
116
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
|
|
120
117
|
return invalid('Sources require an immutable sourceId UUID');
|
|
121
118
|
}
|
|
122
|
-
if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
|
|
123
|
-
|| reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
|
|
124
|
-
return invalid('usageSourceRef must name the owner-relative pending path');
|
|
125
|
-
}
|
|
126
119
|
return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
|
|
127
|
-
date: fields.date, job: fields.job, runId: fields.runId,
|
|
120
|
+
date: fields.date, job: fields.job, runId: fields.runId,
|
|
128
121
|
usageIdentity: `learning-source:${fields.sourceId}`,
|
|
129
122
|
sourceId: fields.sourceId,
|
|
130
123
|
synthesized: fields.synthesized, pendingPath, archivePath, state } };
|
|
@@ -8,7 +8,10 @@ exports.ProviderClient = void 0;
|
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
class ProviderClient {
|
|
10
10
|
constructor(fraimKey, serverUrl) {
|
|
11
|
-
|
|
11
|
+
const isPpe = (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE';
|
|
12
|
+
this.serverUrl = serverUrl ||
|
|
13
|
+
(isPpe ? 'https://app-ashley-fraim-ppe.azurewebsites.net'
|
|
14
|
+
: (process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me'));
|
|
12
15
|
this.client = axios_1.default.create({
|
|
13
16
|
baseURL: this.serverUrl,
|
|
14
17
|
headers: {
|
|
@@ -67,7 +67,7 @@ exports.overrideCommand = new commander_1.Command('override')
|
|
|
67
67
|
serverUrl = `http://localhost:${localPort}`;
|
|
68
68
|
}
|
|
69
69
|
else {
|
|
70
|
-
serverUrl = config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
70
|
+
serverUrl = config.remoteUrl || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : (process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me'));
|
|
71
71
|
}
|
|
72
72
|
const apiKey = isLocal ? 'local-dev' : (config.apiKey || process.env.FRAIM_API_KEY);
|
|
73
73
|
const headers = { 'Content-Type': 'application/json' };
|
|
@@ -125,7 +125,7 @@ const runSync = async (options) => {
|
|
|
125
125
|
// Order: user-level config → env var → project config (only if in a project)
|
|
126
126
|
const remoteUrl = useLocal
|
|
127
127
|
? resolveExplicitLocalSyncUrl() || `http://localhost:${process.env.FRAIM_MCP_PORT || (0, git_utils_1.getPort)()}`
|
|
128
|
-
: process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
128
|
+
: (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : (process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
|
|
129
129
|
let apiKey = useLocal ? 'local-dev' : loadUserApiKey() || process.env.FRAIM_API_KEY;
|
|
130
130
|
const hasProject = (0, project_fraim_paths_1.workspaceFraimExists)(projectRoot);
|
|
131
131
|
if (!apiKey && hasProject) {
|
|
@@ -73,12 +73,12 @@ const CACHE_GENERATIONS_KEPT = ${CACHE_GENERATIONS_KEPT};
|
|
|
73
73
|
const CACHE_MIN_AGE_MS = ${CACHE_MIN_AGE_MS};
|
|
74
74
|
const USER_DIR = process.env.FRAIM_USER_DIR || path.join(os.homedir(), '.fraim');
|
|
75
75
|
const STATE_DIR = path.join(USER_DIR, 'mcp-launcher');
|
|
76
|
-
const
|
|
76
|
+
const DIST_TAG = (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'next' : 'latest';
|
|
77
|
+
const STATE_PATH = path.join(STATE_DIR, 'latest-' + DIST_TAG + '.json');
|
|
77
78
|
const CACHE_ROOT = path.join(USER_DIR, 'npm-cache', 'mcp');
|
|
78
|
-
const REFRESH_LOCK = path.join(STATE_DIR, 'refresh.lock');
|
|
79
|
+
const REFRESH_LOCK = path.join(STATE_DIR, 'refresh-' + DIST_TAG + '.lock');
|
|
79
80
|
const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
80
81
|
const launchStartedAt = Date.now();
|
|
81
|
-
const DIST_TAG = (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'next' : 'latest';
|
|
82
82
|
|
|
83
83
|
const commandName = (name) => process.platform === 'win32' ? name + '.cmd' : name;
|
|
84
84
|
const npmCommand = commandName('npm');
|
|
@@ -49,7 +49,10 @@ exports.BASE_MCP_SERVERS = [
|
|
|
49
49
|
// Include API key for IDE configs (Codex, VSCode, etc.)
|
|
50
50
|
// The stdio-server will use this if set, otherwise falls back to ~/.fraim/config.json
|
|
51
51
|
FRAIM_API_KEY: fraimKey,
|
|
52
|
-
FRAIM_REMOTE_URL:
|
|
52
|
+
FRAIM_REMOTE_URL: (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE'
|
|
53
|
+
? 'https://app-ashley-fraim-ppe.azurewebsites.net'
|
|
54
|
+
: 'https://fraim.wellnessatwork.me',
|
|
55
|
+
...(process.env.FRAIM_RELEASE_CHANNEL ? { FRAIM_RELEASE_CHANNEL: process.env.FRAIM_RELEASE_CHANNEL } : {})
|
|
53
56
|
}
|
|
54
57
|
};
|
|
55
58
|
}
|
|
@@ -3,7 +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.
|
|
6
|
+
exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_HOME_DIRNAME = void 0;
|
|
7
7
|
exports.getManagerHomeDir = getManagerHomeDir;
|
|
8
8
|
exports.readManagerSyncMetadata = readManagerSyncMetadata;
|
|
9
9
|
exports.getManagerSyncAgeHours = getManagerSyncAgeHours;
|
|
@@ -30,7 +30,6 @@ const local_folder_sync_1 = require("./local-folder-sync");
|
|
|
30
30
|
const user_config_1 = require("./user-config");
|
|
31
31
|
exports.MANAGER_HOME_DIRNAME = 'manager';
|
|
32
32
|
exports.MANAGER_SYNC_METADATA_FILE = '.manager-sync-metadata.json';
|
|
33
|
-
exports.MANAGER_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
|
|
34
33
|
// Issue #1043 adds 'employees' so a published employee record is walked out of
|
|
35
34
|
// a git snapshot. isManagerPackRelativePath still decides what is admitted.
|
|
36
35
|
const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'scripts', 'employees'];
|
|
@@ -86,24 +85,6 @@ function getManagerSyncAgeHours() {
|
|
|
86
85
|
return null;
|
|
87
86
|
return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
|
|
88
87
|
}
|
|
89
|
-
function decorateManagedManagerFile(content, backend) {
|
|
90
|
-
const normalized = content.replace(/^\uFEFF/, '');
|
|
91
|
-
if (normalized.startsWith(exports.MANAGER_MANAGED_HEADER))
|
|
92
|
-
return normalized;
|
|
93
|
-
const writePath = backend === 'git'
|
|
94
|
-
? 'open a pull request against your manager storage repo'
|
|
95
|
-
: backend === 'local-folder'
|
|
96
|
-
? 'edit the file in your synced folder and let the sync client upload it'
|
|
97
|
-
: 'publish the change through your FRAIM manager storage flow';
|
|
98
|
-
const marker = [
|
|
99
|
-
exports.MANAGER_MANAGED_HEADER,
|
|
100
|
-
'> [!IMPORTANT]',
|
|
101
|
-
'> Synced from your personal manager storage. Local edits are overwritten on the next sync.',
|
|
102
|
-
`> To change this content, ${writePath}.`,
|
|
103
|
-
''
|
|
104
|
-
].join('\n');
|
|
105
|
-
return `${marker}\n${normalized}`;
|
|
106
|
-
}
|
|
107
88
|
function collectGitPackFilesRecursive(dir, relBase, files) {
|
|
108
89
|
if (!fs_1.default.existsSync(dir))
|
|
109
90
|
return;
|
|
@@ -137,7 +118,7 @@ async function fetchCloudPack(remoteUrl, apiKey) {
|
|
|
137
118
|
version: String(response.data?.version ?? '0')
|
|
138
119
|
};
|
|
139
120
|
}
|
|
140
|
-
function materializeCloudHome(files
|
|
121
|
+
function materializeCloudHome(files) {
|
|
141
122
|
const cacheDir = getManagerHomeDir();
|
|
142
123
|
const stagingDir = `${cacheDir}.staging-${process.pid}`;
|
|
143
124
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -150,11 +131,8 @@ function materializeCloudHome(files, metadata) {
|
|
|
150
131
|
continue;
|
|
151
132
|
const destination = path_1.default.join(stagingDir, file.relativePath);
|
|
152
133
|
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
? decorateManagedManagerFile(file.content, metadata.backend)
|
|
156
|
-
: file.content;
|
|
157
|
-
fs_1.default.writeFileSync(destination, content);
|
|
134
|
+
// Sync transports content; provenance lives in the sidecar metadata.
|
|
135
|
+
fs_1.default.writeFileSync(destination, file.content);
|
|
158
136
|
}
|
|
159
137
|
// Provenance is written outside the home by writeHomeMetadata.
|
|
160
138
|
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
@@ -165,7 +143,7 @@ async function discoverCloudManagerStorage(options) {
|
|
|
165
143
|
const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
|
|
166
144
|
if (!apiKey || apiKey === 'local-dev' || apiKey === 'test-mode-key')
|
|
167
145
|
return null;
|
|
168
|
-
const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
146
|
+
const remoteUrl = options?.remoteUrl || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : (process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me'));
|
|
169
147
|
const pack = await fetchCloudPack(remoteUrl, apiKey);
|
|
170
148
|
if (pack.files.length === 0)
|
|
171
149
|
return null;
|
|
@@ -239,7 +217,7 @@ async function syncManagerCache(options) {
|
|
|
239
217
|
writeHomeMetadata(metadata);
|
|
240
218
|
return { status: 'synced', metadata };
|
|
241
219
|
}
|
|
242
|
-
const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
220
|
+
const remoteUrl = options?.remoteUrl || ((process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE' ? 'https://app-ashley-fraim-ppe.azurewebsites.net' : (process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me'));
|
|
243
221
|
const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
|
|
244
222
|
if (!apiKey)
|
|
245
223
|
throw new Error('No FRAIM API key available for the fraim-cloud manager backend.');
|
|
@@ -252,7 +230,7 @@ async function syncManagerCache(options) {
|
|
|
252
230
|
};
|
|
253
231
|
// The cloud has no user-visible folder, so this managed directory is the
|
|
254
232
|
// home for that backend, not a cache beside one.
|
|
255
|
-
materializeCloudHome(pack.files
|
|
233
|
+
materializeCloudHome(pack.files);
|
|
256
234
|
writeHomeMetadata(metadata);
|
|
257
235
|
return { status: 'synced', metadata };
|
|
258
236
|
}
|
|
@@ -3,7 +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.
|
|
6
|
+
exports.ORG_SYNC_METADATA_FILE = exports.ORG_HOME_DIRNAME = void 0;
|
|
7
7
|
exports.isSafePackPath = isSafePackPath;
|
|
8
8
|
exports.isSafeOrgCapabilityPackPath = isSafeOrgCapabilityPackPath;
|
|
9
9
|
exports.getOrgHomeDir = getOrgHomeDir;
|
|
@@ -30,11 +30,8 @@ const user_config_1 = require("./user-config");
|
|
|
30
30
|
const pack_home_1 = require("./pack-home");
|
|
31
31
|
const local_folder_sync_1 = require("./local-folder-sync");
|
|
32
32
|
const capability_pack_1 = require("../../core/capability-pack");
|
|
33
|
-
var capability_pack_2 = require("../../core/capability-pack");
|
|
34
|
-
Object.defineProperty(exports, "shouldDecorateAsMarkdown", { enumerable: true, get: function () { return capability_pack_2.shouldDecorateAsMarkdown; } });
|
|
35
33
|
exports.ORG_HOME_DIRNAME = 'org';
|
|
36
34
|
exports.ORG_SYNC_METADATA_FILE = '.org-sync-metadata.json';
|
|
37
|
-
exports.ORG_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
|
|
38
35
|
/**
|
|
39
36
|
* Subdirectories of the org pack that sync into the cache (spec R7.1).
|
|
40
37
|
* Issue #1043 adds 'employees' so an org-shared employee record reaches the
|
|
@@ -126,24 +123,6 @@ function getOrgSyncAgeHours() {
|
|
|
126
123
|
return null;
|
|
127
124
|
return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
|
|
128
125
|
}
|
|
129
|
-
function decorateManagedOrgFile(content, backend) {
|
|
130
|
-
const normalized = content.replace(/^/, '');
|
|
131
|
-
if (normalized.startsWith(exports.ORG_MANAGED_HEADER))
|
|
132
|
-
return normalized;
|
|
133
|
-
const writePath = backend === 'git'
|
|
134
|
-
? 'open a pull request against your organization repo'
|
|
135
|
-
: backend === 'local-folder'
|
|
136
|
-
? 'edit the file in your synced folder and let the sync client upload it'
|
|
137
|
-
: 'update it through the organization-onboarding flow';
|
|
138
|
-
const marker = [
|
|
139
|
-
exports.ORG_MANAGED_HEADER,
|
|
140
|
-
'> [!IMPORTANT]',
|
|
141
|
-
'> Synced from your organization\'s shared context. Local edits are overwritten on the next sync.',
|
|
142
|
-
`> To change this content, ${writePath}.`,
|
|
143
|
-
''
|
|
144
|
-
].join('\n');
|
|
145
|
-
return `${marker}\n${normalized}`;
|
|
146
|
-
}
|
|
147
126
|
function collectGitPackFilesRecursive(dir, relBase, files) {
|
|
148
127
|
if (!fs_1.default.existsSync(dir))
|
|
149
128
|
return;
|
|
@@ -178,7 +157,7 @@ async function fetchCloudPack(remoteUrl, apiKey) {
|
|
|
178
157
|
};
|
|
179
158
|
}
|
|
180
159
|
/** Replace the cache contents atomically: stage fully, then swap. */
|
|
181
|
-
function materializeCloudHome(files
|
|
160
|
+
function materializeCloudHome(files) {
|
|
182
161
|
const cacheDir = getOrgHomeDir();
|
|
183
162
|
const stagingDir = `${cacheDir}.staging-${process.pid}`;
|
|
184
163
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -189,11 +168,8 @@ function materializeCloudHome(files, metadata) {
|
|
|
189
168
|
continue;
|
|
190
169
|
const destination = path_1.default.join(stagingDir, file.relativePath);
|
|
191
170
|
fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
? decorateManagedOrgFile(file.content, metadata.backend)
|
|
195
|
-
: file.content;
|
|
196
|
-
fs_1.default.writeFileSync(destination, content);
|
|
171
|
+
// Sync transports content; provenance lives in the sidecar metadata.
|
|
172
|
+
fs_1.default.writeFileSync(destination, file.content);
|
|
197
173
|
}
|
|
198
174
|
// Provenance is written outside the home by writeHomeMetadata.
|
|
199
175
|
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
@@ -303,7 +279,7 @@ async function syncOrgCache(options) {
|
|
|
303
279
|
};
|
|
304
280
|
// The cloud has no user-visible folder, so this managed directory is the
|
|
305
281
|
// home for that backend, not a cache beside one.
|
|
306
|
-
materializeCloudHome(pack.files
|
|
282
|
+
materializeCloudHome(pack.files);
|
|
307
283
|
writeHomeMetadata(metadata);
|
|
308
284
|
return { status: 'synced', metadata };
|
|
309
285
|
}
|
|
@@ -21,7 +21,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
21
21
|
exports.CAPABILITY_DIRS = void 0;
|
|
22
22
|
exports.isCapabilityPackPath = isCapabilityPackPath;
|
|
23
23
|
exports.isScriptPath = isScriptPath;
|
|
24
|
-
exports.shouldDecorateAsMarkdown = shouldDecorateAsMarkdown;
|
|
25
24
|
exports.CAPABILITY_DIRS = ['jobs', 'skills', 'rules', 'templates', 'scripts', 'employees'];
|
|
26
25
|
/**
|
|
27
26
|
* Returns true when `rel` is a valid capability-pack member path:
|
|
@@ -97,11 +96,3 @@ function isScriptPath(rel) {
|
|
|
97
96
|
return false;
|
|
98
97
|
return rel.startsWith('scripts/');
|
|
99
98
|
}
|
|
100
|
-
/**
|
|
101
|
-
* True when a sync pack file should receive the managed-content header on
|
|
102
|
-
* materialization. Only markdown files get the header; scripts and JSON ride
|
|
103
|
-
* verbatim. Shared between manager-pack-sync and org-pack-sync.
|
|
104
|
-
*/
|
|
105
|
-
function shouldDecorateAsMarkdown(relativePath) {
|
|
106
|
-
return relativePath.toLowerCase().endsWith('.md');
|
|
107
|
-
}
|
|
@@ -180,27 +180,32 @@ class FraimDbService {
|
|
|
180
180
|
async initializeIndexes() {
|
|
181
181
|
if (!this.db)
|
|
182
182
|
throw new Error('DB not connected during index initialization');
|
|
183
|
-
|
|
183
|
+
// Cosmos DB for MongoDB rejects adding unique indexes to collections that already exist
|
|
184
|
+
// (even empty ones auto-created by the driver). All unique createIndex calls use .catch(() => {})
|
|
185
|
+
// so startup never crashes on a fresh database. On established databases (prod, dev) the indexes
|
|
186
|
+
// already exist and createIndex is a no-op; on a fresh PPE/test database the unique constraint
|
|
187
|
+
// is silently skipped — the application's own upsert/insert logic still enforces logical uniqueness.
|
|
188
|
+
await this.keysCollection.createIndex({ key: 1 }, { unique: true }).catch(() => { });
|
|
184
189
|
await this.keysCollection.createIndex({ userId: 1 });
|
|
185
190
|
await this.keysCollection.createIndex({ stripeSubscriptionId: 1 });
|
|
186
191
|
await this.keysCollection.createIndex({ status: 1 });
|
|
187
192
|
await this.keysCollection.createIndex({ expiresAt: 1 });
|
|
188
|
-
await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true });
|
|
193
|
+
await this.sessionsCollection.createIndex({ sessionId: 1 }, { unique: true }).catch(() => { });
|
|
189
194
|
await this.sessionsCollection.createIndex({ userId: 1 });
|
|
190
195
|
await this.sessionsCollection.createIndex({ lastActive: -1 });
|
|
191
196
|
// Issue #2093 — getActiveSessionByApiKey no longer bounds its match by recency, so a
|
|
192
197
|
// long-lived user's full session history is a candidate. This compound index keeps that
|
|
193
198
|
// lookup an indexed scan with an ordered result instead of a fetch-then-sort in memory.
|
|
194
199
|
await this.sessionsCollection.createIndex({ userId: 1, lastActive: -1 });
|
|
195
|
-
await this.signupsCollection.createIndex({ email: 1 }, { unique: true });
|
|
200
|
+
await this.signupsCollection.createIndex({ email: 1 }, { unique: true }).catch(() => { });
|
|
196
201
|
await this.signupsCollection.createIndex({ timestamp: -1 });
|
|
197
202
|
await this.salesCollection.createIndex({ email: 1 });
|
|
198
203
|
await this.salesCollection.createIndex({ timestamp: -1 });
|
|
199
|
-
await this.accessTokensCollection.createIndex({ token: 1 }, { unique: true });
|
|
204
|
+
await this.accessTokensCollection.createIndex({ token: 1 }, { unique: true }).catch(() => { });
|
|
200
205
|
// TTL index — createIndex is a no-op if the index already exists with the same options.
|
|
201
206
|
// Avoid drop+recreate: Cosmos DB PITR-restored accounts block index drops on restored collections.
|
|
202
207
|
await this.accessTokensCollection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
203
|
-
await this.pendingVerificationsCollection.createIndex({ token: 1 }, { unique: true });
|
|
208
|
+
await this.pendingVerificationsCollection.createIndex({ token: 1 }, { unique: true }).catch(() => { });
|
|
204
209
|
// Issue #563 — org artifacts are unique per (orgId, relativePath); audit reads newest-first.
|
|
205
210
|
// Index creation must never crash startup: Azure Cosmos DB (Mongo API) can
|
|
206
211
|
// reject a unique compound index, and connect() re-throws on failure. Logical
|
|
@@ -224,10 +229,10 @@ class FraimDbService {
|
|
|
224
229
|
await this.pendingVerificationsCollection.createIndex({ email: 1, codeHash: 1 });
|
|
225
230
|
// TTL index — same pattern: create is idempotent, drop is not safe on restored accounts.
|
|
226
231
|
await this.pendingVerificationsCollection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
|
227
|
-
await this.webhookEventsCollection.createIndex({ eventId: 1 }, { unique: true });
|
|
232
|
+
await this.webhookEventsCollection.createIndex({ eventId: 1 }, { unique: true }).catch(() => { });
|
|
228
233
|
await this.webhookEventsCollection.createIndex({ type: 1 });
|
|
229
234
|
await this.webhookEventsCollection.createIndex({ receivedAt: -1 });
|
|
230
|
-
await this.partnerDiscountsCollection.createIndex({ type: 1, match: 1 }, { unique: true });
|
|
235
|
+
await this.partnerDiscountsCollection.createIndex({ type: 1, match: 1 }, { unique: true }).catch(() => { });
|
|
231
236
|
await this.partnerDiscountsCollection.createIndex({ active: 1 });
|
|
232
237
|
try {
|
|
233
238
|
await this.managerTeamsCollection.createIndex({ managerId: 1, memberId: 1 }, { unique: true });
|
|
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
49
49
|
for (const line of fm[1].split(/\r?\n/)) {
|
|
50
50
|
const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
|
|
51
51
|
if (!match) {
|
|
52
|
-
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|
|
|
52
|
+
if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|synthesized)["']?\s*:/.test(line))
|
|
53
53
|
malformedField = true;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
76
76
|
return invalid('Unsupported metadata key spelling');
|
|
77
77
|
if (duplicates.size)
|
|
78
78
|
return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
|
|
79
|
-
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', '
|
|
79
|
+
for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'synthesized']) {
|
|
80
80
|
if (!Object.prototype.hasOwnProperty.call(fields, field))
|
|
81
81
|
return invalid(`Missing metadata: ${field}`);
|
|
82
82
|
}
|
|
@@ -110,21 +110,14 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
|
|
|
110
110
|
return invalid('Source is outside its configured learning location');
|
|
111
111
|
if (state === 'archived' && !fields.synthesized)
|
|
112
112
|
return invalid('Archive has no successful processing date');
|
|
113
|
-
const reference = fields.usageSourceRef;
|
|
114
113
|
const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
|
|
115
114
|
if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
|
|
116
115
|
return invalid('Source escapes its owning root');
|
|
117
|
-
if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
|
|
118
|
-
return invalid('Invalid usageSourceRef');
|
|
119
116
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
|
|
120
117
|
return invalid('Sources require an immutable sourceId UUID');
|
|
121
118
|
}
|
|
122
|
-
if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
|
|
123
|
-
|| reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
|
|
124
|
-
return invalid('usageSourceRef must name the owner-relative pending path');
|
|
125
|
-
}
|
|
126
119
|
return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
|
|
127
|
-
date: fields.date, job: fields.job, runId: fields.runId,
|
|
120
|
+
date: fields.date, job: fields.job, runId: fields.runId,
|
|
128
121
|
usageIdentity: `learning-source:${fields.sourceId}`,
|
|
129
122
|
sourceId: fields.sourceId,
|
|
130
123
|
synthesized: fields.synthesized, pendingPath, archivePath, state } };
|
|
@@ -560,7 +560,9 @@ class FraimLocalMCPServer {
|
|
|
560
560
|
this.orgCacheRefreshInFlight = false;
|
|
561
561
|
this.managerCacheRefreshInFlight = false;
|
|
562
562
|
this.writer = writer || process.stdout.write.bind(process.stdout);
|
|
563
|
-
|
|
563
|
+
const isPpe = (process.env.FRAIM_RELEASE_CHANNEL || '').toUpperCase() === 'PPE';
|
|
564
|
+
this.remoteUrl = isPpe ? 'https://app-ashley-fraim-ppe.azurewebsites.net'
|
|
565
|
+
: (process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
|
|
564
566
|
this.apiKey = this.loadApiKey();
|
|
565
567
|
this.localVersion = this.detectLocalVersion();
|
|
566
568
|
if (!this.apiKey) {
|