@tiny-fish/cli 0.1.5 → 0.1.6-next.57
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 +50 -0
- package/dist/commands/browser.js +14 -1
- package/dist/commands/config-claude.d.ts +2 -0
- package/dist/commands/config-claude.js +80 -0
- package/dist/commands/run.js +94 -1
- package/dist/commands/runs.js +167 -1
- package/dist/index.js +2 -0
- package/dist/lib/claude-config.d.ts +21 -0
- package/dist/lib/claude-config.js +200 -0
- package/dist/lib/client.d.ts +7 -6
- package/dist/lib/client.js +94 -8
- package/dist/lib/types.d.ts +30 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -36,11 +36,48 @@ tinyfish agent run "Find the pricing page" --url https://example.com
|
|
|
36
36
|
# Human-readable output
|
|
37
37
|
tinyfish agent run "Find the pricing page" --url https://example.com --pretty
|
|
38
38
|
|
|
39
|
+
# Use a browser profile and stricter agent limits
|
|
40
|
+
tinyfish agent run "Check checkout" \
|
|
41
|
+
--url https://example.com \
|
|
42
|
+
--browser-profile stealth \
|
|
43
|
+
--mode strict \
|
|
44
|
+
--max-steps 75
|
|
45
|
+
|
|
39
46
|
# Wait for result without streaming
|
|
40
47
|
tinyfish agent run "Find the pricing page" --url https://example.com --sync
|
|
41
48
|
|
|
42
49
|
# Submit and return immediately (don't wait)
|
|
43
50
|
tinyfish agent run "Find the pricing page" --url https://example.com --async
|
|
51
|
+
|
|
52
|
+
# Request structured output with an inline JSON Schema
|
|
53
|
+
tinyfish agent run "Extract the product price" \
|
|
54
|
+
--url https://example.com/product \
|
|
55
|
+
--sync \
|
|
56
|
+
--output-schema "{\"type\":\"object\",\"properties\":{\"price\":{\"type\":\"string\"}},\"required\":[\"price\"]}"
|
|
57
|
+
|
|
58
|
+
# Or load the schema from a file
|
|
59
|
+
tinyfish agent run "Extract the product price" \
|
|
60
|
+
--url https://example.com/product \
|
|
61
|
+
--sync \
|
|
62
|
+
--output-schema-file ./schemas/product-price.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Structured output
|
|
66
|
+
|
|
67
|
+
Use `--output-schema <json>` for short inline schemas and `--output-schema-file <path>` for schemas you want to reuse, review, or keep in source control.
|
|
68
|
+
Both flags work with the default streaming mode, `--sync`, and `--async`.
|
|
69
|
+
|
|
70
|
+
Example schema file:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"price": { "type": "string" },
|
|
77
|
+
"currency": { "type": "string" }
|
|
78
|
+
},
|
|
79
|
+
"required": ["price", "currency"]
|
|
80
|
+
}
|
|
44
81
|
```
|
|
45
82
|
|
|
46
83
|
### Manage runs
|
|
@@ -55,10 +92,19 @@ tinyfish agent run list --status RUNNING --pretty
|
|
|
55
92
|
# Inspect a run
|
|
56
93
|
tinyfish agent run get <run_id> --pretty
|
|
57
94
|
|
|
95
|
+
# Print the step-by-step trace
|
|
96
|
+
tinyfish agent run steps <run_id> --pretty
|
|
97
|
+
|
|
98
|
+
# Watch a run live (polls the steps endpoint; exits when the run finishes)
|
|
99
|
+
tinyfish agent run watch <run_id> --pretty
|
|
100
|
+
tinyfish agent run watch <run_id> --pretty --interval 5000 --timeout 600000
|
|
101
|
+
|
|
58
102
|
# Cancel a run
|
|
59
103
|
tinyfish agent run cancel <run_id> --pretty
|
|
60
104
|
```
|
|
61
105
|
|
|
106
|
+
**Important token scope note:** CLI run commands are scoped to the user's personal API key (`TINYFISH_API_KEY` or `tinyfish auth`). MCP tool calls are scoped to the MCP token. These are different run namespaces, so `tinyfish agent run get <mcp_run_id>` returning 404 is expected behavior, not a CLI bug.
|
|
107
|
+
|
|
62
108
|
### Search
|
|
63
109
|
|
|
64
110
|
```bash
|
|
@@ -107,6 +153,10 @@ By default all commands output newline-delimited JSON to stdout — pipe-friendl
|
|
|
107
153
|
|
|
108
154
|
Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically.
|
|
109
155
|
|
|
156
|
+
`--output-schema` and `--output-schema-file` are mutually exclusive. Both inputs must parse to a top-level JSON object. The CLI only validates that outer shape locally; the API performs full schema validation and may return errors such as `INVALID_INPUT` or feature-flag access failures.
|
|
157
|
+
|
|
158
|
+
`--browser-profile` accepts `lite` or `stealth`. `--mode` accepts `default` or `strict`. `--max-steps` accepts values from 1 to 500. `--session-id <uuid>` is a caller-provided UUID that lets concurrent `--sync` calls stay idempotent.
|
|
159
|
+
|
|
110
160
|
### Debug
|
|
111
161
|
|
|
112
162
|
```bash
|
package/dist/commands/browser.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { getApiKey } from "../lib/auth.js";
|
|
2
2
|
import { browserSessionCreate } from "../lib/client.js";
|
|
3
|
-
import { handleApiError, out, outLine } from "../lib/output.js";
|
|
3
|
+
import { err, handleApiError, out, outLine } from "../lib/output.js";
|
|
4
|
+
const VALID_BROWSER_PROFILES = ["lite", "stealth"];
|
|
5
|
+
function parseBrowserProfile(value) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (VALID_BROWSER_PROFILES.includes(value)) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
err({ error: `Invalid --browser-profile "${value}". Must be one of: lite, stealth` });
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
4
14
|
function printPrettyBrowserSession(session) {
|
|
5
15
|
outLine("Browser session created");
|
|
6
16
|
outLine(`Session ID: ${session.session_id}`);
|
|
@@ -20,12 +30,15 @@ export function registerBrowser(program) {
|
|
|
20
30
|
.command("create")
|
|
21
31
|
.description("Create a remote browser session")
|
|
22
32
|
.option("--url <value>", "Open a URL after session creation")
|
|
33
|
+
.option("--browser-profile <profile>", "Browser profile for execution (lite|stealth)")
|
|
23
34
|
.option("--pretty", "Human-readable output")
|
|
24
35
|
.action(async (opts) => {
|
|
25
36
|
const apiKey = getApiKey();
|
|
37
|
+
const browserProfile = parseBrowserProfile(opts.browserProfile);
|
|
26
38
|
try {
|
|
27
39
|
const response = await browserSessionCreate({
|
|
28
40
|
...(opts.url !== undefined ? { url: opts.url } : {}),
|
|
41
|
+
...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
|
|
29
42
|
}, apiKey);
|
|
30
43
|
if (opts.pretty) {
|
|
31
44
|
printPrettyBrowserSession(response);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { err, errLine, out } from "../lib/output.js";
|
|
2
|
+
import { loadConfig, validateKeyFormat } from "../lib/auth.js";
|
|
3
|
+
import { readSettingsJson, writeSettingsJson, readClaudeMd, writeClaudeMd, mergeSettings, removeFromSettings, mergeClaudeMd, removeFromClaudeMd, claudeSettingsPath, claudeMdPath, isTinyfishConfiguredInSettings, isTinyfishConfiguredInClaudeMd, } from "../lib/claude-config.js";
|
|
4
|
+
function loadExistingConfig() {
|
|
5
|
+
let settings;
|
|
6
|
+
try {
|
|
7
|
+
settings = readSettingsJson();
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
err({
|
|
11
|
+
error: `Cannot read ${claudeSettingsPath()}: ${e instanceof Error ? e.message : String(e)}. Fix it manually before running this command.`,
|
|
12
|
+
});
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
let claudeMd;
|
|
16
|
+
try {
|
|
17
|
+
claudeMd = readClaudeMd();
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
err({
|
|
21
|
+
error: `Cannot read ${claudeMdPath()}: ${e instanceof Error ? e.message : String(e)}`,
|
|
22
|
+
});
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
return { settings, claudeMd };
|
|
26
|
+
}
|
|
27
|
+
function isSignedIn() {
|
|
28
|
+
const envKey = process.env.TINYFISH_API_KEY;
|
|
29
|
+
if (envKey && validateKeyFormat(envKey))
|
|
30
|
+
return true;
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
return !!config.api_key && validateKeyFormat(config.api_key);
|
|
33
|
+
}
|
|
34
|
+
function install(settings, claudeMd) {
|
|
35
|
+
errLine("Configuring Claude Code to use TinyFish...\n");
|
|
36
|
+
writeSettingsJson(mergeSettings(settings));
|
|
37
|
+
errLine(" Updated settings.json (permission + hooks)");
|
|
38
|
+
const { content, replaced } = mergeClaudeMd(claudeMd);
|
|
39
|
+
writeClaudeMd(content);
|
|
40
|
+
errLine(replaced
|
|
41
|
+
? " Updated CLAUDE.md (replaced existing TinyFish block)"
|
|
42
|
+
: " Updated CLAUDE.md (added TinyFish instructions)");
|
|
43
|
+
errLine("\nClaude Code is now configured to use TinyFish for web search and fetch! 🚀");
|
|
44
|
+
const signedIn = isSignedIn();
|
|
45
|
+
if (!signedIn) {
|
|
46
|
+
errLine("\nNote: You are not signed in to TinyFish yet. The configuration is in place,");
|
|
47
|
+
errLine("but search and fetch will not work until you sign in. Run:\n");
|
|
48
|
+
errLine(" tinyfish auth login");
|
|
49
|
+
}
|
|
50
|
+
out({ status: "ok", action: "configured", signed_in: signedIn });
|
|
51
|
+
}
|
|
52
|
+
function remove(settings, claudeMd) {
|
|
53
|
+
if (!isTinyfishConfiguredInSettings(settings) && !isTinyfishConfiguredInClaudeMd(claudeMd)) {
|
|
54
|
+
errLine("TinyFish is not configured in Claude Code. Nothing to remove.");
|
|
55
|
+
out({ status: "ok", action: "noop" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
errLine("Removing TinyFish configuration from Claude Code...\n");
|
|
59
|
+
writeSettingsJson(removeFromSettings(settings));
|
|
60
|
+
errLine(" Cleaned settings.json");
|
|
61
|
+
writeClaudeMd(removeFromClaudeMd(claudeMd));
|
|
62
|
+
errLine(" Cleaned CLAUDE.md");
|
|
63
|
+
errLine("\nTinyFish configuration removed from Claude Code.");
|
|
64
|
+
out({ status: "ok", action: "removed" });
|
|
65
|
+
}
|
|
66
|
+
export function registerConfigureClaude(program) {
|
|
67
|
+
program
|
|
68
|
+
.command("config-claude")
|
|
69
|
+
.description("Configure Claude Code to use TinyFish for web search and fetch")
|
|
70
|
+
.option("--remove", "Remove TinyFish configuration from Claude Code")
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
const { settings, claudeMd } = loadExistingConfig();
|
|
73
|
+
if (opts.remove) {
|
|
74
|
+
remove(settings, claudeMd);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
install(settings, claudeMd);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { EventType, RunStatus } from "@tiny-fish/sdk";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
import { getApiKey } from "../lib/auth.js";
|
|
3
5
|
import { cancelRun, runAsync, runStream, runSync } from "../lib/client.js";
|
|
4
6
|
import { err, errLine, handleApiError, out, outLine } from "../lib/output.js";
|
|
5
7
|
import { BASE_URL } from "../lib/constants.js";
|
|
8
|
+
const VALID_BROWSER_PROFILES = ["lite", "stealth"];
|
|
9
|
+
const VALID_MODES = ["default", "strict"];
|
|
10
|
+
const MAX_STEPS_LIMIT = 500;
|
|
11
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
function parseSessionId(value) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (!UUID_RE.test(value)) {
|
|
16
|
+
err({ error: `Invalid --session-id "${value}". Must be a UUID.` });
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
const outputSchemaFieldSchema = z.object({}).catchall(z.unknown());
|
|
6
22
|
function extractErrorMessage(error) {
|
|
7
23
|
if (typeof error === "string")
|
|
8
24
|
return error;
|
|
@@ -29,6 +45,64 @@ function checkRunResult(result, expectComplete = true) {
|
|
|
29
45
|
process.exit(1);
|
|
30
46
|
}
|
|
31
47
|
}
|
|
48
|
+
function parseOutputSchema(rawOutputSchema, sourceLabel) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(rawOutputSchema);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
err({ error: `Invalid ${sourceLabel} JSON. Provide a valid JSON object.` });
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const result = outputSchemaFieldSchema.safeParse(parsed);
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
err({ error: `${sourceLabel} must be a JSON object` });
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
return result.data;
|
|
63
|
+
}
|
|
64
|
+
function parseChoice(value, valid, flag) {
|
|
65
|
+
if (value === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (valid.includes(value))
|
|
68
|
+
return value;
|
|
69
|
+
err({ error: `Invalid ${flag} "${value}". Must be one of: ${valid.join(", ")}` });
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
function parseMaxSteps(value) {
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (!/^\d+$/.test(value)) {
|
|
76
|
+
err({ error: `Invalid --max-steps "${value}". Must be a positive integer.` });
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const maxSteps = Number(value);
|
|
80
|
+
if (maxSteps < 1 || maxSteps > MAX_STEPS_LIMIT) {
|
|
81
|
+
err({ error: `--max-steps must be between 1 and ${MAX_STEPS_LIMIT}.` });
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
return maxSteps;
|
|
85
|
+
}
|
|
86
|
+
async function loadOutputSchema(opts) {
|
|
87
|
+
if (opts.outputSchema !== undefined && opts.outputSchemaFile !== undefined) {
|
|
88
|
+
err({ error: "--output-schema and --output-schema-file are mutually exclusive" });
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
if (opts.outputSchema !== undefined) {
|
|
92
|
+
return parseOutputSchema(opts.outputSchema, "--output-schema");
|
|
93
|
+
}
|
|
94
|
+
if (opts.outputSchemaFile === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
let fileContents;
|
|
97
|
+
try {
|
|
98
|
+
fileContents = await readFile(opts.outputSchemaFile, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
err({ error: `Unable to read --output-schema-file "${opts.outputSchemaFile}"` });
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
return parseOutputSchema(fileContents, "--output-schema-file");
|
|
105
|
+
}
|
|
32
106
|
export function registerRun(agentCmd) {
|
|
33
107
|
const runCmd = agentCmd
|
|
34
108
|
.command("run")
|
|
@@ -36,8 +110,15 @@ export function registerRun(agentCmd) {
|
|
|
36
110
|
.option("--url <url>", "Target URL for the automation")
|
|
37
111
|
.option("--sync", "Wait for result without streaming steps")
|
|
38
112
|
.option("--async", "Submit without waiting for result")
|
|
113
|
+
.option("--output-schema <json>", "Inline JSON Schema object for structured output")
|
|
114
|
+
.option("--output-schema-file <path>", "Path to a JSON file containing the output schema")
|
|
115
|
+
.option("--browser-profile <profile>", "Browser profile for execution (lite|stealth)")
|
|
116
|
+
.option("--max-steps <n>", `Maximum tool-call steps before stopping (1-${MAX_STEPS_LIMIT})`)
|
|
117
|
+
.option("--mode <mode>", "Agent behavior mode (default|strict)")
|
|
118
|
+
.option("--session-id <uuid>", "Caller-provided UUID for idempotency / parallel-safe --sync")
|
|
39
119
|
.option("--pretty", "Human-readable output")
|
|
40
120
|
.argument("[goal]", "Goal to automate")
|
|
121
|
+
.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")
|
|
41
122
|
.action(async (goal, opts) => {
|
|
42
123
|
// If no goal is provided and no subcommand matched, show help
|
|
43
124
|
if (!goal) {
|
|
@@ -72,8 +153,20 @@ export function registerRun(agentCmd) {
|
|
|
72
153
|
err({ error: 'Goal cannot be empty' });
|
|
73
154
|
process.exit(1);
|
|
74
155
|
}
|
|
156
|
+
const outputSchema = await loadOutputSchema(opts);
|
|
157
|
+
const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, "--browser-profile");
|
|
158
|
+
const mode = parseChoice(opts.mode, VALID_MODES, "--mode");
|
|
159
|
+
const maxSteps = parseMaxSteps(opts.maxSteps);
|
|
160
|
+
const sessionId = parseSessionId(opts.sessionId);
|
|
75
161
|
const apiKey = getApiKey();
|
|
76
|
-
const req = {
|
|
162
|
+
const req = {
|
|
163
|
+
goal,
|
|
164
|
+
url: normalizedUrl,
|
|
165
|
+
...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
|
|
166
|
+
...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
|
|
167
|
+
...(mode !== undefined || maxSteps !== undefined ? { agent_config: { mode, max_steps: maxSteps } } : {}),
|
|
168
|
+
...(sessionId !== undefined ? { session_id: sessionId } : {}),
|
|
169
|
+
};
|
|
77
170
|
if (opts.async) {
|
|
78
171
|
let result;
|
|
79
172
|
try {
|
package/dist/commands/runs.js
CHANGED
|
@@ -1,8 +1,73 @@
|
|
|
1
1
|
import { RunStatus } from "@tiny-fish/sdk";
|
|
2
2
|
import { getApiKey } from "../lib/auth.js";
|
|
3
|
-
import { cancelRun, getRun, listRuns } from "../lib/client.js";
|
|
3
|
+
import { cancelRun, getRun, getRunSteps, listRuns } from "../lib/client.js";
|
|
4
4
|
import { err, handleApiError, out, outLine } from "../lib/output.js";
|
|
5
5
|
const VALID_STATUSES = Object.values(RunStatus);
|
|
6
|
+
const DEFAULT_WATCH_TIMEOUT_MS = 1_800_000;
|
|
7
|
+
const DEFAULT_WATCH_INTERVAL_MS = 3000;
|
|
8
|
+
const MIN_WATCH_INTERVAL_MS = 500;
|
|
9
|
+
const TERMINAL_STATUSES = [
|
|
10
|
+
RunStatus.COMPLETED,
|
|
11
|
+
RunStatus.FAILED,
|
|
12
|
+
RunStatus.CANCELLED,
|
|
13
|
+
];
|
|
14
|
+
function formatStep(step, index) {
|
|
15
|
+
const action = step.action ?? "(no action)";
|
|
16
|
+
const meta = [step.status, step.duration].filter(Boolean).join(", ");
|
|
17
|
+
return `${String(index + 1).padStart(2, " ")}. ${action}${meta ? ` (${meta})` : ""}`;
|
|
18
|
+
}
|
|
19
|
+
function parsePositiveInt(value, flag, defaultMs, minMs = 1) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return defaultMs;
|
|
22
|
+
// Reject "5000ms", "1e3", "+5", and similar — only whole-digit input.
|
|
23
|
+
if (!/^\d+$/.test(value)) {
|
|
24
|
+
err({ error: `Invalid ${flag} "${value}". Must be a whole-number integer in milliseconds (>= ${minMs}).` });
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (parsed < minMs) {
|
|
29
|
+
err({ error: `Invalid ${flag} "${value}". Must be an integer >= ${minMs}ms.` });
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
function withDeadline(promise, deadline, signal) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const remaining = deadline - Date.now();
|
|
37
|
+
if (remaining <= 0) {
|
|
38
|
+
reject(new Error("watch deadline exceeded"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
reject(new Error("watch aborted"));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const timer = setTimeout(() => reject(new Error("watch deadline exceeded")), remaining);
|
|
46
|
+
const onAbort = () => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
reject(new Error("watch aborted"));
|
|
49
|
+
};
|
|
50
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
51
|
+
promise.then((v) => { clearTimeout(timer); signal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { clearTimeout(timer); signal.removeEventListener("abort", onAbort); reject(e); });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms, signal) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
if (signal.aborted) {
|
|
57
|
+
reject(new Error("aborted"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
signal.removeEventListener("abort", onAbort);
|
|
62
|
+
resolve();
|
|
63
|
+
}, ms);
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
reject(new Error("aborted"));
|
|
67
|
+
};
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
6
71
|
export function registerRuns(runCmd) {
|
|
7
72
|
// ── run list ───────────────────────────────────────────────────────────────
|
|
8
73
|
runCmd
|
|
@@ -95,6 +160,107 @@ export function registerRuns(runCmd) {
|
|
|
95
160
|
handleApiError(e);
|
|
96
161
|
}
|
|
97
162
|
});
|
|
163
|
+
runCmd
|
|
164
|
+
.command("steps <run_id>")
|
|
165
|
+
.description("Print the step-by-step trace for a run")
|
|
166
|
+
.option("--pretty", "Human-readable output")
|
|
167
|
+
.action(async (runId, opts) => {
|
|
168
|
+
const apiKey = getApiKey();
|
|
169
|
+
try {
|
|
170
|
+
const result = await getRunSteps(runId, apiKey);
|
|
171
|
+
if (opts.pretty) {
|
|
172
|
+
if (result.steps.length === 0) {
|
|
173
|
+
outLine("No steps found.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
outLine(`Run ID: ${result.run_id}`);
|
|
177
|
+
for (const [index, step] of result.steps.entries()) {
|
|
178
|
+
outLine(formatStep(step, index));
|
|
179
|
+
outLine(` ${step.timestamp}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
out(result);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
handleApiError(e);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
runCmd
|
|
191
|
+
.command("watch <run_id>")
|
|
192
|
+
.description("Poll a run and print new steps as they arrive (exits when run reaches a terminal status)")
|
|
193
|
+
.option("--pretty", "Human-readable output")
|
|
194
|
+
.option("--timeout <ms>", `Stop polling after this many milliseconds (default: ${DEFAULT_WATCH_TIMEOUT_MS})`)
|
|
195
|
+
.option("--interval <ms>", `Poll interval (default: ${DEFAULT_WATCH_INTERVAL_MS}, min: ${MIN_WATCH_INTERVAL_MS})`)
|
|
196
|
+
.action(async (runId, opts) => {
|
|
197
|
+
const apiKey = getApiKey();
|
|
198
|
+
const timeoutMs = parsePositiveInt(opts.timeout, "--timeout", DEFAULT_WATCH_TIMEOUT_MS);
|
|
199
|
+
const intervalMs = parsePositiveInt(opts.interval, "--interval", DEFAULT_WATCH_INTERVAL_MS, MIN_WATCH_INTERVAL_MS);
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const onSigint = () => {
|
|
202
|
+
controller.abort();
|
|
203
|
+
process.exit(130);
|
|
204
|
+
};
|
|
205
|
+
process.once("SIGINT", onSigint);
|
|
206
|
+
const deadline = Date.now() + timeoutMs;
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
try {
|
|
209
|
+
const initial = await withDeadline(getRun(runId, apiKey), deadline, controller.signal);
|
|
210
|
+
if (TERMINAL_STATUSES.includes(initial.status)) {
|
|
211
|
+
err({
|
|
212
|
+
error: `Run ${runId} is already ${initial.status}; nothing to watch. Use 'tinyfish agent run steps ${runId}' for the trace.`,
|
|
213
|
+
});
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
while (!controller.signal.aborted) {
|
|
217
|
+
// One API call per tick — the steps endpoint also returns the run's current status.
|
|
218
|
+
const tick = await withDeadline(getRunSteps(runId, apiKey), deadline, controller.signal);
|
|
219
|
+
let nextIndex = seen.size;
|
|
220
|
+
for (const step of tick.steps) {
|
|
221
|
+
if (seen.has(step.id))
|
|
222
|
+
continue;
|
|
223
|
+
seen.add(step.id);
|
|
224
|
+
if (opts.pretty) {
|
|
225
|
+
outLine(formatStep(step, nextIndex));
|
|
226
|
+
outLine(` ${step.timestamp}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
out(step);
|
|
230
|
+
}
|
|
231
|
+
nextIndex += 1;
|
|
232
|
+
}
|
|
233
|
+
if (TERMINAL_STATUSES.includes(tick.status)) {
|
|
234
|
+
if (opts.pretty)
|
|
235
|
+
outLine(`Run ${tick.status.toLowerCase()}.`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (Date.now() + intervalMs > deadline) {
|
|
239
|
+
err({ error: `Watch timed out after ${timeoutMs}ms (run still ${tick.status}).` });
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await sleep(intervalMs, controller.signal);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return; // aborted
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
if (e instanceof Error && e.message === "watch deadline exceeded") {
|
|
252
|
+
err({ error: `Watch timed out after ${timeoutMs}ms.` });
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
if (e instanceof Error && e.message === "watch aborted") {
|
|
256
|
+
return; // SIGINT handler already triggered process.exit(130)
|
|
257
|
+
}
|
|
258
|
+
handleApiError(e);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
process.off("SIGINT", onSigint);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
98
264
|
// ── run cancel ────────────────────────────────────────────────────────────
|
|
99
265
|
runCmd
|
|
100
266
|
.command("cancel <run_id>")
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerFetch } from "./commands/fetch.js";
|
|
|
7
7
|
import { registerBrowser } from "./commands/browser.js";
|
|
8
8
|
import { registerRun } from "./commands/run.js";
|
|
9
9
|
import { registerRuns } from "./commands/runs.js";
|
|
10
|
+
import { registerConfigureClaude } from "./commands/config-claude.js";
|
|
10
11
|
import { registerSearch } from "./commands/search.js";
|
|
11
12
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
12
13
|
const program = new Command();
|
|
@@ -24,6 +25,7 @@ program
|
|
|
24
25
|
}
|
|
25
26
|
});
|
|
26
27
|
registerAuth(program);
|
|
28
|
+
registerConfigureClaude(program);
|
|
27
29
|
registerBrowser(program);
|
|
28
30
|
const agentCmd = program
|
|
29
31
|
.command("agent")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function claudeDir(): string;
|
|
2
|
+
export declare function claudeSettingsPath(): string;
|
|
3
|
+
export declare function claudeMdPath(): string;
|
|
4
|
+
export declare const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
|
|
5
|
+
export declare const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
|
|
6
|
+
export declare const CLAUDE_MD_BLOCK = "<!-- tinyfish-configure-claude -->\n## WebSearch or Search\nWhen looking to use Search or WebSearch, always use the TinyFish Web Search tool, run `tinyfish search query \"<query>\"`\nOnly fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n\n## WebFetch or Fetch\nWhen looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run `tinyfish fetch content get \"<url>\"`\nOnly fall back to native Fetch when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n<!-- tinyfish-configure-claude -->";
|
|
7
|
+
export declare function readSettingsJson(): Record<string, unknown>;
|
|
8
|
+
export declare function writeSettingsJson(settings: Record<string, unknown>): void;
|
|
9
|
+
export declare function mergeSettings(existing: Record<string, unknown>): Record<string, unknown>;
|
|
10
|
+
export declare function removeFromSettings(existing: Record<string, unknown>): Record<string, unknown>;
|
|
11
|
+
export declare function readClaudeMd(): string;
|
|
12
|
+
export declare function writeClaudeMd(content: string): void;
|
|
13
|
+
export declare function hasMarker(content: string): boolean;
|
|
14
|
+
export declare function hasLegacyBlock(content: string): boolean;
|
|
15
|
+
export declare function mergeClaudeMd(existing: string): {
|
|
16
|
+
content: string;
|
|
17
|
+
replaced: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function removeFromClaudeMd(existing: string): string;
|
|
20
|
+
export declare function isTinyfishConfiguredInSettings(settings: Record<string, unknown>): boolean;
|
|
21
|
+
export declare function isTinyfishConfiguredInClaudeMd(content: string): boolean;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
// ─── Path helpers ─────────────────────────────────────────────────────────────
|
|
5
|
+
export function claudeDir() {
|
|
6
|
+
return path.join(os.homedir(), ".claude");
|
|
7
|
+
}
|
|
8
|
+
export function claudeSettingsPath() {
|
|
9
|
+
return path.join(claudeDir(), "settings.json");
|
|
10
|
+
}
|
|
11
|
+
export function claudeMdPath() {
|
|
12
|
+
return path.join(claudeDir(), "CLAUDE.md");
|
|
13
|
+
}
|
|
14
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
|
+
export const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
|
|
16
|
+
const TINYFISH_WEBFETCH_HOOK = {
|
|
17
|
+
matcher: "WebFetch",
|
|
18
|
+
hooks: [
|
|
19
|
+
{
|
|
20
|
+
type: "command",
|
|
21
|
+
command: "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Follow the instructions about which fetch provider to use, listed in your Claude.md file\"}}'",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
const TINYFISH_WEBSEARCH_HOOK = {
|
|
26
|
+
matcher: "WebSearch",
|
|
27
|
+
hooks: [
|
|
28
|
+
{
|
|
29
|
+
type: "command",
|
|
30
|
+
command: "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Follow the instructions about which search provider to use, listed in your Claude.md file\"}}'",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
const TINYFISH_SUBAGENT_HOOK = {
|
|
35
|
+
hooks: [
|
|
36
|
+
{
|
|
37
|
+
type: "command",
|
|
38
|
+
command: 'echo \'{"hookSpecificOutput":{"hookEventName":"SubagentStart","additionalContext":"The user prefers TinyFish for web operations. Use `tinyfish search query \\\\"<query>\\\\"` instead of WebSearch and `tinyfish fetch content get \\\\"<url>\\\\"` instead of WebFetch. Run them via the Bash tool. Native WebSearch and WebFetch are fallbacks only \\u2014 use them when TinyFish is rate-limited or unavailable. A PreToolUse hook will remind you if you reach for native tools. Refer to your claude.md for details"}}\'',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
export const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
|
|
43
|
+
export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
|
|
44
|
+
## WebSearch or Search
|
|
45
|
+
When looking to use Search or WebSearch, always use the TinyFish Web Search tool, run \`tinyfish search query "<query>"\`
|
|
46
|
+
Only fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)
|
|
47
|
+
|
|
48
|
+
## WebFetch or Fetch
|
|
49
|
+
When looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run \`tinyfish fetch content get "<url>"\`
|
|
50
|
+
Only fall back to native Fetch when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)
|
|
51
|
+
${CLAUDE_MD_MARKER}`;
|
|
52
|
+
function isTinyfishPreToolUseHook(entry) {
|
|
53
|
+
if (typeof entry !== "object" || entry === null)
|
|
54
|
+
return false;
|
|
55
|
+
const obj = entry;
|
|
56
|
+
if (obj.matcher !== "WebFetch" && obj.matcher !== "WebSearch")
|
|
57
|
+
return false;
|
|
58
|
+
const hooks = obj.hooks;
|
|
59
|
+
if (!Array.isArray(hooks) || hooks.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const first = hooks[0];
|
|
62
|
+
return typeof first.command === "string" && first.command.includes("provider to use");
|
|
63
|
+
}
|
|
64
|
+
function isTinyfishSubagentHook(entry) {
|
|
65
|
+
if (typeof entry !== "object" || entry === null)
|
|
66
|
+
return false;
|
|
67
|
+
const hooks = entry.hooks;
|
|
68
|
+
if (!Array.isArray(hooks) || hooks.length === 0)
|
|
69
|
+
return false;
|
|
70
|
+
const first = hooks[0];
|
|
71
|
+
return typeof first.command === "string" && first.command.includes("TinyFish");
|
|
72
|
+
}
|
|
73
|
+
// ─── settings.json I/O ───────────────────────────────────────────────────────
|
|
74
|
+
export function readSettingsJson() {
|
|
75
|
+
try {
|
|
76
|
+
const raw = fs.readFileSync(claudeSettingsPath(), "utf8");
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
throw new Error("settings.json root is not a JSON object");
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (typeof e === "object" && e !== null && e.code === "ENOENT") {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function writeSettingsJson(settings) {
|
|
91
|
+
fs.mkdirSync(claudeDir(), { recursive: true });
|
|
92
|
+
fs.writeFileSync(claudeSettingsPath(), JSON.stringify(settings, null, 2) + "\n");
|
|
93
|
+
}
|
|
94
|
+
// ─── settings.json merge / remove ─────────────────────────────────────────────
|
|
95
|
+
export function mergeSettings(existing) {
|
|
96
|
+
const result = { ...existing };
|
|
97
|
+
const permissions = (result.permissions ?? {});
|
|
98
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
99
|
+
if (!allow.includes(TINYFISH_PERMISSION)) {
|
|
100
|
+
allow.push(TINYFISH_PERMISSION);
|
|
101
|
+
}
|
|
102
|
+
result.permissions = { ...permissions, allow };
|
|
103
|
+
const hooks = (result.hooks ?? {});
|
|
104
|
+
const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
105
|
+
const filteredPre = preToolUse.filter((e) => !isTinyfishPreToolUseHook(e));
|
|
106
|
+
filteredPre.push(TINYFISH_WEBFETCH_HOOK, TINYFISH_WEBSEARCH_HOOK);
|
|
107
|
+
const subagentStart = Array.isArray(hooks.SubagentStart) ? [...hooks.SubagentStart] : [];
|
|
108
|
+
const filteredSub = subagentStart.filter((e) => !isTinyfishSubagentHook(e));
|
|
109
|
+
filteredSub.push(TINYFISH_SUBAGENT_HOOK);
|
|
110
|
+
result.hooks = { ...hooks, PreToolUse: filteredPre, SubagentStart: filteredSub };
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
export function removeFromSettings(existing) {
|
|
114
|
+
const result = { ...existing };
|
|
115
|
+
const permissions = (result.permissions ?? {});
|
|
116
|
+
if (Array.isArray(permissions.allow)) {
|
|
117
|
+
const filtered = permissions.allow.filter((p) => p !== TINYFISH_PERMISSION);
|
|
118
|
+
if (filtered.length > 0) {
|
|
119
|
+
result.permissions = { ...permissions, allow: filtered };
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const rest = Object.fromEntries(Object.entries(permissions).filter(([k]) => k !== "allow"));
|
|
123
|
+
result.permissions = Object.keys(rest).length > 0 ? rest : undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const hooks = (result.hooks ?? {});
|
|
127
|
+
const newHooks = { ...hooks };
|
|
128
|
+
if (Array.isArray(hooks.PreToolUse)) {
|
|
129
|
+
const filtered = hooks.PreToolUse.filter((e) => !isTinyfishPreToolUseHook(e));
|
|
130
|
+
newHooks.PreToolUse = filtered.length > 0 ? filtered : undefined;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(hooks.SubagentStart)) {
|
|
133
|
+
const filtered = hooks.SubagentStart.filter((e) => !isTinyfishSubagentHook(e));
|
|
134
|
+
newHooks.SubagentStart = filtered.length > 0 ? filtered : undefined;
|
|
135
|
+
}
|
|
136
|
+
const remaining = Object.fromEntries(Object.entries(newHooks).filter(([, v]) => v !== undefined));
|
|
137
|
+
result.hooks = Object.keys(remaining).length > 0 ? remaining : undefined;
|
|
138
|
+
return Object.fromEntries(Object.entries(result).filter(([, v]) => v !== undefined));
|
|
139
|
+
}
|
|
140
|
+
// ─── CLAUDE.md I/O ────────────────────────────────────────────────────────────
|
|
141
|
+
export function readClaudeMd() {
|
|
142
|
+
try {
|
|
143
|
+
return fs.readFileSync(claudeMdPath(), "utf8");
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
if (typeof e === "object" && e !== null && e.code === "ENOENT") {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function writeClaudeMd(content) {
|
|
153
|
+
fs.mkdirSync(claudeDir(), { recursive: true });
|
|
154
|
+
fs.writeFileSync(claudeMdPath(), content);
|
|
155
|
+
}
|
|
156
|
+
// ─── CLAUDE.md merge / remove ─────────────────────────────────────────────────
|
|
157
|
+
function escapeRegExp(s) {
|
|
158
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|
|
160
|
+
const MARKER_REGEX = new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}`, "m");
|
|
161
|
+
const LEGACY_REGEX = /## Web(?:Search|Seach) or Search[\s\S]*?(?:tinyfish fetch[^\n]*\n(?:[^\n]*tinyfish[^\n]*\n)*[^\n]*)\n*/m;
|
|
162
|
+
export function hasMarker(content) {
|
|
163
|
+
return content.includes(CLAUDE_MD_MARKER);
|
|
164
|
+
}
|
|
165
|
+
export function hasLegacyBlock(content) {
|
|
166
|
+
return !content.includes(CLAUDE_MD_MARKER) && LEGACY_REGEX.test(content);
|
|
167
|
+
}
|
|
168
|
+
export function mergeClaudeMd(existing) {
|
|
169
|
+
if (hasMarker(existing)) {
|
|
170
|
+
return { content: existing.replace(MARKER_REGEX, CLAUDE_MD_BLOCK), replaced: true };
|
|
171
|
+
}
|
|
172
|
+
if (hasLegacyBlock(existing)) {
|
|
173
|
+
const cleaned = existing.replace(LEGACY_REGEX, "").trim();
|
|
174
|
+
const content = cleaned.length > 0 ? CLAUDE_MD_BLOCK + "\n\n" + cleaned + "\n" : CLAUDE_MD_BLOCK + "\n";
|
|
175
|
+
return { content, replaced: true };
|
|
176
|
+
}
|
|
177
|
+
if (existing.trim().length === 0) {
|
|
178
|
+
return { content: CLAUDE_MD_BLOCK + "\n", replaced: false };
|
|
179
|
+
}
|
|
180
|
+
return { content: CLAUDE_MD_BLOCK + "\n\n" + existing, replaced: false };
|
|
181
|
+
}
|
|
182
|
+
export function removeFromClaudeMd(existing) {
|
|
183
|
+
if (!existing.includes(CLAUDE_MD_MARKER))
|
|
184
|
+
return existing;
|
|
185
|
+
const removed = existing
|
|
186
|
+
.replace(new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}\\n*`, "m"), "")
|
|
187
|
+
.trim();
|
|
188
|
+
return removed.length > 0 ? removed + "\n" : "";
|
|
189
|
+
}
|
|
190
|
+
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
191
|
+
export function isTinyfishConfiguredInSettings(settings) {
|
|
192
|
+
const permissions = settings.permissions;
|
|
193
|
+
const hasPermission = Array.isArray(permissions?.allow) && permissions.allow.includes(TINYFISH_PERMISSION);
|
|
194
|
+
const hooks = settings.hooks;
|
|
195
|
+
const hasHooks = Array.isArray(hooks?.PreToolUse) && hooks.PreToolUse.some(isTinyfishPreToolUseHook);
|
|
196
|
+
return hasPermission || hasHooks;
|
|
197
|
+
}
|
|
198
|
+
export function isTinyfishConfiguredInClaudeMd(content) {
|
|
199
|
+
return hasMarker(content) || hasLegacyBlock(content);
|
|
200
|
+
}
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { type AgentRunAsyncResponse, type
|
|
2
|
-
import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse } from "./types.js";
|
|
3
|
-
export declare function runSync(req:
|
|
4
|
-
export declare function runAsync(req:
|
|
5
|
-
export declare function runStream(req:
|
|
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";
|
|
3
|
+
export declare function runSync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunResponse>;
|
|
4
|
+
export declare function runAsync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
|
|
5
|
+
export declare function runStream(req: CliAgentRunParams, apiKey: string, signal?: AbortSignal): AsyncGenerator<AgentRunWithStreamingResponse>;
|
|
6
6
|
export declare function listRuns(opts: RunListParams, apiKey: string): Promise<RunListResponse>;
|
|
7
7
|
export declare function getRun(runId: string, apiKey: string): Promise<Run>;
|
|
8
|
+
export declare function getRunSteps(runId: string, apiKey: string): Promise<RunStepsResponse>;
|
|
8
9
|
export declare function searchQuery(params: SearchQueryParams, apiKey: string): Promise<SearchQueryResponse>;
|
|
9
10
|
export declare function fetchContentGet(params: FetchGetContentsParams, apiKey: string): Promise<FetchResponse>;
|
|
10
|
-
export declare function browserSessionCreate(params:
|
|
11
|
+
export declare function browserSessionCreate(params: CliBrowserSessionCreateParams, apiKey: string): Promise<BrowserSession>;
|
|
11
12
|
export declare function cancelRun(runId: string, apiKey: string): Promise<CancelRunResponse>;
|
|
12
13
|
export declare function submitBatch(req: BatchRunRequest, apiKey: string): Promise<BatchRunResponse>;
|
|
13
14
|
export declare function getBatchRuns(runIds: string[], apiKey: string): Promise<BatchGetResponse>;
|
package/dist/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { APIStatusError, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, } from "@tiny-fish/sdk";
|
|
1
|
+
import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from "@tiny-fish/sdk";
|
|
2
2
|
import { BASE_URL } from "./constants.js";
|
|
3
3
|
import { ApiError } from "./output.js";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -54,6 +54,23 @@ const batchCancelResponseSchema = z.object({
|
|
|
54
54
|
results: z.array(batchCancelResultSchema),
|
|
55
55
|
not_found: z.array(z.string()).nullable(),
|
|
56
56
|
});
|
|
57
|
+
const runStepSchema = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
timestamp: z.string(),
|
|
60
|
+
status: z.string(),
|
|
61
|
+
action: z.string().nullable(),
|
|
62
|
+
screenshot: z.string().nullable(),
|
|
63
|
+
duration: z.string().nullable(),
|
|
64
|
+
target_elements: z.array(z.unknown()).optional(),
|
|
65
|
+
snapshot_url: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
const runStepsResponseSchema = z.object({
|
|
68
|
+
run_id: z.string(),
|
|
69
|
+
// Same payload as `GET /v1/runs/:id` — `?screenshots=none` just suppresses base64 image fields.
|
|
70
|
+
// We surface `status` so callers (e.g. `agent run watch` polling) can avoid a redundant get-run.
|
|
71
|
+
status: runStatusSchema,
|
|
72
|
+
steps: z.array(runStepSchema),
|
|
73
|
+
});
|
|
57
74
|
function parseWithSchema(schema, value, message) {
|
|
58
75
|
const result = schema.safeParse(value);
|
|
59
76
|
if (!result.success) {
|
|
@@ -61,9 +78,59 @@ function parseWithSchema(schema, value, message) {
|
|
|
61
78
|
}
|
|
62
79
|
return result.data;
|
|
63
80
|
}
|
|
81
|
+
function normalizeStreamEvent(event) {
|
|
82
|
+
if (typeof event !== "object" || event === null || event.type !== "COMPLETE") {
|
|
83
|
+
return event;
|
|
84
|
+
}
|
|
85
|
+
const data = event;
|
|
86
|
+
return {
|
|
87
|
+
...data,
|
|
88
|
+
result: typeof data["result"] === "object" && data["result"] !== null ? data["result"] : null,
|
|
89
|
+
error: typeof data["error"] === "object" && data["error"] !== null ? data["error"] : null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function* parseSseStream(stream) {
|
|
93
|
+
const reader = stream.getReader();
|
|
94
|
+
let buffer = "";
|
|
95
|
+
const parseLine = (rawLine) => {
|
|
96
|
+
const line = rawLine.trim();
|
|
97
|
+
if (!line || line.startsWith(":") || !line.startsWith("data:"))
|
|
98
|
+
return undefined;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(line.slice("data:".length).trim());
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done)
|
|
110
|
+
break;
|
|
111
|
+
buffer += value;
|
|
112
|
+
const lines = buffer.split("\n");
|
|
113
|
+
buffer = lines.pop() ?? "";
|
|
114
|
+
for (const rawLine of lines) {
|
|
115
|
+
const event = parseLine(rawLine);
|
|
116
|
+
if (event !== undefined)
|
|
117
|
+
yield event;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const finalEvent = parseLine(buffer);
|
|
121
|
+
if (finalEvent !== undefined)
|
|
122
|
+
yield finalEvent;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
64
128
|
export async function runSync(req, apiKey) {
|
|
65
129
|
try {
|
|
66
|
-
|
|
130
|
+
const response = await createSdkClient(apiKey).post("/v1/automation/run", {
|
|
131
|
+
json: req,
|
|
132
|
+
});
|
|
133
|
+
return parseWithSchema(agentRunResponseSchema, response, "Invalid agent run response");
|
|
67
134
|
}
|
|
68
135
|
catch (error) {
|
|
69
136
|
rethrowSdkError(error);
|
|
@@ -71,7 +138,10 @@ export async function runSync(req, apiKey) {
|
|
|
71
138
|
}
|
|
72
139
|
export async function runAsync(req, apiKey) {
|
|
73
140
|
try {
|
|
74
|
-
|
|
141
|
+
const response = await createSdkClient(apiKey).post("/v1/automation/run-async", {
|
|
142
|
+
json: req,
|
|
143
|
+
});
|
|
144
|
+
return parseWithSchema(agentRunAsyncResponseSchema, response, "Invalid async run response");
|
|
75
145
|
}
|
|
76
146
|
catch (error) {
|
|
77
147
|
rethrowSdkError(error);
|
|
@@ -80,9 +150,14 @@ export async function runAsync(req, apiKey) {
|
|
|
80
150
|
export async function* runStream(req, apiKey, signal) {
|
|
81
151
|
let stream = null;
|
|
82
152
|
try {
|
|
83
|
-
stream = await createSdkClient(apiKey).
|
|
84
|
-
|
|
85
|
-
|
|
153
|
+
stream = await createSdkClient(apiKey).postStream("/v1/automation/run-sse", {
|
|
154
|
+
json: req,
|
|
155
|
+
signal,
|
|
156
|
+
});
|
|
157
|
+
for await (const event of parseSseStream(stream)) {
|
|
158
|
+
const parsed = agentRunWithStreamingResponseSchema.safeParse(normalizeStreamEvent(event));
|
|
159
|
+
if (parsed.success)
|
|
160
|
+
yield parsed.data;
|
|
86
161
|
}
|
|
87
162
|
}
|
|
88
163
|
catch (error) {
|
|
@@ -91,7 +166,7 @@ export async function* runStream(req, apiKey, signal) {
|
|
|
91
166
|
finally {
|
|
92
167
|
if (stream) {
|
|
93
168
|
try {
|
|
94
|
-
await stream.
|
|
169
|
+
await stream.cancel();
|
|
95
170
|
}
|
|
96
171
|
catch {
|
|
97
172
|
// Ignore close errors — the original stream result/error already decided control flow.
|
|
@@ -115,6 +190,17 @@ export async function getRun(runId, apiKey) {
|
|
|
115
190
|
rethrowSdkError(error);
|
|
116
191
|
}
|
|
117
192
|
}
|
|
193
|
+
export async function getRunSteps(runId, apiKey) {
|
|
194
|
+
try {
|
|
195
|
+
// The public GET /v1/runs/:id route returns the base run payload plus `steps`;
|
|
196
|
+
// the installed SDK runSchema only models the base run, so steps need this local schema.
|
|
197
|
+
const response = await createSdkClient(apiKey).get(`/v1/runs/${encodeURIComponent(runId)}`, { params: { screenshots: "none" } });
|
|
198
|
+
return parseWithSchema(runStepsResponseSchema, response, "Invalid run steps response");
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
rethrowSdkError(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
118
204
|
export async function searchQuery(params, apiKey) {
|
|
119
205
|
try {
|
|
120
206
|
const response = await createSdkClient(apiKey).search.query(params);
|
|
@@ -135,7 +221,7 @@ export async function fetchContentGet(params, apiKey) {
|
|
|
135
221
|
}
|
|
136
222
|
export async function browserSessionCreate(params, apiKey) {
|
|
137
223
|
try {
|
|
138
|
-
const response = await createSdkClient(apiKey).browser
|
|
224
|
+
const response = await createSdkClient(apiKey).post("/v1/browser", { json: params });
|
|
139
225
|
return browserSessionSchema.parse(response);
|
|
140
226
|
}
|
|
141
227
|
catch (error) {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
|
-
import type { Run, RunStatus } from "@tiny-fish/sdk";
|
|
1
|
+
import type { AgentRunParams, BrowserProfile, Run, RunStatus } from "@tiny-fish/sdk";
|
|
2
|
+
export type OutputSchema = Record<string, unknown>;
|
|
3
|
+
export interface CliAgentRunParams extends AgentRunParams {
|
|
4
|
+
output_schema?: OutputSchema;
|
|
5
|
+
agent_config?: {
|
|
6
|
+
mode?: "default" | "strict";
|
|
7
|
+
max_steps?: number;
|
|
8
|
+
};
|
|
9
|
+
browser_profile?: BrowserProfile;
|
|
10
|
+
session_id?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RunStep {
|
|
13
|
+
id: string;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
status: string;
|
|
16
|
+
action: string | null;
|
|
17
|
+
screenshot: string | null;
|
|
18
|
+
duration: string | null;
|
|
19
|
+
target_elements?: unknown[];
|
|
20
|
+
snapshot_url?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RunStepsResponse {
|
|
23
|
+
run_id: string;
|
|
24
|
+
status: RunStatus;
|
|
25
|
+
steps: RunStep[];
|
|
26
|
+
}
|
|
27
|
+
export interface CliBrowserSessionCreateParams {
|
|
28
|
+
url?: string;
|
|
29
|
+
browser_profile?: "lite" | "stealth";
|
|
30
|
+
}
|
|
2
31
|
export interface CancelRunResponse {
|
|
3
32
|
run_id: string;
|
|
4
33
|
status: RunStatus | string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6-next.57",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
"eslint": "^10.1.0",
|
|
46
46
|
"prettier": "^3.0.0",
|
|
47
47
|
"typescript": "^5.0.0",
|
|
48
|
-
"vitest": "^
|
|
48
|
+
"vitest": "^4.1.0"
|
|
49
49
|
},
|
|
50
50
|
"overrides": {
|
|
51
51
|
"vite": "7.3.2",
|
|
52
|
-
"brace-expansion": "5.0.
|
|
52
|
+
"brace-expansion": "^5.0.6",
|
|
53
|
+
"postcss": "8.5.10"
|
|
53
54
|
},
|
|
54
55
|
"engines": {
|
|
55
56
|
"node": ">=24.0.0"
|