bwx 0.7.0 → 0.8.1
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/LICENSE +21 -0
- package/README.md +29 -2
- package/package.json +21 -2
- package/skill/bwx/SKILL.md +17 -0
- package/src/bw/fields.ts +11 -2
- package/src/bw/runner.ts +17 -1
- package/src/cli/commands/get.ts +73 -17
- package/src/cli/io.ts +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vladislav Forsh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -69,10 +69,10 @@ unlock. `valid` means reads work now, `stale` means the cached session was rejec
|
|
|
69
69
|
|
|
70
70
|
| Command | Description |
|
|
71
71
|
|---------|-------------|
|
|
72
|
-
| `bwx get <field> <item>` | Get field (`password`, `username`, `totp`, `notes`, `uri`, `item`) |
|
|
72
|
+
| `bwx get <field> <item>` | Get field (`password`, `username`, `totp`, `notes`, `uri`, `item`), `--raw` |
|
|
73
73
|
| `bwx get totp <item>` | TOTP code (`--fresh [n]`, `--seed`) |
|
|
74
74
|
| `bwx get <card field> <item>` | Card field (`number`, `cvv`, `cardholder`, `brand`, `expiry`, …) |
|
|
75
|
-
| `bwx field <name> <item>` | Get custom field by name (same argument order as `get`) |
|
|
75
|
+
| `bwx field <name> <item>` | Get custom field by name (same argument order as `get`), `--raw` |
|
|
76
76
|
| `bwx search <query>` | Search items (`--type`, `--folder`, `--limit`, `--full-items`) |
|
|
77
77
|
| `bwx list [type]` | List items (`--type`, `--folder`, `--limit`, `--full-items`) |
|
|
78
78
|
| `bwx folders` | List folder names and IDs |
|
|
@@ -104,6 +104,29 @@ error: No item matching "GitHub PAT vforsh old". Did you mean:
|
|
|
104
104
|
3683fb9a-… note GitHub PAT (yaforsh) [codex-monitor-pr]
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### Piping a secret
|
|
108
|
+
|
|
109
|
+
`get` and `field` end their output with a newline, which reads fine in a terminal and
|
|
110
|
+
lands *inside the secret* for a consumer that treats stdin literally. `--raw` suppresses
|
|
111
|
+
that one newline — and only that one: newlines stored in the value are its own and are
|
|
112
|
+
written through.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
bwx get password "GitHub" --raw | argus fill app --selector 'input[type=password]' --value-stdin
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`--raw` values are byte-for-byte, so whitespace around a password survives too (the
|
|
119
|
+
default output trims it). `--raw` and `--json` are mutually exclusive — the JSON envelope
|
|
120
|
+
quotes and wraps the value, so there is no raw byte stream to hand over — and combining
|
|
121
|
+
them is a usage error rather than a silent winner.
|
|
122
|
+
|
|
123
|
+
For handing a secret to a whole command rather than to one stdin, `bwx run` keeps it out
|
|
124
|
+
of argv entirely:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
bwx run --env GITHUB_TOKEN=password:"GitHub PAT" -- gh api /user
|
|
128
|
+
```
|
|
129
|
+
|
|
107
130
|
### TOTP
|
|
108
131
|
|
|
109
132
|
Codes are computed locally from the stored secret. A TOTP therefore costs no extra `bw`
|
|
@@ -360,3 +383,7 @@ Breaking changes, all of them cases where the old behavior failed quietly:
|
|
|
360
383
|
## Stack
|
|
361
384
|
|
|
362
385
|
Bun, TypeScript, Commander, Zod, picocolors.
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT — see [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bwx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Extended Bitwarden CLI for scripts and agents — auto-unlock via macOS Keychain, session caching, local TOTP, structured output",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitwarden",
|
|
7
|
+
"bitwarden-cli",
|
|
7
8
|
"cli",
|
|
8
9
|
"vault",
|
|
9
10
|
"secrets",
|
|
11
|
+
"secrets-management",
|
|
12
|
+
"totp",
|
|
13
|
+
"keychain",
|
|
14
|
+
"macos",
|
|
10
15
|
"ai-agents",
|
|
11
16
|
"bun"
|
|
12
17
|
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Vladislav Forsh <vforsh@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/vforsh/bwx.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/vforsh/bwx#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/vforsh/bwx/issues"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"engines": {
|
|
30
|
+
"bun": ">=1.0.0"
|
|
31
|
+
},
|
|
13
32
|
"files": [
|
|
14
33
|
"bin",
|
|
15
34
|
"src",
|
package/skill/bwx/SKILL.md
CHANGED
|
@@ -20,6 +20,7 @@ bwx get item "GitHub" # full item JSON
|
|
|
20
20
|
bwx get number "Visa" # card number (also: cvv, expiry, cardholder, brand)
|
|
21
21
|
bwx get "API Key" "Acme" # custom field by name
|
|
22
22
|
bwx field "API Key" "Acme" # custom-field-only lookup, same arg order
|
|
23
|
+
bwx get password "GitHub" --raw # no trailing newline, for piping into stdin
|
|
23
24
|
|
|
24
25
|
bwx search github # fuzzy search, human table
|
|
25
26
|
bwx search github --json # id/type/name/username only — no secrets
|
|
@@ -78,6 +79,22 @@ exactly are delegated to `bw get totp` instead of guessed at.
|
|
|
78
79
|
Custom fields only, so an item whose custom field is named `password` is still reachable.
|
|
79
80
|
Takes its arguments in the same order as `get` (this changed in 0.5.0).
|
|
80
81
|
|
|
82
|
+
### `--raw` (on `get` and `field`)
|
|
83
|
+
|
|
84
|
+
Output normally ends in a newline, which a consumer that reads stdin literally takes as
|
|
85
|
+
the last byte of the secret. `--raw` suppresses that one newline and nothing else —
|
|
86
|
+
newlines inside the value are written through, and the value is byte-for-byte, so
|
|
87
|
+
surrounding whitespace survives (the default output trims it).
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bwx get password "GitHub" --raw | argus fill app --selector 'input[type=password]' --value-stdin
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Prefer this over `$(bwx get …)` when piping a secret: command substitution eats trailing
|
|
94
|
+
newlines *and* puts the secret in the child's argv, where any same-user process can read
|
|
95
|
+
it. `--raw` and `--json` cannot be combined (the envelope quotes and wraps the value); use
|
|
96
|
+
`bwx run --env` when a whole command needs the secret in its environment instead.
|
|
97
|
+
|
|
81
98
|
### Card fields
|
|
82
99
|
|
|
83
100
|
```bash
|
package/src/bw/fields.ts
CHANGED
|
@@ -40,6 +40,14 @@ export interface ReadFieldOptions {
|
|
|
40
40
|
* field is called e.g. `password` is still reachable.
|
|
41
41
|
*/
|
|
42
42
|
customOnly?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Return the value exactly as stored rather than trimmed. Only `bwx get --raw`
|
|
45
|
+
* asks for this; every other caller compares or parses the value, where
|
|
46
|
+
* surrounding whitespace is framing rather than content. Custom and card
|
|
47
|
+
* fields are read out of the item JSON and are verbatim either way — this
|
|
48
|
+
* reaches the fields `bw get <field>` prints on stdout.
|
|
49
|
+
*/
|
|
50
|
+
verbatim?: boolean;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -81,7 +89,7 @@ export async function readItemField(
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
return isBuiltinField(field)
|
|
84
|
-
? readBuiltinField(field, item, opts)
|
|
92
|
+
? readBuiltinField(field, item, opts, options?.verbatim)
|
|
85
93
|
: readCustomField(field, item, opts);
|
|
86
94
|
});
|
|
87
95
|
}
|
|
@@ -330,9 +338,10 @@ async function readBuiltinField(
|
|
|
330
338
|
field: string,
|
|
331
339
|
item: string,
|
|
332
340
|
opts: GlobalOptions,
|
|
341
|
+
verbatim = false,
|
|
333
342
|
): Promise<string> {
|
|
334
343
|
const result = await withSession(opts, () =>
|
|
335
|
-
runBwOrThrow(["get", field, item]),
|
|
344
|
+
runBwOrThrow(["get", field, item], { verbatim }),
|
|
336
345
|
);
|
|
337
346
|
|
|
338
347
|
if (!result) {
|
package/src/bw/runner.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface RunBwOptions {
|
|
|
11
11
|
/** Explicit session, or `null` to run without one (login/unlock/status). */
|
|
12
12
|
session?: string | null;
|
|
13
13
|
env?: Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Return stdout as `bw` wrote it, less the single newline `bw` adds to frame
|
|
16
|
+
* its own output. The default trim is right for values bwx parses or compares
|
|
17
|
+
* and wrong for a secret handed back byte-for-byte: whitespace around a
|
|
18
|
+
* password belongs to the password.
|
|
19
|
+
*/
|
|
20
|
+
verbatim?: boolean;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
/** Deadline for a single `bw` call; overridable with `--timeout`. */
|
|
@@ -66,7 +73,11 @@ export async function runBw(
|
|
|
66
73
|
);
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
stdout: options?.verbatim ? unframe(stdout) : stdout.trim(),
|
|
78
|
+
stderr: stderr.trim(),
|
|
79
|
+
exitCode,
|
|
80
|
+
};
|
|
70
81
|
} finally {
|
|
71
82
|
guard?.disarm();
|
|
72
83
|
}
|
|
@@ -87,6 +98,11 @@ export async function runBwOrThrow(
|
|
|
87
98
|
return result.stdout;
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
/** Drops the one newline `bw` prints after a value, and nothing the value owns. */
|
|
102
|
+
function unframe(stdout: string): string {
|
|
103
|
+
return stdout.replace(/\r?\n$/, "");
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
/**
|
|
91
107
|
* Child environment for `bw`. `BW_SESSION` is set from our session, or removed
|
|
92
108
|
* outright so an ambient one from the caller's shell cannot silently substitute
|
package/src/cli/commands/get.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { getGlobalOpts } from "../program.ts";
|
|
3
|
-
import { emitData, emitLog } from "../io.ts";
|
|
3
|
+
import { emitData, emitLog, emitRaw } from "../io.ts";
|
|
4
4
|
import { BUILTIN_FIELDS, readItemField, readTotp, readTotpSecret } from "../../bw/fields.ts";
|
|
5
5
|
import { CARD_FIELDS } from "../../bw/cards.ts";
|
|
6
6
|
import { CliError, ExitCode } from "../errors.ts";
|
|
@@ -9,6 +9,16 @@ import type { GlobalOptions } from "../types.ts";
|
|
|
9
9
|
/** Seconds left below which `--fresh` waits, when the flag is given bare. */
|
|
10
10
|
const DEFAULT_FRESH_SECONDS = 5;
|
|
11
11
|
|
|
12
|
+
/** Written once so `get` and `field` describe `--raw` in the same words. */
|
|
13
|
+
const RAW_DESCRIPTION =
|
|
14
|
+
"Suppress only the trailing newline bwx adds; newlines stored in the value are kept";
|
|
15
|
+
|
|
16
|
+
interface GetOptions {
|
|
17
|
+
fresh?: string | boolean;
|
|
18
|
+
seed?: boolean;
|
|
19
|
+
raw?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export function registerGet(program: Command): void {
|
|
13
23
|
program
|
|
14
24
|
.command("get")
|
|
@@ -23,13 +33,15 @@ export function registerGet(program: Command): void {
|
|
|
23
33
|
`Wait for the next TOTP window if fewer than N seconds remain (default ${DEFAULT_FRESH_SECONDS})`,
|
|
24
34
|
)
|
|
25
35
|
.option("--seed", "Emit the stored TOTP secret instead of a code")
|
|
36
|
+
.option("--raw", RAW_DESCRIPTION)
|
|
26
37
|
.action(async function (
|
|
27
38
|
this: Command,
|
|
28
39
|
field: string,
|
|
29
40
|
item: string,
|
|
30
|
-
localOpts:
|
|
41
|
+
localOpts: GetOptions,
|
|
31
42
|
) {
|
|
32
43
|
const opts = getGlobalOpts(this);
|
|
44
|
+
assertRawUsable(localOpts, opts);
|
|
33
45
|
|
|
34
46
|
if (field === "totp") {
|
|
35
47
|
await emitTotp(item, localOpts, opts);
|
|
@@ -38,9 +50,18 @@ export function registerGet(program: Command): void {
|
|
|
38
50
|
|
|
39
51
|
assertTotpOnlyFlags(field, localOpts);
|
|
40
52
|
|
|
41
|
-
const value = await readItemField(field, item, opts
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const value = await readItemField(field, item, opts, {
|
|
54
|
+
verbatim: localOpts.raw,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// `item` is the one field that is itself JSON, so it is emitted as
|
|
58
|
+
// structure — except under `--raw`, which promised the bytes `bw` gave us.
|
|
59
|
+
if (field === "item" && !localOpts.raw) {
|
|
60
|
+
emitData(JSON.parse(value), opts);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
emitValue(value, localOpts, opts);
|
|
44
65
|
});
|
|
45
66
|
|
|
46
67
|
program
|
|
@@ -50,19 +71,57 @@ export function registerGet(program: Command): void {
|
|
|
50
71
|
// alongside it — the two used to take their arguments back to front.
|
|
51
72
|
.argument("<name>", "Custom field name")
|
|
52
73
|
.argument("<item>", "Item name or ID")
|
|
53
|
-
.
|
|
74
|
+
.option("--raw", RAW_DESCRIPTION)
|
|
75
|
+
.action(async function (
|
|
76
|
+
this: Command,
|
|
77
|
+
name: string,
|
|
78
|
+
item: string,
|
|
79
|
+
localOpts: { raw?: boolean },
|
|
80
|
+
) {
|
|
54
81
|
const opts = getGlobalOpts(this);
|
|
82
|
+
assertRawUsable(localOpts, opts);
|
|
83
|
+
|
|
55
84
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
opts,
|
|
59
|
-
);
|
|
85
|
+
const value = await readItemField(name, item, opts, { customOnly: true });
|
|
86
|
+
emitValue(value, localOpts, opts);
|
|
60
87
|
} catch (err) {
|
|
61
88
|
throw withOrderHint(err);
|
|
62
89
|
}
|
|
63
90
|
});
|
|
64
91
|
}
|
|
65
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Emits one field value. `--raw` writes it alone, so piping a secret into a
|
|
95
|
+
* consumer that reads stdin literally does not append bwx's newline to it;
|
|
96
|
+
* `meta` describes the JSON envelope and so never applies on that path.
|
|
97
|
+
*/
|
|
98
|
+
function emitValue(
|
|
99
|
+
value: string,
|
|
100
|
+
localOpts: { raw?: boolean },
|
|
101
|
+
opts: GlobalOptions,
|
|
102
|
+
meta?: Record<string, unknown>,
|
|
103
|
+
): void {
|
|
104
|
+
if (localOpts.raw) {
|
|
105
|
+
emitRaw(value);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
emitData(value, opts, meta);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `--json` is an envelope — the value arrives quoted, escaped, and wrapped in
|
|
113
|
+
* `{ "data": … }` — so its trailing newline is the least of what `--raw` would
|
|
114
|
+
* have to undo. Saying so beats silently letting one win.
|
|
115
|
+
*/
|
|
116
|
+
function assertRawUsable(localOpts: { raw?: boolean }, opts: GlobalOptions): void {
|
|
117
|
+
if (localOpts.raw && opts.json) {
|
|
118
|
+
throw new CliError(
|
|
119
|
+
"--raw and --json cannot be combined: --json wraps the value in an envelope. Use one or the other.",
|
|
120
|
+
ExitCode.BadArgs,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
66
125
|
/**
|
|
67
126
|
* Emits a TOTP alongside how long it stays valid. The remainder rides in the
|
|
68
127
|
* `--json` envelope's `meta` rather than replacing `data`, so `data` is still
|
|
@@ -70,7 +129,7 @@ export function registerGet(program: Command): void {
|
|
|
70
129
|
*/
|
|
71
130
|
async function emitTotp(
|
|
72
131
|
item: string,
|
|
73
|
-
localOpts:
|
|
132
|
+
localOpts: GetOptions,
|
|
74
133
|
opts: GlobalOptions,
|
|
75
134
|
): Promise<void> {
|
|
76
135
|
if (localOpts.seed) {
|
|
@@ -80,7 +139,7 @@ async function emitTotp(
|
|
|
80
139
|
ExitCode.BadArgs,
|
|
81
140
|
);
|
|
82
141
|
}
|
|
83
|
-
|
|
142
|
+
emitValue(await readTotpSecret(item, opts), localOpts, opts);
|
|
84
143
|
return;
|
|
85
144
|
}
|
|
86
145
|
|
|
@@ -91,7 +150,7 @@ async function emitTotp(
|
|
|
91
150
|
});
|
|
92
151
|
|
|
93
152
|
emitLog(`Valid for ${totp.secondsRemaining}s`, opts);
|
|
94
|
-
|
|
153
|
+
emitValue(totp.code, localOpts, opts, {
|
|
95
154
|
secondsRemaining: totp.secondsRemaining,
|
|
96
155
|
period: totp.period,
|
|
97
156
|
});
|
|
@@ -119,10 +178,7 @@ function parseFreshSeconds(raw: string | boolean | undefined): number | undefine
|
|
|
119
178
|
* Accepting `--fresh` on a password would read as "give me a fresh password",
|
|
120
179
|
* which is not what it does. Rejecting it beats quietly ignoring it.
|
|
121
180
|
*/
|
|
122
|
-
function assertTotpOnlyFlags(
|
|
123
|
-
field: string,
|
|
124
|
-
localOpts: { fresh?: string | boolean; seed?: boolean },
|
|
125
|
-
): void {
|
|
181
|
+
function assertTotpOnlyFlags(field: string, localOpts: GetOptions): void {
|
|
126
182
|
const used = [
|
|
127
183
|
localOpts.fresh !== undefined ? "--fresh" : null,
|
|
128
184
|
localOpts.seed ? "--seed" : null,
|
package/src/cli/io.ts
CHANGED
|
@@ -34,6 +34,16 @@ export function emitData(
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Writes a value and nothing else — no newline, no formatting. {@link emitData}'s
|
|
39
|
+
* newline is a courtesy to terminals, but a consumer that reads stdin literally
|
|
40
|
+
* takes it as the last byte of the secret. Newlines *inside* the value are the
|
|
41
|
+
* value's own and are left alone.
|
|
42
|
+
*/
|
|
43
|
+
export function emitRaw(value: string): void {
|
|
44
|
+
process.stdout.write(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
export function emitLog(message: string, opts: GlobalOptions): void {
|
|
38
48
|
if (opts.quiet || opts.json) return;
|
|
39
49
|
process.stderr.write(message + "\n");
|