@tiny-fish/cli 0.1.5-next.50 → 0.1.6

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.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerConfigureClaude(program: Command): void;
@@ -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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.1.5-next.50",
3
+ "version": "0.1.6",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {