@gethmy/mcp 2.22.0 → 2.24.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.
- package/README.md +14 -14
- package/dist/cli.js +216 -70
- package/dist/index.js +181 -45
- package/dist/lib/api-client.js +78 -26
- package/dist/lib/config.js +79 -24
- package/dist/lib/oauth-refresh.js +76 -24
- package/package.json +2 -2
- package/src/api-client.ts +15 -0
- package/src/cli.ts +25 -14
- package/src/config.ts +201 -27
- package/src/remote.ts +34 -6
- package/src/server.ts +165 -12
- package/src/tui/setup.ts +21 -6
package/dist/lib/config.js
CHANGED
|
@@ -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 =
|
|
82
|
-
if (
|
|
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
|
|
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
|
|
137
|
-
if (options?.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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({
|
|
164
|
+
saveConfig({
|
|
165
|
+
activeWorkspaceId: context.workspaceId,
|
|
166
|
+
activeProjectId: context.projectId
|
|
167
|
+
});
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
|
-
function
|
|
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
|
|
153
|
-
return
|
|
180
|
+
if (localConfig) {
|
|
181
|
+
return {
|
|
182
|
+
workspaceId: localConfig.workspaceId ?? null,
|
|
183
|
+
projectId: localConfig.projectId ?? null
|
|
184
|
+
};
|
|
154
185
|
}
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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 =
|
|
82
|
-
if (
|
|
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
|
|
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
|
|
137
|
-
if (options?.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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({
|
|
164
|
+
saveConfig({
|
|
165
|
+
activeWorkspaceId: context.workspaceId,
|
|
166
|
+
activeProjectId: context.projectId
|
|
167
|
+
});
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
|
-
function
|
|
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
|
|
153
|
-
return
|
|
180
|
+
if (localConfig) {
|
|
181
|
+
return {
|
|
182
|
+
workspaceId: localConfig.workspaceId ?? null,
|
|
183
|
+
projectId: localConfig.projectId ?? null
|
|
184
|
+
};
|
|
154
185
|
}
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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((
|
|
293
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
242
294
|
}
|
|
243
295
|
async function withRefreshLock(fn) {
|
|
244
296
|
const path = lockPath();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "MCP server for Harmony
|
|
3
|
+
"version": "2.24.0",
|
|
4
|
+
"description": "MCP server for Harmony, the shared surface for human–agent teams — agents claim cards, report progress, and move work on your board.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
package/src/api-client.ts
CHANGED
|
@@ -1088,6 +1088,15 @@ export class HarmonyApiClient {
|
|
|
1088
1088
|
* NULL until the terminal cost frame reported the observed model.
|
|
1089
1089
|
*/
|
|
1090
1090
|
modelName?: string;
|
|
1091
|
+
/**
|
|
1092
|
+
* Who is calling (Task 5, "the drivers"): `daemon` (the local agent
|
|
1093
|
+
* daemon), `interactive` (a human-driven `/hmy` session), or `script`
|
|
1094
|
+
* (e.g. the e2e harness). Names the holder in a 409 conflict message so
|
|
1095
|
+
* a second caller knows who to wait on. Optional and nullable
|
|
1096
|
+
* server-side — omitting it is unchanged (pre-existing callers keep
|
|
1097
|
+
* working); the MCP tool dispatch defaults to `interactive`.
|
|
1098
|
+
*/
|
|
1099
|
+
driver?: "daemon" | "interactive" | "script";
|
|
1091
1100
|
},
|
|
1092
1101
|
): Promise<{
|
|
1093
1102
|
session: unknown;
|
|
@@ -2139,6 +2148,9 @@ export class HarmonyApiClient {
|
|
|
2139
2148
|
description?: string;
|
|
2140
2149
|
steps?: unknown;
|
|
2141
2150
|
triggerType?: string;
|
|
2151
|
+
/** Auto-bind rule (#892); validated server-side, passed through as-is. */
|
|
2152
|
+
autoBind?: unknown;
|
|
2153
|
+
catalogId?: string;
|
|
2142
2154
|
}): Promise<{ playbook: unknown }> {
|
|
2143
2155
|
return this.request("POST", "/playbooks", data);
|
|
2144
2156
|
}
|
|
@@ -2151,6 +2163,9 @@ export class HarmonyApiClient {
|
|
|
2151
2163
|
steps?: unknown;
|
|
2152
2164
|
enabled?: boolean;
|
|
2153
2165
|
state?: string;
|
|
2166
|
+
triggerType?: string;
|
|
2167
|
+
/** `null` clears the rule; absent leaves it untouched (#892). */
|
|
2168
|
+
autoBind?: unknown;
|
|
2154
2169
|
},
|
|
2155
2170
|
): Promise<{ playbook: unknown }> {
|
|
2156
2171
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
package/src/cli.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { program } from "commander";
|
|
4
4
|
import {
|
|
5
5
|
areSkillsInstalled,
|
|
6
|
+
findLocalConfigPath,
|
|
7
|
+
getActiveContext,
|
|
6
8
|
getActiveProjectId,
|
|
7
9
|
getActiveWorkspaceId,
|
|
8
10
|
getConfigPath,
|
|
9
|
-
getLocalConfigPath,
|
|
10
11
|
hasLocalConfig,
|
|
11
12
|
isConfigured,
|
|
12
13
|
loadConfig,
|
|
@@ -22,7 +23,9 @@ const { version } = require("../package.json");
|
|
|
22
23
|
|
|
23
24
|
program
|
|
24
25
|
.name("@gethmy/mcp")
|
|
25
|
-
.description(
|
|
26
|
+
.description(
|
|
27
|
+
"MCP server for Harmony — the shared surface for human–agent teams",
|
|
28
|
+
)
|
|
26
29
|
.version(version);
|
|
27
30
|
|
|
28
31
|
program
|
|
@@ -83,7 +86,9 @@ program
|
|
|
83
86
|
console.log("\nContext:");
|
|
84
87
|
|
|
85
88
|
if (hasLocal) {
|
|
86
|
-
|
|
89
|
+
// The file that is actually READ, which since #893 may be an ancestor's
|
|
90
|
+
// — printing the cwd path would name a file that does not exist.
|
|
91
|
+
console.log(` Local config: ${findLocalConfigPath()}`);
|
|
87
92
|
console.log(
|
|
88
93
|
` Workspace: ${localConfig?.workspaceId || "(not set)"}`,
|
|
89
94
|
);
|
|
@@ -101,16 +106,14 @@ program
|
|
|
101
106
|
// Show effective (active) context
|
|
102
107
|
const effectiveWorkspace = getActiveWorkspaceId();
|
|
103
108
|
const effectiveProject = getActiveProjectId();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
? "global"
|
|
113
|
-
: "";
|
|
109
|
+
// The local file wins as a WHOLE file (#893), so the source is the file,
|
|
110
|
+
// not the field: with a local config in scope BOTH ids come from it, and
|
|
111
|
+
// a key it omits reads as unset rather than falling through to global.
|
|
112
|
+
// Reporting "global" for such a field would describe a fallback that no
|
|
113
|
+
// longer happens.
|
|
114
|
+
const contextSource = hasLocal ? "local" : "global";
|
|
115
|
+
const wsSource = effectiveWorkspace ? contextSource : "";
|
|
116
|
+
const projSource = effectiveProject ? contextSource : "";
|
|
114
117
|
|
|
115
118
|
console.log("\n Active (effective):");
|
|
116
119
|
console.log(
|
|
@@ -119,6 +122,14 @@ program
|
|
|
119
122
|
console.log(
|
|
120
123
|
` Project: ${effectiveProject || "(not set)"}${projSource ? ` ← ${projSource}` : ""}`,
|
|
121
124
|
);
|
|
125
|
+
|
|
126
|
+
// Say it out loud when the pair does not hold together (#893). A project
|
|
127
|
+
// with no workspace leaves every workspace-scoped tool guessing, and this
|
|
128
|
+
// status output is where a person looks when one of them misbehaves.
|
|
129
|
+
const report = getActiveContext();
|
|
130
|
+
if (!report.consistent && report.note) {
|
|
131
|
+
console.log(`\n ⚠ ${report.note}`);
|
|
132
|
+
}
|
|
122
133
|
} else {
|
|
123
134
|
console.log("Status: Not configured\n");
|
|
124
135
|
console.log("Run: npx @gethmy/mcp setup");
|
|
@@ -146,7 +157,7 @@ program
|
|
|
146
157
|
|
|
147
158
|
program
|
|
148
159
|
.command("setup")
|
|
149
|
-
.description("
|
|
160
|
+
.description("Setup wizard for Harmony MCP (recommended)")
|
|
150
161
|
.argument(
|
|
151
162
|
"[slug]",
|
|
152
163
|
"Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)",
|