@tiny-fish/cli 0.1.7-next.65 → 0.1.7-next.67
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 +3 -0
- package/dist/commands/fetch.js +16 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +161 -0
- package/dist/index.js +2 -0
- package/dist/lib/client.d.ts +3 -1
- package/dist/lib/client.js +38 -3
- package/dist/lib/types.d.ts +21 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -130,6 +130,9 @@ tinyfish fetch content get https://agentql.com --format markdown
|
|
|
130
130
|
# Include links and image links
|
|
131
131
|
tinyfish fetch content get https://agentql.com --links --image-links
|
|
132
132
|
|
|
133
|
+
# Bound each URL independently
|
|
134
|
+
tinyfish fetch content get https://agentql.com --per-url-timeout-ms 45000
|
|
135
|
+
|
|
133
136
|
# Human-readable output
|
|
134
137
|
tinyfish fetch content get https://agentql.com --pretty
|
|
135
138
|
```
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { InvalidArgumentError } from "commander";
|
|
1
2
|
import { getApiKey } from "../lib/auth.js";
|
|
2
3
|
import { fetchContentGet } from "../lib/client.js";
|
|
3
4
|
import { handleApiError, out, outLine } from "../lib/output.js";
|
|
5
|
+
const MIN_PER_URL_TIMEOUT_MS = 1;
|
|
6
|
+
const MAX_PER_URL_TIMEOUT_MS = 110000;
|
|
7
|
+
function parsePerUrlTimeoutMs(value) {
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (!Number.isInteger(parsed) ||
|
|
10
|
+
parsed < MIN_PER_URL_TIMEOUT_MS ||
|
|
11
|
+
parsed > MAX_PER_URL_TIMEOUT_MS) {
|
|
12
|
+
throw new InvalidArgumentError(`must be an integer between ${MIN_PER_URL_TIMEOUT_MS} and ${MAX_PER_URL_TIMEOUT_MS}`);
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
4
16
|
function printPrettyFetch(response) {
|
|
5
17
|
outLine(`Results: ${response.results.length}`);
|
|
6
18
|
outLine(`Errors: ${response.errors.length}`);
|
|
@@ -42,6 +54,7 @@ export function registerFetch(program) {
|
|
|
42
54
|
.option("--format <format>", "Output format: markdown, html, or json")
|
|
43
55
|
.option("--links", "Include extracted links")
|
|
44
56
|
.option("--image-links", "Include extracted image links")
|
|
57
|
+
.option("--per-url-timeout-ms <milliseconds>", "Per-URL timeout budget in milliseconds", parsePerUrlTimeoutMs)
|
|
45
58
|
.option("--pretty", "Human-readable output")
|
|
46
59
|
.action(async (urls, opts) => {
|
|
47
60
|
const apiKey = getApiKey();
|
|
@@ -51,6 +64,9 @@ export function registerFetch(program) {
|
|
|
51
64
|
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
52
65
|
...(opts.links !== undefined ? { links: opts.links } : {}),
|
|
53
66
|
...(opts.imageLinks !== undefined ? { image_links: opts.imageLinks } : {}),
|
|
67
|
+
...(opts.perUrlTimeoutMs !== undefined
|
|
68
|
+
? { per_url_timeout_ms: opts.perUrlTimeoutMs }
|
|
69
|
+
: {}),
|
|
54
70
|
}, apiKey);
|
|
55
71
|
if (opts.pretty) {
|
|
56
72
|
printPrettyFetch(response);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { getApiKey } from "../lib/auth.js";
|
|
6
|
+
import { profileCreate, profileUpload } from "../lib/client.js";
|
|
7
|
+
import { err, errLine, handleApiError, out, outLine } from "../lib/output.js";
|
|
8
|
+
// ── UserFacingError ───────────────────────────────────────────────────────────
|
|
9
|
+
// Thrown by helpers to signal a terminal error that should be printed and exit 1
|
|
10
|
+
// without going through handleApiError (which would print a second error line).
|
|
11
|
+
class UserFacingError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "UserFacingError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
18
|
+
function getChromeCookiesPath() {
|
|
19
|
+
const platform = process.platform;
|
|
20
|
+
if (platform === "darwin") {
|
|
21
|
+
return path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome", "Default", "Cookies");
|
|
22
|
+
}
|
|
23
|
+
if (platform === "linux") {
|
|
24
|
+
return path.join(os.homedir(), ".config", "google-chrome", "Default", "Cookies");
|
|
25
|
+
}
|
|
26
|
+
// Windows guard — handled in extractChromeCookies
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
function validateDomain(domain) {
|
|
30
|
+
if (domain.includes("/") ||
|
|
31
|
+
domain.includes("://") ||
|
|
32
|
+
/\s/.test(domain) ||
|
|
33
|
+
/:\d/.test(domain)) {
|
|
34
|
+
err({ error: "domain must be a plain hostname (e.g. github.com)" });
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Chrome-internal timestamp (microseconds since 1601-01-01) → Unix seconds
|
|
39
|
+
const CHROME_EPOCH_DELTA_SECONDS = 11644473600;
|
|
40
|
+
function chromeTimeToUnix(chromeMicros) {
|
|
41
|
+
return Math.floor(chromeMicros / 1e6) - CHROME_EPOCH_DELTA_SECONDS;
|
|
42
|
+
}
|
|
43
|
+
async function extractChromeCookies(domain) {
|
|
44
|
+
if (process.platform === "win32") {
|
|
45
|
+
throw new UserFacingError("Cookie extraction is not supported on Windows yet");
|
|
46
|
+
}
|
|
47
|
+
const cookiesPath = getChromeCookiesPath();
|
|
48
|
+
if (!fs.existsSync(cookiesPath)) {
|
|
49
|
+
throw new UserFacingError(`Chrome cookies file not found at ${cookiesPath}. Is Google Chrome installed?`);
|
|
50
|
+
}
|
|
51
|
+
// chrome-cookies-secure expects a profile directory and appends "/Cookies".
|
|
52
|
+
// Copy the file into a temp dir so we avoid the Chrome file lock.
|
|
53
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinyfish-chrome-cookies-"));
|
|
54
|
+
try {
|
|
55
|
+
fs.copyFileSync(cookiesPath, path.join(tmpDir, "Cookies"));
|
|
56
|
+
// chrome-cookies-secure is a CommonJS module — use createRequire to load it
|
|
57
|
+
const require = createRequire(import.meta.url);
|
|
58
|
+
const chromeCookies = require("chrome-cookies-secure");
|
|
59
|
+
// "puppeteer" format returns an array with capitalized boolean fields and
|
|
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
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
try {
|
|
76
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Ignore cleanup errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Pretty printers ───────────────────────────────────────────────────────────
|
|
84
|
+
function printPrettyProfileCreate(profile) {
|
|
85
|
+
outLine("Profile created");
|
|
86
|
+
outLine(`ID: ${profile.id}`);
|
|
87
|
+
outLine(`Name: ${profile.name}`);
|
|
88
|
+
outLine(`Created at: ${profile.created_at}`);
|
|
89
|
+
}
|
|
90
|
+
function printPrettyProfileUpload(result) {
|
|
91
|
+
if (result.profile_id)
|
|
92
|
+
outLine(`Profile ID: ${result.profile_id}`);
|
|
93
|
+
outLine(`Cookies uploaded: ${result.cookie_count}`);
|
|
94
|
+
outLine(`Domains updated: ${result.domains_updated.join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
export function registerProfile(program) {
|
|
97
|
+
const profileCmd = program
|
|
98
|
+
.command("profile")
|
|
99
|
+
.description("Profile commands")
|
|
100
|
+
.enablePositionalOptions();
|
|
101
|
+
profileCmd
|
|
102
|
+
.command("create")
|
|
103
|
+
.description("Create a browser profile")
|
|
104
|
+
.requiredOption("--name <name>", "Profile name")
|
|
105
|
+
.option("--pretty", "Human-readable output")
|
|
106
|
+
.action(async (opts) => {
|
|
107
|
+
const apiKey = getApiKey();
|
|
108
|
+
try {
|
|
109
|
+
const response = await profileCreate(opts.name, apiKey);
|
|
110
|
+
if (opts.pretty) {
|
|
111
|
+
printPrettyProfileCreate(response);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
out(response);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
handleApiError(error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
profileCmd
|
|
122
|
+
.command("import-cookies")
|
|
123
|
+
.description("Import Chrome cookies for a domain into a profile")
|
|
124
|
+
.requiredOption("--domain <domain>", "Domain to import cookies for (e.g. github.com)")
|
|
125
|
+
.option("--profile-id <id>", "Profile ID to import cookies into (creates new profile if omitted)")
|
|
126
|
+
.option("--pretty", "Human-readable output")
|
|
127
|
+
.action(async (opts) => {
|
|
128
|
+
const apiKey = getApiKey();
|
|
129
|
+
validateDomain(opts.domain);
|
|
130
|
+
let profileId = opts.profileId;
|
|
131
|
+
try {
|
|
132
|
+
// Extract cookies before creating profile to avoid orphan profiles
|
|
133
|
+
// if extraction fails (Windows, no Chrome, no cookies).
|
|
134
|
+
const cookies = await extractChromeCookies(opts.domain);
|
|
135
|
+
if (cookies.length === 0) {
|
|
136
|
+
throw new UserFacingError(`No Chrome cookies found for ${opts.domain}. Are you logged in to this site in Chrome?`);
|
|
137
|
+
}
|
|
138
|
+
if (profileId === undefined) {
|
|
139
|
+
const created = await profileCreate(opts.domain, apiKey);
|
|
140
|
+
profileId = created.id;
|
|
141
|
+
}
|
|
142
|
+
const result = await profileUpload(profileId, cookies, apiKey);
|
|
143
|
+
if (result.domains_failed && result.domains_failed.length > 0) {
|
|
144
|
+
errLine(`Warning: failed to update cookies for domains: ${result.domains_failed.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
if (opts.pretty) {
|
|
147
|
+
printPrettyProfileUpload(result);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
out(result);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof UserFacingError) {
|
|
155
|
+
err({ error: error.message });
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
handleApiError(error);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { registerAuth } from "./commands/auth.js";
|
|
|
5
5
|
import { registerBatch } from "./commands/batch.js";
|
|
6
6
|
import { registerFetch } from "./commands/fetch.js";
|
|
7
7
|
import { registerBrowser } from "./commands/browser.js";
|
|
8
|
+
import { registerProfile } from "./commands/profile.js";
|
|
8
9
|
import { registerRun } from "./commands/run.js";
|
|
9
10
|
import { registerRuns } from "./commands/runs.js";
|
|
10
11
|
import { registerConfigureClaude } from "./commands/config-claude.js";
|
|
@@ -27,6 +28,7 @@ program
|
|
|
27
28
|
registerAuth(program);
|
|
28
29
|
registerConfigureClaude(program);
|
|
29
30
|
registerBrowser(program);
|
|
31
|
+
registerProfile(program);
|
|
30
32
|
const agentCmd = program
|
|
31
33
|
.command("agent")
|
|
32
34
|
.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, RunStepsResponse } from "./types.js";
|
|
2
|
+
import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse } 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>;
|
|
@@ -13,3 +13,5 @@ export declare function cancelRun(runId: string, apiKey: string): Promise<Cancel
|
|
|
13
13
|
export declare function submitBatch(req: BatchRunRequest, apiKey: string): Promise<BatchRunResponse>;
|
|
14
14
|
export declare function getBatchRuns(runIds: string[], apiKey: string): Promise<BatchGetResponse>;
|
|
15
15
|
export declare function cancelBatchRuns(runIds: string[], apiKey: string): Promise<BatchCancelResponse>;
|
|
16
|
+
export declare function profileCreate(name: string, apiKey: string): Promise<ProfileCreateResponse>;
|
|
17
|
+
export declare function profileUpload(profileId: string, cookies: CookieRecord[], apiKey: string): Promise<ProfileUploadResponse>;
|
package/dist/lib/client.js
CHANGED
|
@@ -35,10 +35,12 @@ const cancelRunResponseSchema = z.object({
|
|
|
35
35
|
});
|
|
36
36
|
const batchRunResponseSchema = z.object({
|
|
37
37
|
run_ids: z.array(z.string()).nullable(),
|
|
38
|
-
error: z
|
|
38
|
+
error: z
|
|
39
|
+
.object({
|
|
39
40
|
code: z.string(),
|
|
40
41
|
message: z.string(),
|
|
41
|
-
})
|
|
42
|
+
})
|
|
43
|
+
.nullable(),
|
|
42
44
|
});
|
|
43
45
|
const batchGetResponseSchema = z.object({
|
|
44
46
|
data: z.array(runSchema),
|
|
@@ -79,7 +81,9 @@ function parseWithSchema(schema, value, message) {
|
|
|
79
81
|
return result.data;
|
|
80
82
|
}
|
|
81
83
|
function normalizeStreamEvent(event) {
|
|
82
|
-
if (typeof event !== "object" ||
|
|
84
|
+
if (typeof event !== "object" ||
|
|
85
|
+
event === null ||
|
|
86
|
+
event.type !== "COMPLETE") {
|
|
83
87
|
return event;
|
|
84
88
|
}
|
|
85
89
|
const data = event;
|
|
@@ -270,3 +274,34 @@ export async function cancelBatchRuns(runIds, apiKey) {
|
|
|
270
274
|
rethrowSdkError(error);
|
|
271
275
|
}
|
|
272
276
|
}
|
|
277
|
+
const profileCreateResponseSchema = z.object({
|
|
278
|
+
id: z.string(),
|
|
279
|
+
name: z.string(),
|
|
280
|
+
created_at: z.string(),
|
|
281
|
+
});
|
|
282
|
+
const profileUploadResponseSchema = z.object({
|
|
283
|
+
profile_id: z.string().optional(),
|
|
284
|
+
cookie_count: z.number(),
|
|
285
|
+
domains_updated: z.array(z.string()),
|
|
286
|
+
domains_failed: z.array(z.string()).optional(),
|
|
287
|
+
});
|
|
288
|
+
export async function profileCreate(name, apiKey) {
|
|
289
|
+
try {
|
|
290
|
+
const response = await createSdkClient(apiKey).post("/v1/profiles", {
|
|
291
|
+
json: { name },
|
|
292
|
+
});
|
|
293
|
+
return parseWithSchema(profileCreateResponseSchema, response, "Invalid profile create response");
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
rethrowSdkError(error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export async function profileUpload(profileId, cookies, apiKey) {
|
|
300
|
+
try {
|
|
301
|
+
const response = await createSdkClient(apiKey).post(`/v1/profiles/${encodeURIComponent(profileId)}/upload`, { json: { cookies } });
|
|
302
|
+
return parseWithSchema(profileUploadResponseSchema, response, "Invalid profile upload response");
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
rethrowSdkError(error);
|
|
306
|
+
}
|
|
307
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -69,3 +69,24 @@ export interface LocalBatch {
|
|
|
69
69
|
submitted: number;
|
|
70
70
|
created_at: string;
|
|
71
71
|
}
|
|
72
|
+
export interface ProfileCreateResponse {
|
|
73
|
+
id: string;
|
|
74
|
+
name: string;
|
|
75
|
+
created_at: string;
|
|
76
|
+
}
|
|
77
|
+
export interface CookieRecord {
|
|
78
|
+
name: string;
|
|
79
|
+
value: string;
|
|
80
|
+
domain: string;
|
|
81
|
+
path: string;
|
|
82
|
+
httpOnly: boolean;
|
|
83
|
+
secure: boolean;
|
|
84
|
+
sameSite?: string;
|
|
85
|
+
expires?: number;
|
|
86
|
+
}
|
|
87
|
+
export interface ProfileUploadResponse {
|
|
88
|
+
profile_id?: string;
|
|
89
|
+
cookie_count: number;
|
|
90
|
+
domains_updated: string[];
|
|
91
|
+
domains_failed?: string[];
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.1.7-next.
|
|
3
|
+
"version": "0.1.7-next.67",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@tiny-fish/sdk": "^0.0.7",
|
|
36
|
+
"chrome-cookies-secure": "^3.0.2",
|
|
36
37
|
"commander": "^12.0.0",
|
|
37
38
|
"globals": "^17.4.0",
|
|
38
39
|
"zod": "^4.3.6"
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"esbuild": "0.28.1",
|
|
52
53
|
"vite": "7.3.5",
|
|
53
54
|
"brace-expansion": "^5.0.6",
|
|
54
|
-
"postcss": "8.5.10"
|
|
55
|
+
"postcss": "8.5.10",
|
|
56
|
+
"tar": "^7"
|
|
55
57
|
},
|
|
56
58
|
"engines": {
|
|
57
59
|
"node": ">=24.0.0"
|