@tiny-fish/cli 0.30.2-next.257 → 0.30.2-next.258

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.
Files changed (77) hide show
  1. package/dist/commands/auth.d.ts +1 -1
  2. package/dist/commands/auth.js +52 -52
  3. package/dist/commands/batch.d.ts +1 -1
  4. package/dist/commands/batch.js +54 -46
  5. package/dist/commands/browser.d.ts +1 -1
  6. package/dist/commands/browser.js +14 -14
  7. package/dist/commands/config-claude.d.ts +1 -1
  8. package/dist/commands/config-claude.js +22 -22
  9. package/dist/commands/connect.d.ts +3 -3
  10. package/dist/commands/connect.js +150 -150
  11. package/dist/commands/doctor.d.ts +4 -4
  12. package/dist/commands/doctor.js +104 -104
  13. package/dist/commands/fetch.d.ts +1 -1
  14. package/dist/commands/fetch.js +24 -27
  15. package/dist/commands/onboard.d.ts +1 -1
  16. package/dist/commands/onboard.js +50 -50
  17. package/dist/commands/profile.d.ts +3 -3
  18. package/dist/commands/profile.js +45 -45
  19. package/dist/commands/run.d.ts +1 -1
  20. package/dist/commands/run.js +78 -65
  21. package/dist/commands/runs.d.ts +1 -1
  22. package/dist/commands/runs.js +63 -50
  23. package/dist/commands/search.d.ts +1 -1
  24. package/dist/commands/search.js +16 -16
  25. package/dist/commands/upgrade.d.ts +2 -2
  26. package/dist/commands/upgrade.js +36 -36
  27. package/dist/commands/vault.d.ts +1 -1
  28. package/dist/commands/vault.js +48 -45
  29. package/dist/commands/wallet.d.ts +1 -1
  30. package/dist/commands/wallet.js +14 -14
  31. package/dist/index.js +2 -2
  32. package/dist/lib/auth.d.ts +5 -5
  33. package/dist/lib/auth.js +56 -56
  34. package/dist/lib/batch-store.d.ts +1 -1
  35. package/dist/lib/batch-store.js +4 -4
  36. package/dist/lib/chrome-cookies.d.ts +1 -1
  37. package/dist/lib/chrome-cookies.js +24 -24
  38. package/dist/lib/claude-config.js +37 -37
  39. package/dist/lib/cli-install.js +12 -12
  40. package/dist/lib/client.d.ts +2 -2
  41. package/dist/lib/client.js +61 -61
  42. package/dist/lib/connect-all.js +140 -134
  43. package/dist/lib/connect-auth.d.ts +2 -2
  44. package/dist/lib/connect-auth.js +15 -15
  45. package/dist/lib/connect-clients.d.ts +6 -6
  46. package/dist/lib/connect-clients.js +143 -143
  47. package/dist/lib/connect-fallback.d.ts +1 -1
  48. package/dist/lib/connect-fallback.js +38 -38
  49. package/dist/lib/connect-picker.d.ts +4 -4
  50. package/dist/lib/connect-picker.js +28 -28
  51. package/dist/lib/connect-runtime.d.ts +7 -7
  52. package/dist/lib/connect-runtime.js +61 -61
  53. package/dist/lib/constants.js +13 -13
  54. package/dist/lib/cursor-config.d.ts +1 -1
  55. package/dist/lib/cursor-config.js +30 -30
  56. package/dist/lib/doctor-report.d.ts +2 -2
  57. package/dist/lib/doctor-report.js +22 -22
  58. package/dist/lib/harness-detect.js +39 -39
  59. package/dist/lib/harness.js +9 -9
  60. package/dist/lib/hermes-env.js +26 -23
  61. package/dist/lib/install-root.js +12 -12
  62. package/dist/lib/notice.d.ts +1 -1
  63. package/dist/lib/notice.js +12 -12
  64. package/dist/lib/output.js +14 -14
  65. package/dist/lib/registration-detect.d.ts +3 -3
  66. package/dist/lib/registration-detect.js +82 -82
  67. package/dist/lib/setup-telemetry.d.ts +4 -4
  68. package/dist/lib/setup-telemetry.js +34 -34
  69. package/dist/lib/signals.js +1 -1
  70. package/dist/lib/skill-install.d.ts +3 -3
  71. package/dist/lib/skill-install.js +40 -40
  72. package/dist/lib/types.d.ts +8 -8
  73. package/dist/lib/verify.d.ts +1 -1
  74. package/dist/lib/verify.js +28 -28
  75. package/dist/program.d.ts +1 -1
  76. package/dist/program.js +28 -28
  77. package/package.json +1 -1
