@gethmy/mcp 2.23.0 → 2.25.0

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.
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  // src/config.ts
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { join } from "node:path";
20
+ import { dirname, join, parse, resolve } from "node:path";
21
21
  function getConfigDir() {
22
22
  return join(homedir(), ".harmony-mcp");
23
23
  }
@@ -27,6 +27,22 @@ function getConfigPath() {
27
27
  function getLocalConfigPath(cwd) {
28
28
  return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
29
29
  }
30
+ function findLocalConfigPath(cwd) {
31
+ const home = resolve(homedir());
32
+ let dir = resolve(cwd || process.cwd());
33
+ const { root } = parse(dir);
34
+ for (;; ) {
35
+ if (dir !== home && dir !== root) {
36
+ const candidate = join(dir, LOCAL_CONFIG_FILENAME);
37
+ if (existsSync(candidate))
38
+ return candidate;
39
+ }
40
+ const parent = dirname(dir);
41
+ if (parent === dir)
42
+ return null;
43
+ dir = parent;
44
+ }
45
+ }
30
46
  function emptyConfig() {
31
47
  return {
32
48
  apiKey: null,
@@ -78,8 +94,8 @@ function saveConfig(config) {
78
94
  });
79
95
  }
80
96
  function loadLocalConfig(cwd) {
81
- const localConfigPath = getLocalConfigPath(cwd);
82
- if (!existsSync(localConfigPath)) {
97
+ const localConfigPath = findLocalConfigPath(cwd);
98
+ if (localConfigPath === null) {
83
99
  return null;
84
100
  }
85
101
  try {
@@ -94,7 +110,7 @@ function loadLocalConfig(cwd) {
94
110
  }
95
111
  }
96
112
  function saveLocalConfig(config, cwd) {
97
- const localConfigPath = getLocalConfigPath(cwd);
113
+ const localConfigPath = findLocalConfigPath(cwd) ?? getLocalConfigPath(cwd);
98
114
  const existingConfig = loadLocalConfig(cwd) || {
99
115
  workspaceId: null,
100
116
  projectId: null
@@ -108,7 +124,7 @@ function saveLocalConfig(config, cwd) {
108
124
  writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
109
125
  }
110
126
  function hasLocalConfig(cwd) {
111
- return existsSync(getLocalConfigPath(cwd));
127
+ return findLocalConfigPath(cwd) !== null;
112
128
  }
113
129
  function getActiveCredential() {
114
130
  const config = loadConfig();
@@ -133,33 +149,69 @@ function getUserEmail() {
133
149
  function setUserEmail(email) {
134
150
  saveConfig({ userEmail: email });
135
151
  }
136
- function setActiveWorkspace(workspaceId, options) {
137
- if (options?.local) {
138
- saveLocalConfig({ workspaceId }, options.cwd);
139
- } else {
140
- saveConfig({ activeWorkspaceId: workspaceId });
152
+ function setActiveContext(context, options) {
153
+ if (options?.global) {
154
+ saveConfig({
155
+ activeWorkspaceId: context.workspaceId,
156
+ activeProjectId: context.projectId
157
+ });
158
+ return;
141
159
  }
142
- }
143
- function setActiveProject(projectId, options) {
144
- if (options?.local) {
145
- saveLocalConfig({ projectId }, options.cwd);
160
+ const localPath = findLocalConfigPath(options?.cwd);
161
+ if (options?.local || localPath !== null) {
162
+ saveLocalConfig({ workspaceId: context.workspaceId, projectId: context.projectId }, options?.cwd);
146
163
  } else {
147
- saveConfig({ activeProjectId: projectId });
164
+ saveConfig({
165
+ activeWorkspaceId: context.workspaceId,
166
+ activeProjectId: context.projectId
167
+ });
148
168
  }
149
169
  }
150
- function getActiveWorkspaceId(cwd) {
170
+ function setActiveWorkspace(workspaceId, options) {
171
+ const currentWorkspaceId = getActiveWorkspaceId(options?.cwd);
172
+ const keepProject = currentWorkspaceId === workspaceId;
173
+ setActiveContext({
174
+ workspaceId,
175
+ projectId: keepProject ? getActiveProjectId(options?.cwd) : null
176
+ }, options);
177
+ }
178
+ function readActiveContext(cwd) {
151
179
  const localConfig = loadLocalConfig(cwd);
152
- if (localConfig?.workspaceId) {
153
- return localConfig.workspaceId;
180
+ if (localConfig) {
181
+ return {
182
+ workspaceId: localConfig.workspaceId ?? null,
183
+ projectId: localConfig.projectId ?? null
184
+ };
154
185
  }
155
- return loadConfig().activeWorkspaceId;
186
+ const globalConfig = loadConfig();
187
+ return {
188
+ workspaceId: globalConfig.activeWorkspaceId,
189
+ projectId: globalConfig.activeProjectId
190
+ };
191
+ }
192
+ function getActiveWorkspaceId(cwd) {
193
+ return readActiveContext(cwd).workspaceId;
156
194
  }
157
195
  function getActiveProjectId(cwd) {
158
- const localConfig = loadLocalConfig(cwd);
159
- if (localConfig?.projectId) {
160
- return localConfig.projectId;
196
+ return readActiveContext(cwd).projectId;
197
+ }
198
+ function getActiveContext(cwd) {
199
+ return describeActiveContext({
200
+ projectId: getActiveProjectId(cwd),
201
+ workspaceId: getActiveWorkspaceId(cwd)
202
+ });
203
+ }
204
+ function describeActiveContext(context) {
205
+ const { projectId, workspaceId } = context;
206
+ if (projectId && !workspaceId) {
207
+ return {
208
+ projectId,
209
+ workspaceId,
210
+ consistent: false,
211
+ note: `An active project (${projectId}) is set with no active workspace, so ` + "workspace-scoped tools cannot resolve one from it. Re-set it with " + "harmony_set_project_context, or pass workspaceId explicitly."
212
+ };
161
213
  }
162
- return loadConfig().activeProjectId;
214
+ return { projectId, workspaceId, consistent: true, note: null };
163
215
  }
164
216
  function isConfigured() {
165
217
  const config = loadConfig();
@@ -238,7 +290,7 @@ function lockPath() {
238
290
  return join2(getConfigDir(), LOCK_FILENAME);
239
291
  }
240
292
  function sleep(ms) {
241
- return new Promise((resolve) => setTimeout(resolve, ms));
293
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
242
294
  }
243
295
  async function withRefreshLock(fn) {
244
296
  const path = lockPath();
@@ -436,15 +488,7 @@ ${lines.join(`
436
488
  `)}`;
437
489
  }
438
490
  function generatePrompt(options) {
439
- const {
440
- card,
441
- column,
442
- variant,
443
- customConstraints,
444
- memories,
445
- assembledContext,
446
- assemblyId
447
- } = options;
491
+ const { card, column, variant, customConstraints, memories, assemblyId } = options;
448
492
  const contextOpts = {
449
493
  includeTitle: true,
450
494
  includeDescription: true,
@@ -522,10 +566,7 @@ ${card.description}`);
522
566
  roleFraming.outputSuggestions.forEach((s) => {
523
567
  sections.push(`- ${s}`);
524
568
  });
525
- if (assembledContext) {
526
- sections.push(`
527
- ${assembledContext}`);
528
- } else if (memories && memories.length > 0) {
569
+ if (memories && memories.length > 0) {
529
570
  sections.push(`
530
571
  ## Relevant Memories`);
531
572
  sections.push(`*${memories.length} memories recalled from knowledge graph:*`);
@@ -536,7 +577,7 @@ ${assembledContext}`);
536
577
  sections.push(memory.content);
537
578
  }
538
579
  }
539
- const oneThingLine = synthesizeOneThing(card, subtasks, links, assembledContext);
580
+ const oneThingLine = synthesizeOneThing(card, subtasks, links);
540
581
  if (oneThingLine) {
541
582
  sections.push(`
542
583
  ## Recommended Next Step
@@ -563,7 +604,7 @@ ${customConstraints}`);
563
604
  *Card #${card.short_id} | Generated for ${variant} mode*`);
564
605
  const prompt = sections.join(`
565
606
  `);
566
- const memoryCount = assembledContext ? (assembledContext.match(/^### /gm) || []).length : memories?.length || 0;
607
+ const memoryCount = memories?.length ?? 0;
567
608
  return {
568
609
  prompt,
569
610
  variant,
@@ -584,40 +625,7 @@ ${customConstraints}`);
584
625
  version: PROMPT_TEMPLATE_VERSION
585
626
  };
586
627
  }
587
- function extractSessionInsights(assembledContext) {
588
- const result = {
589
- lastSessionStatus: null,
590
- lastSessionTask: null,
591
- lastSessionProgress: null,
592
- blockers: [],
593
- procedureNextStep: null
594
- };
595
- const sessionMatches = assembledContext.match(/### Session:.*?\n([\s\S]*?)(?=\n###|\n## |\n---|\n\*Assembly|$)/g);
596
- if (sessionMatches && sessionMatches.length > 0) {
597
- const latest = sessionMatches[0];
598
- if (/Completed work on/i.test(latest)) {
599
- result.lastSessionStatus = "completed";
600
- } else if (/Paused work on|status:\s*paused/i.test(latest)) {
601
- result.lastSessionStatus = "paused";
602
- }
603
- const taskMatch = latest.match(/Final task:\s*(.+)/);
604
- if (taskMatch)
605
- result.lastSessionTask = taskMatch[1].trim();
606
- const progressMatch = latest.match(/Progress:\s*(\d+)%/);
607
- if (progressMatch)
608
- result.lastSessionProgress = parseInt(progressMatch[1], 10);
609
- }
610
- const blockerMatches = assembledContext.match(/(?:blocker|blocked by|blocking):\s*(.+)/gi);
611
- if (blockerMatches) {
612
- result.blockers = blockerMatches.map((m) => m.replace(/(?:blocker|blocked by|blocking):\s*/i, "").trim());
613
- }
614
- const stepMatches = assembledContext.match(/^\d+\.\s+(?!.*\*\*\[key step\]\*\*.*✓)(.+?)(?:\s*\*\*\[key step\]\*\*)?$/gm);
615
- if (stepMatches && stepMatches.length > 0) {
616
- result.procedureNextStep = stepMatches[0].replace(/^\d+\.\s+/, "").replace(/\s*\*\*\[key step\]\*\*.*$/, "").trim();
617
- }
618
- return result;
619
- }
620
- function synthesizeOneThing(card, subtasks, links, assembledContext) {
628
+ function synthesizeOneThing(card, subtasks, links) {
621
629
  if (card.done)
622
630
  return null;
623
631
  const blockers = links.filter((l) => l.display_type === "is_blocked_by" && l.direction === "incoming");
@@ -625,14 +633,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
625
633
  const blocker = blockers[0];
626
634
  return `Unblock first: resolve #${blocker.target_card.short_id} "${blocker.target_card.title}" which is blocking this card.`;
627
635
  }
628
- const session = assembledContext ? extractSessionInsights(assembledContext) : null;
629
- if (session?.blockers && session.blockers.length > 0) {
630
- return `Resolve blocker: ${session.blockers[0]}`;
631
- }
632
- if (session?.lastSessionStatus === "paused" && session.lastSessionTask) {
633
- const progress = session.lastSessionProgress ? ` (was ${session.lastSessionProgress}% complete)` : "";
634
- return `Resume previous session${progress}: "${session.lastSessionTask}".`;
635
- }
636
636
  if (subtasks.length > 0) {
637
637
  const completed = subtasks.filter((s) => s.completed).length;
638
638
  if (completed === subtasks.length) {
@@ -643,12 +643,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
643
643
  return `Work on next subtask: "${nextSubtask.title}" (${completed}/${subtasks.length} done).`;
644
644
  }
645
645
  }
646
- if (session?.procedureNextStep) {
647
- return `Follow procedure: ${session.procedureNextStep}`;
648
- }
649
- if (session?.lastSessionStatus === "completed" && session.lastSessionTask) {
650
- return `Previous session completed ("${session.lastSessionTask}"). Review results and continue with remaining work.`;
651
- }
652
646
  if (card.due_date && (card.priority === "urgent" || card.priority === "high")) {
653
647
  return `High-priority task with deadline ${card.due_date}. Start implementation immediately.`;
654
648
  }
@@ -833,6 +827,9 @@ var init_prompt_builder = __esm(() => {
833
827
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
834
828
  };
835
829
  });
830
+
831
+ // src/api-client.ts
832
+ import { randomUUID as randomUUID2 } from "node:crypto";
836
833
  // ../harmony-shared/dist/agentStaleness.js
837
834
  var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
838
835
  var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
@@ -954,6 +951,9 @@ var TIMINGS = {
954
951
  QUERY_STALE_TIME: 1000 * 60 * 5,
955
952
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
956
953
  };
954
+ // ../harmony-shared/dist/gateConfigError.js
955
+ var GATE_CONFIG_ERROR_KEY = "configError";
956
+ var GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
957
957
  // ../harmony-shared/dist/playbookStage.js
958
958
  var STAGE_DAEMON_OWNED_TOOLS = [
959
959
  "mcp__harmony__harmony_end_agent_session",
@@ -993,7 +993,20 @@ function getRetryDelay(attempt) {
993
993
  const delay = Math.min(RETRY_CONFIG.baseDelayMs * 2 ** attempt, RETRY_CONFIG.maxDelayMs);
994
994
  return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
995
995
  }
996
- var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
996
+ var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
997
+ function buildMemoryQuery(title, description) {
998
+ const DESCRIPTION_CAP = 600;
999
+ const trimmedTitle = title.trim();
1000
+ const trimmedBody = (description ?? "").trim();
1001
+ if (!trimmedTitle && !trimmedBody)
1002
+ return "";
1003
+ if (!trimmedTitle)
1004
+ return trimmedBody.slice(0, DESCRIPTION_CAP);
1005
+ if (!trimmedBody)
1006
+ return trimmedTitle;
1007
+ return `${trimmedTitle}
1008
+ ${trimmedBody.slice(0, DESCRIPTION_CAP)}`;
1009
+ }
997
1010
 
998
1011
  class Semaphore {
999
1012
  permits;
@@ -1006,7 +1019,7 @@ class Semaphore {
1006
1019
  this.permits--;
1007
1020
  return;
1008
1021
  }
1009
- return new Promise((resolve) => this.queue.push(resolve));
1022
+ return new Promise((resolve2) => this.queue.push(resolve2));
1010
1023
  }
1011
1024
  release() {
1012
1025
  const next = this.queue.shift();
@@ -1464,6 +1477,14 @@ class HarmonyApiClient {
1464
1477
  params.set("sinceSeq", String(sinceSeq));
1465
1478
  return this.request("GET", `/cards/${cardId}/agent-messages?${params.toString()}`);
1466
1479
  }
1480
+ async postBudgetDecision(cardId, data) {
1481
+ return this.request("POST", `/cards/${cardId}/budget-decisions`, data);
1482
+ }
1483
+ async getBudgetDecisions(cardId, sinceIso) {
1484
+ const params = new URLSearchParams;
1485
+ params.set("sinceIso", sinceIso);
1486
+ return this.request("GET", `/cards/${cardId}/budget-decisions?${params.toString()}`);
1487
+ }
1467
1488
  async updateAgentProgress(cardId, data) {
1468
1489
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
1469
1490
  }
@@ -1509,6 +1530,8 @@ class HarmonyApiClient {
1509
1530
  params.set("offset", String(options.offset));
1510
1531
  if (options.include_superseded)
1511
1532
  params.set("include_superseded", "true");
1533
+ if (options.consumer)
1534
+ params.set("consumer", options.consumer);
1512
1535
  if (options.include_episodes)
1513
1536
  params.set("include_episodes", "true");
1514
1537
  return this.request("GET", `/memory/entities?${params.toString()}`);
@@ -1564,6 +1587,12 @@ class HarmonyApiClient {
1564
1587
  if (options.topK !== undefined) {
1565
1588
  entities = entities.slice(0, options.topK);
1566
1589
  }
1590
+ if (options.consumer) {
1591
+ const deliveredIds = entities.map((e) => e.id).filter((id) => typeof id === "string");
1592
+ if (deliveredIds.length > 0) {
1593
+ this.batchTouchMemoryEntities(deliveredIds, options.consumer).catch(() => {});
1594
+ }
1595
+ }
1567
1596
  return { entities };
1568
1597
  }
1569
1598
  async deleteMemoryEntity(entityId) {
@@ -1572,9 +1601,10 @@ class HarmonyApiClient {
1572
1601
  async touchMemoryEntity(entityId) {
1573
1602
  return this.request("POST", `/memory/entities/${entityId}/touch`);
1574
1603
  }
1575
- async batchTouchMemoryEntities(entityIds) {
1604
+ async batchTouchMemoryEntities(entityIds, consumer) {
1576
1605
  return this.request("POST", "/memory/entities/batch-touch", {
1577
- entity_ids: entityIds
1606
+ entity_ids: entityIds,
1607
+ ...consumer ? { consumer } : {}
1578
1608
  });
1579
1609
  }
1580
1610
  async createMemoryRelation(data) {
@@ -1600,8 +1630,12 @@ class HarmonyApiClient {
1600
1630
  params.append("tags", tag);
1601
1631
  if (options?.include_superseded)
1602
1632
  params.set("include_superseded", "true");
1633
+ if (options?.consumer)
1634
+ params.set("consumer", options.consumer);
1603
1635
  if (options?.include_episodes)
1604
1636
  params.set("include_episodes", "true");
1637
+ if (options?.assembly_id)
1638
+ params.set("assembly_id", options.assembly_id);
1605
1639
  return this.request("GET", `/memory/search?${params.toString()}`);
1606
1640
  }
1607
1641
  async getVaultIndex(options) {
@@ -1613,6 +1647,8 @@ class HarmonyApiClient {
1613
1647
  params.set("type", options.type);
1614
1648
  if (options.limit !== undefined)
1615
1649
  params.set("limit", String(options.limit));
1650
+ if (options.consumer)
1651
+ params.set("consumer", options.consumer);
1616
1652
  if (options.include_episodes)
1617
1653
  params.set("include_episodes", "true");
1618
1654
  return this.request("GET", `/memory/index?${params.toString()}`);
@@ -1626,6 +1662,8 @@ class HarmonyApiClient {
1626
1662
  params.set("type", options.type);
1627
1663
  if (options.limit !== undefined)
1628
1664
  params.set("limit", String(options.limit));
1665
+ if (options.consumer)
1666
+ params.set("consumer", options.consumer);
1629
1667
  if (options.include_episodes)
1630
1668
  params.set("include_episodes", "true");
1631
1669
  return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
@@ -1685,6 +1723,8 @@ class HarmonyApiClient {
1685
1723
  params.set("type", options.type);
1686
1724
  if (options?.limit !== undefined)
1687
1725
  params.set("limit", String(options.limit));
1726
+ if (options?.consumer)
1727
+ params.set("consumer", options.consumer);
1688
1728
  if (options?.include_episodes)
1689
1729
  params.set("include_episodes", "true");
1690
1730
  return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
@@ -1790,14 +1830,15 @@ class HarmonyApiClient {
1790
1830
  } catch {}
1791
1831
  }
1792
1832
  const variant = options.variant || "execute";
1793
- const assembledContextStr = undefined;
1794
- const assemblyId = undefined;
1833
+ const assemblyId = randomUUID2();
1795
1834
  let memories;
1796
1835
  try {
1797
1836
  if (options.workspaceId && cardData.title) {
1798
- const memoryResult = await this.searchMemoryEntities(options.workspaceId, cardData.title, {
1837
+ const memoryResult = await this.searchMemoryEntities(options.workspaceId, buildMemoryQuery(cardData.title, cardData.description), {
1799
1838
  project_id: options.projectId,
1800
- limit: 5
1839
+ limit: 5,
1840
+ consumer: "agent-prompt",
1841
+ assembly_id: assemblyId
1801
1842
  });
1802
1843
  if (memoryResult.entities?.length > 0) {
1803
1844
  memories = memoryResult.entities.map((e) => ({
@@ -1821,7 +1862,6 @@ class HarmonyApiClient {
1821
1862
  contextOptions: options.contextOptions,
1822
1863
  customConstraints: options.customConstraints,
1823
1864
  memories,
1824
- assembledContext: assembledContextStr,
1825
1865
  assemblyId
1826
1866
  });
1827
1867
  try {
@@ -1931,6 +1971,7 @@ export {
1931
1971
  resetClient,
1932
1972
  requestWithBearer,
1933
1973
  getClient,
1974
+ buildMemoryQuery,
1934
1975
  HarmonyUnauthorizedError,
1935
1976
  HarmonyApiClient
1936
1977
  };
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  // src/config.ts
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { join } from "node:path";
20
+ import { dirname, join, parse, resolve } from "node:path";
21
21
  function getConfigDir() {
22
22
  return join(homedir(), ".harmony-mcp");
23
23
  }
@@ -27,6 +27,22 @@ function getConfigPath() {
27
27
  function getLocalConfigPath(cwd) {
28
28
  return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
29
29
  }
30
+ function findLocalConfigPath(cwd) {
31
+ const home = resolve(homedir());
32
+ let dir = resolve(cwd || process.cwd());
33
+ const { root } = parse(dir);
34
+ for (;; ) {
35
+ if (dir !== home && dir !== root) {
36
+ const candidate = join(dir, LOCAL_CONFIG_FILENAME);
37
+ if (existsSync(candidate))
38
+ return candidate;
39
+ }
40
+ const parent = dirname(dir);
41
+ if (parent === dir)
42
+ return null;
43
+ dir = parent;
44
+ }
45
+ }
30
46
  function emptyConfig() {
31
47
  return {
32
48
  apiKey: null,
@@ -78,8 +94,8 @@ function saveConfig(config) {
78
94
  });
79
95
  }
80
96
  function loadLocalConfig(cwd) {
81
- const localConfigPath = getLocalConfigPath(cwd);
82
- if (!existsSync(localConfigPath)) {
97
+ const localConfigPath = findLocalConfigPath(cwd);
98
+ if (localConfigPath === null) {
83
99
  return null;
84
100
  }
85
101
  try {
@@ -94,7 +110,7 @@ function loadLocalConfig(cwd) {
94
110
  }
95
111
  }
96
112
  function saveLocalConfig(config, cwd) {
97
- const localConfigPath = getLocalConfigPath(cwd);
113
+ const localConfigPath = findLocalConfigPath(cwd) ?? getLocalConfigPath(cwd);
98
114
  const existingConfig = loadLocalConfig(cwd) || {
99
115
  workspaceId: null,
100
116
  projectId: null
@@ -108,7 +124,7 @@ function saveLocalConfig(config, cwd) {
108
124
  writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
109
125
  }
110
126
  function hasLocalConfig(cwd) {
111
- return existsSync(getLocalConfigPath(cwd));
127
+ return findLocalConfigPath(cwd) !== null;
112
128
  }
113
129
  function getActiveCredential() {
114
130
  const config = loadConfig();
@@ -133,33 +149,69 @@ function getUserEmail() {
133
149
  function setUserEmail(email) {
134
150
  saveConfig({ userEmail: email });
135
151
  }
136
- function setActiveWorkspace(workspaceId, options) {
137
- if (options?.local) {
138
- saveLocalConfig({ workspaceId }, options.cwd);
139
- } else {
140
- saveConfig({ activeWorkspaceId: workspaceId });
152
+ function setActiveContext(context, options) {
153
+ if (options?.global) {
154
+ saveConfig({
155
+ activeWorkspaceId: context.workspaceId,
156
+ activeProjectId: context.projectId
157
+ });
158
+ return;
141
159
  }
142
- }
143
- function setActiveProject(projectId, options) {
144
- if (options?.local) {
145
- saveLocalConfig({ projectId }, options.cwd);
160
+ const localPath = findLocalConfigPath(options?.cwd);
161
+ if (options?.local || localPath !== null) {
162
+ saveLocalConfig({ workspaceId: context.workspaceId, projectId: context.projectId }, options?.cwd);
146
163
  } else {
147
- saveConfig({ activeProjectId: projectId });
164
+ saveConfig({
165
+ activeWorkspaceId: context.workspaceId,
166
+ activeProjectId: context.projectId
167
+ });
148
168
  }
149
169
  }
150
- function getActiveWorkspaceId(cwd) {
170
+ function setActiveWorkspace(workspaceId, options) {
171
+ const currentWorkspaceId = getActiveWorkspaceId(options?.cwd);
172
+ const keepProject = currentWorkspaceId === workspaceId;
173
+ setActiveContext({
174
+ workspaceId,
175
+ projectId: keepProject ? getActiveProjectId(options?.cwd) : null
176
+ }, options);
177
+ }
178
+ function readActiveContext(cwd) {
151
179
  const localConfig = loadLocalConfig(cwd);
152
- if (localConfig?.workspaceId) {
153
- return localConfig.workspaceId;
180
+ if (localConfig) {
181
+ return {
182
+ workspaceId: localConfig.workspaceId ?? null,
183
+ projectId: localConfig.projectId ?? null
184
+ };
154
185
  }
155
- return loadConfig().activeWorkspaceId;
186
+ const globalConfig = loadConfig();
187
+ return {
188
+ workspaceId: globalConfig.activeWorkspaceId,
189
+ projectId: globalConfig.activeProjectId
190
+ };
191
+ }
192
+ function getActiveWorkspaceId(cwd) {
193
+ return readActiveContext(cwd).workspaceId;
156
194
  }
157
195
  function getActiveProjectId(cwd) {
158
- const localConfig = loadLocalConfig(cwd);
159
- if (localConfig?.projectId) {
160
- return localConfig.projectId;
196
+ return readActiveContext(cwd).projectId;
197
+ }
198
+ function getActiveContext(cwd) {
199
+ return describeActiveContext({
200
+ projectId: getActiveProjectId(cwd),
201
+ workspaceId: getActiveWorkspaceId(cwd)
202
+ });
203
+ }
204
+ function describeActiveContext(context) {
205
+ const { projectId, workspaceId } = context;
206
+ if (projectId && !workspaceId) {
207
+ return {
208
+ projectId,
209
+ workspaceId,
210
+ consistent: false,
211
+ note: `An active project (${projectId}) is set with no active workspace, so ` + "workspace-scoped tools cannot resolve one from it. Re-set it with " + "harmony_set_project_context, or pass workspaceId explicitly."
212
+ };
161
213
  }
162
- return loadConfig().activeProjectId;
214
+ return { projectId, workspaceId, consistent: true, note: null };
163
215
  }
164
216
  function isConfigured() {
165
217
  const config = loadConfig();
@@ -214,7 +266,7 @@ init_config();
214
266
  export {
215
267
  setUserEmail,
216
268
  setActiveWorkspace,
217
- setActiveProject,
269
+ setActiveContext,
218
270
  saveLocalConfig,
219
271
  saveConfig,
220
272
  loadLocalConfig,
@@ -232,5 +284,8 @@ export {
232
284
  getActiveWorkspaceId,
233
285
  getActiveProjectId,
234
286
  getActiveCredential,
287
+ getActiveContext,
288
+ findLocalConfigPath,
289
+ describeActiveContext,
235
290
  areSkillsInstalled
236
291
  };