@tiny-fish/cli 0.9.1-next.124 → 0.10.1

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.
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as path from "node:path";
3
3
  import spawn from "cross-spawn";
4
- import { persistApiKeyToEnvironment, saveConfig, validateKeyFormat } from "../lib/auth.js";
4
+ import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConfig, saveConnectContext, validateKeyFormat, } from "../lib/auth.js";
5
5
  import { CLI_VERSION } from "../lib/constants.js";
6
6
  import { errLine } from "../lib/output.js";
7
7
  const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
@@ -347,7 +347,7 @@ async function connectNativeMcpClient(client, options) {
347
347
  for (const removal of client.removals)
348
348
  removeExistingRegistration(client, removal);
349
349
  const mcpUrl = new URL(options.mcpUrl);
350
- mcpUrl.searchParams.set("source", "tinyfish_cli");
350
+ mcpUrl.searchParams.set("source", CONNECT_SOURCE);
351
351
  mcpUrl.searchParams.set("client", client.connectClient);
352
352
  mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
353
353
  failureStage = client.loginArgs ? "registration" : "registration_or_authentication";
@@ -378,6 +378,9 @@ async function connectNativeMcpClient(client, options) {
378
378
  installWebSkill(client);
379
379
  failureStage = "authentication";
380
380
  ensureCliAuthenticated(client.connectClient, options.apiKey);
381
+ // Persist once here (skill installed + authenticated) so both success paths, launch
382
+ // or not, record the install; buildConnectHeaders reads it on subsequent requests.
383
+ saveConnectContext(client.connectClient, telemetry.attemptId);
381
384
  if (!options.launch) {
382
385
  errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
383
386
  await telemetry.track("completed");
@@ -449,6 +452,7 @@ export async function connectOpenClaw(options) {
449
452
  });
450
453
  }
451
454
  }