package/dist/lib/auth.js CHANGED
@@ -1,38 +1,38 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import spawn from "cross-spawn";
5
- import { err, errLine } from "./output.js";
6
- import { TINYFISH_API_KEY_VAR } from "./constants.js";
7
- const KEY_PREFIXES = ["sk-tinyfish-", "sk-mino-"];
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import spawn from 'cross-spawn';
5
+ import { err, errLine } from './output.js';
6
+ import { TINYFISH_API_KEY_VAR } from './constants.js';
7
+ const KEY_PREFIXES = ['sk-tinyfish-', 'sk-mino-'];
8
8
  const MIN_KEY_LENGTH = 20;
9
- export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
9
+ export { DASHBOARD_URL, getDashboardUrl } from './constants.js';
10
10
  export function configDir() {
11
11
  // os.homedir() is cross-platform; process.env.HOME is undefined on Windows
12
- return path.join(os.homedir(), ".tinyfish");
12
+ return path.join(os.homedir(), '.tinyfish');
13
13
  }
14
14
  function configFile() {
15
- return path.join(configDir(), "config.json");
15
+ return path.join(configDir(), 'config.json');
16
16
  }
17
17
  /** Install channel baked onto connect context; the connect flow is always CLI-driven. */
18
- export const CONNECT_SOURCE = "tinyfish_cli";
18
+ export const CONNECT_SOURCE = 'tinyfish_cli';
19
19
  // An env var, not a flag: released CLIs ignore an unknown one, and the installer's PATH
20
20
  // prepend means a copied command can still run a CLI far older than the page that wrote it.
