minovative-mind-cli 2.3.1 → 2.3.3

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 CHANGED
@@ -121,11 +121,13 @@ Hot-swap during a session using `/models`:
121
121
 
122
122
  ---
123
123
 
124
- ## 🌐 Multi-Workspace & Cross-Repo Support
124
+ ## 🌐 Multi-Workspace & Cross-Repo Support (Master & Sub-Workspaces)
125
125
 
126
- Minovative Mind CLI doesn't restrict you to a single repository. You can link multiple external workspaces to your current session and the AI will seamlessly operate across all of them simultaneously.
126
+ Minovative Mind CLI doesn't restrict you to a single repository. You can logically group multiple external repositories into **Master Workspaces** (Profiles) containing dedicated **Sub-Workspaces** (mapped to short aliases like `@backend` or `@frontend`).
127
127
 
128
- By prefixing file paths with `@alias/` (e.g. `@backend/src/api.ts` and `@frontend/src/App.tsx`), the Context Agent and Thread Agents can investigate, refactor, and coordinate changes across your entire tech stack in a single prompt. The terminal highlights cross-workspace actions with a blue `[alias]` visual tag. Use the "Edit Workspace" menu option to configure your linked projects.
128
+ The Context Agent and Thread Agents operate strictly within the boundaries of the active Master Workspace, ignoring other profiles to keep the context window highly targeted and memory-efficient.
129
+
130
+ By prefixing file paths with `@alias/` (e.g., `@backend/src/api.ts` and `@frontend/src/App.tsx`), the agents can investigate, refactor, and coordinate changes across your entire nested stack in a single prompt. The terminal highlights cross-workspace actions with a blue `[alias]` visual tag. Use the `/workspaces` command to create profiles, link sub-workspaces, or switch active environments.
129
131
 
130
132
  ---
131
133
 
@@ -11,7 +11,7 @@ import { chatHistoryService } from '../chatHistoryService.js';
11
11
  import { printLogo, brandBg, brandFg } from '../../utils/logo.js';
12
12
  import { readPaste } from '../../utils/paste.js';
13
13
  import { setApprovalMode, getApprovalMode, isSubAgentsEnabled, setSubAgentsEnabled } from '../agent-tools.js';
14
- import { ProxyChatSession, setGlobalActiveModel, getGlobalActiveModel, } from '../ai.js';
14
+ import { ProxyChatSession, setGlobalActiveModel, getGlobalActiveModel } from '../ai.js';
15
15
  import { GEMINI_MODELS } from '../../utils/config.js';
16
16
  const execAsync = promisify(exec);
17
17
  /**
@@ -134,7 +134,24 @@ export async function handleSlashCommand(command, context) {
134
134
  const prevUsage = context.chatSessionState?.previousUsageMetadata;
135
135
  const currentModel = chat.getModel();
136
136
  const globalModel = getGlobalActiveModel();
137
- const displayModel = globalModel === 'auto' ? `Auto (Last turn: ${currentModel})` : currentModel;
137
+ const modelUsageCounts = context.chatSessionState?.modelUsageCounts || {};
138
+ const modelsUsed = Object.keys(modelUsageCounts);
139
+ let modelStatsStr = '';
140
+ if (modelsUsed.length > 0) {
141
+ const totalTurns = Object.values(modelUsageCounts).reduce((a, b) => a + b, 0);
142
+ const parts = modelsUsed.map((m) => {
143
+ const percent = Math.round((modelUsageCounts[m] / totalTurns) * 100);
144
+ return `${m} ${percent}%`;
145
+ });
146
+ modelStatsStr = parts.join(', ');
147
+ }
148
+ let displayModel = '';
149
+ if (globalModel === 'auto') {
150
+ displayModel = modelStatsStr ? `Auto (${modelStatsStr})` : `Auto (Last turn: ${currentModel})`;
151
+ }
152
+ else {
153
+ displayModel = modelStatsStr || currentModel;
154
+ }
138
155
  const autoApprove = getApprovalMode() === 'skip-all' ? 'Enabled' : 'Disabled';
139
156
  const subAgents = isSubAgentsEnabled() ? 'Enabled' : 'Disabled';
140
157
  const planMode = context.isPlanMode ? 'Enabled' : 'Disabled';
@@ -405,6 +422,7 @@ export async function handleSlashCommand(command, context) {
405
422
  chatSessionState.totalOutputTokens = session.totalOutputTokens || 0;
406
423
  chatSessionState.latestUsageMetadata = session.latestUsageMetadata;
407
424
  chatSessionState.previousUsageMetadata = session.previousUsageMetadata;
425
+ chatSessionState.modelUsageCounts = session.modelUsageCounts || {};
408
426
  chat.setSessionInfo(session.id, workspaceRoot);
409
427
  }
410
428
  process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
@@ -495,7 +513,9 @@ export async function handleSlashCommand(command, context) {
495
513
  p.log.info(`${pc.dim('Model:')} ${pc.cyan(session.modelName)}`);
496
514
  }
497
515
  if (session.totalTokens !== undefined) {
498
- const inputStr = session.totalInputTokens ? ` (Input: ${session.totalInputTokens.toLocaleString()}, Output: ${(session.totalOutputTokens || 0).toLocaleString()})` : '';
516
+ const inputStr = session.totalInputTokens
517
+ ? ` (Input: ${session.totalInputTokens.toLocaleString()}, Output: ${(session.totalOutputTokens || 0).toLocaleString()})`
518
+ : '';
499
519
  p.log.info(`${pc.dim('Session Token Usage:')} ${pc.cyan(session.totalTokens.toLocaleString() + ' total tokens')}${pc.dim(inputStr)}`);
500
520
  }
501
521
  if (session.gitBranch) {
@@ -537,9 +557,9 @@ export async function handleSlashCommand(command, context) {
537
557
  while (true) {
538
558
  const allRoots = workspaceRegistry.getAllRoots(workspaceRoot);
539
559
  const options = [];
560
+ const hasProfiles = workspaceRegistry.hasProfiles();
540
561
  options.push({ value: 'add', label: 'Add Workspace' });
541
- const externalRoots = allRoots.filter((r) => r.alias);
542
- if (externalRoots.length > 0) {
562
+ if (hasProfiles) {
543
563
  options.push({ value: 'edit', label: 'Edit Workspace' });
544
564
  options.push({ value: 'remove', label: 'Remove Workspace' });
545
565
  options.push({ value: 'list', label: 'List Workspaces' });
@@ -553,6 +573,40 @@ export async function handleSlashCommand(command, context) {
553
573
  break;
554
574
  }
555
575
  if (action === 'add') {
576
+ const type = await p['select']({
577
+ message: 'What type of workspace would you like to add?',
578
+ options: [
579
+ { value: 'master', label: 'Master Workspace (Create)' },
580
+ { value: 'sub', label: 'Sub-workspace', hint: 'Requires an existing Master Workspace' },
581
+ ],
582
+ });
583
+ if (p.isCancel(type))
584
+ continue;
585
+ let profileStr;
586
+ if (type === 'master') {
587
+ profileStr = (await p['text']({
588
+ message: 'Enter profile name (e.g. work, personal):',
589
+ validate: (val) => {
590
+ if (!val)
591
+ return 'Profile name is required';
592
+ },
593
+ }));
594
+ if (p.isCancel(profileStr))
595
+ continue;
596
+ }
597
+ else {
598
+ const profiles = workspaceRegistry.listProfiles();
599
+ if (profiles.length === 0) {
600
+ p.log.error('No master workspaces (profiles) exist. Please create one first.');
601
+ continue;
602
+ }
603
+ profileStr = (await p['select']({
604
+ message: 'Select a Master Workspace (For Sub-Workspaces):',
605
+ options: profiles.map((p) => ({ value: p.name, label: p.name })),
606
+ }));
607
+ if (p.isCancel(profileStr))
608
+ continue;
609
+ }
556
610
  const aliasStr = await p['text']({
557
611
  message: 'Enter a short alias (e.g. backend, ui):',
558
612
  validate: (val) => {
@@ -567,7 +621,7 @@ export async function handleSlashCommand(command, context) {
567
621
  if (p.isCancel(aliasStr))
568
622
  continue;
569
623
  const rootPathStr = await p['text']({
570
- message: 'Enter the absolute path to the workspace root (e.g. /Users/name/Projects/app):',
624
+ message: 'Enter the absolute path to the workspace root:',
571
625
  validate: (val) => {
572
626
  if (!val)
573
627
  return 'Path is required';
@@ -586,17 +640,17 @@ export async function handleSlashCommand(command, context) {
586
640
  p.log.error('Path is not a directory.');
587
641
  continue;
588
642
  }
589
- workspaceRegistry.register(aliasStr, cleanRootPathStr);
590
- p.log.success(`Added @${aliasStr} -> ${cleanRootPathStr}`);
643
+ workspaceRegistry.register(profileStr, aliasStr, cleanRootPathStr);
644
+ p.log.success(`Added @${aliasStr} -> ${cleanRootPathStr} (Profile: ${profileStr})`);
591
645
  }
592
646
  catch (e) {
593
- p.log.error(`Invalid path or directory does not exist: ${cleanRootPathStr}`);
647
+ p.log.error(e.message || `Invalid path or directory does not exist: ${cleanRootPathStr}`);
594
648
  }
595
649
  }
596
650
  else if (action === 'edit') {
597
- const editOptions = externalRoots.map((r) => ({
651
+ const editOptions = workspaceRegistry.list().map((r) => ({
598
652
  value: r.alias,
599
- label: `@${r.alias} -> ${r.root}`,
653
+ label: `@${r.alias} -> ${r.absolutePath}`,
600
654
  }));
601
655
  editOptions.push({ value: 'cancel', label: 'Cancel' });
602
656
  const aliasToEdit = await p['select']({
@@ -608,6 +662,16 @@ export async function handleSlashCommand(command, context) {
608
662
  const ws = workspaceRegistry.get(aliasToEdit);
609
663
  if (!ws)
610
664
  continue;
665
+ const newProfileStr = await p['text']({
666
+ message: `Enter new profile name (current: ${ws.profile}):`,
667
+ initialValue: ws.profile,
668
+ validate: (val) => {
669
+ if (!val)
670
+ return 'Profile name is required';
671
+ },
672
+ });
673
+ if (p.isCancel(newProfileStr))
674
+ continue;
611
675
  const newAliasStr = await p['text']({
612
676
  message: `Enter new alias (current: ${ws.alias}):`,
613
677
  initialValue: ws.alias,
@@ -645,22 +709,22 @@ export async function handleSlashCommand(command, context) {
645
709
  }
646
710
  // Remove old alias first to avoid duplicate alias error, or to clean up
647
711
  workspaceRegistry.unregister(ws.alias);
648
- workspaceRegistry.register(newAliasStr, cleanNewRootPathStr);
649
- p.log.success(`Updated @${newAliasStr} -> ${cleanNewRootPathStr}`);
712
+ workspaceRegistry.register(newProfileStr, newAliasStr, cleanNewRootPathStr);
713
+ p.log.success(`Updated @${newAliasStr} -> ${cleanNewRootPathStr} (Profile: ${newProfileStr})`);
650
714
  }
651
715
  catch (e) {
652
716
  // If register failed, try to rollback
653
717
  p.log.error(`Failed to update workspace: ${e instanceof Error ? e.message : String(e)}`);
654
718
  try {
655
- workspaceRegistry.register(ws.alias, ws.absolutePath);
719
+ workspaceRegistry.register(ws.profile, ws.alias, ws.absolutePath);
656
720
  }
657
721
  catch { }
658
722
  }
659
723
  }
660
724
  else if (action === 'remove') {
661
- const removeOptions = externalRoots.map((r) => ({
725
+ const removeOptions = workspaceRegistry.list().map((r) => ({
662
726
  value: r.alias,
663
- label: `@${r.alias} -> ${r.root}`,
727
+ label: `@${r.alias} -> ${r.absolutePath}`,
664
728
  }));
665
729
  removeOptions.push({ value: 'cancel', label: 'Cancel' });
666
730
  const aliasToRemove = await p['select']({
@@ -674,8 +738,9 @@ export async function handleSlashCommand(command, context) {
674
738
  }
675
739
  else if (action === 'list') {
676
740
  for (const { alias, root } of allRoots) {
741
+ const ws = alias ? workspaceRegistry.get(alias) : null;
677
742
  if (alias) {
678
- p.log.step(`${pc.blue(`@${alias}`)} -> ${pc.dim(root)}`);
743
+ p.log.step(`${pc.blue(`@${alias}`)} [${ws?.profile || 'default'}] -> ${pc.dim(root)}`);
679
744
  }
680
745
  else {
681
746
  p.log.step(`${pc.cyan('(primary)')} -> ${pc.dim(root)}`);
@@ -18,6 +18,7 @@ export interface SlashCommandContext {
18
18
  totalOutputTokens: number;
19
19
  latestUsageMetadata?: any;
20
20
  previousUsageMetadata?: any;
21
+ modelUsageCounts?: Record<string, number>;
21
22
  };
22
23
  }
23
24
  export interface SlashCommandResult {
@@ -57,6 +57,7 @@ export declare function executeSingleTurn(workspaceRoot: string, userInput: stri
57
57
  totalOutputTokens: number;
58
58
  latestUsageMetadata?: any;
59
59
  previousUsageMetadata?: any;
60
+ modelUsageCounts?: Record<string, number>;
60
61
  }, isPlanMode: boolean, cachedContextResult?: any): Promise<{
61
62
  planModeReturn?: string;
62
63
  contextResult?: any;
@@ -86,7 +86,7 @@ export async function startAgentLoop(workspaceRoot, version) {
86
86
  chatHistoryService.init(workspaceRoot);
87
87
  const chat = createSharedChatSession();
88
88
  const inputHandler = new AsyncInputHandler();
89
- const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0 };
89
+ const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0, modelUsageCounts: {} };
90
90
  chat.setSessionInfo(chatSessionState.id, workspaceRoot);
91
91
  const sessionInputHistory = [];
92
92
  let isRawPasteMode = false;
@@ -115,6 +115,9 @@ export async function startAgentLoop(workspaceRoot, version) {
115
115
  chatSessionState.totalTokens = 0;
116
116
  chatSessionState.totalInputTokens = 0;
117
117
  chatSessionState.totalOutputTokens = 0;
118
+ chatSessionState.modelUsageCounts = {};
119
+ chatSessionState.latestUsageMetadata = undefined;
120
+ chatSessionState.previousUsageMetadata = undefined;
118
121
  chat.setSessionInfo(chatSessionState.id, workspaceRoot);
119
122
  }
120
123
  inputHandler.stop();
@@ -377,6 +380,12 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
377
380
  }
378
381
  // Print Usage Stats
379
382
  const usage = getAndResetTurnUsage();
383
+ chatSessionState.modelUsageCounts = chatSessionState.modelUsageCounts || {};
384
+ if (usage.modelsUsed) {
385
+ for (const [m, count] of Object.entries(usage.modelsUsed)) {
386
+ chatSessionState.modelUsageCounts[m] = (chatSessionState.modelUsageCounts[m] || 0) + count;
387
+ }
388
+ }
380
389
  chatSessionState.previousUsageMetadata = chatSessionState.latestUsageMetadata;
381
390
  chatSessionState.latestUsageMetadata = usage;
382
391
  chatSessionState.totalTokens += usage.totalTokenCount;
@@ -433,6 +442,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
433
442
  subAgents: isSubAgentsEnabled(),
434
443
  latestUsageMetadata: chatSessionState.latestUsageMetadata,
435
444
  previousUsageMetadata: chatSessionState.previousUsageMetadata,
445
+ modelUsageCounts: chatSessionState.modelUsageCounts,
436
446
  })
437
447
  .catch((e) => debugLog('Failed to auto-save session: ' + e));
438
448
  }
@@ -511,6 +521,12 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
511
521
  changeLogger.markComplete();
512
522
  }
513
523
  if (usage) {
524
+ chatSessionState.modelUsageCounts = chatSessionState.modelUsageCounts || {};
525
+ if (usage.modelsUsed) {
526
+ for (const [m, count] of Object.entries(usage.modelsUsed)) {
527
+ chatSessionState.modelUsageCounts[m] = (chatSessionState.modelUsageCounts[m] || 0) + count;
528
+ }
529
+ }
514
530
  chatSessionState.previousUsageMetadata = chatSessionState.latestUsageMetadata;
515
531
  chatSessionState.latestUsageMetadata = usage;
516
532
  chatSessionState.totalTokens += usage.totalTokenCount || 0;
@@ -541,6 +557,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
541
557
  subAgents: isSubAgentsEnabled(),
542
558
  latestUsageMetadata: chatSessionState.latestUsageMetadata,
543
559
  previousUsageMetadata: chatSessionState.previousUsageMetadata,
560
+ modelUsageCounts: chatSessionState.modelUsageCounts,
544
561
  });
545
562
  }
546
563
  catch (e) {
@@ -564,6 +581,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
564
581
  subAgents: isSubAgentsEnabled(),
565
582
  latestUsageMetadata: chatSessionState.latestUsageMetadata,
566
583
  previousUsageMetadata: chatSessionState.previousUsageMetadata,
584
+ modelUsageCounts: chatSessionState.modelUsageCounts,
567
585
  })
568
586
  .catch((e) => debugLog('Failed to auto-save session: ' + e));
569
587
  }
@@ -35,6 +35,8 @@ export interface ChatSessionData {
35
35
  latestUsageMetadata?: any;
36
36
  /** Optional metadata about the token usage from the turn prior to the last turn. */
37
37
  previousUsageMetadata?: any;
38
+ /** Optional map tracking the number of turns each model was used during the session. */
39
+ modelUsageCounts?: Record<string, number>;
38
40
  }
39
41
  /**
40
42
  * Service responsible for managing the persistence, retrieval, and deletion of chat session histories.
@@ -21,6 +21,7 @@ export declare function getAndResetTurnUsage(): {
21
21
  totalTokenCount: number;
22
22
  creditsUsed: number;
23
23
  remainingBalance: number | undefined;
24
+ modelsUsed: Record<string, number>;
24
25
  };
25
26
  export declare function peekTurnUsage(): {
26
27
  promptTokens: number;
@@ -29,6 +30,7 @@ export declare function peekTurnUsage(): {
29
30
  totalTokenCount: number;
30
31
  creditsUsed: number;
31
32
  remainingBalance: number | undefined;
33
+ modelsUsed: Record<string, number>;
32
34
  };
33
35
  /**
34
36
  * Client service interacting directly with the serverless Gemini proxy endpoint.
@@ -39,7 +39,8 @@ let globalSessionAccumulatedUsage = {
39
39
  cachedTokens: 0,
40
40
  totalTokenCount: 0,
41
41
  creditsUsed: 0,
42
- remainingBalance: undefined
42
+ remainingBalance: undefined,
43
+ modelsUsed: {}
43
44
  };
44
45
  export function getAndResetTurnUsage() {
45
46
  const current = { ...globalSessionAccumulatedUsage };
@@ -49,7 +50,8 @@ export function getAndResetTurnUsage() {
49
50
  cachedTokens: 0,
50
51
  totalTokenCount: 0,
51
52
  creditsUsed: 0,
52
- remainingBalance: undefined
53
+ remainingBalance: undefined,
54
+ modelsUsed: {}
53
55
  };
54
56
  return current;
55
57
  }
@@ -190,6 +192,7 @@ export class ProxyClient {
190
192
  if (data.usage.remainingBalance !== undefined) {
191
193
  globalSessionAccumulatedUsage.remainingBalance = data.usage.remainingBalance;
192
194
  }
195
+ globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
193
196
  }
194
197
  if (data.groundingMetadata) {
195
198
  groundingMetadata = data.groundingMetadata;
@@ -8,6 +8,17 @@ export interface RegisteredWorkspace {
8
8
  absolutePath: string;
9
9
  /** Unix epoch timestamp (ms) when this workspace was registered. */
10
10
  registeredAt: number;
11
+ /** The profile (main workspace) this workspace belongs to. */
12
+ profile: string;
13
+ }
14
+ /**
15
+ * Represents a Profile (Main Workspace) that groups multiple registered workspaces.
16
+ */
17
+ export interface Profile {
18
+ /** Unique name of the profile. */
19
+ name: string;
20
+ /** Unix epoch timestamp (ms) when this profile was created. */
21
+ createdAt: number;
11
22
  }
12
23
  /**
13
24
  * Result of resolving an `@alias/relative/path` string against the workspace registry.
@@ -39,6 +50,8 @@ export interface ResolvedWorkspacePath {
39
50
  declare class WorkspaceRegistry {
40
51
  /** In-memory map of alias → registered workspace. */
41
52
  private workspaces;
53
+ /** In-memory map of profile name → Profile. */
54
+ private profiles;
42
55
  /** Whether the registry has been loaded from disk. */
43
56
  private initialized;
44
57
  /**
@@ -47,8 +60,9 @@ declare class WorkspaceRegistry {
47
60
  */
48
61
  init(): void;
49
62
  /**
50
- * Registers a new external workspace root with the given alias.
63
+ * Registers a new external workspace root with the given alias under a profile.
51
64
  *
65
+ * @param profile - The profile (main workspace) name.
52
66
  * @param alias - Short identifier for the workspace (e.g., "backend").
53
67
  * Must be lowercase alphanumeric with hyphens/underscores, 1–30 chars.
54
68
  * @param absolutePath - Absolute path to the workspace root directory.
@@ -56,7 +70,7 @@ declare class WorkspaceRegistry {
56
70
  * @returns The created `RegisteredWorkspace`, or throws on validation failure.
57
71
  * @throws Error if the alias is invalid, the path doesn't exist, or the alias is already taken.
58
72
  */
