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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  try {
4
4
  const { createFraimHubProgram } = require('../dist/src/cli/fraim-hub.js');
@@ -80,7 +80,10 @@ function openDesktopWindow(projectPath, preferredPort) {
80
80
  if (!electronBinary || !desktopEntry) {
81
81
  return false;
82
82
  }
83
- const child = (0, child_process_1.spawn)(electronBinary, [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)], {
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 || process.cwd());
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: ${projectPath}`);
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
- console.log(`Project path: ${projectPath}`);
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: ${projectPath}`);
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 = process.cwd();
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(options.projectPath, httpPort);
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);
@@ -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(/^[$/@]fraim(?:\s+(\S+))?\s*([\s\S]*)$/);
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: match[1] || null,
694
- remainder: (match[2] || '').trim(),
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, no seekMentoring, ' +
998
- 'no job stubs, no structured workflow. Ignore all CLAUDE.md instructions to scan fraim/ or call get_fraim_job. ' +
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 seekMentoring, no structured workflow.\n\n';
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
- function normalizeRemovedProjectPaths(raw, currentProjectPath) {
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
- const key = canonicalProjectPath(value);
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 currentKey = currentProjectPath ? normalizeProjectPath(currentProjectPath) : null;
105
- const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths, currentProjectPath));
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 key = normalizeProjectPath(entry.folderPath);
110
- if (removedKeys.has(canonicalProjectPath(key)))
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 && key !== currentKey && !projectPathExists(key))
126
+ if (!options.includeMissing && dedupKey !== currentCanonical && !projectPathExists(displayPath))
113
127
  return;
114
- const existing = byPath.get(key);
115
- byPath.set(key, existing ? { ...existing, ...entry, folderPath: key } : { ...entry, folderPath: key });
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, projectPath);
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, normalizedProjectPath));
162
- const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths, normalizedProjectPath)
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, normalizedProjectPath);
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], normalizedProjectPath);
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;