@mercury-fw/cli-engine 0.39.0 → 0.41.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/CHANGELOG.md +29 -0
- package/cli-executor.ts +8 -1
- package/cli-tool.ts +28 -5
- package/dist/cli-executor.d.ts +6 -1
- package/dist/cli-tool.d.ts +11 -2
- package/dist/index.d.ts +4 -2
- package/dist/person-login.d.ts +19 -0
- package/index.ts +4 -2
- package/package.json +3 -2
- package/person-login.ts +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @mercury-fw/cli-engine
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @mercury-fw/plugin-types@0.41.0
|
|
8
|
+
- @mercury-fw/utils@0.41.0
|
|
9
|
+
|
|
10
|
+
## 0.40.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 0739f77: A plugin acts as the person Mercury is talking to, who logs in to the service through Mercury.
|
|
15
|
+
|
|
16
|
+
- Plugin contract version 4: a plugin declares `actsAs: "person" | "mercury"`, and one acting as the person contributes a `login`; its tools get the turn's `person` and `requireLogin` in their context.
|
|
17
|
+
- A person is offered only the plugins acting as the person: their prompt fragments, skills and tools. A plugin acting as Mercury, or declaring nothing, is offered on the terminal only until people can be allowed to make Mercury act as itself; the startup log names it.
|
|
18
|
+
- `createCliTool` adds `--user` with the person's id to every command, staged confirmations included, and when the CLI exits with code 3 (not logged in) returns their login instead. `createCliPersonLogin` builds a plugin's `login` on a CLI's two-step remote login. A failed `runCli` carries `exitCode`.
|
|
19
|
+
- Channel contract version 4: channels get `logins` (`accept` a callback URL, `complete` a login) and `detectLoginRequired`.
|
|
20
|
+
- The HTTP channel, with `HTTP_SURFACE_PUBLIC_URL` set, streams a `login` event with the link to open and takes the person back at `GET /login/callback`, public and protected by the single-use state.
|
|
21
|
+
- `mercury.cliCredentials` can declare `userSetup`, run by `mfw credentials setup <plugin> --user-app`: the app people log in through, set up without logging anyone in.
|
|
22
|
+
- plugin-jira and plugin-bitbucket act as the person, on CLIs 2.3.0: set up their people's app with `--user-app` and register `<HTTP_SURFACE_PUBLIC_URL>/login/callback` on it. Jira's skill now says `currentUser()` is the person.
|
|
23
|
+
- plugin-atlassian-admin acts as Mercury, on CLI 0.2.0, whose `init` asks for the key without echoing it: until permissions exist it's available on the terminal only.
|
|
24
|
+
- A third-party plugin needs `apiVersion: 4` to load, and `actsAs: "person"` with a `login` to be offered to people.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- Updated dependencies [0739f77]
|
|
29
|
+
- @mercury-fw/plugin-types@0.40.0
|
|
30
|
+
- @mercury-fw/utils@0.40.0
|
|
31
|
+
|
|
3
32
|
## 0.39.0
|
|
4
33
|
|
|
5
34
|
### Minor Changes
|
package/cli-executor.ts
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
import type { CliResult } from "@mercury-fw/plugin-types";
|
|
26
26
|
export type { CliResult };
|
|
27
27
|
|
|
28
|
+
/** The exit code the CLIs Mercury's plugins run use when the identity a
|
|
29
|
+
* command runs as (the service, or the person `--user` names) has no login,
|
|
30
|
+
* or one that has to be made again: what tells it apart from any other
|
|
31
|
+
* failure. */
|
|
32
|
+
export const CLI_NOT_LOGGED_IN_EXIT_CODE = 3;
|
|
33
|
+
|
|
28
34
|
/**
|
|
29
35
|
* Thin wrapper around `Bun.spawn` with both stdout and stderr piped.
|
|
30
36
|
* Extracted into its own function (rather than calling `Bun.spawn`
|
|
@@ -43,7 +49,7 @@ function spawnPiped(binary: string, args: string[]) {
|
|
|
43
49
|
*
|
|
44
50
|
* Resolves to `{ ok: false, error }` — never rejects/throws — for the
|
|
45
51
|
* binary not existing on `PATH` or a non-zero exit code (the error
|
|
46
|
-
* includes the exit code and stderr). Success is exit code 0, full stop:
|
|
52
|
+
* includes the exit code and stderr, and `exitCode` carries the code). Success is exit code 0, full stop:
|
|
47
53
|
* if stdout happens to be valid JSON it's parsed into `data`, otherwise
|
|
48
54
|
* the raw trimmed text is `data` instead. Non-JSON stdout on a 0 exit is
|
|
49
55
|
* not a parse failure — `--help` output is exactly this shape (plain
|
|
@@ -72,6 +78,7 @@ export async function runCli(
|
|
|
72
78
|
return {
|
|
73
79
|
ok: false,
|
|
74
80
|
error: `${binary} exited with code ${exitCode}: ${stderr.trim()}`,
|
|
81
|
+
exitCode,
|
|
75
82
|
};
|
|
76
83
|
}
|
|
77
84
|
|
package/cli-tool.ts
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
import { tool, type JSONValue } from "ai";
|
|
24
24
|
import { z } from "zod";
|
|
25
25
|
import { parseCommand } from "./command-parser.ts";
|
|
26
|
-
import type
|
|
27
|
-
import type { StageConfirmation, ExecutableTool } from "@mercury-fw/plugin-types";
|
|
26
|
+
import { CLI_NOT_LOGGED_IN_EXIT_CODE, type runCli, type CliResult } from "./cli-executor.ts";
|
|
27
|
+
import type { StageConfirmation, ExecutableTool, LoginRequired } from "@mercury-fw/plugin-types";
|
|
28
|
+
import { cliUserId } from "@mercury-fw/utils";
|
|
28
29
|
|
|
29
30
|
// `CliPostProcessor` is part of the plugin contract (a plugin's `build()`
|
|
30
31
|
// returns one) — it lives in `@mercury-fw/plugin-types` and is re-exported here
|
|
@@ -125,6 +126,15 @@ export function omitDisplayForModel(output: unknown): unknown {
|
|
|
125
126
|
return rest;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/** Drops the link from a `loginRequired` result before it reaches the model:
|
|
130
|
+
* it carries the login's state, the only credential of the callback the
|
|
131
|
+
* person comes back to, so the channel shows it and the model never sees it. */
|
|
132
|
+
function omitLoginLinkForModel(output: unknown): unknown {
|
|
133
|
+
if (typeof output !== "object" || output === null || (output as { loginRequired?: unknown }).loginRequired !== true) return output;
|
|
134
|
+
const { authorizeUrl: _url, ...rest } = output as Record<string, unknown>;
|
|
135
|
+
return rest;
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
/**
|
|
129
139
|
* Builds the `runCommand` tool: the model writes a whole CLI invocation as
|
|
130
140
|
* one string, `execute` parses it (`parseCommand`), checks the binary
|
|
@@ -163,8 +173,16 @@ export function createCliTool(
|
|
|
163
173
|
* composition root. Optional: with none wired the inline `display` is left
|
|
164
174
|
* untouched and no `displayRef` is minted. */
|
|
165
175
|
stashDisplay?: (artifact: string) => string;
|
|
176
|
+
/** The person the turn is for (see `SessionToolContext.person`): every
|
|
177
|
+
* command runs as them, with `--user` and the id their CLI knows them by.
|
|
178
|
+
* Absent or `null`: the CLI's own service identity. */
|
|
179
|
+
person?: { key: string } | null;
|
|
180
|
+
/** What the tool hands back when the person's CLI says they aren't logged
|
|
181
|
+
* in (`CLI_NOT_LOGGED_IN_EXIT_CODE`): their login, started. */
|
|
182
|
+
requireLogin?: () => Promise<LoginRequired>;
|
|
166
183
|
},
|
|
167
184
|
): { runCommand: ExecutableTool } {
|
|
185
|
+
const identity = opts.person ? ["--user", cliUserId(opts.person.key)] : [];
|
|
168
186
|
// Anchor the example on a binary this tool actually runs rather than
|
|
169
187
|
// a hardcoded one: a fixed `jira …` example misleads the model on an instance
|
|
170
188
|
// without Jira. The concrete, CLI-specific example (real subcommand + flags)
|
|
@@ -218,7 +236,7 @@ export function createCliTool(
|
|
|
218
236
|
// staged args run, regardless of whether the model remembered to
|
|
219
237
|
// include it on the first attempt (observed live: it usually
|
|
220
238
|
// doesn't).
|
|
221
|
-
const argsToStage = parsed.args.includes("--confirm") ? parsed.args : [...parsed.args, "--confirm"];
|
|
239
|
+
const argsToStage = [...(parsed.args.includes("--confirm") ? parsed.args : [...parsed.args, "--confirm"]), ...identity];
|
|
222
240
|
// Stage the doing as an opaque thunk — the core confirmation subsystem
|
|
223
241
|
// never learns this is a CLI command. `describe` (the normalized argv)
|
|
224
242
|
// is the paper-trail text; `summary` on the result (the raw command the
|
|
@@ -236,7 +254,12 @@ export function createCliTool(
|
|
|
236
254
|
};
|
|
237
255
|
}
|
|
238
256
|
|
|
239
|
-
const result = await runCliFn(parsed.binary, parsed.args);
|
|
257
|
+
const result = await runCliFn(parsed.binary, [...parsed.args, ...identity]);
|
|
258
|
+
// The person has no login (or an expired one) with this CLI's service:
|
|
259
|
+
// nothing to retry, they log in first.
|
|
260
|
+
if (!result.ok && result.exitCode === CLI_NOT_LOGGED_IN_EXIT_CODE && opts.person && opts.requireLogin) {
|
|
261
|
+
return opts.requireLogin();
|
|
262
|
+
}
|
|
240
263
|
const processed = opts.postProcess
|
|
241
264
|
? opts.postProcess({ binary: parsed.binary, args: parsed.args, prefix: match.prefix }, result)
|
|
242
265
|
: result;
|
|
@@ -256,7 +279,7 @@ export function createCliTool(
|
|
|
256
279
|
}
|
|
257
280
|
return processed;
|
|
258
281
|
},
|
|
259
|
-
toModelOutput: ({ output }) => ({ type: "json", value: omitDisplayForModel(output) as JSONValue }),
|
|
282
|
+
toModelOutput: ({ output }) => ({ type: "json", value: omitLoginLinkForModel(omitDisplayForModel(output)) as JSONValue }),
|
|
260
283
|
});
|
|
261
284
|
|
|
262
285
|
return { runCommand };
|
package/dist/cli-executor.d.ts
CHANGED
|
@@ -19,13 +19,18 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { CliResult } from "@mercury-fw/plugin-types";
|
|
21
21
|
export type { CliResult };
|
|
22
|
+
/** The exit code the CLIs Mercury's plugins run use when the identity a
|
|
23
|
+
* command runs as (the service, or the person `--user` names) has no login,
|
|
24
|
+
* or one that has to be made again: what tells it apart from any other
|
|
25
|
+
* failure. */
|
|
26
|
+
export declare const CLI_NOT_LOGGED_IN_EXIT_CODE = 3;
|
|
22
27
|
/**
|
|
23
28
|
* Runs `binary` with `args`, waits for it to exit, and parses its
|
|
24
29
|
* stdout as JSON when possible.
|
|
25
30
|
*
|
|
26
31
|
* Resolves to `{ ok: false, error }` — never rejects/throws — for the
|
|
27
32
|
* binary not existing on `PATH` or a non-zero exit code (the error
|
|
28
|
-
* includes the exit code and stderr). Success is exit code 0, full stop:
|
|
33
|
+
* includes the exit code and stderr, and `exitCode` carries the code). Success is exit code 0, full stop:
|
|
29
34
|
* if stdout happens to be valid JSON it's parsed into `data`, otherwise
|
|
30
35
|
* the raw trimmed text is `data` instead. Non-JSON stdout on a 0 exit is
|
|
31
36
|
* not a parse failure — `--help` output is exactly this shape (plain
|
package/dist/cli-tool.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { StageConfirmation, ExecutableTool } from "@mercury-fw/plugin-types";
|
|
1
|
+
import { type runCli } from "./cli-executor.ts";
|
|
2
|
+
import type { StageConfirmation, ExecutableTool, LoginRequired } from "@mercury-fw/plugin-types";
|
|
3
3
|
import type { CliPostProcessor } from "@mercury-fw/plugin-types";
|
|
4
4
|
export type { CliPostProcessor };
|
|
5
5
|
export type AllowedCommand = {
|
|
@@ -102,6 +102,15 @@ export declare function createCliTool(runCliFn: typeof runCli, configs: Record<s
|
|
|
102
102
|
* composition root. Optional: with none wired the inline `display` is left
|
|
103
103
|
* untouched and no `displayRef` is minted. */
|
|
104
104
|
stashDisplay?: (artifact: string) => string;
|
|
105
|
+
/** The person the turn is for (see `SessionToolContext.person`): every
|
|
106
|
+
* command runs as them, with `--user` and the id their CLI knows them by.
|
|
107
|
+
* Absent or `null`: the CLI's own service identity. */
|
|
108
|
+
person?: {
|
|
109
|
+
key: string;
|
|
110
|
+
} | null;
|
|
111
|
+
/** What the tool hands back when the person's CLI says they aren't logged
|
|
112
|
+
* in (`CLI_NOT_LOGGED_IN_EXIT_CODE`): their login, started. */
|
|
113
|
+
requireLogin?: () => Promise<LoginRequired>;
|
|
105
114
|
}): {
|
|
106
115
|
runCommand: ExecutableTool;
|
|
107
116
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The core imports what it needs from here; a CLI-based plugin (Jira,
|
|
9
9
|
* Bitbucket) rides on this one. Nothing here imports the app — its only
|
|
10
|
-
* dependencies are `@mercury-fw/plugin-types` (the shared contract)
|
|
10
|
+
* dependencies are `@mercury-fw/plugin-types` (the shared contract),
|
|
11
|
+
* `@mercury-fw/utils` (the id a CLI knows a person by) and the
|
|
11
12
|
* external `ai`/`zod`/`shell-quote` packages — so the mechanism is a self
|
|
12
13
|
* contained unit that an instance with no CLI plugin never pulls in.
|
|
13
14
|
*/
|
|
14
15
|
export { parseCommand, type ParsedCommand } from "./command-parser.ts";
|
|
15
|
-
export { runCli, type CliResult } from "./cli-executor.ts";
|
|
16
|
+
export { runCli, CLI_NOT_LOGGED_IN_EXIT_CODE, type CliResult } from "./cli-executor.ts";
|
|
17
|
+
export { createCliPersonLogin } from "./person-login.ts";
|
|
16
18
|
export { createCliTool, matchCommand, stripGlobalFlags, formatPrefixes, omitDisplayForModel, type CliConfig, type AllowedCommand, type GlobalFlag, type CommandMatch, type CliPostProcessor, } from "./cli-tool.ts";
|
|
17
19
|
export { CliConfigFileSchema, type CliConfigFile } from "./cli-config-schema.ts";
|
|
18
20
|
export { toCliConfig, loadCliConfigFromObject, parseCliConfig, type CliConfigFromObjectResult, } from "./cli-config-loader.ts";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A person's login through a CLI's two-step remote login, for a plugin acting
|
|
3
|
+
* as the person (`PersonLogin`): `auth login --user <id> --remote` prints the
|
|
4
|
+
* link the person opens and the `state` the provider sends back with the
|
|
5
|
+
* code; `auth login --user <id> --code <code> --state <state>` finishes it.
|
|
6
|
+
* The CLI keeps the pending login (verifier, state, expiry) itself, in the
|
|
7
|
+
* person's own folder, so nothing secret passes through here but the code.
|
|
8
|
+
*/
|
|
9
|
+
import type { PersonLogin } from "@mercury-fw/plugin-types";
|
|
10
|
+
import type { runCli } from "./cli-executor.ts";
|
|
11
|
+
/**
|
|
12
|
+
* Builds `binary`'s `PersonLogin`. `redirectUri`: whether the CLI takes the
|
|
13
|
+
* callback as `--redirect-uri` (jira, confluence, zitadel), or the provider
|
|
14
|
+
* always sends the person back to the one callback registered on the app
|
|
15
|
+
* (bitbucket), in which case that has to be Mercury's.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createCliPersonLogin(runCliFn: typeof runCli, binary: string, { redirectUri }: {
|
|
18
|
+
redirectUri: boolean;
|
|
19
|
+
}): PersonLogin;
|
package/index.ts
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The core imports what it needs from here; a CLI-based plugin (Jira,
|
|
9
9
|
* Bitbucket) rides on this one. Nothing here imports the app — its only
|
|
10
|
-
* dependencies are `@mercury-fw/plugin-types` (the shared contract)
|
|
10
|
+
* dependencies are `@mercury-fw/plugin-types` (the shared contract),
|
|
11
|
+
* `@mercury-fw/utils` (the id a CLI knows a person by) and the
|
|
11
12
|
* external `ai`/`zod`/`shell-quote` packages — so the mechanism is a self
|
|
12
13
|
* contained unit that an instance with no CLI plugin never pulls in.
|
|
13
14
|
*/
|
|
14
15
|
export { parseCommand, type ParsedCommand } from "./command-parser.ts";
|
|
15
|
-
export { runCli, type CliResult } from "./cli-executor.ts";
|
|
16
|
+
export { runCli, CLI_NOT_LOGGED_IN_EXIT_CODE, type CliResult } from "./cli-executor.ts";
|
|
17
|
+
export { createCliPersonLogin } from "./person-login.ts";
|
|
16
18
|
export {
|
|
17
19
|
createCliTool,
|
|
18
20
|
matchCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/cli-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@mercury-fw/plugin-types": "0.
|
|
26
|
+
"@mercury-fw/plugin-types": "0.41.0",
|
|
27
|
+
"@mercury-fw/utils": "0.41.0",
|
|
27
28
|
"ai": "^7.0.126",
|
|
28
29
|
"shell-quote": "^1.11.0",
|
|
29
30
|
"zod": "^4.6.5"
|
package/person-login.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A person's login through a CLI's two-step remote login, for a plugin acting
|
|
3
|
+
* as the person (`PersonLogin`): `auth login --user <id> --remote` prints the
|
|
4
|
+
* link the person opens and the `state` the provider sends back with the
|
|
5
|
+
* code; `auth login --user <id> --code <code> --state <state>` finishes it.
|
|
6
|
+
* The CLI keeps the pending login (verifier, state, expiry) itself, in the
|
|
7
|
+
* person's own folder, so nothing secret passes through here but the code.
|
|
8
|
+
*/
|
|
9
|
+
import type { PersonLogin } from "@mercury-fw/plugin-types";
|
|
10
|
+
import { cliUserId } from "@mercury-fw/utils";
|
|
11
|
+
import type { runCli } from "./cli-executor.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds `binary`'s `PersonLogin`. `redirectUri`: whether the CLI takes the
|
|
15
|
+
* callback as `--redirect-uri` (jira, confluence, zitadel), or the provider
|
|
16
|
+
* always sends the person back to the one callback registered on the app
|
|
17
|
+
* (bitbucket), in which case that has to be Mercury's.
|
|
18
|
+
*/
|
|
19
|
+
export function createCliPersonLogin(
|
|
20
|
+
runCliFn: typeof runCli,
|
|
21
|
+
binary: string,
|
|
22
|
+
{ redirectUri }: { redirectUri: boolean },
|
|
23
|
+
): PersonLogin {
|
|
24
|
+
return {
|
|
25
|
+
start: async (personKey, callbackUrl) => {
|
|
26
|
+
const args = ["auth", "login", "--user", cliUserId(personKey), "--remote"];
|
|
27
|
+
const result = await runCliFn(binary, redirectUri ? [...args, "--redirect-uri", callbackUrl] : args);
|
|
28
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
29
|
+
const { authorize_url: authorizeUrl, state } = (result.data ?? {}) as { authorize_url?: unknown; state?: unknown };
|
|
30
|
+
if (typeof authorizeUrl !== "string" || typeof state !== "string") {
|
|
31
|
+
return { ok: false, error: `${binary} auth login --remote didn't print an authorize_url and a state` };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, authorizeUrl, state };
|
|
34
|
+
},
|
|
35
|
+
complete: async (personKey, code, state) => {
|
|
36
|
+
const result = await runCliFn(binary, ["auth", "login", "--user", cliUserId(personKey), "--code", code, "--state", state]);
|
|
37
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|