fraim 2.0.203 → 2.0.204
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/README.md +3 -3
- package/bin/fraim.js +8 -18
- package/dist/src/cli/commands/first-run.js +2 -37
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +1 -1
- package/dist/src/cli/fraim.js +0 -2
- package/dist/src/cli/setup/ide-invocation-surfaces.js +6 -6
- package/dist/src/cli/utils/managed-agent-paths.js +76 -6
- package/dist/src/cli/utils/version-utils.js +2 -2
- package/dist/src/config/persona-capability-bundles.js +3 -3
- package/dist/src/core/job-phases.js +168 -0
- package/dist/src/core/utils/ports.js +32 -0
- package/dist/src/core/utils/stub-generator.js +24 -2
- package/dist/src/first-run/session-service.js +106 -24
- package/dist/src/local-mcp-server/learning-context-builder.js +1 -1
- package/dist/src/local-mcp-server/stdio-server.js +41 -9
- package/dist/src/routes/analytics.js +2 -2
- package/dist/src/services/registry-service.js +2 -2
- package/index.js +1 -1
- package/package.json +61 -181
- package/dist/src/ai-hub/catalog.js +0 -406
- package/dist/src/ai-hub/cert-store.js +0 -70
- package/dist/src/ai-hub/conversation-store.js +0 -359
- package/dist/src/ai-hub/desktop-main.js +0 -449
- package/dist/src/ai-hub/hosts.js +0 -1664
- package/dist/src/ai-hub/hub-latest-version.js +0 -52
- package/dist/src/ai-hub/hub-launch-decision.js +0 -57
- package/dist/src/ai-hub/hub-runtime-file.js +0 -43
- package/dist/src/ai-hub/managed-browser.js +0 -269
- package/dist/src/ai-hub/manager-turns.js +0 -64
- package/dist/src/ai-hub/office-sideload.js +0 -156
- package/dist/src/ai-hub/openclaw-bridge.js +0 -250
- package/dist/src/ai-hub/preferences.js +0 -201
- package/dist/src/ai-hub/remote-hub-gateway.js +0 -88
- package/dist/src/ai-hub/server.js +0 -3827
- package/dist/src/ai-hub/types.js +0 -2
- package/dist/src/ai-hub/word-sideload.js +0 -95
- package/dist/src/cli/commands/hub.js +0 -238
- 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
- package/dist/src/services/email-service-clean.js +0 -782
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +0 -33
- package/extensions/office-word/taskpane.html +0 -242
- package/public/ai-hub/index.html +0 -894
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +0 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +0 -30
- package/public/ai-hub/review.css +0 -381
- package/public/ai-hub/script.js +0 -11015
- package/public/ai-hub/styles.css +0 -4873
- package/public/portfolio/ashley.html +0 -523
- package/public/portfolio/auditya.html +0 -83
- package/public/portfolio/banke.html +0 -83
- package/public/portfolio/beza.html +0 -659
- package/public/portfolio/careena.html +0 -632
- package/public/portfolio/casey.html +0 -568
- package/public/portfolio/celia.html +0 -490
- package/public/portfolio/deidre.html +0 -642
- package/public/portfolio/gautam.html +0 -597
- package/public/portfolio/hari.html +0 -469
- package/public/portfolio/huxley.html +0 -1354
- package/public/portfolio/index.html +0 -741
- package/public/portfolio/maestro.html +0 -518
- package/public/portfolio/mandy.html +0 -590
- package/public/portfolio/mona.html +0 -597
- package/public/portfolio/pam.html +0 -887
- package/public/portfolio/procella.html +0 -107
- package/public/portfolio/qasm.html +0 -569
- package/public/portfolio/ricardo.html +0 -489
- package/public/portfolio/sade.html +0 -560
- package/public/portfolio/sam.html +0 -654
- package/public/portfolio/sechar.html +0 -580
- package/public/portfolio/sreya.html +0 -599
- package/public/portfolio/swen.html +0 -601
- /package/dist/src/{ai-hub → core}/brand-store.js +0 -0
|
@@ -1,406 +0,0 @@
|
|
|
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.summarizeProject = summarizeProject;
|
|
7
|
-
exports.discoverEmployeeJobs = discoverEmployeeJobs;
|
|
8
|
-
exports.discoverManagerTemplates = discoverManagerTemplates;
|
|
9
|
-
exports.loadJobPhases = loadJobPhases;
|
|
10
|
-
exports.loadAllJobPhaseIds = loadAllJobPhaseIds;
|
|
11
|
-
exports.labelForPhaseId = labelForPhaseId;
|
|
12
|
-
exports.getAiHubCategories = getAiHubCategories;
|
|
13
|
-
const fs_1 = __importDefault(require("fs"));
|
|
14
|
-
const path_1 = __importDefault(require("path"));
|
|
15
|
-
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
16
|
-
// Directories scanned for employee jobs at runtime, in lowest-to-highest
|
|
17
|
-
// precedence order. Later entries win on {categoryId, jobId} collision.
|
|
18
|
-
//
|
|
19
|
-
// FRAIM execution normally reads only the two `fraim/` layers: the synced
|
|
20
|
-
// baseline and the personalized override. The Hub can explicitly opt into the
|
|
21
|
-
// source `registry/` fallback when running against the FRAIM repo before sync.
|
|
22
|
-
// - <project>/fraim/ai-employee/jobs/<category>/ - synced baseline
|
|
23
|
-
// - <project>/fraim/personalized-employee/jobs/<category>/ - taught/customized override
|
|
24
|
-
// Only the personalized-employee layer is "personalized" (issue #566).
|
|
25
|
-
const EMPLOYEE_JOB_LAYERS = [
|
|
26
|
-
{ segments: ['ai-employee', 'jobs'], personalized: false },
|
|
27
|
-
{ segments: ['personalized-employee', 'jobs'], personalized: true },
|
|
28
|
-
];
|
|
29
|
-
// Manager templates use the matching layer model.
|
|
30
|
-
const MANAGER_JOB_LAYERS = [
|
|
31
|
-
{ segments: ['ai-manager', 'jobs'], personalized: false },
|
|
32
|
-
{ segments: ['personalized-employee', 'manager-jobs'], personalized: true },
|
|
33
|
-
];
|
|
34
|
-
const REGISTRY_EMPLOYEE_JOB_LAYERS = [
|
|
35
|
-
{ base: 'project', segments: ['registry', 'jobs', 'ai-employee'], personalized: false },
|
|
36
|
-
];
|
|
37
|
-
const REGISTRY_MANAGER_JOB_LAYERS = [
|
|
38
|
-
{ base: 'project', segments: ['registry', 'jobs', 'ai-manager'], personalized: false },
|
|
39
|
-
];
|
|
40
|
-
const KNOWN_LABEL_OVERRIDES = {
|
|
41
|
-
// Project conventions where a directory name should render as a recognized
|
|
42
|
-
// short form rather than its mechanical title-case spelling.
|
|
43
|
-
'go-to-market': 'GTM',
|
|
44
|
-
gtm: 'GTM',
|
|
45
|
-
'qa': 'QA',
|
|
46
|
-
rfp: 'RFP',
|
|
47
|
-
ai: 'AI',
|
|
48
|
-
// Manager-side groups whose directory names are already idiomatic and
|
|
49
|
-
// shouldn't be split on dashes by the default humanizer.
|
|
50
|
-
'1-1': '1:1',
|
|
51
|
-
};
|
|
52
|
-
const sectionValue = (content, heading) => {
|
|
53
|
-
const match = content.match(new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?:\\r?\\n## |\\r?\\n---|$)`));
|
|
54
|
-
if (!match)
|
|
55
|
-
return [];
|
|
56
|
-
return match[1]
|
|
57
|
-
.split(/\r?\n/)
|
|
58
|
-
.map((line) => line.trim())
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
.map((line) => line.replace(/^[-*]\s*/, '').trim());
|
|
61
|
-
};
|
|
62
|
-
const humanizeName = (raw) => {
|
|
63
|
-
const lower = raw.toLowerCase();
|
|
64
|
-
if (KNOWN_LABEL_OVERRIDES[lower])
|
|
65
|
-
return KNOWN_LABEL_OVERRIDES[lower];
|
|
66
|
-
return raw
|
|
67
|
-
.split(/[-_\s]+/)
|
|
68
|
-
.filter((part) => part.length > 0)
|
|
69
|
-
.map((part) => {
|
|
70
|
-
const partLower = part.toLowerCase();
|
|
71
|
-
if (KNOWN_LABEL_OVERRIDES[partLower])
|
|
72
|
-
return KNOWN_LABEL_OVERRIDES[partLower];
|
|
73
|
-
return `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}`;
|
|
74
|
-
})
|
|
75
|
-
.join(' ');
|
|
76
|
-
};
|
|
77
|
-
const toPosix = (value) => value.replace(/\\/g, '/');
|
|
78
|
-
function readSubdirectoryNames(dirPath) {
|
|
79
|
-
if (!fs_1.default.existsSync(dirPath))
|
|
80
|
-
return [];
|
|
81
|
-
return fs_1.default
|
|
82
|
-
.readdirSync(dirPath, { withFileTypes: true })
|
|
83
|
-
.filter((entry) => entry.isDirectory())
|
|
84
|
-
.map((entry) => entry.name)
|
|
85
|
-
.sort((a, b) => a.localeCompare(b));
|
|
86
|
-
}
|
|
87
|
-
function readMarkdownFileNames(dirPath) {
|
|
88
|
-
if (!fs_1.default.existsSync(dirPath))
|
|
89
|
-
return [];
|
|
90
|
-
return fs_1.default
|
|
91
|
-
.readdirSync(dirPath, { withFileTypes: true })
|
|
92
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
93
|
-
.map((entry) => entry.name)
|
|
94
|
-
.sort((a, b) => a.localeCompare(b));
|
|
95
|
-
}
|
|
96
|
-
function parseJobStub(filePath, categoryId, categoryLabel, projectPath, personalized) {
|
|
97
|
-
const content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
98
|
-
const fileName = path_1.default.basename(filePath, '.md');
|
|
99
|
-
const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
|
|
100
|
-
const outcome = sectionValue(content, 'Outcome');
|
|
101
|
-
return {
|
|
102
|
-
id: fileName,
|
|
103
|
-
title: humanizeName(fileName),
|
|
104
|
-
categoryId,
|
|
105
|
-
categoryLabel,
|
|
106
|
-
intent,
|
|
107
|
-
outcome,
|
|
108
|
-
stubPath: toPosix(path_1.default.relative(projectPath, filePath)),
|
|
109
|
-
personalized: !!personalized,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function parseManagerStub(filePath, groupId, groupLabel, projectPath) {
|
|
113
|
-
const content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
114
|
-
const fileName = path_1.default.basename(filePath, '.md');
|
|
115
|
-
const intent = sectionValue(content, 'Intent')[0] || 'No intent summary available.';
|
|
116
|
-
return {
|
|
117
|
-
id: fileName,
|
|
118
|
-
title: humanizeName(fileName),
|
|
119
|
-
groupId,
|
|
120
|
-
groupLabel,
|
|
121
|
-
intent,
|
|
122
|
-
stubPath: toPosix(path_1.default.relative(projectPath, filePath)),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function summarizeProject(projectPath) {
|
|
126
|
-
if (!projectPath) {
|
|
127
|
-
return {
|
|
128
|
-
path: '',
|
|
129
|
-
exists: false,
|
|
130
|
-
hasFraim: false,
|
|
131
|
-
message: 'Choose a project path to load FRAIM jobs.',
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (!fs_1.default.existsSync(projectPath)) {
|
|
135
|
-
return {
|
|
136
|
-
path: projectPath,
|
|
137
|
-
exists: false,
|
|
138
|
-
hasFraim: false,
|
|
139
|
-
message: 'Project path does not exist on this machine.',
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
|
|
143
|
-
const hasFraim = fs_1.default.existsSync(fraimDir);
|
|
144
|
-
if (!hasFraim) {
|
|
145
|
-
return {
|
|
146
|
-
path: projectPath,
|
|
147
|
-
exists: true,
|
|
148
|
-
hasFraim: false,
|
|
149
|
-
needsOnboarding: true,
|
|
150
|
-
message: 'This folder has not been set up with FRAIM yet. Onboarding will get your AI employee started.',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
path: projectPath,
|
|
155
|
-
exists: true,
|
|
156
|
-
hasFraim: true,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function resolveLayerRoot(projectPath, layer) {
|
|
160
|
-
if (layer.base === 'project')
|
|
161
|
-
return path_1.default.join(projectPath, ...layer.segments);
|
|
162
|
-
return path_1.default.join((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath), ...layer.segments);
|
|
163
|
-
}
|
|
164
|
-
function discoverLayers(projectPath, layers) {
|
|
165
|
-
const out = [];
|
|
166
|
-
for (const layer of layers) {
|
|
167
|
-
const layerRoot = resolveLayerRoot(projectPath, layer);
|
|
168
|
-
for (const categoryName of readSubdirectoryNames(layerRoot)) {
|
|
169
|
-
out.push({
|
|
170
|
-
layerRoot,
|
|
171
|
-
categoryId: categoryName,
|
|
172
|
-
categoryDir: path_1.default.join(layerRoot, categoryName),
|
|
173
|
-
personalized: layer.personalized,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
178
|
-
}
|
|
179
|
-
function discoverEmployeeJobs(projectPath, options = {}) {
|
|
180
|
-
const project = summarizeProject(projectPath);
|
|
181
|
-
if (!project.exists || (!project.hasFraim && !options.includeRegistry))
|
|
182
|
-
return [];
|
|
183
|
-
const layers = discoverLayers(projectPath, options.includeRegistry
|
|
184
|
-
? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS]
|
|
185
|
-
: EMPLOYEE_JOB_LAYERS);
|
|
186
|
-
// Group by categoryId so all layers contribute to the same labelled category.
|
|
187
|
-
const jobsByKey = new Map();
|
|
188
|
-
for (const layer of layers) {
|
|
189
|
-
const categoryLabel = humanizeName(layer.categoryId);
|
|
190
|
-
for (const fileName of readMarkdownFileNames(layer.categoryDir)) {
|
|
191
|
-
const filePath = path_1.default.join(layer.categoryDir, fileName);
|
|
192
|
-
const job = parseJobStub(filePath, layer.categoryId, categoryLabel, projectPath, layer.personalized);
|
|
193
|
-
// Later layers override earlier layers on {category, jobId} collision —
|
|
194
|
-
// personalized-employee wins over the synced ai-employee baseline. The
|
|
195
|
-
// winning job carries its own layer's `personalized` flag (issue #566).
|
|
196
|
-
jobsByKey.set(`${job.categoryId}::${job.id}`, job);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Stable sort: category label, then job title.
|
|
200
|
-
return [...jobsByKey.values()].sort((a, b) => {
|
|
201
|
-
if (a.categoryLabel !== b.categoryLabel)
|
|
202
|
-
return a.categoryLabel.localeCompare(b.categoryLabel);
|
|
203
|
-
return a.title.localeCompare(b.title);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function discoverManagerTemplates(projectPath, options = {}) {
|
|
207
|
-
const project = summarizeProject(projectPath);
|
|
208
|
-
if (!project.exists || (!project.hasFraim && !options.includeRegistry))
|
|
209
|
-
return [];
|
|
210
|
-
const layers = discoverLayers(projectPath, options.includeRegistry
|
|
211
|
-
? [...REGISTRY_MANAGER_JOB_LAYERS, ...MANAGER_JOB_LAYERS]
|
|
212
|
-
: MANAGER_JOB_LAYERS);
|
|
213
|
-
const templatesByKey = new Map();
|
|
214
|
-
for (const layer of layers) {
|
|
215
|
-
const groupLabel = humanizeName(layer.categoryId);
|
|
216
|
-
for (const fileName of readMarkdownFileNames(layer.categoryDir)) {
|
|
217
|
-
const filePath = path_1.default.join(layer.categoryDir, fileName);
|
|
218
|
-
const template = parseManagerStub(filePath, layer.categoryId, groupLabel, projectPath);
|
|
219
|
-
templatesByKey.set(`${template.groupId}::${template.id}`, template);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return [...templatesByKey.values()].sort((a, b) => {
|
|
223
|
-
if (a.groupLabel !== b.groupLabel)
|
|
224
|
-
return a.groupLabel.localeCompare(b.groupLabel);
|
|
225
|
-
return a.title.localeCompare(b.title);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
// Issue #347 — load the phase list for a job, in declaration order, with
|
|
229
|
-
// only the phases reachable on the active run's path. The Hub UI uses
|
|
230
|
-
// this to render the pizza tracker.
|
|
231
|
-
// Namespace prefixes the friendly-label formatter strips. Order matters
|
|
232
|
-
// only for tie-breaking; we always pick the longest match.
|
|
233
|
-
const KNOWN_PHASE_PREFIXES = [
|
|
234
|
-
'implement-',
|
|
235
|
-
'address-',
|
|
236
|
-
'spec-',
|
|
237
|
-
'design-',
|
|
238
|
-
'context-',
|
|
239
|
-
];
|
|
240
|
-
function friendlyPhaseLabel(phaseId, override) {
|
|
241
|
-
if (override)
|
|
242
|
-
return override;
|
|
243
|
-
let body = phaseId;
|
|
244
|
-
for (const prefix of KNOWN_PHASE_PREFIXES) {
|
|
245
|
-
if (body.startsWith(prefix) && body.length > prefix.length) {
|
|
246
|
-
body = body.slice(prefix.length);
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
const spaced = body.replace(/-/g, ' ').trim();
|
|
251
|
-
if (spaced.length === 0)
|
|
252
|
-
return phaseId;
|
|
253
|
-
// Spec R1.2: first letter uppercase, rest lowercase.
|
|
254
|
-
return spaced[0].toUpperCase() + spaced.slice(1).toLowerCase();
|
|
255
|
-
}
|
|
256
|
-
function readJobFrontmatter(filePath) {
|
|
257
|
-
const raw = fs_1.default.readFileSync(filePath, 'utf8');
|
|
258
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
259
|
-
if (!match)
|
|
260
|
-
return null;
|
|
261
|
-
const block = match[1].trim();
|
|
262
|
-
if (!block.startsWith('{'))
|
|
263
|
-
return null;
|
|
264
|
-
try {
|
|
265
|
-
return JSON.parse(block);
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
function findJobStubPath(projectPath, jobId) {
|
|
272
|
-
// Walk both employee and manager layer sets. The analytics surfaces can
|
|
273
|
-
// inspect any FRAIM job run, including manager-side jobs such as
|
|
274
|
-
// sleep-on-learnings.
|
|
275
|
-
const layers = discoverLayers(projectPath, [
|
|
276
|
-
...EMPLOYEE_JOB_LAYERS,
|
|
277
|
-
...MANAGER_JOB_LAYERS,
|
|
278
|
-
]);
|
|
279
|
-
let resolved = null;
|
|
280
|
-
for (const layer of layers) {
|
|
281
|
-
const candidate = path_1.default.join(layer.categoryDir, `${jobId}.md`);
|
|
282
|
-
if (fs_1.default.existsSync(candidate)) {
|
|
283
|
-
resolved = candidate; // do not break — later layers override
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (resolved)
|
|
287
|
-
return resolved;
|
|
288
|
-
// Fallback: look in the hub's own built-in stubs. This covers first-run
|
|
289
|
-
// jobs (e.g. project-onboarding) that run against a project that hasn't
|
|
290
|
-
// been synced yet, so no local stubs exist under the project directory.
|
|
291
|
-
// In the compiled dist, __dirname is dist/ai-hub/ so this resolves to
|
|
292
|
-
// dist/registry/stubs/jobs/.
|
|
293
|
-
const hubStubsDir = path_1.default.resolve(__dirname, '..', 'registry', 'stubs', 'jobs');
|
|
294
|
-
for (const hostType of ['ai-employee', 'ai-manager']) {
|
|
295
|
-
const hostDir = path_1.default.join(hubStubsDir, hostType);
|
|
296
|
-
for (const category of readSubdirectoryNames(hostDir)) {
|
|
297
|
-
const candidate = path_1.default.join(hostDir, category, `${jobId}.md`);
|
|
298
|
-
if (fs_1.default.existsSync(candidate))
|
|
299
|
-
return candidate;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
// Resolve a phase's `onSuccess` edge to the next phase id given the run's
|
|
305
|
-
// discriminant. Returns null when the edge is absent or terminal.
|
|
306
|
-
function nextPhase(edge, discriminant) {
|
|
307
|
-
if (edge == null)
|
|
308
|
-
return null;
|
|
309
|
-
if (typeof edge === 'string')
|
|
310
|
-
return edge;
|
|
311
|
-
if (typeof edge === 'object') {
|
|
312
|
-
if (typeof edge[discriminant] === 'string')
|
|
313
|
-
return edge[discriminant];
|
|
314
|
-
if (typeof edge.default === 'string')
|
|
315
|
-
return edge.default;
|
|
316
|
-
}
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
// Parse the ordered phase list from a job stub's ## Steps section.
|
|
320
|
-
// Real FRAIM job stubs use Markdown steps rather than JSON frontmatter;
|
|
321
|
-
// this is the fallback parser that makes the pizza tracker work for them.
|
|
322
|
-
function loadJobPhasesFromSteps(filePath) {
|
|
323
|
-
const raw = fs_1.default.readFileSync(filePath, 'utf8');
|
|
324
|
-
const stepsMatch = raw.match(/## Steps\r?\n([\s\S]*?)(?:\r?\n## |\r?\n---|$)/);
|
|
325
|
-
if (!stepsMatch)
|
|
326
|
-
return [];
|
|
327
|
-
const phases = [];
|
|
328
|
-
for (const line of stepsMatch[1].split(/\r?\n/)) {
|
|
329
|
-
const m = line.match(/`([a-z][a-z0-9-]*)`/);
|
|
330
|
-
if (m)
|
|
331
|
-
phases.push({ id: m[1], label: friendlyPhaseLabel(m[1]) });
|
|
332
|
-
}
|
|
333
|
-
return phases;
|
|
334
|
-
}
|
|
335
|
-
function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
|
|
336
|
-
const stubPath = findJobStubPath(projectPath, jobId);
|
|
337
|
-
if (!stubPath)
|
|
338
|
-
return [];
|
|
339
|
-
const fm = readJobFrontmatter(stubPath);
|
|
340
|
-
if (!fm || !fm.initialPhase || !fm.phases)
|
|
341
|
-
return loadJobPhasesFromSteps(stubPath);
|
|
342
|
-
const visited = new Set();
|
|
343
|
-
const ordered = [];
|
|
344
|
-
let cursor = fm.initialPhase;
|
|
345
|
-
// Walk onSuccess edges to compute the path; guard against cycles by
|
|
346
|
-
// tracking visited phase ids. A phase can only appear once in the path.
|
|
347
|
-
while (cursor && !visited.has(cursor)) {
|
|
348
|
-
visited.add(cursor);
|
|
349
|
-
ordered.push(cursor);
|
|
350
|
-
const phaseDef = fm.phases[cursor];
|
|
351
|
-
if (!phaseDef)
|
|
352
|
-
break;
|
|
353
|
-
cursor = nextPhase(phaseDef.onSuccess, discriminant);
|
|
354
|
-
}
|
|
355
|
-
const labels = fm.phaseLabels || {};
|
|
356
|
-
return ordered.map((id) => ({ id, label: friendlyPhaseLabel(id, labels[id]) }));
|
|
357
|
-
}
|
|
358
|
-
// Issue #347 — return the set of ALL phase ids declared in the job's
|
|
359
|
-
// frontmatter (not just the onSuccess-reachable path). Used by
|
|
360
|
-
// deriveStages to vet whether a visited phase id genuinely belongs to
|
|
361
|
-
// this job before appending it to the tracker, so cross-job pollution
|
|
362
|
-
// (agent calling seekMentoring for a different job mid-run) cannot
|
|
363
|
-
// surface stages from elsewhere.
|
|
364
|
-
function loadAllJobPhaseIds(jobId, projectPath) {
|
|
365
|
-
const stubPath = findJobStubPath(projectPath, jobId);
|
|
366
|
-
if (!stubPath)
|
|
367
|
-
return new Set();
|
|
368
|
-
const fm = readJobFrontmatter(stubPath);
|
|
369
|
-
if (!fm || !fm.phases) {
|
|
370
|
-
const phases = loadJobPhasesFromSteps(stubPath);
|
|
371
|
-
return new Set(phases.map((p) => p.id));
|
|
372
|
-
}
|
|
373
|
-
return new Set(Object.keys(fm.phases));
|
|
374
|
-
}
|
|
375
|
-
// Issue #347 — public exposure of the friendly-label rule so callers
|
|
376
|
-
// outside catalog.ts can render labels for phases that exist in the
|
|
377
|
-
// frontmatter but weren't on the reachable path returned by loadJobPhases.
|
|
378
|
-
function labelForPhaseId(phaseId, jobId, projectPath) {
|
|
379
|
-
const stubPath = findJobStubPath(projectPath, jobId);
|
|
380
|
-
if (stubPath) {
|
|
381
|
-
const fm = readJobFrontmatter(stubPath);
|
|
382
|
-
if (fm?.phaseLabels?.[phaseId])
|
|
383
|
-
return fm.phaseLabels[phaseId];
|
|
384
|
-
}
|
|
385
|
-
return friendlyPhaseLabel(phaseId);
|
|
386
|
-
}
|
|
387
|
-
function getAiHubCategories(projectPath, options = {}) {
|
|
388
|
-
const project = summarizeProject(projectPath);
|
|
389
|
-
if (!project.exists || (!project.hasFraim && !options.includeRegistry))
|
|
390
|
-
return [];
|
|
391
|
-
// A category is any directory found at any layer; deduplicate by id.
|
|
392
|
-
const layers = discoverLayers(projectPath, options.includeRegistry
|
|
393
|
-
? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS]
|
|
394
|
-
: EMPLOYEE_JOB_LAYERS);
|
|
395
|
-
const seen = new Map();
|
|
396
|
-
for (const layer of layers) {
|
|
397
|
-
if (!seen.has(layer.categoryId)) {
|
|
398
|
-
seen.set(layer.categoryId, {
|
|
399
|
-
id: layer.categoryId,
|
|
400
|
-
label: humanizeName(layer.categoryId),
|
|
401
|
-
description: `${humanizeName(layer.categoryId)} jobs discovered in this project.`,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return [...seen.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
406
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
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.loadOrCreateCert = loadOrCreateCert;
|
|
7
|
-
exports.trustCert = trustCert;
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const os_1 = __importDefault(require("os"));
|
|
11
|
-
const child_process_1 = require("child_process");
|
|
12
|
-
function certDir() {
|
|
13
|
-
const base = process.env.APPDATA ||
|
|
14
|
-
(process.platform === 'darwin'
|
|
15
|
-
? path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support')
|
|
16
|
-
: path_1.default.join(os_1.default.homedir(), '.config'));
|
|
17
|
-
return path_1.default.join(base, 'FRAIM', 'certs');
|
|
18
|
-
}
|
|
19
|
-
function certPaths() {
|
|
20
|
-
const dir = certDir();
|
|
21
|
-
return { keyPath: path_1.default.join(dir, 'localhost.key'), certPath: path_1.default.join(dir, 'localhost.crt') };
|
|
22
|
-
}
|
|
23
|
-
async function loadOrCreateCert() {
|
|
24
|
-
const { keyPath, certPath } = certPaths();
|
|
25
|
-
if (fs_1.default.existsSync(keyPath) && fs_1.default.existsSync(certPath)) {
|
|
26
|
-
return { key: fs_1.default.readFileSync(keyPath, 'utf8'), cert: fs_1.default.readFileSync(certPath, 'utf8') };
|
|
27
|
-
}
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
-
const selfsigned = require('selfsigned');
|
|
30
|
-
const pem = await selfsigned.generate([{ name: 'commonName', value: 'localhost' }], {
|
|
31
|
-
keySize: 2048,
|
|
32
|
-
days: 3650,
|
|
33
|
-
algorithm: 'sha256',
|
|
34
|
-
extensions: [
|
|
35
|
-
{ name: 'subjectAltName', altNames: [{ type: 2, value: 'localhost' }, { type: 7, ip: '127.0.0.1' }] },
|
|
36
|
-
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true },
|
|
37
|
-
{ name: 'extKeyUsage', serverAuth: true },
|
|
38
|
-
],
|
|
39
|
-
});
|
|
40
|
-
fs_1.default.mkdirSync(path_1.default.dirname(keyPath), { recursive: true });
|
|
41
|
-
fs_1.default.writeFileSync(keyPath, pem.private, { mode: 0o600 });
|
|
42
|
-
fs_1.default.writeFileSync(certPath, pem.cert, { mode: 0o644 });
|
|
43
|
-
// NOTE: we deliberately do NOT install this into the OS trusted-root store here.
|
|
44
|
-
// Desktop Word/PowerPoint reach the pane over plain HTTP loopback and need no cert.
|
|
45
|
-
// Trusting a root CA triggers a Windows security popup on every add/remove and is
|
|
46
|
-
// invasive, so it is reserved for explicit Word-Online opt-in via trustCert() below.
|
|
47
|
-
return { key: pem.private, cert: pem.cert };
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Installs the cert as a trusted root CA in the user's OS certificate store.
|
|
51
|
-
* ONLY call this for explicit Word-Online support — it prompts a Windows security
|
|
52
|
-
* dialog. It is idempotent: it no-ops if a matching localhost cert is already
|
|
53
|
-
* trusted, so it never produces the delete/add popup churn.
|
|
54
|
-
* Windows: certutil -addstore -user Root (skipped if already present)
|
|
55
|
-
* macOS: security add-trusted-cert -r trustRoot -k login.keychain
|
|
56
|
-
*/
|
|
57
|
-
function trustCert(certPath) {
|
|
58
|
-
if (process.platform === 'win32') {
|
|
59
|
-
// Skip if a localhost cert is already trusted — avoids the confirmation popup.
|
|
60
|
-
const check = (0, child_process_1.spawnSync)('certutil', ['-user', '-store', 'Root', 'localhost'], { encoding: 'utf8' });
|
|
61
|
-
if (check.status === 0 && /localhost/i.test(check.stdout))
|
|
62
|
-
return;
|
|
63
|
-
(0, child_process_1.spawnSync)('certutil', ['-addstore', '-user', 'Root', certPath], { stdio: 'ignore' });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (process.platform === 'darwin') {
|
|
67
|
-
const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
|
|
68
|
-
(0, child_process_1.spawnSync)('security', ['add-trusted-cert', '-r', 'trustRoot', '-k', keychain, certPath], { stdio: 'ignore' });
|
|
69
|
-
}
|
|
70
|
-
}
|