@tiny-fish/cli 0.1.8-next.70 → 0.1.8-next.71

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
@@ -150,6 +150,33 @@ tinyfish browser session create --url https://agentql.com
150
150
  tinyfish browser session create --pretty
151
151
  ```
152
152
 
153
+ ### Vault
154
+
155
+ Connect a credential provider (1Password / Bitwarden), then have runs consume those credentials.
156
+
157
+ Provider secrets are read from the environment, never passed as flags — the same model as `TINYFISH_API_KEY`. This keeps tokens out of shell history and process listings.
158
+
159
+ ```bash
160
+ # Connect 1Password (token from env)
161
+ TINYFISH_VAULT_TOKEN=<service-account-token> \
162
+ tinyfish vault connection add --provider 1password
163
+
164
+ # Connect Bitwarden (client secret + master password from env; client ID is a flag)
165
+ TINYFISH_VAULT_CLIENT_SECRET=<secret> TINYFISH_VAULT_MASTER_PASSWORD=<password> \
166
+ tinyfish vault connection add --provider bitwarden --client-id <client-id>
167
+
168
+ # List / disconnect connections
169
+ tinyfish vault connection list
170
+ tinyfish vault connection remove <connectionId>
171
+
172
+ # List credential items. If empty right after connecting, sync first.
173
+ tinyfish vault item sync
174
+ tinyfish vault item list
175
+ # → each item has an `itemId` (e.g. cred:conn-123:Personal:item-abc123).
176
+ ```
177
+
178
+ Credential items are sourced from the connected provider — the CLI has no freeform credential create/edit (mirrors the API).
179
+
153
180
  ### Output format
154
181
 
155
182
  By default all commands output newline-delimited JSON to stdout — pipe-friendly for agents and scripts. Add `--pretty` for human-readable output.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerVault(program: Command): void;
@@ -0,0 +1,177 @@
1
+ import { getApiKey } from "../lib/auth.js";
2
+ import { vaultConnect, vaultConnections, vaultDisconnect, vaultItems, vaultSync, } from "../lib/client.js";
3
+ import { err, handleApiError, out, outLine } from "../lib/output.js";
4
+ const VALID_PROVIDERS = ["1password", "bitwarden"];
5
+ // Provider secrets are read from the environment, never flags — same model as
6
+ // the API key (TINYFISH_API_KEY). This keeps tokens out of shell history and
7
+ // process listings, and needs no interactive prompt (agent-safe).
8
+ const TOKEN_ENV = "TINYFISH_VAULT_TOKEN";
9
+ const CLIENT_SECRET_ENV = "TINYFISH_VAULT_CLIENT_SECRET";
10
+ const MASTER_PASSWORD_ENV = "TINYFISH_VAULT_MASTER_PASSWORD";
11
+ function requireEnv(name, field, provider) {
12
+ const value = process.env[name];
13
+ if (!value) {
14
+ err({ error: `${provider} requires ${field} via $${name}` });
15
+ process.exit(1);
16
+ }
17
+ return value;
18
+ }
19
+ function buildConnectRequest(opts) {
20
+ if (!opts.provider || !VALID_PROVIDERS.includes(opts.provider)) {
21
+ err({ error: `--provider must be one of: ${VALID_PROVIDERS.join(", ")}` });
22
+ process.exit(1);
23
+ }
24
+ if (opts.provider === "1password") {
25
+ return { provider: "1password", token: requireEnv(TOKEN_ENV, "a service-account token", "1password") };
26
+ }
27
+ // bitwarden
28
+ if (!opts.clientId) {
29
+ err({ error: "bitwarden requires --client-id" });
30
+ process.exit(1);
31
+ }
32
+ return {
33
+ provider: "bitwarden",
34
+ clientId: opts.clientId,
35
+ clientSecret: requireEnv(CLIENT_SECRET_ENV, "a client secret", "bitwarden"),
36
+ masterPassword: requireEnv(MASTER_PASSWORD_ENV, "a master password", "bitwarden"),
37
+ ...(opts.serverUrl ? { serverUrl: opts.serverUrl } : {}),
38
+ };
39
+ }
40
+ // ── Pretty printers ───────────────────────────────────────────────────────────
41
+ function printConnections(connections) {
42
+ if (connections.length === 0) {
43
+ outLine("No vault connections.");
44
+ return;
45
+ }
46
+ for (const c of connections) {
47
+ outLine(`${c.id} ${c.provider} ${c.connectionStatus} (validated: ${c.lastValidatedAt ?? "never"})`);
48
+ }
49
+ }
50
+ function printItems(items) {
51
+ if (items.length === 0) {
52
+ outLine("No vault items. Connect a provider and run `tinyfish vault item sync`.");
53
+ return;
54
+ }
55
+ for (const item of items) {
56
+ const totp = item.hasTotp ? " [TOTP]" : "";
57
+ outLine(`${item.itemId} ${item.label} (${item.vaultName}) ${item.domains.join(", ")}${totp}`);
58
+ }
59
+ }
60
+ function printConnect(result) {
61
+ outLine("Vault connected");
62
+ outLine(`Connection ID: ${result.connectionId}`);
63
+ outLine(`Provider: ${result.provider}`);
64
+ outLine(`Items: ${result.items.length}`);
65
+ }
66
+ function printSync(result) {
67
+ const { added, updated, removed } = result.sync_summary;
68
+ outLine(`Synced ${result.items.length} items (added ${added}, updated ${updated}, removed ${removed})`);
69
+ }
70
+ // ── Command registration ──────────────────────────────────────────────────────
71
+ export function registerVault(program) {
72
+ const vaultCmd = program
73
+ .command("vault")
74
+ .description("Vault credential commands")
75
+ .enablePositionalOptions();
76
+ const connectionCmd = vaultCmd
77
+ .command("connection")
78
+ .description("Manage vault provider connections")
79
+ .enablePositionalOptions();
80
+ connectionCmd
81
+ .command("list")
82
+ .description("List connected vault providers")
83
+ .option("--pretty", "Human-readable output")
84
+ .action(async (opts) => {
85
+ const apiKey = getApiKey();
86
+ try {
87
+ const response = await vaultConnections(apiKey);
88
+ if (opts.pretty)
89
+ printConnections(response.connections);
90
+ else
91
+ out(response);
92
+ }
93
+ catch (error) {
94
+ handleApiError(error);
95
+ }
96
+ });
97
+ connectionCmd
98
+ .command("add")
99
+ .description(`Connect a vault provider. Secrets come from the environment, not flags: ` +
100
+ `1password reads $${TOKEN_ENV}; bitwarden reads $${CLIENT_SECRET_ENV} and $${MASTER_PASSWORD_ENV}.`)
101
+ .requiredOption("--provider <provider>", `Vault provider (${VALID_PROVIDERS.join("|")})`)
102
+ .option("--client-id <id>", "Bitwarden client ID (non-secret)")
103
+ .option("--server-url <url>", "Self-hosted Bitwarden server URL")
104
+ .option("--pretty", "Human-readable output")
105
+ .action(async (opts) => {
106
+ const apiKey = getApiKey();
107
+ const req = buildConnectRequest(opts);
108
+ try {
109
+ const response = await vaultConnect(req, apiKey);
110
+ if (opts.pretty)
111
+ printConnect(response);
112
+ else
113
+ out(response);
114
+ }
115
+ catch (error) {
116
+ handleApiError(error);
117
+ }
118
+ });
119
+ connectionCmd
120
+ .command("remove")
121
+ .description("Disconnect a vault provider and delete its stored credentials")
122
+ .argument("<connectionId>", "Connection ID to disconnect")
123
+ .option("--pretty", "Human-readable output")
124
+ .action(async (connectionId, opts) => {
125
+ const apiKey = getApiKey();
126
+ try {
127
+ const response = await vaultDisconnect(connectionId, apiKey);
128
+ if (opts.pretty)
129
+ outLine(`Disconnected ${response.connectionId}`);
130
+ else
131
+ out(response);
132
+ }
133
+ catch (error) {
134
+ handleApiError(error);
135
+ }
136
+ });
137
+ const itemCmd = vaultCmd
138
+ .command("item")
139
+ .description("Inspect vault credential items")
140
+ .enablePositionalOptions();
141
+ itemCmd
142
+ .command("list")
143
+ .description("List vault credential items across all connections. Each item's `itemId` " +
144
+ "is what you pass to `agent run --credential-item-ids`. If the list is empty " +
145
+ "after connecting, run `vault item sync` first.")
146
+ .option("--pretty", "Human-readable output")
147
+ .action(async (opts) => {
148
+ const apiKey = getApiKey();
149
+ try {
150
+ const response = await vaultItems(apiKey);
151
+ if (opts.pretty)
152
+ printItems(response.items);
153
+ else
154
+ out(response);
155
+ }
156
+ catch (error) {
157
+ handleApiError(error);
158
+ }
159
+ });
160
+ itemCmd
161
+ .command("sync")
162
+ .description("Re-sync credential items from connected providers")
163
+ .option("--pretty", "Human-readable output")
164
+ .action(async (opts) => {
165
+ const apiKey = getApiKey();
166
+ try {
167
+ const response = await vaultSync(apiKey);
168
+ if (opts.pretty)
169
+ printSync(response);
170
+ else
171
+ out(response);
172
+ }
173
+ catch (error) {
174
+ handleApiError(error);
175
+ }
176
+ });
177
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { registerBatch } from "./commands/batch.js";
6
6
  import { registerFetch } from "./commands/fetch.js";
7
7
  import { registerBrowser } from "./commands/browser.js";
8
8
  import { registerProfile } from "./commands/profile.js";
9
+ import { registerVault } from "./commands/vault.js";
9
10
  import { registerRun } from "./commands/run.js";
10
11
  import { registerRuns } from "./commands/runs.js";
11
12
  import { registerConfigureClaude } from "./commands/config-claude.js";
@@ -29,6 +30,7 @@ registerAuth(program);
29
30
  registerConfigureClaude(program);
30
31
  registerBrowser(program);
31
32
  registerProfile(program);
33
+ registerVault(program);
32
34
  const agentCmd = program
33
35
  .command("agent")
34
36
  .description("Agent automation commands")
@@ -1,5 +1,5 @@
1
1
  import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type FetchGetContentsParams, type FetchResponse, type BrowserSession, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse } from "@tiny-fish/sdk";
2
- import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse } from "./types.js";
2
+ import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from "./types.js";
3
3
  export declare function runSync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunResponse>;
4
4
  export declare function runAsync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
5
5
  export declare function runStream(req: CliAgentRunParams, apiKey: string, signal?: AbortSignal): AsyncGenerator<AgentRunWithStreamingResponse>;
@@ -15,3 +15,8 @@ export declare function getBatchRuns(runIds: string[], apiKey: string): Promise<
15
15
  export declare function cancelBatchRuns(runIds: string[], apiKey: string): Promise<BatchCancelResponse>;
16
16
  export declare function profileCreate(name: string, apiKey: string): Promise<ProfileCreateResponse>;
17
17
  export declare function profileUpload(profileId: string, cookies: CookieRecord[], apiKey: string): Promise<ProfileUploadResponse>;
18
+ export declare function vaultConnections(apiKey: string): Promise<VaultConnectionsListResponse>;
19
+ export declare function vaultConnect(req: VaultConnectRequest, apiKey: string): Promise<VaultConnectResponse>;
20
+ export declare function vaultDisconnect(connectionId: string, apiKey: string): Promise<VaultDisconnectResponse>;
21
+ export declare function vaultItems(apiKey: string): Promise<VaultItemsListResponse>;
22
+ export declare function vaultSync(apiKey: string): Promise<VaultItemsSyncResponse>;
@@ -11,6 +11,30 @@ class TinyFishCliClient extends TinyFish {
11
11
  "User-Agent": `${base["User-Agent"] ?? ""} @tiny-fish/cli/${CLI_VERSION}`.trim(),
12
12
  };
13
13
  }
14
+ // The SDK base client only exposes get/post/postStream; DELETE is needed for
15
+ // disconnecting a vault connection. ponytail: raw fetch, no retry/timeout —
16
+ // matches the CLI's maxRetries:0; reuse the SDK request helper if more DELETE
17
+ // routes appear.
18
+ async del(path) {
19
+ const response = await fetch(`${this.baseURL}${path}`, {
20
+ method: "DELETE",
21
+ headers: this._buildHeaders(),
22
+ });
23
+ if (!response.ok) {
24
+ let message = response.statusText;
25
+ try {
26
+ const body = (await response.json());
27
+ message = body?.error?.message ?? body?.message ?? message;
28
+ }
29
+ catch {
30
+ // Non-JSON body — keep statusText
31
+ }
32
+ throw new ApiError(response.status, message);
33
+ }
34
+ if (response.status === 204)
35
+ return undefined;
36
+ return (await response.json());
37
+ }
14
38
  }
15
39
  function createSdkClient(apiKey, timeout) {
16
40
  return new TinyFishCliClient({
@@ -307,3 +331,99 @@ export async function profileUpload(profileId, cookies, apiKey) {
307
331
  rethrowSdkError(error);
308
332
  }
309
333
  }
334
+ // ── Vault ───────────────────────────────────────────────────────────────────
335
+ // /v1/vault/* returns camelCase fields (unlike the snake_case rest of the API).
336
+ const vaultProviderSchema = z.enum(["1password", "bitwarden"]);
337
+ const vaultFieldMetadataSchema = z.object({
338
+ fieldId: z.string(),
339
+ label: z.string(),
340
+ type: z.enum(["STRING", "CONCEALED", "OTP"]),
341
+ });
342
+ const vaultItemSchema = z.object({
343
+ itemId: z.string(),
344
+ connectionId: z.string().nullable(),
345
+ label: z.string(),
346
+ vaultName: z.string(),
347
+ domains: z.array(z.string()),
348
+ fieldMetadata: z.array(vaultFieldMetadataSchema),
349
+ hasTotp: z.boolean(),
350
+ });
351
+ const vaultConnectionSchema = z.object({
352
+ id: z.string(),
353
+ provider: vaultProviderSchema,
354
+ connectionStatus: z.string(),
355
+ lastValidatedAt: z.string().nullable(),
356
+ });
357
+ const vaultConnectionsListResponseSchema = z.object({
358
+ connections: z.array(vaultConnectionSchema),
359
+ });
360
+ const vaultConnectResponseSchema = z.object({
361
+ connectionId: z.string(),
362
+ connected: z.literal(true),
363
+ provider: vaultProviderSchema,
364
+ items: z.array(z.unknown()), // server returns snake_case items; CLI only uses .length
365
+ });
366
+ const vaultDisconnectResponseSchema = z.object({
367
+ disconnected: z.literal(true),
368
+ connectionId: z.string(),
369
+ });
370
+ const vaultItemsListResponseSchema = z.object({
371
+ items: z.array(vaultItemSchema),
372
+ });
373
+ const vaultItemsSyncResponseSchema = z.object({
374
+ items: z.array(z.unknown()), // server returns snake_case items; CLI only uses sync_summary
375
+ sync_summary: z.object({
376
+ added: z.number(),
377
+ updated: z.number(),
378
+ removed: z.number(),
379
+ }),
380
+ });
381
+ export async function vaultConnections(apiKey) {
382
+ try {
383
+ const response = await createSdkClient(apiKey).get("/v1/vault/connections");
384
+ return parseWithSchema(vaultConnectionsListResponseSchema, response, "Invalid vault connections response");
385
+ }
386
+ catch (error) {
387
+ rethrowSdkError(error);
388
+ }
389
+ }
390
+ export async function vaultConnect(req, apiKey) {
391
+ try {
392
+ const response = await createSdkClient(apiKey).post("/v1/vault/connections", {
393
+ json: req,
394
+ });
395
+ return parseWithSchema(vaultConnectResponseSchema, response, "Invalid vault connect response");
396
+ }
397
+ catch (error) {
398
+ rethrowSdkError(error);
399
+ }
400
+ }
401
+ export async function vaultDisconnect(connectionId, apiKey) {
402
+ try {
403
+ const response = await createSdkClient(apiKey).del(`/v1/vault/connections/${encodeURIComponent(connectionId)}`);
404
+ return parseWithSchema(vaultDisconnectResponseSchema, response, "Invalid vault disconnect response");
405
+ }
406
+ catch (error) {
407
+ rethrowSdkError(error);
408
+ }
409
+ }
410
+ export async function vaultItems(apiKey) {
411
+ try {
412
+ const response = await createSdkClient(apiKey).get("/v1/vault/items");
413
+ return parseWithSchema(vaultItemsListResponseSchema, response, "Invalid vault items response");
414
+ }
415
+ catch (error) {
416
+ rethrowSdkError(error);
417
+ }
418
+ }
419
+ export async function vaultSync(apiKey) {
420
+ try {
421
+ const response = await createSdkClient(apiKey).post("/v1/vault/items/sync", {
422
+ json: {},
423
+ });
424
+ return parseWithSchema(vaultItemsSyncResponseSchema, response, "Invalid vault sync response");
425
+ }
426
+ catch (error) {
427
+ rethrowSdkError(error);
428
+ }
429
+ }
@@ -9,6 +9,61 @@ export interface CliAgentRunParams extends AgentRunParams {
9
9
  browser_profile?: BrowserProfile;
10
10
  session_id?: string;
11
11
  }
12
+ export type VaultProvider = "1password" | "bitwarden";
13
+ export interface VaultConnection {
14
+ id: string;
15
+ provider: VaultProvider;
16
+ connectionStatus: string;
17
+ lastValidatedAt: string | null;
18
+ }
19
+ export interface VaultFieldMetadata {
20
+ fieldId: string;
21
+ label: string;
22
+ type: "STRING" | "CONCEALED" | "OTP";
23
+ }
24
+ export interface VaultItem {
25
+ itemId: string;
26
+ connectionId: string | null;
27
+ label: string;
28
+ vaultName: string;
29
+ domains: string[];
30
+ fieldMetadata: VaultFieldMetadata[];
31
+ hasTotp: boolean;
32
+ }
33
+ export type VaultConnectRequest = {
34
+ provider: "1password";
35
+ token: string;
36
+ } | {
37
+ provider: "bitwarden";
38
+ clientId: string;
39
+ clientSecret: string;
40
+ masterPassword: string;
41
+ serverUrl?: string;
42
+ };
43
+ export interface VaultConnectionsListResponse {
44
+ connections: VaultConnection[];
45
+ }
46
+ export interface VaultConnectResponse {
47
+ connectionId: string;
48
+ connected: true;
49
+ provider: VaultProvider;
50
+ items: unknown[];
51
+ }
52
+ export interface VaultDisconnectResponse {
53
+ disconnected: true;
54
+ connectionId: string;
55
+ }
56
+ export interface VaultItemsListResponse {
57
+ items: VaultItem[];
58
+ }
59
+ export interface VaultItemsSyncResponse {
60
+ items: unknown[];
61
+ sync_summary: {
62
+ added: number;
63
+ updated: number;
64
+ removed: number;
65
+ };
66
+ }
12
67
  export interface RunStep {
13
68
  id: string;
14
69
  timestamp: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.1.8-next.70",
3
+ "version": "0.1.8-next.71",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {