@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.
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +52 -52
- package/dist/commands/batch.d.ts +1 -1
- package/dist/commands/batch.js +54 -46
- package/dist/commands/browser.d.ts +1 -1
- package/dist/commands/browser.js +14 -14
- package/dist/commands/config-claude.d.ts +1 -1
- package/dist/commands/config-claude.js +22 -22
- package/dist/commands/connect.d.ts +3 -3
- package/dist/commands/connect.js +150 -150
- package/dist/commands/doctor.d.ts +4 -4
- package/dist/commands/doctor.js +104 -104
- package/dist/commands/fetch.d.ts +1 -1
- package/dist/commands/fetch.js +24 -27
- package/dist/commands/onboard.d.ts +1 -1
- package/dist/commands/onboard.js +50 -50
- package/dist/commands/profile.d.ts +3 -3
- package/dist/commands/profile.js +45 -45
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +78 -65
- package/dist/commands/runs.d.ts +1 -1
- package/dist/commands/runs.js +63 -50
- package/dist/commands/search.d.ts +1 -1
- package/dist/commands/search.js +16 -16
- package/dist/commands/upgrade.d.ts +2 -2
- package/dist/commands/upgrade.js +36 -36
- package/dist/commands/vault.d.ts +1 -1
- package/dist/commands/vault.js +48 -45
- package/dist/commands/wallet.d.ts +1 -1
- package/dist/commands/wallet.js +14 -14
- package/dist/index.js +2 -2
- package/dist/lib/auth.d.ts +5 -5
- package/dist/lib/auth.js +56 -56
- package/dist/lib/batch-store.d.ts +1 -1
- package/dist/lib/batch-store.js +4 -4
- package/dist/lib/chrome-cookies.d.ts +1 -1
- package/dist/lib/chrome-cookies.js +24 -24
- package/dist/lib/claude-config.js +37 -37
- package/dist/lib/cli-install.js +12 -12
- package/dist/lib/client.d.ts +2 -2
- package/dist/lib/client.js +61 -61
- package/dist/lib/connect-all.js +140 -134
- package/dist/lib/connect-auth.d.ts +2 -2
- package/dist/lib/connect-auth.js +15 -15
- package/dist/lib/connect-clients.d.ts +6 -6
- package/dist/lib/connect-clients.js +143 -143
- package/dist/lib/connect-fallback.d.ts +1 -1
- package/dist/lib/connect-fallback.js +38 -38
- package/dist/lib/connect-picker.d.ts +4 -4
- package/dist/lib/connect-picker.js +28 -28
- package/dist/lib/connect-runtime.d.ts +7 -7
- package/dist/lib/connect-runtime.js +61 -61
- package/dist/lib/constants.js +13 -13
- package/dist/lib/cursor-config.d.ts +1 -1
- package/dist/lib/cursor-config.js +30 -30
- package/dist/lib/doctor-report.d.ts +2 -2
- package/dist/lib/doctor-report.js +22 -22
- package/dist/lib/harness-detect.js +39 -39
- package/dist/lib/harness.js +9 -9
- package/dist/lib/hermes-env.js +26 -23
- package/dist/lib/install-root.js +12 -12
- package/dist/lib/notice.d.ts +1 -1
- package/dist/lib/notice.js +12 -12
- package/dist/lib/output.js +14 -14
- package/dist/lib/registration-detect.d.ts +3 -3
- package/dist/lib/registration-detect.js +82 -82
- package/dist/lib/setup-telemetry.d.ts +4 -4
- package/dist/lib/setup-telemetry.js +34 -34
- package/dist/lib/signals.js +1 -1
- package/dist/lib/skill-install.d.ts +3 -3
- package/dist/lib/skill-install.js +40 -40
- package/dist/lib/types.d.ts +8 -8
- package/dist/lib/verify.d.ts +1 -1
- package/dist/lib/verify.js +28 -28
- package/dist/program.d.ts +1 -1
- package/dist/program.js +28 -28
- package/package.json +1 -1
package/dist/lib/auth.js
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as os from
|
|
3
|
-
import * as path from
|
|
4
|
-
import spawn from
|
|
5
|
-
import { err, errLine } from
|
|
6
|
-
import { TINYFISH_API_KEY_VAR } from
|
|
7
|
-
const KEY_PREFIXES = [
|
|
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
|
|
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(),
|
|
12
|
+
return path.join(os.homedir(), '.tinyfish');
|
|
13
13
|
}
|
|
14
14
|
function configFile() {
|
|
15
|
-
return path.join(configDir(),
|
|
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 =
|
|
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 =
|
|
22
|
-
const RECORDED_AUTH_MODES = [
|
|
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(),
|
|
26
|
-
if (raw === null || typeof 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[
|
|
31
|
-
config.api_key = obj[
|
|
32
|
-
const connect = parseConnectMap(obj[
|
|
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[
|
|
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 !==
|
|
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 !==
|
|
51
|
+
if (typeof attemptId !== 'string')
|
|
52
52
|
continue;
|
|
53
53
|
const authMode = record?.auth_mode;
|
|
54
54
|
out[client] =
|
|
55
|
-
typeof 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 !==
|
|
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(),
|
|
149
|
+
raw = fs.readFileSync(configFile(), 'utf8');
|
|
150
150
|
}
|
|
151
151
|
catch (e) {
|
|
152
|
-
return e.code ===
|
|
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 ===
|
|
157
|
-
return entry ?
|
|
156
|
+
const entry = parsed && typeof parsed === 'object' ? parsed.connect?.[client] : undefined;
|
|
157
|
+
return entry ? 'present' : 'absent';
|
|
158
158
|
}
|
|
159
159
|
catch {
|
|
160
|
-
return
|
|
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) !==
|
|
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 ===
|
|
185
|
-
const result = spawn.sync(
|
|
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(
|
|
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[
|
|
195
|
-
const fish = shell ===
|
|
196
|
-
const macBashProfile = [
|
|
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,
|
|
199
|
-
const profile = shell ===
|
|
200
|
-
? path.join(home,
|
|
201
|
-
: shell ===
|
|
202
|
-
? platform ===
|
|
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,
|
|
204
|
+
: path.join(home, '.bashrc')
|
|
205
205
|
: fish
|
|
206
|
-
? path.join(home,
|
|
207
|
-
: path.join(home,
|
|
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,
|
|
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(
|
|
214
|
+
.split('\n')
|
|
215
215
|
.filter((line) => !line.startsWith(prefix))
|
|
216
|
-
.join(
|
|
217
|
-
const updated = `${cleaned}${cleaned && !cleaned.endsWith(
|
|
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) +
|
|
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:
|
|
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:
|
|
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:
|
|
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 ===
|
|
287
|
-
return { error:
|
|
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:
|
|
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 (
|
|
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
|
|
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;
|
package/dist/lib/batch-store.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import { configDir } from
|
|
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,
|
|
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 ===
|
|
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,7 +1,7 @@
|
|
|
1
|
-
import { execFileSync } from
|
|
2
|
-
import { createDecipheriv, pbkdf2Sync } from
|
|
3
|
-
import { createRequire } from
|
|
4
|
-
import { z } from
|
|
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 =
|
|
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 = [
|
|
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)(
|
|
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 !==
|
|
26
|
-
return pbkdf2Sync(
|
|
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(
|
|
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:
|
|
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,
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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)(
|
|
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(() =>
|
|
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 ===
|
|
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
|
|
2
|
-
import * as os from
|
|
3
|
-
import * as path from
|
|
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(),
|
|
6
|
+
return path.join(os.homedir(), '.claude');
|
|
7
7
|
}
|
|
8
8
|
export function claudeSettingsPath() {
|
|
9
|
-
return path.join(claudeDir(),
|
|
9
|
+
return path.join(claudeDir(), 'settings.json');
|
|
10
10
|
}
|
|
11
11
|
export function claudeMdPath() {
|
|
12
|
-
return path.join(claudeDir(),
|
|
12
|
+
return path.join(claudeDir(), 'CLAUDE.md');
|
|
13
13
|
}
|
|
14
14
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
|
-
export const TINYFISH_PERMISSION =
|
|
15
|
+
export const TINYFISH_PERMISSION = 'Bash(tinyfish:*)';
|
|
16
16
|
const TINYFISH_WEBFETCH_HOOK = {
|
|
17
|
-
matcher:
|
|
17
|
+
matcher: 'WebFetch',
|
|
18
18
|
hooks: [
|
|
19
19
|
{
|
|
20
|
-
type:
|
|
21
|
-
command:
|
|
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:
|
|
26
|
+
matcher: 'WebSearch',
|
|
27
27
|
hooks: [
|
|
28
28
|
{
|
|
29
|
-
type:
|
|
30
|
-
command:
|
|
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:
|
|
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 =
|
|
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 !==
|
|
53
|
+
if (typeof entry !== 'object' || entry === null)
|
|
54
54
|
return false;
|
|
55
55
|
const obj = entry;
|
|
56
|
-
if (obj.matcher !==
|
|
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 ===
|
|
62
|
+
return typeof first.command === 'string' && first.command.includes('provider to use');
|
|
63
63
|
}
|
|
64
64
|
function isTinyfishSubagentHook(entry) {
|
|
65
|
-
if (typeof entry !==
|
|
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 ===
|
|
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(),
|
|
76
|
+
const raw = fs.readFileSync(claudeSettingsPath(), 'utf8');
|
|
77
77
|
const parsed = JSON.parse(raw);
|
|
78
|
-
if (parsed !== null && typeof parsed ===
|
|
78
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
79
79
|
return parsed;
|
|
80
80
|
}
|
|
81
|
-
throw new Error(
|
|
81
|
+
throw new Error('settings.json root is not a JSON object');
|
|
82
82
|
}
|
|
83
83
|
catch (e) {
|
|
84
|
-
if (typeof e ===
|
|
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) +
|
|
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 !==
|
|
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(),
|
|
143
|
+
return fs.readFileSync(claudeMdPath(), 'utf8');
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
|
146
|
-
if (typeof e ===
|
|
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)}`,
|
|
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,
|
|
174
|
-
const content = cleaned.length > 0 ? CLAUDE_MD_BLOCK +
|
|
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 +
|
|
178
|
+
return { content: CLAUDE_MD_BLOCK + '\n', replaced: false };
|
|
179
179
|
}
|
|
180
|
-
return { content: CLAUDE_MD_BLOCK +
|
|
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*`,
|
|
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 +
|
|
188
|
+
return removed.length > 0 ? removed + '\n' : '';
|
|
189
189
|
}
|
|
190
190
|
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
191
191
|
export function isTinyfishConfiguredInSettings(settings) {
|
package/dist/lib/cli-install.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import spawn from
|
|
2
|
-
import { spawnStepError } from
|
|
3
|
-
import { TINYFISH_CLI_PACKAGE } from
|
|
4
|
-
import { installRoot } from
|
|
5
|
-
import { errLine } from
|
|
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:
|
|
16
|
-
: { encoding:
|
|
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 ===
|
|
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(
|
|
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 ? [
|
|
36
|
-
const result = spawn.sync(
|
|
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(
|
|
39
|
+
const error = spawnStepError('Could not install the TinyFish CLI', result);
|
|
40
40
|
replay(capturedOutput(result));
|
|
41
41
|
throw error;
|
|
42
42
|
}
|