@tiny-fish/cli 0.1.7-next.67 → 0.1.8-next.70
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/dist/commands/profile.d.ts +12 -0
- package/dist/commands/profile.js +181 -27
- package/dist/lib/client.js +4 -2
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +4 -0
- package/package.json +1 -1
|
@@ -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/lib/client.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
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
|
}
|
|
12
14
|
}
|
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) {
|