59
- register(alias: string, absolutePath: string): RegisteredWorkspace;
73
+ register(profile: string, alias: string, absolutePath: string): RegisteredWorkspace;
60
74
  /**
61
75
  * Removes a registered workspace by alias.
62
76
  *
@@ -69,13 +83,21 @@ declare class WorkspaceRegistry {
69
83
  */
70
84
  list(): RegisteredWorkspace[];
71
85
  /**
72
- * Returns the number of registered workspaces.
86
+ * Returns all profiles (main workspaces).
73
87
  */
74
- get size(): number;
88
+ listProfiles(): Profile[];
75
89
  /**
76
- * Checks whether any external workspaces are registered.
90
+ * Checks whether any profiles (main workspaces) exist.
91
+ */
92
+ hasProfiles(): boolean;
93
+ /**
94
+ * Checks whether any workspaces exist.
77
95
  */
78
96
  hasWorkspaces(): boolean;
97
+ /**
98
+ * Returns all registered workspaces for a given profile.
99
+ */
100
+ getWorkspacesByProfile(profile: string): RegisteredWorkspace[];
79
101
  /**
80
102
  * Looks up a workspace by alias.
81
103
  *
@@ -24,6 +24,8 @@ const REGISTRY_FILE = path.join(GLOBAL_CONFIG_DIR, 'workspaces.json');
24
24
  class WorkspaceRegistry {
25
25
  /** In-memory map of alias → registered workspace. */
26
26
  workspaces = new Map();
27
+ /** In-memory map of profile name → Profile. */
28
+ profiles = new Map();
27
29
  /** Whether the registry has been loaded from disk. */
28
30
  initialized = false;
29
31
  /**
@@ -37,8 +39,9 @@ class WorkspaceRegistry {
37
39
  this.loadFromDisk();
38
40
  }
39
41
  /**
40
- * Registers a new external workspace root with the given alias.
42
+ * Registers a new external workspace root with the given alias under a profile.
41
43
  *
44
+ * @param profile - The profile (main workspace) name.
42
45
  * @param alias - Short identifier for the workspace (e.g., "backend").
43
46
  * Must be lowercase alphanumeric with hyphens/underscores, 1–30 chars.
44
47
  * @param absolutePath - Absolute path to the workspace root directory.
@@ -46,8 +49,16 @@ class WorkspaceRegistry {
46
49
  * @returns The created `RegisteredWorkspace`, or throws on validation failure.
47
50
  * @throws Error if the alias is invalid, the path doesn't exist, or the alias is already taken.
48
51
  */
49
- register(alias, absolutePath) {
52
+ register(profile, alias, absolutePath) {
53
+ const normalizedProfile = profile.toLowerCase().trim();
50
54
  const normalizedAlias = alias.toLowerCase().trim();
55
+ // Ensure profile exists
56
+ if (!this.profiles.has(normalizedProfile)) {
57
+ this.profiles.set(normalizedProfile, {
58
+ name: normalizedProfile,
59
+ createdAt: Date.now(),
60
+ });
61
+ }
51
62
  // Validate alias format
52
63
  if (!ALIAS_PATTERN.test(normalizedAlias)) {
53
64
  throw new Error(`Invalid workspace alias "${normalizedAlias}". Must be 1-30 characters, ` +
@@ -85,10 +96,11 @@ class WorkspaceRegistry {
85
96
  alias: normalizedAlias,
86
97
  absolutePath: normalizedPath,
87
98
  registeredAt: Date.now(),
99
+ profile: normalizedProfile,
88
100
  };
89
101
  this.workspaces.set(normalizedAlias, workspace);
90
102
  this.saveToDisk();
91
- debugLog(`Registered workspace: @${normalizedAlias} → ${normalizedPath}`);
103
+ debugLog(`Registered workspace: @${normalizedAlias} → ${normalizedPath} (Profile: ${normalizedProfile})`);
92
104
  return workspace;
93
105
  }
94
106
  /**
@@ -113,17 +125,29 @@ class WorkspaceRegistry {
113
125
  return Array.from(this.workspaces.values()).sort((a, b) => a.alias.localeCompare(b.alias));
114
126
  }
115
127
  /**
116
- * Returns the number of registered workspaces.
128
+ * Returns all profiles (main workspaces).
117
129
  */
118
- get size() {
119
- return this.workspaces.size;
130
+ listProfiles() {
131
+ return Array.from(this.profiles.values()).sort((a, b) => a.name.localeCompare(b.name));
120
132
  }
121
133
  /**
122
- * Checks whether any external workspaces are registered.
134
+ * Checks whether any profiles (main workspaces) exist.
135
+ */
136
+ hasProfiles() {
137
+ return this.profiles.size > 0;
138
+ }
139
+ /**
140
+ * Checks whether any workspaces exist.
123
141
  */
124
142
  hasWorkspaces() {
125
143
  return this.workspaces.size > 0;
126
144
  }
145
+ /**
146
+ * Returns all registered workspaces for a given profile.
147
+ */
148
+ getWorkspacesByProfile(profile) {
149
+ return this.list().filter((ws) => ws.profile === profile.toLowerCase().trim());
150
+ }
127
151
  /**
128
152
  * Looks up a workspace by alias.
129
153
  *
@@ -216,12 +240,31 @@ class WorkspaceRegistry {
216
240
  }
217
241
  const raw = fs.readFileSync(REGISTRY_FILE, 'utf-8');
218
242
  const data = JSON.parse(raw);
219
- if (!Array.isArray(data)) {
243
+ if (!data || !Array.isArray(data.workspaces)) {
244
+ // Fallback for old schema
245
+ const oldData = JSON.parse(raw);
246
+ if (Array.isArray(oldData)) {
247
+ for (const entry of oldData) {
248
+ if (typeof entry.alias === 'string' &&
249
+ typeof entry.absolutePath === 'string' &&
250
+ ALIAS_PATTERN.test(entry.alias)) {
251
+ if (fs.existsSync(entry.absolutePath)) {
252
+ const profile = 'default';
253
+ this.workspaces.set(entry.alias, { ...entry, profile });
254
+ if (!this.profiles.has(profile)) {
255
+ this.profiles.set(profile, { name: profile, createdAt: Date.now() });
256
+ }
257
+ }
258
+ }
259
+ }
260
+ debugLog(`Migrated ${this.workspaces.size} workspace(s) from old schema.`);
261
+ return;
262
+ }
220
263
  debugLog('Workspace registry file has invalid format, starting fresh.');
221
264
  return;
222
265
  }
223
266
  // Validate each entry and skip invalid ones
224
- for (const entry of data) {
267
+ for (const entry of data.workspaces) {
225
268
  if (typeof entry.alias === 'string' &&
226
269
  typeof entry.absolutePath === 'string' &&
227
270
  ALIAS_PATTERN.test(entry.alias)) {
@@ -234,7 +277,10 @@ class WorkspaceRegistry {
234
277
  }
235
278
  }
236
279
  }
237
- debugLog(`Loaded ${this.workspaces.size} workspace(s) from global registry.`);
280
+ for (const profile of data.profiles) {
281
+ this.profiles.set(profile.name, profile);
282
+ }
283
+ debugLog(`Loaded ${this.workspaces.size} workspace(s) and ${this.profiles.size} profile(s) from global registry.`);
238
284
  }
239
285
  catch (err) {
240
286
  debugLog(`Failed to load workspace registry: ${err instanceof Error ? err.message : String(err)}`);
@@ -250,12 +296,15 @@ class WorkspaceRegistry {
250
296
  if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
251
297
  fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
252
298
  }
253
- const data = Array.from(this.workspaces.values());
299
+ const data = {
300
+ workspaces: Array.from(this.workspaces.values()),
301
+ profiles: Array.from(this.profiles.values()),
302
+ };
254
303
  const tempPath = `${REGISTRY_FILE}.${Date.now()}.tmp`;
255
304
  fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
256
305
  // Atomic rename to prevent corruption
257
306
  fs.renameSync(tempPath, REGISTRY_FILE);
258
- debugLog(`Saved ${data.length} workspace(s) to global registry.`);
307
+ debugLog(`Saved ${data.workspaces.length} workspace(s) and ${data.profiles.length} profile(s) to global registry.`);
259
308
  }
260
309
  catch (err) {
261
310
  debugLog(`Failed to save workspace registry: ${err instanceof Error ? err.message : String(err)}`);
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  }
67
67
  },
68
- "version": "2.3.1"
68
+ "version": "2.3.3"
69
69
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minovative-mind-cli",
3
3
  "description": "An automated AI agent powered by Vertex AI that helps you write software",
4
- "version": "2.3.1",
4
+ "version": "2.3.3",
5
5
  "author": "Daniel Ward",
6
6
  "bin": {
7
7
  "minovative-mind-cli": "bin/run.js"