fraim-hub 2.0.207 → 2.0.209
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/bin/fraim-hub.js +1 -1
- package/dist/src/ai-hub/cli.js +13 -7
- package/dist/src/ai-hub/desktop-main.js +4 -3
- package/dist/src/ai-hub/hosts.js +16 -6
- package/dist/src/ai-hub/manager-turns.js +9 -0
- package/dist/src/ai-hub/preferences.js +34 -20
- package/dist/src/ai-hub/server.js +112 -51
- package/dist/src/ai-hub/word-sideload.js +95 -0
- package/dist/src/cli/fraim-hub.js +1 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +8 -5
- package/extensions/office-word/taskpane.html +6 -6
- package/index.js +1 -1
- package/package.json +2 -2
- package/public/ai-hub/index.html +10 -9
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
- package/public/ai-hub/script.js +35 -8
- package/public/ai-hub/styles.css +20 -0
- package/public/portfolio/ashley.html +1 -1
- package/public/portfolio/casey.html +1 -1
- package/public/portfolio/celia.html +1 -1
- package/public/portfolio/gautam.html +1 -1
- package/public/portfolio/hari.html +1 -1
- package/public/portfolio/maestro.html +1 -1
- package/public/portfolio/mandy.html +1 -1
- package/public/portfolio/pam.html +6 -6
- package/public/portfolio/qasm.html +1 -1
- package/public/portfolio/sade.html +1 -1
- package/public/portfolio/sam.html +1 -1
- package/public/portfolio/swen.html +6 -6
package/bin/fraim-hub.js
CHANGED
package/dist/src/ai-hub/cli.js
CHANGED
|
@@ -80,7 +80,10 @@ function openDesktopWindow(projectPath, preferredPort) {
|
|
|
80
80
|
if (!electronBinary || !desktopEntry) {
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
|
-
const
|
|
83
|
+
const args = projectPath
|
|
84
|
+
? [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)]
|
|
85
|
+
: [desktopEntry, '--port', String(preferredPort)];
|
|
86
|
+
const child = (0, child_process_1.spawn)(electronBinary, args, {
|
|
84
87
|
detached: true,
|
|
85
88
|
stdio: 'ignore',
|
|
86
89
|
});
|
|
@@ -182,7 +185,7 @@ async function reconcileRunningHub(flags) {
|
|
|
182
185
|
async function runHub(options) {
|
|
183
186
|
const { AiHubServer, findAvailablePort } = await Promise.resolve().then(() => __importStar(require('./server')));
|
|
184
187
|
const preferredPort = options.port || (0, git_utils_1.getPort)() + 100;
|
|
185
|
-
const projectPath = path_1.default.resolve(options.projectPath
|
|
188
|
+
const projectPath = options.projectPath ? path_1.default.resolve(options.projectPath) : undefined;
|
|
186
189
|
if (options.open) {
|
|
187
190
|
const wantDesktop = !options.browser;
|
|
188
191
|
if (wantDesktop) {
|
|
@@ -191,22 +194,25 @@ async function runHub(options) {
|
|
|
191
194
|
const openedDesktop = wantDesktop && openDesktopWindow(projectPath, preferredPort);
|
|
192
195
|
if (!openedDesktop) {
|
|
193
196
|
const port = await findAvailablePort(preferredPort);
|
|
194
|
-
const server = new AiHubServer({ projectPath });
|
|
197
|
+
const server = new AiHubServer(projectPath ? { projectPath } : {});
|
|
195
198
|
await server.start(port);
|
|
196
199
|
const url = `http://127.0.0.1:${port}/ai-hub/`;
|
|
197
200
|
console.log(`AI Hub running at ${url}`);
|
|
198
|
-
console.log(`Project path: ${
|
|
201
|
+
console.log(`Project path: ${server.getProjectPath()}`);
|
|
199
202
|
openBrowser(url);
|
|
200
203
|
return;
|
|
201
204
|
}
|
|
202
205
|
console.log('AI Hub desktop shell launched.');
|
|
203
|
-
|
|
206
|
+
if (projectPath)
|
|
207
|
+
console.log(`Project path: ${projectPath}`);
|
|
208
|
+
else
|
|
209
|
+
console.log('Project path: loaded from saved Hub state');
|
|
204
210
|
return;
|
|
205
211
|
}
|
|
206
212
|
const port = await findAvailablePort(preferredPort);
|
|
207
|
-
const server = new AiHubServer({ projectPath });
|
|
213
|
+
const server = new AiHubServer(projectPath ? { projectPath } : {});
|
|
208
214
|
await server.start(port);
|
|
209
215
|
const url = `http://127.0.0.1:${port}/ai-hub/`;
|
|
210
216
|
console.log(`AI Hub running at ${url}`);
|
|
211
|
-
console.log(`Project path: ${
|
|
217
|
+
console.log(`Project path: ${server.getProjectPath()}`);
|
|
212
218
|
}
|
|
@@ -33,7 +33,7 @@ function preferredWindowSize() {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
function parseArgs(argv) {
|
|
36
|
-
let projectPath
|
|
36
|
+
let projectPath;
|
|
37
37
|
let preferredPort = 43091;
|
|
38
38
|
for (let i = 0; i < argv.length; i += 1) {
|
|
39
39
|
if (argv[i] === '--project-path' && argv[i + 1]) {
|
|
@@ -355,7 +355,7 @@ async function launchDesktopShell(options) {
|
|
|
355
355
|
// Fast on subsequent launches (file read); ~200ms on first launch (key gen).
|
|
356
356
|
const certBundle = await (0, cert_store_1.loadOrCreateCert)();
|
|
357
357
|
server = new server_1.AiHubServer({
|
|
358
|
-
projectPath: options.projectPath,
|
|
358
|
+
...(options.projectPath ? { projectPath: options.projectPath } : {}),
|
|
359
359
|
// Issue #701: no local DB. Persona/manager-team state resolves through the hosted
|
|
360
360
|
// server via the Hub's default remote gateway (authenticated by the user's API key).
|
|
361
361
|
httpsPort,
|
|
@@ -370,6 +370,7 @@ async function launchDesktopShell(options) {
|
|
|
370
370
|
},
|
|
371
371
|
});
|
|
372
372
|
await server.start(httpPort);
|
|
373
|
+
const resolvedProjectPath = server.getProjectPath();
|
|
373
374
|
// #755: record the live instance so `fraim hub` can detect a stale build and
|
|
374
375
|
// replace it instead of re-focusing an outdated process.
|
|
375
376
|
try {
|
|
@@ -383,7 +384,7 @@ async function launchDesktopShell(options) {
|
|
|
383
384
|
catch (err) {
|
|
384
385
|
console.warn('[fraim] could not write hub-runtime.json:', err);
|
|
385
386
|
}
|
|
386
|
-
ensureWordSideload(
|
|
387
|
+
ensureWordSideload(resolvedProjectPath, httpPort);
|
|
387
388
|
const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
|
|
388
389
|
createTray(hubUrl);
|
|
389
390
|
await createWindow(hubUrl);
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -686,12 +686,22 @@ function parseFraimInvocation(message) {
|
|
|
686
686
|
const trimmed = message.trim();
|
|
687
687
|
if (!trimmed)
|
|
688
688
|
return null;
|
|
689
|
-
const match = trimmed.match(/^[
|
|
689
|
+
const match = trimmed.match(/^([$\/@]fraim)([\s\S]*)$/);
|
|
690
690
|
if (!match)
|
|
691
691
|
return null;
|
|
692
|
+
const rest = match[2] || '';
|
|
693
|
+
if (rest && !/^\s/.test(rest))
|
|
694
|
+
return null;
|
|
695
|
+
const sameLineJobMatch = rest.match(/^[ \t]+(\S+)([\s\S]*)$/);
|
|
696
|
+
if (sameLineJobMatch) {
|
|
697
|
+
return {
|
|
698
|
+
jobId: sameLineJobMatch[1],
|
|
699
|
+
remainder: (sameLineJobMatch[2] || '').trim(),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
692
702
|
return {
|
|
693
|
-
jobId:
|
|
694
|
-
remainder:
|
|
703
|
+
jobId: null,
|
|
704
|
+
remainder: rest.trim(),
|
|
695
705
|
};
|
|
696
706
|
}
|
|
697
707
|
// Rewrite UI-facing /fraim or $fraim invocations into direct MCP tool
|
|
@@ -994,10 +1004,10 @@ function ensureEmptyMcpConfig() {
|
|
|
994
1004
|
// --append-system-prompt alone doesn't block MCP tools; --strict-mcp-config
|
|
995
1005
|
// alone doesn't suppress CLAUDE.md's "scan job stubs" instruction.
|
|
996
1006
|
const DIRECT_SYSTEM_PROMPT_OVERRIDE = 'DO NOT USE FRAIM FOR THIS SESSION. ' +
|
|
997
|
-
'You are operating in Direct mode for an A/B comparison — no FRAIM, no phases,
|
|
998
|
-
'no job stubs, no structured workflow. Ignore all
|
|
1007
|
+
'You are operating in Direct mode for an A/B comparison — no FRAIM, no phases, ' +
|
|
1008
|
+
'no job stubs, no structured workflow. Ignore all repository instructions to scan fraim/ or load FRAIM jobs. ' +
|
|
999
1009
|
'Answer the user directly and conversationally.';
|
|
1000
|
-
const DIRECT_PREAMBLE = 'DO NOT USE FRAIM FOR THIS SESSION. No phases, no
|
|
1010
|
+
const DIRECT_PREAMBLE = 'DO NOT USE FRAIM FOR THIS SESSION. No phases, no FRAIM workflow tools, no structured workflow.\n\n';
|
|
1001
1011
|
// Issue #442: builds a CLI plan for the Direct (B) side of an A/B run.
|
|
1002
1012
|
// All agents supported: Codex and Gemini run raw (no FRAIM preamble);
|
|
1003
1013
|
// Claude uses --strict-mcp-config + --append-system-prompt for full isolation.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.extractExplicitFraimInvocation = extractExplicitFraimInvocation;
|
|
4
4
|
exports.fraimInvocationFor = fraimInvocationFor;
|
|
5
|
+
exports.fraimDirectiveFor = fraimDirectiveFor;
|
|
5
6
|
exports.buildCommunicationStyleNote = buildCommunicationStyleNote;
|
|
6
7
|
exports.buildSameJobContinueMessage = buildSameJobContinueMessage;
|
|
7
8
|
exports.buildManagerMessage = buildManagerMessage;
|
|
@@ -25,6 +26,9 @@ function fraimInvocationFor(employeeId, jobId) {
|
|
|
25
26
|
const symbol = employeeId === 'codex' ? '$fraim' : '/fraim';
|
|
26
27
|
return `${symbol} ${jobId}`;
|
|
27
28
|
}
|
|
29
|
+
function fraimDirectiveFor(employeeId) {
|
|
30
|
+
return employeeId === 'codex' ? '$fraim' : '/fraim';
|
|
31
|
+
}
|
|
28
32
|
// #521: a Hub-injected communication-style note so the employee's messages to the
|
|
29
33
|
// manager stay about the JOB and its outcomes — not the FRAIM machinery (seeking
|
|
30
34
|
// mentoring, tool calls, git, phase switches). The structured signals (the
|
|
@@ -54,6 +58,11 @@ function buildManagerMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
|
54
58
|
const explicit = extractExplicitFraimInvocation(trimmed);
|
|
55
59
|
const effectiveJobId = explicit?.jobId || jobId;
|
|
56
60
|
const invocation = fraimInvocationFor(employeeId, effectiveJobId);
|
|
61
|
+
if (!invocation && effectiveJobId === '__freeform__' && kind === 'start') {
|
|
62
|
+
const directive = fraimDirectiveFor(employeeId);
|
|
63
|
+
const remainder = explicit ? explicit.remainder : trimmed;
|
|
64
|
+
return remainder ? `${directive}\n\n${remainder}` : directive;
|
|
65
|
+
}
|
|
57
66
|
if (!invocation)
|
|
58
67
|
return explicit?.remainder || trimmed;
|
|
59
68
|
const remainder = explicit ? explicit.remainder : trimmed;
|
|
@@ -39,18 +39,21 @@ function projectPathExists(projectPath) {
|
|
|
39
39
|
function projectIdForPath(projectPath) {
|
|
40
40
|
return `p-${(0, crypto_1.createHash)('sha1').update(canonicalProjectPath(projectPath)).digest('base64url').slice(0, 16)}`;
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// Issue #788: tombstones are canonicalized and de-duplicated, but NEVER dropped for
|
|
43
|
+
// being the current project. The "current project is always visible" rule (#719) is a
|
|
44
|
+
// DISPLAY concern handled in normalizeAiHubProjectList (the current entry bypasses the
|
|
45
|
+
// tombstone filter). The original #726 code dropped the current-project tombstone HERE,
|
|
46
|
+
// so load()/saveProjects()/bootstrap silently erased it from disk whenever the deleted
|
|
47
|
+
// folder was the open workspace — an implicit revive that resurrected the project on the
|
|
48
|
+
// next add. Revive must be explicit (reviveRemovedPaths only; #726 contract).
|
|
49
|
+
function normalizeRemovedProjectPaths(raw) {
|
|
43
50
|
if (!Array.isArray(raw))
|
|
44
51
|
return [];
|
|
45
|
-
const currentKey = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
|
|
46
52
|
const seen = new Set();
|
|
47
53
|
for (const value of raw) {
|
|
48
54
|
if (typeof value !== 'string' || value.trim().length === 0)
|
|
49
55
|
continue;
|
|
50
|
-
|
|
51
|
-
if (currentKey && key === currentKey)
|
|
52
|
-
continue;
|
|
53
|
-
seen.add(key);
|
|
56
|
+
seen.add(canonicalProjectPath(value));
|
|
54
57
|
}
|
|
55
58
|
return Array.from(seen);
|
|
56
59
|
}
|
|
@@ -100,22 +103,33 @@ function normalizeProjectEntry(raw, fallbackPath) {
|
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
106
|
+
// Issue #788: project identity is the CANONICAL path (case-insensitive on win32),
|
|
107
|
+
// matching both the tombstone comparison and the conversation store. De-duping by the
|
|
108
|
+
// case-preserving path (the old behavior) let the same folder in two casings — one from
|
|
109
|
+
// preferences.projects, one from a conversation bucket key — survive as two entries: the
|
|
110
|
+
// reported "2 instances". The map value keeps the first-seen case-preserving folderPath
|
|
111
|
+
// for display (the current project is added first, so its real casing wins).
|
|
103
112
|
const byPath = new Map();
|
|
104
|
-
const
|
|
105
|
-
const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths
|
|
106
|
-
const add = (entry) => {
|
|
113
|
+
const currentCanonical = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
|
|
114
|
+
const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths));
|
|
115
|
+
const add = (entry, isCurrent = false) => {
|
|
107
116
|
if (!entry)
|
|
108
117
|
return;
|
|
109
|
-
const
|
|
110
|
-
|
|
118
|
+
const displayPath = normalizeProjectPath(entry.folderPath);
|
|
119
|
+
const dedupKey = canonicalProjectPath(displayPath);
|
|
120
|
+
// Issue #788: the current project is always shown in its own workspace, even when
|
|
121
|
+
// tombstoned (#719 never-empty Hub). This is a display override ONLY — the tombstone
|
|
122
|
+
// is neither erased here nor in persistence, so switching away re-hides the project
|
|
123
|
+
// and no subsequent add can resurrect it.
|
|
124
|
+
if (!isCurrent && removedKeys.has(dedupKey))
|
|
111
125
|
return;
|
|
112
|
-
if (!options.includeMissing &&
|
|
126
|
+
if (!options.includeMissing && dedupKey !== currentCanonical && !projectPathExists(displayPath))
|
|
113
127
|
return;
|
|
114
|
-
const existing = byPath.get(
|
|
115
|
-
byPath.set(
|
|
128
|
+
const existing = byPath.get(dedupKey);
|
|
129
|
+
byPath.set(dedupKey, existing ? { ...existing, ...entry, folderPath: existing.folderPath } : { ...entry, folderPath: displayPath });
|
|
116
130
|
};
|
|
117
131
|
if (currentProjectPath)
|
|
118
|
-
add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath));
|
|
132
|
+
add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath), true);
|
|
119
133
|
for (const project of projects)
|
|
120
134
|
add(normalizeProjectEntry(project));
|
|
121
135
|
return withUniqueProjectIds(Array.from(byPath.values()));
|
|
@@ -130,7 +144,7 @@ class AiHubPreferencesStore {
|
|
|
130
144
|
}
|
|
131
145
|
try {
|
|
132
146
|
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
133
|
-
const removedProjectPaths = normalizeRemovedProjectPaths(raw.removedProjectPaths
|
|
147
|
+
const removedProjectPaths = normalizeRemovedProjectPaths(raw.removedProjectPaths);
|
|
134
148
|
return {
|
|
135
149
|
projectPath: raw.projectPath || projectPath,
|
|
136
150
|
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
@@ -158,8 +172,8 @@ class AiHubPreferencesStore {
|
|
|
158
172
|
saveProjects(projectPath, projects, options = {}) {
|
|
159
173
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
160
174
|
const preferences = this.load(normalizedProjectPath);
|
|
161
|
-
const reviveKeys = new Set(normalizeRemovedProjectPaths(options.reviveRemovedPaths
|
|
162
|
-
const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths
|
|
175
|
+
const reviveKeys = new Set(normalizeRemovedProjectPaths(options.reviveRemovedPaths));
|
|
176
|
+
const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths)
|
|
163
177
|
.filter((removedPath) => !reviveKeys.has(removedPath));
|
|
164
178
|
const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath, { removedProjectPaths });
|
|
165
179
|
this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
|
|
@@ -168,7 +182,7 @@ class AiHubPreferencesStore {
|
|
|
168
182
|
mergeProjects(projectPath, projects) {
|
|
169
183
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
170
184
|
const preferences = this.load(normalizedProjectPath);
|
|
171
|
-
const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths
|
|
185
|
+
const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths);
|
|
172
186
|
const nextProjects = normalizeAiHubProjectList([...(preferences.projects || []), ...projects], normalizedProjectPath, { removedProjectPaths });
|
|
173
187
|
this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
|
|
174
188
|
return nextProjects;
|
|
@@ -176,7 +190,7 @@ class AiHubPreferencesStore {
|
|
|
176
190
|
removeProject(projectPath, projects, removedProjectPath) {
|
|
177
191
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
178
192
|
const preferences = this.load(normalizedProjectPath);
|
|
179
|
-
const removedProjectPaths = normalizeRemovedProjectPaths([...(preferences.removedProjectPaths || []), removedProjectPath]
|
|
193
|
+
const removedProjectPaths = normalizeRemovedProjectPaths([...(preferences.removedProjectPaths || []), removedProjectPath]);
|
|
180
194
|
const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath, { removedProjectPaths });
|
|
181
195
|
this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
|
|
182
196
|
return nextProjects;
|