21
- export const CONNECT_ATTEMPT_ENV = "TINYFISH_CONNECT_ATTEMPT_ID";
22
- const RECORDED_AUTH_MODES = ["api-key", "oauth", "deferred"];
21
+ export const CONNECT_ATTEMPT_ENV = 'TINYFISH_CONNECT_ATTEMPT_ID';
22
+ const RECORDED_AUTH_MODES = ['api-key', 'oauth', 'deferred'];
23
23
  export function loadConfig() {
24
24
  try {
25
- const raw = JSON.parse(fs.readFileSync(configFile(), "utf8"));
26
- if (raw === null || typeof raw !== "object" || Array.isArray(raw))
25
+ const raw = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
26
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
27
27
  return {};
28
28
  const obj = raw;
29
29
  const config = {};
30
- if (typeof obj["api_key"] === "string")
31
- config.api_key = obj["api_key"];
32
- const connect = parseConnectMap(obj["connect"]);
30
+ if (typeof obj['api_key'] === 'string')
31
+ config.api_key = obj['api_key'];
32
+ const connect = parseConnectMap(obj['connect']);
33
33
  if (connect)
34
34
  config.connect = connect;
35
- const pending = parsePendingAttempt(obj["pending_connect_attempt"]);
35
+ const pending = parsePendingAttempt(obj['pending_connect_attempt']);
36
36
  if (pending)
37
37
  config.pending_connect_attempt = pending;
38
38
  return config;
@@ -42,17 +42,17 @@ export function loadConfig() {
42
42
  }
43
43
  }
44
44
  function parseConnectMap(value) {
45
- if (value === null || typeof value !== "object" || Array.isArray(value))
45
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
46
46
  return undefined;
47
47
  const out = {};
48
48
  for (const [client, entry] of Object.entries(value)) {
49
49
  const record = entry;
50
50
  const attemptId = record?.attempt_id;
51
- if (typeof attemptId !== "string")
51
+ if (typeof attemptId !== 'string')
52
52
  continue;
53
53
  const authMode = record?.auth_mode;
54
54
  out[client] =
55
- typeof authMode === "string" && RECORDED_AUTH_MODES.includes(authMode)
55
+ typeof authMode === 'string' && RECORDED_AUTH_MODES.includes(authMode)
56
56
  ? { attempt_id: attemptId, auth_mode: authMode }
57
57
  : { attempt_id: attemptId };
58
58
  }
@@ -60,7 +60,7 @@ function parseConnectMap(value) {
60
60
  }
61
61
  function parsePendingAttempt(value) {
62
62
  const entry = value;
63
- if (typeof entry?.id !== "string" || typeof entry.ts !== "number")
63
+ if (typeof entry?.id !== 'string' || typeof entry.ts !== 'number')
64
64
  return undefined;
65
65
  return isAttemptId(entry.id) ? { id: entry.id, ts: entry.ts } : undefined;
66
66
  }
@@ -146,24 +146,24 @@ export function saveConnectContext(client, attemptId, authMode) {
146
146
  export function connectContextState(client) {
147
147
  let raw;
148
148
  try {
149
- raw = fs.readFileSync(configFile(), "utf8");
149
+ raw = fs.readFileSync(configFile(), 'utf8');
150
150
  }
151
151
  catch (e) {
152
- return e.code === "ENOENT" ? "absent" : "unreadable";
152
+ return e.code === 'ENOENT' ? 'absent' : 'unreadable';
153
153
  }
154
154
  try {
155
155
  const parsed = JSON.parse(raw);
156
- const entry = parsed && typeof parsed === "object" ? parsed.connect?.[client] : undefined;
157
- return entry ? "present" : "absent";
156
+ const entry = parsed && typeof parsed === 'object' ? parsed.connect?.[client] : undefined;
157
+ return entry ? 'present' : 'absent';
158
158
  }
159
159
  catch {
160
- return "unreadable";
160
+ return 'unreadable';
161
161
  }
162
162
  }
163
163
  /** Kept marker after uninstall makes doctor offer to reinstall the harness. */
164
164
  export function clearConnectContext(client) {
165
165
  // Read-first: writing through an unreadable config would rewrite it as {}.
166
- if (connectContextState(client) !== "present")
166
+ if (connectContextState(client) !== 'present')
167
167
  return;
168
168
  try {
169
169
  updateConfig((config) => {
@@ -181,40 +181,40 @@ export function clearConnectContext(client) {
181
181
  }
182
182
  export function persistApiKeyToEnvironment(apiKey, options = {}) {
183
183
  const platform = options.platform ?? process.platform;
184
- if (platform === "win32") {
185
- const result = spawn.sync("setx", [TINYFISH_API_KEY_VAR, apiKey], { stdio: "ignore" });
184
+ if (platform === 'win32') {
185
+ const result = spawn.sync('setx', [TINYFISH_API_KEY_VAR, apiKey], { stdio: 'ignore' });
186
186
  if (result.error || result.status !== 0) {
187
- throw new Error("Could not set TINYFISH_API_KEY in the Windows user environment", {
187
+ throw new Error('Could not set TINYFISH_API_KEY in the Windows user environment', {
188
188
  cause: result.error,
189
189
  });
190
190
  }
191
191
  return;
192
192
  }
193
193
  const home = os.homedir();
194
- const shell = path.basename(options.shell ?? process.env["SHELL"] ?? "");
195
- const fish = shell === "fish";
196
- const macBashProfile = [".bash_profile", ".bash_login", ".profile"]
194
+ const shell = path.basename(options.shell ?? process.env['SHELL'] ?? '');
195
+ const fish = shell === 'fish';
196
+ const macBashProfile = ['.bash_profile', '.bash_login', '.profile']
197
197
  .map((name) => path.join(home, name)) // nosemgrep: path-join-resolve-traversal -- fixed filenames under os.homedir()
198
- .find((candidate) => fs.existsSync(candidate)) ?? path.join(home, ".bash_profile");
199
- const profile = shell === "zsh"
200
- ? path.join(home, ".zshrc")
201
- : shell === "bash"
202
- ? platform === "darwin"
198
+ .find((candidate) => fs.existsSync(candidate)) ?? path.join(home, '.bash_profile');
199
+ const profile = shell === 'zsh'
200
+ ? path.join(home, '.zshrc')
201
+ : shell === 'bash'
202
+ ? platform === 'darwin'
203
203
  ? macBashProfile
204
- : path.join(home, ".bashrc")
204
+ : path.join(home, '.bashrc')
205
205
  : fish
206
- ? path.join(home, ".config", "fish", "config.fish")
207
- : path.join(home, ".profile");
206
+ ? path.join(home, '.config', 'fish', 'config.fish')
207
+ : path.join(home, '.profile');
208
208
  const quotedKey = `'${apiKey.replaceAll("'", "'\\''")}'`;
209
209
  const prefix = fish ? `set -gx ${TINYFISH_API_KEY_VAR} ` : `export ${TINYFISH_API_KEY_VAR}=`;
210
210
  const assignment = `${prefix}${quotedKey}`;
211
- const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : "";
211
+ const existing = fs.existsSync(profile) ? fs.readFileSync(profile, 'utf8') : '';
212
212
  // Drops prior assignments, so repeated logins do not stack stale keys.
213
213
  const cleaned = existing
214
- .split("\n")
214
+ .split('\n')
215
215
  .filter((line) => !line.startsWith(prefix))
216
- .join("\n");
217
- const updated = `${cleaned}${cleaned && !cleaned.endsWith("\n") ? "\n" : ""}${assignment}\n`;
216
+ .join('\n');
217
+ const updated = `${cleaned}${cleaned && !cleaned.endsWith('\n') ? '\n' : ''}${assignment}\n`;
218
218
  fs.mkdirSync(path.dirname(profile), { recursive: true });
219
219
  fs.writeFileSync(profile, updated, { mode: 0o600 });
220
220
  fs.chmodSync(profile, 0o600);
@@ -250,19 +250,19 @@ export function validateKeyFormat(key) {
250
250
  }
251
251
  export function maskKey(key) {
252
252
  if (key.length <= 8)
253
- return "***";
254
- return key.slice(0, 12) + "..." + key.slice(-4);
253
+ return '***';
254
+ return key.slice(0, 12) + '...' + key.slice(-4);
255
255
  }
256
256
  // The one flag/env/config precedence chain; `source` says which place a caller must name.
257
257
  function lookupApiKey(explicitApiKey) {
258
258
  if (explicitApiKey)
259
- return { value: explicitApiKey, source: "explicit" };
259
+ return { value: explicitApiKey, source: 'explicit' };
260
260
  const envKey = process.env[TINYFISH_API_KEY_VAR];
261
261
  if (envKey)
262
- return { value: envKey, source: "env" };
262
+ return { value: envKey, source: 'env' };
263
263
  // No try/catch: loadConfig swallows its own errors and returns {}.
264
264
  const stored = loadConfig().api_key;
265
- return stored ? { value: stored, source: "config" } : { source: "none" };
265
+ return stored ? { value: stored, source: 'config' } : { source: 'none' };
266
266
  }
267
267
  /** Key for optional paths (telemetry, auth probes) — absent is a valid answer, so never exits. */
268
268
  export function resolvedApiKey(explicitApiKey) {
@@ -283,19 +283,19 @@ export function validatedApiKey(explicitApiKey) {
283
283
  */
284
284
  export function apiKeyStatus() {
285
285
  const { value, source } = lookupApiKey();
286
- if (!value || source === "none") {
287
- return { error: "No API key found. Run: tinyfish auth login", source: "none" };
286
+ if (!value || source === 'none') {
287
+ return { error: 'No API key found. Run: tinyfish auth login', source: 'none' };
288
288
  }
289
289
  // Shape is the server's call. A local prefix rule turns a key format we have not shipped
290
290
  // support for yet into a hard failure the user cannot work around, and the 401 says more.
291
291
  if (!isHeaderSafe(value)) {
292
- return { error: "The API key contains control characters and cannot be sent", source };
292
+ return { error: 'The API key contains control characters and cannot be sent', source };
293
293
  }
294
294
  return { key: value, source };
295
295
  }
296
296
  export function getApiKey() {
297
297
  const resolved = apiKeyStatus();
298
- if ("key" in resolved)
298
+ if ('key' in resolved)
299
299
  return resolved.key;
300
300
  err({ error: resolved.error });
301
301
  process.exit(1);
@@ -1,4 +1,4 @@
1
- import type { LocalBatch } from "./types.js";
1
+ import type { LocalBatch } from './types.js';
2
2
  export declare function loadBatches(): LocalBatch[];
3
3
  /** Appends `batch` to the persistent store. Reads existing entries first. */
4
4
  export declare function saveBatch(batch: LocalBatch): void;
@@ -1,19 +1,19 @@
1
- import * as fs from "fs";
2
- import { configDir } from "./auth.js";
1
+ import * as fs from 'fs';
2
+ import { configDir } from './auth.js';
3
3
  function batchesFile() {
4
4
  return `${configDir()}/batches.json`;
5
5
  }
6
6
  export function loadBatches() {
7
7
  const file = batchesFile();
8
8
  try {
9
- const raw = JSON.parse(fs.readFileSync(file, "utf8"));
9
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
10
10
  if (Array.isArray(raw))
11
11
  return raw;
12
12
  return [];
13
13
  }
14
14
  catch (e) {
15
15
  // File does not exist — treat as empty store (expected on first use)
16
- if (typeof e === "object" && e !== null && e.code === "ENOENT")
16
+ if (typeof e === 'object' && e !== null && e.code === 'ENOENT')
17
17
  return [];
18
18
  // Any other error (corrupt JSON, permission denied) — rethrow rather than
19
19
  // silently returning [] and risking saveBatch() overwriting the store.
@@ -1,4 +1,4 @@
1
- import type { CookieRecord } from "./types.js";
1
+ import type { CookieRecord } from './types.js';
2
2
  export interface CookiesDb {
3
3
  prepare(sql: string): {
4
4
  all(...params: string[]): unknown[];
@@ -1,7 +1,7 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createDecipheriv, pbkdf2Sync } from "node:crypto";
3
- import { createRequire } from "node:module";
4
- import { z } from "zod";
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createDecipheriv, pbkdf2Sync } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
4
+ import { z } from 'zod';
5
5
  // Reads Chrome's `Cookies` SQLite DB and decrypts values with the Safe Storage key.
6
6
  // Scheme per platform (Windows is unsupported and guarded by the caller):
7
7
  // macOS — password from the login keychain, pbkdf2(pw, "saltysalt", 1003, 16, sha1)
@@ -9,50 +9,50 @@ import { z } from "zod";
9
9
  // Values are AES-128-CBC with a 16-space IV, prefixed "v10"/"v11", and modern Chrome
10
10
  // prepends a 32-byte domain hash to the plaintext.
11
11
  const KEY_LENGTH = 16;
12
- const SALT = "saltysalt";
13
- const CBC_IV = Buffer.alloc(KEY_LENGTH, " ");
12
+ const SALT = 'saltysalt';
13
+ const CBC_IV = Buffer.alloc(KEY_LENGTH, ' ');
14
14
  const DOMAIN_HASH_LENGTH = 32;
15
- const SUPPORTED_PREFIXES = ["v10", "v11"];
15
+ const SUPPORTED_PREFIXES = ['v10', 'v11'];
16
16
  // Chrome-internal timestamp (microseconds since 1601-01-01) → Unix seconds
17
17
  const CHROME_EPOCH_DELTA_SECONDS = 11644473600;
18
18
  export function openCookiesDb(dbPath) {
19
19
  // node:sqlite is stable on the Node >=24 floor in `engines`; createRequire avoids
20
20
  // needing @types/node >=22 for the import type.
21
- const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
21
+ const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
22
22
  return new DatabaseSync(dbPath);
23
23
  }
24
24
  export function deriveChromeKey() {
25
- if (process.platform !== "darwin") {
26
- return pbkdf2Sync("peanuts", SALT, 1, KEY_LENGTH, "sha1");
25
+ if (process.platform !== 'darwin') {
26
+ return pbkdf2Sync('peanuts', SALT, 1, KEY_LENGTH, 'sha1');
27
27
  }
28
28
  let password;
29
29
  try {
30
- password = execFileSync("/usr/bin/security", ["find-generic-password", "-w", "-s", "Chrome Safe Storage", "-a", "Chrome"],
30
+ password = execFileSync('/usr/bin/security', ['find-generic-password', '-w', '-s', 'Chrome Safe Storage', '-a', 'Chrome'],
31
31
  // stderr is dropped so `security` diagnostics never pollute our JSON output.
32
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
32
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
33
33
  }
34
34
  catch {
35
35
  // Deliberately not chaining the cause — it can carry keychain material.
36
36
  throw new Error('Could not read Chrome\'s encryption key from the macOS keychain. Approve the Keychain prompt ("Always Allow") and retry.');
37
37
  }
38
- return pbkdf2Sync(password, SALT, 1003, KEY_LENGTH, "sha1");
38
+ return pbkdf2Sync(password, SALT, 1003, KEY_LENGTH, 'sha1');
39
39
  }
40
40
  // Returns null when the row cannot be decrypted — a wrong-key decrypt yields garbage,
41
41
  // which would silently upload broken cookies.
42
42
  export function decryptCookieValue(encrypted, key, stats) {
43
- const prefix = Buffer.from(encrypted.subarray(0, 3)).toString("latin1");
43
+ const prefix = Buffer.from(encrypted.subarray(0, 3)).toString('latin1');
44
44
  if (!SUPPORTED_PREFIXES.includes(prefix))
45
45
  return null;
46
46
  // Off macOS the key is Chrome's v10 "peanuts" fallback; v11 rows are
47
47
  // keyring-encrypted and would decrypt to garbage with it.
48
- if (prefix === "v11" && process.platform !== "darwin") {
48
+ if (prefix === 'v11' && process.platform !== 'darwin') {
49
49
  if (stats)
50
50
  stats.keyringSkipped++;
51
51
  return null;
52
52
  }
53
53
  let decoded;
54
54
  try {
55
- const decipher = createDecipheriv("aes-128-cbc", key, CBC_IV);
55
+ const decipher = createDecipheriv('aes-128-cbc', key, CBC_IV);
56
56
  decipher.setAutoPadding(false);
57
57
  decoded = Buffer.concat([decipher.update(encrypted.subarray(3)), decipher.final()]);
58
58
  }
@@ -64,7 +64,7 @@ export function decryptCookieValue(encrypted, key, stats) {
64
64
  const padding = decoded[decoded.length - 1];
65
65
  // A zero final byte means a legacy row with no padding and no domain-hash prefix.
66
66
  if (!padding)
67
- return decoded.toString("utf8");
67
+ return decoded.toString('utf8');
68
68
  // Wrong-key decrypts yield random bytes; require valid PKCS#7 padding or skip the row.
69
69
  if (padding > KEY_LENGTH || decoded.length - padding < DOMAIN_HASH_LENGTH)
70
70
  return null;
@@ -72,18 +72,18 @@ export function decryptCookieValue(encrypted, key, stats) {
72
72
  if (decoded[i] !== padding)
73
73
  return null;
74
74
  }
75
- return decoded.subarray(DOMAIN_HASH_LENGTH, decoded.length - padding).toString("utf8");
75
+ return decoded.subarray(DOMAIN_HASH_LENGTH, decoded.length - padding).toString('utf8');
76
76
  }
77
77
  // A cookie applies to a host when its host_key is the host itself or one of its
78
78
  // dot-delimited parents (RFC 6265 §5.1.3), with Chrome's optional leading dot.
79
79
  // Public-suffix parents (github.io, co.uk) are dropped — a row there would leak
80
80
  // into every registrable domain under the suffix.
81
81
  function hostKeyCandidates(host) {
82
- const { parse } = createRequire(import.meta.url)("tldts");
83
- const labels = host.split(".");
82
+ const { parse } = createRequire(import.meta.url)('tldts');
83
+ const labels = host.split('.');
84
84
  const parents = labels
85
85
  .slice(1, -1)
86
- .map((_, i) => labels.slice(i + 1).join("."))
86
+ .map((_, i) => labels.slice(i + 1).join('.'))
87
87
  .filter((p) => parse(p, { allowPrivateDomains: true }).domain);
88
88
  return [host, ...parents].flatMap((h) => [h, `.${h}`]);
89
89
  }
@@ -126,7 +126,7 @@ export function readCookiesForHost(db, key, host, stats) {
126
126
  `SELECT host_key, name, value, encrypted_value, is_secure, is_httponly,
127
127
  CAST(expires_utc AS REAL) AS expires_utc
128
128
  FROM cookies
129
- WHERE path = '/' AND host_key IN (${candidates.map(() => "?").join(", ")})
129
+ WHERE path = '/' AND host_key IN (${candidates.map(() => '?').join(', ')})
130
130
  ORDER BY creation_utc DESC`)
131
131
  .all(...candidates);
132
132
  // Malformed rows are skipped, matching the decrypt-failure policy.
@@ -141,7 +141,7 @@ export function readCookiesForHost(db, key, host, stats) {
141
141
  if (seen.has(row.name))
142
142
  continue;
143
143
  let value = row.value;
144
- if (value === "" && row.encrypted_value.length > 0) {
144
+ if (value === '' && row.encrypted_value.length > 0) {
145
145
  const decrypted = decryptCookieValue(row.encrypted_value, key, stats);
146
146
  if (decrypted === null)
147
147
  continue;
@@ -152,7 +152,7 @@ export function readCookiesForHost(db, key, host, stats) {
152
152
  name: row.name,
153
153
  value,
154
154
  domain: row.host_key,
155
- path: "/",
155
+ path: '/',
156
156
  secure: row.is_secure === 1,
157
157
  httpOnly: row.is_httponly === 1,
158
158
  ...(row.expires_utc > 0
@@ -1,45 +1,45 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
4
  // ─── Path helpers ─────────────────────────────────────────────────────────────
5
5
  export function claudeDir() {
6
- return path.join(os.homedir(), ".claude");
6
+ return path.join(os.homedir(), '.claude');
7
7
  }
8
8
  export function claudeSettingsPath() {
9
- return path.join(claudeDir(), "settings.json");
9
+ return path.join(claudeDir(), 'settings.json');
10
10
  }
11
11
  export function claudeMdPath() {
12
- return path.join(claudeDir(), "CLAUDE.md");
12
+ return path.join(claudeDir(), 'CLAUDE.md');
13
13
  }
14
14
  // ─── Constants ────────────────────────────────────────────────────────────────
15
- export const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
15
+ export const TINYFISH_PERMISSION = 'Bash(tinyfish:*)';
16
16
  const TINYFISH_WEBFETCH_HOOK = {
17
- matcher: "WebFetch",
17
+ matcher: 'WebFetch',
18
18
  hooks: [
19
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\"}}'",
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
22
  },
23
23
  ],
24
24
  };
25
25
  const TINYFISH_WEBSEARCH_HOOK = {
26
- matcher: "WebSearch",
26
+ matcher: 'WebSearch',
27
27
  hooks: [
28
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\"}}'",
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
31
  },
32
32
  ],
33
33
  };
34
34
  const TINYFISH_SUBAGENT_HOOK = {
35
35
  hooks: [
36
36
  {
37
- type: "command",
37
+ type: 'command',
38
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
39
  },
40
40
  ],
41
41
  };
42
- export const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
42
+ export const CLAUDE_MD_MARKER = '<!-- tinyfish-configure-claude -->';
43
43
  export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
44
44
  ## WebSearch or Search
45
45
  When looking to use Search or WebSearch, always use the TinyFish Web Search tool, run \`tinyfish search query "<query>"\`
@@ -50,38 +50,38 @@ When looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run \
50
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
51
  ${CLAUDE_MD_MARKER}`;
52
52
  function isTinyfishPreToolUseHook(entry) {
53
- if (typeof entry !== "object" || entry === null)
53
+ if (typeof entry !== 'object' || entry === null)
54
54
  return false;
55
55
  const obj = entry;
56
- if (obj.matcher !== "WebFetch" && obj.matcher !== "WebSearch")
56
+ if (obj.matcher !== 'WebFetch' && obj.matcher !== 'WebSearch')
57
57
  return false;
58
58
  const hooks = obj.hooks;
59
59
  if (!Array.isArray(hooks) || hooks.length === 0)
60
60
  return false;
61
61
  const first = hooks[0];
62
- return typeof first.command === "string" && first.command.includes("provider to use");
62
+ return typeof first.command === 'string' && first.command.includes('provider to use');
63
63
  }
64
64
  function isTinyfishSubagentHook(entry) {
65
- if (typeof entry !== "object" || entry === null)
65
+ if (typeof entry !== 'object' || entry === null)
66
66
  return false;
67
67
  const hooks = entry.hooks;
68
68
  if (!Array.isArray(hooks) || hooks.length === 0)
69
69
  return false;
70
70
  const first = hooks[0];
71
- return typeof first.command === "string" && first.command.includes("TinyFish");
71
+ return typeof first.command === 'string' && first.command.includes('TinyFish');
72
72
  }
73
73
  // ─── settings.json I/O ───────────────────────────────────────────────────────
74
74
  export function readSettingsJson() {
75
75
  try {
76
- const raw = fs.readFileSync(claudeSettingsPath(), "utf8");
76
+ const raw = fs.readFileSync(claudeSettingsPath(), 'utf8');
77
77
  const parsed = JSON.parse(raw);
78
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
78
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
79
79
  return parsed;
80
80
  }
81
- throw new Error("settings.json root is not a JSON object");
81
+ throw new Error('settings.json root is not a JSON object');
82
82
  }
83
83
  catch (e) {
84
- if (typeof e === "object" && e !== null && e.code === "ENOENT") {
84
+ if (typeof e === 'object' && e !== null && e.code === 'ENOENT') {
85
85
  return {};
86
86
  }
87
87
  throw e;
@@ -89,7 +89,7 @@ export function readSettingsJson() {
89
89
  }
90
90
  export function writeSettingsJson(settings) {
91
91
  fs.mkdirSync(claudeDir(), { recursive: true });
92
- fs.writeFileSync(claudeSettingsPath(), JSON.stringify(settings, null, 2) + "\n");
92
+ fs.writeFileSync(claudeSettingsPath(), JSON.stringify(settings, null, 2) + '\n');
93
93
  }
94
94
  // ─── settings.json merge / remove ─────────────────────────────────────────────
95
95
  export function mergeSettings(existing) {
@@ -119,7 +119,7 @@ export function removeFromSettings(existing) {
119
119
  result.permissions = { ...permissions, allow: filtered };
120
120
  }
121
121
  else {
122
- const rest = Object.fromEntries(Object.entries(permissions).filter(([k]) => k !== "allow"));
122
+ const rest = Object.fromEntries(Object.entries(permissions).filter(([k]) => k !== 'allow'));
123
123
  result.permissions = Object.keys(rest).length > 0 ? rest : undefined;
124
124
  }
125
125
  }
@@ -140,11 +140,11 @@ export function removeFromSettings(existing) {
140
140
  // ─── CLAUDE.md I/O ────────────────────────────────────────────────────────────
141
141
  export function readClaudeMd() {
142
142
  try {
143
- return fs.readFileSync(claudeMdPath(), "utf8");
143
+ return fs.readFileSync(claudeMdPath(), 'utf8');
144
144
  }
145
145
  catch (e) {
146
- if (typeof e === "object" && e !== null && e.code === "ENOENT") {
147
- return "";
146
+ if (typeof e === 'object' && e !== null && e.code === 'ENOENT') {
147
+ return '';
148
148
  }
149
149
  throw e;
150
150
  }
@@ -155,9 +155,9 @@ export function writeClaudeMd(content) {
155
155
  }
156
156
  // ─── CLAUDE.md merge / remove ─────────────────────────────────────────────────
157
157
  function escapeRegExp(s) {
158
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
159
159
  }
160
- const MARKER_REGEX = new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}`, "m");
160
+ const MARKER_REGEX = new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}`, 'm');
161
161
  const LEGACY_REGEX = /## Web(?:Search|Seach) or Search[\s\S]*?(?:tinyfish fetch[^\n]*\n(?:[^\n]*tinyfish[^\n]*\n)*[^\n]*)\n*/m;
162
162
  export function hasMarker(content) {
163
163
  return content.includes(CLAUDE_MD_MARKER);
@@ -170,22 +170,22 @@ export function mergeClaudeMd(existing) {
170
170
  return { content: existing.replace(MARKER_REGEX, CLAUDE_MD_BLOCK), replaced: true };
171
171
  }
172
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";
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
175
  return { content, replaced: true };
176
176
  }
177
177
  if (existing.trim().length === 0) {
178
- return { content: CLAUDE_MD_BLOCK + "\n", replaced: false };
178
+ return { content: CLAUDE_MD_BLOCK + '\n', replaced: false };
179
179
  }
180
- return { content: CLAUDE_MD_BLOCK + "\n\n" + existing, replaced: false };
180
+ return { content: CLAUDE_MD_BLOCK + '\n\n' + existing, replaced: false };
181
181
  }
182
182
  export function removeFromClaudeMd(existing) {
183
183
  if (!existing.includes(CLAUDE_MD_MARKER))
184
184
  return existing;
185
185
  const removed = existing
186
- .replace(new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}\\n*`, "m"), "")
186
+ .replace(new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}\\n*`, 'm'), '')
187
187
  .trim();
188
- return removed.length > 0 ? removed + "\n" : "";
188
+ return removed.length > 0 ? removed + '\n' : '';
189
189
  }
190
190
  // ─── Detection ────────────────────────────────────────────────────────────────
191
191
  export function isTinyfishConfiguredInSettings(settings) {
@@ -1,8 +1,8 @@
1
- import spawn from "cross-spawn";
2
- import { spawnStepError } from "./connect-runtime.js";
3
- import { TINYFISH_CLI_PACKAGE } from "./constants.js";
4
- import { installRoot } from "./install-root.js";
5
- import { errLine } from "./output.js";
1
+ import spawn from 'cross-spawn';
2
+ import { spawnStepError } from './connect-runtime.js';
3
+ import { TINYFISH_CLI_PACKAGE } from './constants.js';
4
+ import { installRoot } from './install-root.js';
5
+ import { errLine } from './output.js';
6
6
  export const SKILL_INSTALL_TIMEOUT_MS = 120_000;
7
7
  const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
8
8
  // spawnSync caps piped output at 1 MiB by default.
@@ -12,15 +12,15 @@ export class HoistedFailureReportedError extends Error {
12
12
  }
13
13
  export function captureStdio(verbose) {
14
14
  return verbose
15
- ? { stdio: "inherit", timeout: SKILL_INSTALL_TIMEOUT_MS }
16
- : { encoding: "utf8", maxBuffer: STEP_MAX_BUFFER, timeout: SKILL_INSTALL_TIMEOUT_MS };
15
+ ? { stdio: 'inherit', timeout: SKILL_INSTALL_TIMEOUT_MS }
16
+ : { encoding: 'utf8', maxBuffer: STEP_MAX_BUFFER, timeout: SKILL_INSTALL_TIMEOUT_MS };
17
17
  }
18
18
  /** Inherited stdio leaves these null; only piped runs yield text. */
19
19
  export function capturedOutput(result) {
20
20
  return `${asText(result.stdout)}${asText(result.stderr)}`;
21
21
  }
22
22
  function asText(stream) {
23
- return typeof stream === "string" ? stream : "";
23
+ return typeof stream === 'string' ? stream : '';
24
24
  }
25
25
  /** A verbose run captured nothing, so this is a no-op. */
26
26
  export function replay(output) {
@@ -29,14 +29,14 @@ export function replay(output) {
29
29
  }
30
30
  export function installTinyFishCli({ verbose, announce }) {
31
31
  if (announce ?? verbose)
32
- errLine("Installing the TinyFish CLI...");
32
+ errLine('Installing the TinyFish CLI...');
33
33
  // Without the prefix, a curl install upgrades npm's global tree and never the PATH binary.
34
34
  const root = installRoot();
35
- const prefixArgs = root ? ["--prefix", root.prefix] : [];
36
- const result = spawn.sync("npm", ["install", "--global", ...prefixArgs, TINYFISH_CLI_INSTALL_SPEC], captureStdio(verbose));
35
+ const prefixArgs = root ? ['--prefix', root.prefix] : [];
36
+ const result = spawn.sync('npm', ['install', '--global', ...prefixArgs, TINYFISH_CLI_INSTALL_SPEC], captureStdio(verbose));
37
37
  if (result.error || result.status !== 0) {
38
38
  // Built first: it runs the interrupt check, and abandonment must not replay.
39
- const error = spawnStepError("Could not install the TinyFish CLI", result);
39
+ const error = spawnStepError('Could not install the TinyFish CLI', result);
40
40
  replay(capturedOutput(result));
41
41
  throw error;
42
42
  }