455
+ saveConnectContext("openclaw", telemetry.attemptId);
452
456
  if (!options.launch) {
453
457
  errLine("TinyFish is connected. Open OpenClaw to start using it.");
454
458
  await telemetry.track("completed");
@@ -1,10 +1,17 @@
1
1
  export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
2
2
  export declare function configDir(): string;
3
+ /** Install channel baked onto connect context; the connect flow is always CLI-driven. */
4
+ export declare const CONNECT_SOURCE = "tinyfish_cli";
5
+ interface ConnectEntry {
6
+ attempt_id: string;
7
+ }
3
8
  interface TinyfishConfig {
4
9
  api_key?: string;
10
+ connect?: Record<string, ConnectEntry>;
5
11
  }
6
12
  export declare function loadConfig(): TinyfishConfig;
7
13
  export declare function saveConfig(apiKey: string): void;
14
+ export declare function saveConnectContext(client: string, attemptId: string): void;
8
15
  interface EnvironmentOptions {
9
16
  platform?: typeof process.platform;
10
17
  shell?: string;
package/dist/lib/auth.js CHANGED
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
4
  import spawn from "cross-spawn";
5
- import { err } from "./output.js";
5
+ import { err, errLine } from "./output.js";
6
6
  const KEY_PREFIXES = ["sk-tinyfish-", "sk-mino-"];
7
7
  const MIN_KEY_LENGTH = 20;
8
8
  export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
@@ -13,19 +13,37 @@ export function configDir() {
13
13
  function configFile() {
14
14
  return path.join(configDir(), "config.json");
15
15
  }
16
+ /** Install channel baked onto connect context; the connect flow is always CLI-driven. */
17
+ export const CONNECT_SOURCE = "tinyfish_cli";
16
18
  export function loadConfig() {
17
19
  try {
18
20
  const raw = JSON.parse(fs.readFileSync(configFile(), "utf8"));
19
- if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
20
- const obj = raw;
21
- return { api_key: typeof obj["api_key"] === "string" ? obj["api_key"] : undefined };
22
- }
23
- return {};
21
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
22
+ return {};
23
+ const obj = raw;
24
+ const config = {};
25
+ if (typeof obj["api_key"] === "string")
26
+ config.api_key = obj["api_key"];
27
+ const connect = parseConnectMap(obj["connect"]);
28
+ if (connect)
29
+ config.connect = connect;
30
+ return config;
24
31
  }
25
32
  catch {
26
33
  return {};
27
34
  }
28
35
  }
36
+ function parseConnectMap(value) {
37
+ if (value === null || typeof value !== "object" || Array.isArray(value))
38
+ return undefined;
39
+ const out = {};
40
+ for (const [client, entry] of Object.entries(value)) {
41
+ const attemptId = entry?.attempt_id;
42
+ if (typeof attemptId === "string")
43
+ out[client] = { attempt_id: attemptId };
44
+ }
45
+ return Object.keys(out).length > 0 ? out : undefined;
46
+ }
29
47
  export function saveConfig(apiKey) {
30
48
  const dir = configDir();
31
49
  try {
@@ -42,6 +60,20 @@ export function saveConfig(apiKey) {
42
60
  process.exit(1);
43
61
  }
44
62
  }
63
+ export function saveConnectContext(client, attemptId) {
64
+ const dir = configDir();
65
+ try {
66
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
67
+ const existing = loadConfig();
68
+ const connect = { ...existing.connect, [client]: { attempt_id: attemptId } };
69
+ fs.writeFileSync(configFile(), JSON.stringify({ ...existing, connect }), { mode: 0o600 });
70
+ }
71
+ catch (e) {
72
+ // Best-effort attribution marker: a write failure must not fail a connect that has
73
+ // already succeeded (MCP registered, skill installed, authenticated). Warn and continue.
74
+ errLine(`Warning: could not persist connect context: ${e instanceof Error ? e.message : String(e)}`);
75
+ }
76
+ }
45
77
  export function persistApiKeyToEnvironment(apiKey, options = {}) {
46
78
  const platform = options.platform ?? process.platform;
47
79
  if (platform === "win32") {
@@ -1,5 +1,9 @@
1
1
  import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type FetchGetContentsParams, type FetchResponse, type BrowserSession, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse } from "@tiny-fish/sdk";
2
2
  import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from "./types.js";
3
+ type ConnectMap = Record<string, {
4
+ attempt_id: string;
5
+ }> | undefined;
6
+ export declare function buildConnectHeaders(connect: ConnectMap, caller: string | null): Record<string, string>;
3
7
  export declare function runSync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunResponse>;
4
8
  export declare function runAsync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
5
9
  export declare function runStream(req: CliAgentRunParams, apiKey: string, signal?: AbortSignal): AsyncGenerator<AgentRunWithStreamingResponse>;
@@ -20,3 +24,4 @@ export declare function vaultConnect(req: VaultConnectRequest, apiKey: string):
20
24
  export declare function vaultDisconnect(connectionId: string, apiKey: string): Promise<VaultDisconnectResponse>;
21
25
  export declare function vaultItems(apiKey: string): Promise<VaultItemsListResponse>;
22
26
  export declare function vaultSync(apiKey: string): Promise<VaultItemsSyncResponse>;
27
+ export {};
@@ -1,8 +1,33 @@
1
1
  import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from "@tiny-fish/sdk";
2
+ import { CONNECT_SOURCE, loadConfig } from "./auth.js";
2
3
  import { API_URL_OVERRIDE, CLI_VERSION } from "./constants.js";
3
4
  import { detectHarness, detectHumanInitiated } from "./harness.js";
4
5
  import { ApiError } from "./output.js";
5
6
  import { z } from "zod";
7
+ // Install-level connect context (PF-3169). A known caller is credited only to its own
8
+ // install; under an unknown caller we credit a lone install, else send source only.
9
+ // Never guess a client. Request-level harness truth is client_name (PF-3168).
10
+ export function buildConnectHeaders(connect, caller) {
11
+ if (!connect)
12
+ return {};
13
+ const clients = Object.keys(connect);
14
+ if (clients.length === 0)
15
+ return {};
16
+ if (caller) {
17
+ const entry = connect[caller];
18
+ return entry ? connectHeadersFor(caller, entry.attempt_id) : {};
19
+ }
20
+ if (clients.length === 1)
21
+ return connectHeadersFor(clients[0], connect[clients[0]].attempt_id);
22
+ return { "X-TF-Connect-Source": CONNECT_SOURCE };
23
+ }
24
+ function connectHeadersFor(client, attemptId) {
25
+ return {
26
+ "X-TF-Connect-Source": CONNECT_SOURCE,
27
+ "X-TF-Connect-Client": client,
28
+ "X-TF-Connect-Attempt-Id": attemptId,
29
+ };
30
+ }
6
31
  class TinyFishCliClient extends TinyFish {
7
32
  _buildHeaders() {
8
33
  const base = super._buildHeaders();
@@ -28,6 +53,14 @@ class TinyFishCliClient extends TinyFish {
28
53
  headers["X-TF-Client-Version"] = clientVersion;
29
54
  // Agent-vs-human signal, independent of harness detection or skill install.
30
55
  headers["X-TF-Human-Initiated"] = detectHumanInitiated() ? "true" : "false";
56
+ // Reuse the already-resolved client_name so connect_client can never diverge from it
57
+ // (same sanitize + precedence, not a second raw read of TF_CLIENT_NAME).
58
+ const connectCaller = clientName || null;
59
+ for (const [key, value] of Object.entries(buildConnectHeaders(loadConfig().connect, connectCaller))) {
60
+ const clean = sanitize(value);
61
+ if (clean)
62
+ headers[key] = clean;
63
+ }
31
64
  return headers;
32
65
  }
33
66
  // The SDK base client only exposes get/post/postStream; DELETE is needed for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.9.1-next.124",
3
+ "version": "0.10.1",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "prepublishOnly": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@tiny-fish/sdk": "^0.0.11",
35
+ "@tiny-fish/sdk": "^0.2.0",
36
36
  "chrome-cookies-secure": "^3.0.2",
37
37
  "commander": "^12.0.0",
38
38
  "cross-spawn": "^7.0.6",