@tiny-fish/cli 0.1.8 → 0.2.0-next.77
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 +35 -0
- package/dist/commands/fetch.js +24 -1
- package/dist/commands/profile.d.ts +12 -0
- package/dist/commands/profile.js +181 -27
- package/dist/commands/run.js +10 -0
- package/dist/commands/vault.d.ts +2 -0
- package/dist/commands/vault.js +177 -0
- package/dist/index.js +2 -0
- package/dist/lib/client.d.ts +6 -1
- package/dist/lib/client.js +124 -2
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/types.d.ts +57 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -150,6 +150,41 @@ 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) — that's
|
|
176
|
+
# what --credential-item-id takes below.
|
|
177
|
+
|
|
178
|
+
# Consume vault credentials in a run (uses all enabled items)
|
|
179
|
+
tinyfish agent run "log in and export the invoices" --url https://example.com --use-vault
|
|
180
|
+
|
|
181
|
+
# ...or scope to specific items by their itemId from `vault item list`
|
|
182
|
+
tinyfish agent run "..." --url https://example.com --use-vault \
|
|
183
|
+
--credential-item-id cred:conn-123:Personal:item-abc123
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`--credential-item-id` requires `--use-vault`. Omit it to use all enabled items. The IDs are the `itemId` values from `tinyfish vault item list` (run `vault item sync` first if the list is empty after connecting). Credential items are sourced from the connected provider — the CLI has no freeform credential create/edit (mirrors the API).
|
|
187
|
+
|
|
153
188
|
### Output format
|
|
154
189
|
|
|
155
190
|
By default all commands output newline-delimited JSON to stdout — pipe-friendly for agents and scripts. Add `--pretty` for human-readable output.
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InvalidArgumentError } from "commander";
|
|
2
2
|
import { getApiKey } from "../lib/auth.js";
|
|
3
3
|
import { fetchContentGet } from "../lib/client.js";
|
|
4
|
-
import { handleApiError, out, outLine } from "../lib/output.js";
|
|
4
|
+
import { err, handleApiError, out, outLine } from "../lib/output.js";
|
|
5
5
|
const MIN_PER_URL_TIMEOUT_MS = 1;
|
|
6
6
|
const MAX_PER_URL_TIMEOUT_MS = 110000;
|
|
7
7
|
function parsePerUrlTimeoutMs(value) {
|
|
@@ -26,6 +26,12 @@ function printPrettyFetch(response) {
|
|
|
26
26
|
outLine(` Title: ${result.title}`);
|
|
27
27
|
if (result.final_url && result.final_url !== result.url)
|
|
28
28
|
outLine(` Final URL: ${result.final_url}`);
|
|
29
|
+
if (result.not_modified)
|
|
30
|
+
outLine(` Not Modified: true`);
|
|
31
|
+
if (result.etag)
|
|
32
|
+
outLine(` ETag: ${result.etag}`);
|
|
33
|
+
if (result.last_modified)
|
|
34
|
+
outLine(` Last-Modified: ${result.last_modified}`);
|
|
29
35
|
outLine("");
|
|
30
36
|
}
|
|
31
37
|
}
|
|
@@ -55,8 +61,18 @@ export function registerFetch(program) {
|
|
|
55
61
|
.option("--links", "Include extracted links")
|
|
56
62
|
.option("--image-links", "Include extracted image links")
|
|
57
63
|
.option("--per-url-timeout-ms <milliseconds>", "Per-URL timeout budget in milliseconds", parsePerUrlTimeoutMs)
|
|
64
|
+
.option("--if-none-match <etag>", "ETag validator for a conditional GET (single URL only)")
|
|
65
|
+
.option("--if-modified-since <http-date>", "Last-Modified validator for a conditional GET (single URL only)")
|
|
66
|
+
.option("--include-etag-and-last-modified", "Include etag / last_modified validators on each result")
|
|
58
67
|
.option("--pretty", "Human-readable output")
|
|
59
68
|
.action(async (urls, opts) => {
|
|
69
|
+
if ((opts.ifNoneMatch !== undefined || opts.ifModifiedSince !== undefined) &&
|
|
70
|
+
urls.length > 1) {
|
|
71
|
+
err({
|
|
72
|
+
error: "--if-none-match and --if-modified-since can only be used with a single URL, not a batch",
|
|
73
|
+
});
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
60
76
|
const apiKey = getApiKey();
|
|
61
77
|
try {
|
|
62
78
|
const response = await fetchContentGet({
|
|
@@ -67,6 +83,13 @@ export function registerFetch(program) {
|
|
|
67
83
|
...(opts.perUrlTimeoutMs !== undefined
|
|
68
84
|
? { per_url_timeout_ms: opts.perUrlTimeoutMs }
|
|
69
85
|
: {}),
|
|
86
|
+
...(opts.ifNoneMatch !== undefined ? { if_none_match: opts.ifNoneMatch } : {}),
|
|
87
|
+
...(opts.ifModifiedSince !== undefined
|
|
88
|
+
? { if_modified_since: opts.ifModifiedSince }
|
|
89
|
+
: {}),
|
|
90
|
+
...(opts.includeEtagAndLastModified !== undefined
|
|
91
|
+
? { include_etag_and_last_modified: opts.includeEtagAndLastModified }
|
|
92
|
+
: {}),
|
|
70
93
|
}, apiKey);
|
|
71
94
|
if (opts.pretty) {
|
|
72
95
|
printPrettyFetch(response);
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import type { CookieRecord } from "../lib/types.js";
|
|
3
|
+
export declare function extractChromeCookies(domain: string): Promise<CookieRecord[]>;
|
|
4
|
+
export declare function extractAllChromeCookies(): Promise<CookieRecord[]>;
|
|
5
|
+
type TldtsParse = (host: string, opts: {
|
|
6
|
+
allowPrivateDomains: boolean;
|
|
7
|
+
}) => {
|
|
8
|
+
domain?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function groupByCanonicalDomain(cookies: CookieRecord[], parse: TldtsParse): Map<string, CookieRecord[]>;
|
|
11
|
+
export declare const MAX_COOKIES_PER_UPLOAD = 3000;
|
|
12
|
+
export declare function buildChunks(groups: Map<string, CookieRecord[]>): CookieRecord[][];
|
|
2
13
|
export declare function registerProfile(program: Command): void;
|
|
14
|
+
export {};
|
package/dist/commands/profile.js
CHANGED
|
@@ -23,14 +23,11 @@ function getChromeCookiesPath() {
|
|
|
23
23
|
if (platform === "linux") {
|
|
24
24
|
return path.join(os.homedir(), ".config", "google-chrome", "Default", "Cookies");
|
|
25
25
|
}
|
|
26
|
-
// Windows guard — handled in
|
|
26
|
+
// Windows guard — handled in withCookiesDb
|
|
27
27
|
return "";
|
|
28
28
|
}
|
|
29
29
|
function validateDomain(domain) {
|
|
30
|
-
if (domain.includes("/") ||
|
|
31
|
-
domain.includes("://") ||
|
|
32
|
-
/\s/.test(domain) ||
|
|
33
|
-
/:\d/.test(domain)) {
|
|
30
|
+
if (domain.includes("/") || domain.includes("://") || /\s/.test(domain) || /:\d/.test(domain)) {
|
|
34
31
|
err({ error: "domain must be a plain hostname (e.g. github.com)" });
|
|
35
32
|
process.exit(1);
|
|
36
33
|
}
|
|
@@ -40,7 +37,8 @@ const CHROME_EPOCH_DELTA_SECONDS = 11644473600;
|
|
|
40
37
|
function chromeTimeToUnix(chromeMicros) {
|
|
41
38
|
return Math.floor(chromeMicros / 1e6) - CHROME_EPOCH_DELTA_SECONDS;
|
|
42
39
|
}
|
|
43
|
-
|
|
40
|
+
// Sets up chrome-cookies-secure in a temp dir and calls fn; cleans up after.
|
|
41
|
+
async function withCookiesDb(fn) {
|
|
44
42
|
if (process.platform === "win32") {
|
|
45
43
|
throw new UserFacingError("Cookie extraction is not supported on Windows yet");
|
|
46
44
|
}
|
|
@@ -56,20 +54,7 @@ async function extractChromeCookies(domain) {
|
|
|
56
54
|
// chrome-cookies-secure is a CommonJS module — use createRequire to load it
|
|
57
55
|
const require = createRequire(import.meta.url);
|
|
58
56
|
const chromeCookies = require("chrome-cookies-secure");
|
|
59
|
-
|
|
60
|
-
// Chrome-internal microsecond timestamps — convert to our CookieRecord shape.
|
|
61
|
-
const raw = await chromeCookies.getCookiesPromised(`https://${domain}`, "puppeteer", tmpDir);
|
|
62
|
-
return raw.map((c) => ({
|
|
63
|
-
name: c.name,
|
|
64
|
-
value: c.value,
|
|
65
|
-
domain: c.domain,
|
|
66
|
-
path: c.path,
|
|
67
|
-
secure: c.Secure ?? false,
|
|
68
|
-
httpOnly: c.HttpOnly ?? false,
|
|
69
|
-
...(c.expires && c.expires > 0
|
|
70
|
-
? { expires: chromeTimeToUnix(c.expires) }
|
|
71
|
-
: {}),
|
|
72
|
-
}));
|
|
57
|
+
return await fn(tmpDir, chromeCookies);
|
|
73
58
|
}
|
|
74
59
|
finally {
|
|
75
60
|
try {
|
|
@@ -80,6 +65,132 @@ async function extractChromeCookies(domain) {
|
|
|
80
65
|
}
|
|
81
66
|
}
|
|
82
67
|
}
|
|
68
|
+
// Extract cookies for a single domain using an already-open cookies DB.
|
|
69
|
+
async function extractDomain(tmpDir, chromeCookies, domain) {
|
|
70
|
+
// "puppeteer" format returns an array with capitalized boolean fields and
|
|
71
|
+
// Chrome-internal microsecond timestamps — convert to our CookieRecord shape.
|
|
72
|
+
const raw = await chromeCookies.getCookiesPromised(`https://${domain}`, "puppeteer", tmpDir);
|
|
73
|
+
return raw.map((c) => ({
|
|
74
|
+
name: c.name,
|
|
75
|
+
value: c.value,
|
|
76
|
+
domain: c.domain,
|
|
77
|
+
path: c.path,
|
|
78
|
+
secure: c.Secure ?? false,
|
|
79
|
+
httpOnly: c.HttpOnly ?? false,
|
|
80
|
+
...(c.expires && c.expires > 0 ? { expires: chromeTimeToUnix(c.expires) } : {}),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
// Exported for testing
|
|
84
|
+
export async function extractChromeCookies(domain) {
|
|
85
|
+
return withCookiesDb((tmpDir, chromeCookies) => extractDomain(tmpDir, chromeCookies, domain));
|
|
86
|
+
}
|
|
87
|
+
// Exported for testing
|
|
88
|
+
export async function extractAllChromeCookies() {
|
|
89
|
+
return withCookiesDb(async (tmpDir, chromeCookies) => {
|
|
90
|
+
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
91
|
+
let sqliteDb = null;
|
|
92
|
+
let hostKeys;
|
|
93
|
+
try {
|
|
94
|
+
sqliteDb = new DatabaseSync(path.join(tmpDir, "Cookies")); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
95
|
+
// Only import domains with auth-relevant cookies — skips tracking/analytics junk.
|
|
96
|
+
// A domain qualifies if it has any HttpOnly cookie (session/auth indicator) or
|
|
97
|
+
// a cookie whose name suggests authentication.
|
|
98
|
+
const rows = sqliteDb
|
|
99
|
+
.prepare(`SELECT DISTINCT host_key FROM cookies
|
|
100
|
+
WHERE is_httponly = 1
|
|
101
|
+
OR name LIKE '%session%'
|
|
102
|
+
OR name LIKE '%token%'
|
|
103
|
+
OR name LIKE '%auth%'
|
|
104
|
+
OR name LIKE '%sid%'
|
|
105
|
+
OR name LIKE '%login%'
|
|
106
|
+
OR name LIKE '%user%'
|
|
107
|
+
OR name LIKE '%account%'`)
|
|
108
|
+
.all();
|
|
109
|
+
hostKeys = rows.map((r) => r.host_key);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
throw new UserFacingError(`Failed to read Chrome cookies database: ${e instanceof Error ? e.message : String(e)}`);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
try {
|
|
116
|
+
sqliteDb?.close();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 2. Canonicalize host_keys → registrable domains (mirrors frontend/app/lib/services/domain-utils.ts)
|
|
123
|
+
const require = createRequire(import.meta.url);
|
|
124
|
+
const { parse } = require("tldts");
|
|
125
|
+
const registrableDomains = new Set();
|
|
126
|
+
for (const hk of hostKeys) {
|
|
127
|
+
const cleaned = hk.startsWith(".") ? hk.slice(1) : hk;
|
|
128
|
+
const result = parse(cleaned, { allowPrivateDomains: true });
|
|
129
|
+
if (result.domain)
|
|
130
|
+
registrableDomains.add(result.domain.toLowerCase());
|
|
131
|
+
}
|
|
132
|
+
errLine(`Scanning ${registrableDomains.size} domains from Chrome...`);
|
|
133
|
+
// 3. Extract per-domain and concat
|
|
134
|
+
const all = [];
|
|
135
|
+
for (const domain of registrableDomains) {
|
|
136
|
+
try {
|
|
137
|
+
const cookies = await extractDomain(tmpDir, chromeCookies, domain);
|
|
138
|
+
all.push(...cookies);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// skip domains chrome-cookies-secure can't parse (IPs, .local, etc.)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 4. Dedup by name+domain+path
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
return all.filter((c) => {
|
|
147
|
+
const key = `${c.name}\t${c.domain}\t${c.path}`;
|
|
148
|
+
if (seen.has(key))
|
|
149
|
+
return false;
|
|
150
|
+
seen.add(key);
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Exported for testing
|
|
156
|
+
export function groupByCanonicalDomain(cookies, parse) {
|
|
157
|
+
const groups = new Map();
|
|
158
|
+
for (const c of cookies) {
|
|
159
|
+
const cleaned = c.domain.startsWith(".") ? c.domain.slice(1) : c.domain;
|
|
160
|
+
const cd = parse(cleaned, { allowPrivateDomains: true }).domain?.toLowerCase() ?? cleaned;
|
|
161
|
+
const bucket = groups.get(cd) ?? [];
|
|
162
|
+
bucket.push(c);
|
|
163
|
+
groups.set(cd, bucket);
|
|
164
|
+
}
|
|
165
|
+
return groups;
|
|
166
|
+
}
|
|
167
|
+
// Exported for testing
|
|
168
|
+
export const MAX_COOKIES_PER_UPLOAD = 3000;
|
|
169
|
+
// Exported for testing
|
|
170
|
+
export function buildChunks(groups) {
|
|
171
|
+
const chunks = [];
|
|
172
|
+
let current = [];
|
|
173
|
+
for (const group of groups.values()) {
|
|
174
|
+
// A group that alone exceeds the cap is its own chunk (never split a canonical domain)
|
|
175
|
+
if (group.length > MAX_COOKIES_PER_UPLOAD) {
|
|
176
|
+
if (current.length > 0) {
|
|
177
|
+
chunks.push(current);
|
|
178
|
+
current = [];
|
|
179
|
+
}
|
|
180
|
+
chunks.push(group);
|
|
181
|
+
}
|
|
182
|
+
else if (current.length + group.length > MAX_COOKIES_PER_UPLOAD) {
|
|
183
|
+
chunks.push(current);
|
|
184
|
+
current = [...group];
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
current.push(...group);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (current.length > 0)
|
|
191
|
+
chunks.push(current);
|
|
192
|
+
return chunks;
|
|
193
|
+
}
|
|
83
194
|
// ── Pretty printers ───────────────────────────────────────────────────────────
|
|
84
195
|
function printPrettyProfileCreate(profile) {
|
|
85
196
|
outLine("Profile created");
|
|
@@ -120,26 +231,69 @@ export function registerProfile(program) {
|
|
|
120
231
|
});
|
|
121
232
|
profileCmd
|
|
122
233
|
.command("import-cookies")
|
|
123
|
-
.description("Import Chrome cookies
|
|
124
|
-
.
|
|
234
|
+
.description("Import Chrome cookies into a profile. Use --domain for a single domain or --all-domains for every domain in your Chrome profile. Exactly one must be provided.")
|
|
235
|
+
.option("--domain <domain>", "Domain to import cookies for (e.g. github.com)")
|
|
236
|
+
.option("--all-domains", "Import cookies for all registrable domains (e.g. github.com) found in your Chrome profile. IPs and non-resolvable hostnames (e.g. localhost) are skipped.")
|
|
125
237
|
.option("--profile-id <id>", "Profile ID to import cookies into (creates new profile if omitted)")
|
|
126
238
|
.option("--pretty", "Human-readable output")
|
|
127
239
|
.action(async (opts) => {
|
|
240
|
+
// Mutual-exclusivity guard — must provide exactly one of --domain / --all-domains
|
|
241
|
+
if (!opts.domain && !opts.allDomains) {
|
|
242
|
+
err({ error: "Provide exactly one of --domain or --all-domains" });
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
if (opts.domain && opts.allDomains) {
|
|
246
|
+
err({ error: "Provide exactly one of --domain or --all-domains" });
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
128
249
|
const apiKey = getApiKey();
|
|
129
|
-
|
|
250
|
+
if (opts.domain) {
|
|
251
|
+
validateDomain(opts.domain);
|
|
252
|
+
}
|
|
130
253
|
let profileId = opts.profileId;
|
|
131
254
|
try {
|
|
132
255
|
// Extract cookies before creating profile to avoid orphan profiles
|
|
133
256
|
// if extraction fails (Windows, no Chrome, no cookies).
|
|
134
|
-
const cookies =
|
|
257
|
+
const cookies = opts.allDomains
|
|
258
|
+
? await extractAllChromeCookies()
|
|
259
|
+
: await extractChromeCookies(opts.domain);
|
|
135
260
|
if (cookies.length === 0) {
|
|
136
|
-
throw new UserFacingError(
|
|
261
|
+
throw new UserFacingError(opts.allDomains
|
|
262
|
+
? "No Chrome cookies found. Is Google Chrome installed and have you logged in to any sites?"
|
|
263
|
+
: `No Chrome cookies found for ${opts.domain}. Are you logged in to this site in Chrome?`);
|
|
137
264
|
}
|
|
138
265
|
if (profileId === undefined) {
|
|
139
|
-
const
|
|
266
|
+
const name = opts.allDomains ? "chrome-import" : opts.domain;
|
|
267
|
+
const created = await profileCreate(name, apiKey);
|
|
140
268
|
profileId = created.id;
|
|
141
269
|
}
|
|
142
|
-
|
|
270
|
+
let result;
|
|
271
|
+
if (opts.allDomains) {
|
|
272
|
+
// Batch upload — never split a canonical domain across chunks
|
|
273
|
+
const require = createRequire(import.meta.url);
|
|
274
|
+
const { parse } = require("tldts");
|
|
275
|
+
const groups = groupByCanonicalDomain(cookies, parse);
|
|
276
|
+
const chunks = buildChunks(groups);
|
|
277
|
+
let cookieCount = 0;
|
|
278
|
+
const domainsUpdated = [];
|
|
279
|
+
const domainsFailed = [];
|
|
280
|
+
for (const chunk of chunks) {
|
|
281
|
+
const r = await profileUpload(profileId, chunk, apiKey);
|
|
282
|
+
cookieCount += r.cookie_count;
|
|
283
|
+
domainsUpdated.push(...r.domains_updated);
|
|
284
|
+
if (r.domains_failed)
|
|
285
|
+
domainsFailed.push(...r.domains_failed);
|
|
286
|
+
}
|
|
287
|
+
result = {
|
|
288
|
+
profile_id: profileId,
|
|
289
|
+
cookie_count: cookieCount,
|
|
290
|
+
domains_updated: domainsUpdated,
|
|
291
|
+
...(domainsFailed.length > 0 ? { domains_failed: domainsFailed } : {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
result = await profileUpload(profileId, cookies, apiKey);
|
|
296
|
+
}
|
|
143
297
|
if (result.domains_failed && result.domains_failed.length > 0) {
|
|
144
298
|
errLine(`Warning: failed to update cookies for domains: ${result.domains_failed.join(", ")}`);
|
|
145
299
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -116,6 +116,8 @@ export function registerRun(agentCmd) {
|
|
|
116
116
|
.option("--max-steps <n>", `Maximum tool-call steps before stopping (1-${MAX_STEPS_LIMIT})`)
|
|
117
117
|
.option("--mode <mode>", "Agent behavior mode (default|strict)")
|
|
118
118
|
.option("--session-id <uuid>", "Caller-provided UUID for idempotency / parallel-safe --sync")
|
|
119
|
+
.option("--use-vault", "Consume vault credentials in this run")
|
|
120
|
+
.option("--credential-item-id <id>", "Scope vault to a specific credential item ID (repeat for multiple; get IDs from `vault item list`; requires --use-vault)", (v, acc) => [...acc, v], [])
|
|
119
121
|
.option("--pretty", "Human-readable output")
|
|
120
122
|
.argument("[goal]", "Goal to automate")
|
|
121
123
|
.addHelpText("after", "\nToken scope note:\n CLI runs use your personal API key. MCP runs use the MCP token.\n These are different namespaces; getting an MCP run ID with the CLI returns 404 by design.\n")
|
|
@@ -153,6 +155,12 @@ export function registerRun(agentCmd) {
|
|
|
153
155
|
err({ error: 'Goal cannot be empty' });
|
|
154
156
|
process.exit(1);
|
|
155
157
|
}
|
|
158
|
+
// The server rejects credential_item_ids without use_vault (400); fail fast here.
|
|
159
|
+
const hasCredentialItemIds = opts.credentialItemId.length > 0;
|
|
160
|
+
if (hasCredentialItemIds && !opts.useVault) {
|
|
161
|
+
err({ error: "--credential-item-id requires --use-vault" });
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
156
164
|
const outputSchema = await loadOutputSchema(opts);
|
|
157
165
|
const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, "--browser-profile");
|
|
158
166
|
const mode = parseChoice(opts.mode, VALID_MODES, "--mode");
|
|
@@ -166,6 +174,8 @@ export function registerRun(agentCmd) {
|
|
|
166
174
|
...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
|
|
167
175
|
...(mode !== undefined || maxSteps !== undefined ? { agent_config: { mode, max_steps: maxSteps } } : {}),
|
|
168
176
|
...(sessionId !== undefined ? { session_id: sessionId } : {}),
|
|
177
|
+
...(opts.useVault ? { use_vault: true } : {}),
|
|
178
|
+
...(hasCredentialItemIds ? { credential_item_ids: opts.credentialItemId } : {}),
|
|
169
179
|
};
|
|
170
180
|
if (opts.async) {
|
|
171
181
|
let result;
|
|
@@ -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-id`. 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")
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/client.js
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from "@tiny-fish/sdk";
|
|
2
|
-
import { BASE_URL } from "./constants.js";
|
|
2
|
+
import { BASE_URL, CLI_VERSION } from "./constants.js";
|
|
3
3
|
import { ApiError } from "./output.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
class TinyFishCliClient extends TinyFish {
|
|
6
6
|
_buildHeaders() {
|
|
7
|
+
const base = super._buildHeaders();
|
|
7
8
|
return {
|
|
8
|
-
...
|
|
9
|
+
...base,
|
|
9
10
|
"X-TF-Request-Origin": "tinyfish-cli",
|
|
11
|
+
"User-Agent": `${base["User-Agent"] ?? ""} @tiny-fish/cli/${CLI_VERSION}`.trim(),
|
|
10
12
|
};
|
|
11
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
|
+
}
|
|
12
38
|
}
|
|
13
39
|
function createSdkClient(apiKey, timeout) {
|
|
14
40
|
return new TinyFishCliClient({
|
|
@@ -305,3 +331,99 @@ export async function profileUpload(profileId, cookies, apiKey) {
|
|
|
305
331
|
rethrowSdkError(error);
|
|
306
332
|
}
|
|
307
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
|
+
}
|
package/dist/lib/constants.d.ts
CHANGED
package/dist/lib/constants.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
const _require = createRequire(import.meta.url);
|
|
3
|
+
const _pkg = _require("../../package.json");
|
|
4
|
+
export const CLI_VERSION = _pkg.version;
|
|
1
5
|
/** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted (must be https). */
|
|
2
6
|
const apiUrlOverride = process.env["TINYFISH_API_URL"]?.trim();
|
|
3
7
|
if (apiUrlOverride) {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -8,6 +8,63 @@ export interface CliAgentRunParams extends AgentRunParams {
|
|
|
8
8
|
};
|
|
9
9
|
browser_profile?: BrowserProfile;
|
|
10
10
|
session_id?: string;
|
|
11
|
+
use_vault?: boolean;
|
|
12
|
+
credential_item_ids?: string[];
|
|
13
|
+
}
|
|
14
|
+
export type VaultProvider = "1password" | "bitwarden";
|
|
15
|
+
export interface VaultConnection {
|
|
16
|
+
id: string;
|
|
17
|
+
provider: VaultProvider;
|
|
18
|
+
connectionStatus: string;
|
|
19
|
+
lastValidatedAt: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface VaultFieldMetadata {
|
|
22
|
+
fieldId: string;
|
|
23
|
+
label: string;
|
|
24
|
+
type: "STRING" | "CONCEALED" | "OTP";
|
|
25
|
+
}
|
|
26
|
+
export interface VaultItem {
|
|
27
|
+
itemId: string;
|
|
28
|
+
connectionId: string | null;
|
|
29
|
+
label: string;
|
|
30
|
+
vaultName: string;
|
|
31
|
+
domains: string[];
|
|
32
|
+
fieldMetadata: VaultFieldMetadata[];
|
|
33
|
+
hasTotp: boolean;
|
|
34
|
+
}
|
|
35
|
+
export type VaultConnectRequest = {
|
|
36
|
+
provider: "1password";
|
|
37
|
+
token: string;
|
|
38
|
+
} | {
|
|
39
|
+
provider: "bitwarden";
|
|
40
|
+
clientId: string;
|
|
41
|
+
clientSecret: string;
|
|
42
|
+
masterPassword: string;
|
|
43
|
+
serverUrl?: string;
|
|
44
|
+
};
|
|
45
|
+
export interface VaultConnectionsListResponse {
|
|
46
|
+
connections: VaultConnection[];
|
|
47
|
+
}
|
|
48
|
+
export interface VaultConnectResponse {
|
|
49
|
+
connectionId: string;
|
|
50
|
+
connected: true;
|
|
51
|
+
provider: VaultProvider;
|
|
52
|
+
items: unknown[];
|
|
53
|
+
}
|
|
54
|
+
export interface VaultDisconnectResponse {
|
|
55
|
+
disconnected: true;
|
|
56
|
+
connectionId: string;
|
|
57
|
+
}
|
|
58
|
+
export interface VaultItemsListResponse {
|
|
59
|
+
items: VaultItem[];
|
|
60
|
+
}
|
|
61
|
+
export interface VaultItemsSyncResponse {
|
|
62
|
+
items: unknown[];
|
|
63
|
+
sync_summary: {
|
|
64
|
+
added: number;
|
|
65
|
+
updated: number;
|
|
66
|
+
removed: number;
|
|
67
|
+
};
|
|
11
68
|
}
|
|
12
69
|
export interface RunStep {
|
|
13
70
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-next.77",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepublishOnly": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@tiny-fish/sdk": "^0.0.
|
|
35
|
+
"@tiny-fish/sdk": "^0.0.11",
|
|
36
36
|
"chrome-cookies-secure": "^3.0.2",
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
38
|
"globals": "^17.4.0",
|