@tiny-fish/cli 0.39.1-next.311 → 0.40.1-next.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -278,6 +278,13 @@ tinyfish fetch content get https://agentql.com --links --image-links
278
278
  # Bound each URL independently
279
279
  tinyfish fetch content get https://agentql.com --per-url-timeout-ms 45000
280
280
 
281
+ # Ranked verbatim snippets answering a query (beta; markdown only, max 10 URLs)
282
+ tinyfish fetch content get https://agentql.com --highlights "pricing tiers"
283
+
284
+ # Tune snippets: count (1-20, default 5), total characters, keep full-page text
285
+ tinyfish fetch content get https://agentql.com --highlights "pricing tiers" \
286
+ --max-snippets 3 --max-characters 1200 --include-full-page-text
287
+
281
288
  # Human-readable output
282
289
  tinyfish fetch content get https://agentql.com --pretty
283
290
  ```
@@ -1,6 +1,6 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
3
- import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
3
+ import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
4
4
  import { captureStdio, capturedOutput, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
5
5
  import { ensureCliAuthenticated } from '../lib/connect-auth.js';
6
6
  import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
@@ -9,6 +9,7 @@ import { ConnectInterruptedError, ConnectStepError, HoistedFailureReportedError,
9
9
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
10
10
  import { runConnectAll } from '../lib/connect-all.js';
11
11
  import { ompMcpPath, readOmpTinyfishEntry, writeOmpMcpConfig } from '../lib/omp-config.js';
12
+ import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState, piSkillDirMismatch, piMcpPath, readPiTinyfishEntry, writePiMcpConfig, } from '../lib/pi-config.js';
12
13
  import { ALL_HARNESSES, AuthMode, HARNESS_DISPLAY_NAMES, RELOAD_ACTION, } from '../lib/harness-detect.js';
13
14
  import { detectHumanInitiated } from '../lib/harness.js';
14
15
  import { TINYFISH_API_KEY_VAR } from '../lib/constants.js';
@@ -169,6 +170,22 @@ async function prepareInstallAuth(client, options, keyAuthSupported) {
169
170
  }
170
171
  return { keyedAdd, buildAddArgs };
171
172
  }
173
+ /** Inherit for an inline-OAuth add; capture otherwise, piping prompts when seeded. */
174
+ function addSpawnOptions(options, interactiveAdd, seeded) {
175
+ if (interactiveAdd)
176
+ return { stdio: 'inherit', timeout: options.authTimeoutMs };
177
+ if (!seeded)
178
+ return captureStdio(options.verbose ?? false);
179
+ return {
180
+ encoding: 'utf8',
181
+ // Without it, the 1 MiB default overflows into a misread Ctrl+C.
182
+ maxBuffer: STEP_MAX_BUFFER,
183
+ // The unattended path had no bound at all before; captureStdio's is the floor.
184
+ timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
185
+ input: seeded.stdinInput,
186
+ env: seeded.env,
187
+ };
188
+ }
172
189
  /** Returns the seeded-credential note when a key was written into a user-owned file. */
173
190
  function performMcpAdd(client, options, plan, attemptId) {
174
191
  const { keyedAdd, buildAddArgs } = plan;
@@ -183,20 +200,11 @@ function performMcpAdd(client, options, plan, attemptId) {
183
200
  const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
184
201
  // After the removals: an interrupt there leaves no key behind.
185
202
  const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
186
- const addOptions = interactiveAdd
187
- ? { stdio: 'inherit', timeout: options.authTimeoutMs }
188
- : seeded
189
- ? {
190
- encoding: 'utf8',
191
- // Without it, the 1 MiB default overflows into a misread Ctrl+C.
192
- maxBuffer: STEP_MAX_BUFFER,
193
- // The unattended path had no bound at all before; captureStdio's is the floor.
194
- timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
195
- input: seeded.stdinInput,
196
- env: seeded.env,
197
- }
198
- : captureStdio(options.verbose ?? false);
199
- const addResult = spawn.sync(client.command, addArgs, addOptions);
203
+ // Writing the entry ourselves skips the harness probe that saves it disabled.
204
+ if (seeded?.register(mcpUrl.toString())) {
205
+ return { note: seeded.note, seededHome: seeded.home };
206
+ }
207
+ const addResult = spawn.sync(client.command, addArgs, addSpawnOptions(options, interactiveAdd, seeded));
200
208
  // Rolls back first: signInStepError throws on a Ctrl+C.
201
209
  const failAdd = (build) => {
202
210
  seeded?.rollback();
@@ -214,7 +222,7 @@ function performMcpAdd(client, options, plan, attemptId) {
214
222
  // The tag keeps the path-bearing prose out of telemetry.
215
223
  { failureDetail: registration.tag }));
216
224
  }
217
- return seeded?.note;
225
+ return { note: seeded?.note, seededHome: seeded?.home };
218
226
  }
219
227
  function performLoginStep(client, pendingLogin, options) {
220
228
  errLine('Signing in to TinyFish...');
@@ -289,7 +297,7 @@ async function connectNativeMcpClient(client, options) {
289
297
  // A probed client authenticates inside `mcp add` too, so a failure there is either.
290
298
  state.stage =
291
299
  loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
292
- const seededNote = performMcpAdd(client, options, plan, telemetry.attemptId);
300
+ const { note: seededNote, seededHome } = performMcpAdd(client, options, plan, telemetry.attemptId);
293
301
  telemetry.track('checkpoint', { phase: 'registered' });
294
302
  const { authMode, signInDeferred } = completeAuthFlow(client, options, state, telemetry, {
295
303
  useKeyAuth,
@@ -301,7 +309,7 @@ async function connectNativeMcpClient(client, options) {
301
309
  state.authMode = authMode;
302
310
  // Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
303
311
  settle(state, telemetry, 'completed', { authMode });
304
- await runPostInstallSteps(client, options, state, telemetry, signInDeferred);
312
+ await runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome);
305
313
  });
306
314
  return state.authMode;
307
315
  }
@@ -317,7 +325,7 @@ function connectedLine(client, signInDeferred) {
317
325
  `${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
318
326
  }
319
327
  // Runs after `completed` settled: steps fail alone, the rest still run.
320
- async function runPostInstallSteps(client, options, state, telemetry, signInDeferred) {
328
+ async function runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome) {
321
329
  let failed = false;
322
330
  // Modelled on upgrade's attempt(); interrupts rethrow and abandon remaining steps.
323
331
  const attempt = async (stage, run) => {
@@ -348,6 +356,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
348
356
  extraPostInstall({
349
357
  apiKey: validatedApiKey(options.apiKey),
350
358
  verbose: options.verbose ?? false,
359
+ seededHome,
351
360
  });
352
361
  telemetry.track('checkpoint', {
353
362
  phase: 'plugin_installed',
@@ -501,6 +510,31 @@ export async function connectOpenClaw(options) {
501
510
  }
502
511
  });
503
512
  }
513
+ const PI_RELOAD_COPY = 'TinyFish is configured in pi. Restart pi, then run `/mcp-auth tinyfish` there if it asks you ' +
514
+ 'to sign in.';
515
+ // The skill is the working path either way, so the note must not read as a failure.
516
+ const PI_ADAPTER_DORMANT_NOTE = 'Note: pi has no built-in MCP support, so the entry in its mcp.json stays dormant until you ' +
517
+ `run \`${PI_ADAPTER_INSTALL_COMMAND}\`. The TinyFish skill works without it.`;
518
+ // Only the dormant case was spoken for, leaving a working MCP install silent.
519
+ const PI_ADAPTER_LIVE_NOTE = 'pi-mcp-adapter is installed, so pi loads the TinyFish MCP tools as well as the skill once ' +
520
+ 'you restart it.';
521
+ function piAdapterNote() {
522
+ const state = piMcpAdapterState();
523
+ if (state === 'installed')
524
+ return PI_ADAPTER_LIVE_NOTE;
525
+ return state === 'absent' ? PI_ADAPTER_DORMANT_NOTE : undefined;
526
+ }
527
+ // The `skills` CLI writes to the default dir even when pi reads another one.
528
+ function piPostConnectNotes() {
529
+ const notes = [piAdapterNote()];
530
+ const skills = piSkillDirMismatch();
531
+ if (skills) {
532
+ notes.push(`Note: PI_CODING_AGENT_DIR points pi at ${skills.readFrom}, but the TinyFish skill ` +
533
+ `installed to ${skills.installedTo}. Copy or symlink it across for pi to load it.`);
534
+ }
535
+ const printable = notes.filter(Boolean);
536
+ return printable.length > 0 ? printable.join('\n') : undefined;
537
+ }
504
538
  // Only interactive omp wires the OAuth handler; headless cannot sign in.
505
539
  const OMP_KEYLESS_COPY = 'TinyFish is configured in omp. Open omp and sign in to TinyFish when prompted, or run ' +
506
540
  '`/mcp reauth tinyfish` there.';
@@ -535,6 +569,22 @@ const CONFIG_FILE_HARNESSES = {
535
569
  reload: OMP_KEYLESS_COPY,
536
570
  },
537
571
  },
572
+ pi: {
573
+ skillTarget: { skillAgent: 'pi', displayName: HARNESS_DISPLAY_NAMES.pi },
574
+ registeredUrl: () => readPiTinyfishEntry().url,
575
+ write: writePiMcpConfig,
576
+ configPath: piMcpPath,
577
+ // No install deeplink exists; the copy carries the restart instead.
578
+ tryLaunch: () => false,
579
+ postConnectNote: piPostConnectNotes,
580
+ copy: {
581
+ verified: "TinyFish is connected and verified (health+auth). Restart pi and it's live.",
582
+ authFailed: (reason) => `TinyFish is configured, but the authenticated check failed (${reason}). ` +
583
+ 'Fix: rotate/re-enter your key with `tinyfish auth login`, then re-run: tinyfish connect pi',
584
+ launched: PI_RELOAD_COPY,
585
+ reload: PI_RELOAD_COPY,
586
+ },
587
+ },
538
588
  };
539
589
  /** Writes the harness's MCP config file itself; no `mcp add` exists. */
540
590
  async function connectConfigFileHarness(harness, options) {
@@ -628,6 +678,9 @@ async function connectConfigFileHarness(harness, options) {
628
678
  else {
629
679
  errLine(spec.copy.reload);
630
680
  }
681
+ const note = spec.postConnectNote?.();
682
+ if (note)
683
+ errLine(note);
631
684
  settle(state, telemetry, 'completed', { authMode: resolvedKey ? AuthMode.ApiKey : 'deferred' });
632
685
  });
633
686
  }
@@ -643,6 +696,7 @@ function launchCursorDeeplink(mcpUrl) {
643
696
  /** Total over the non-native harnesses: a new one fails to compile until wired here. */
644
697
  const LAUNCH_OVERRIDES = {
645
698
  omp: launchOmpWalkthrough,
699
+ pi: launchPiWalkthrough,
646
700
  openclaw: launchOpenClawWalkthrough,
647
701
  // Cursor has no chat CLI to launch.
648
702
  cursor: () => {
@@ -683,6 +737,19 @@ const CONNECTOR_OVERRIDES = {
683
737
  }
684
738
  return undefined;
685
739
  },
740
+ pi: async (options) => {
741
+ await connectConfigFileHarness('pi', options);
742
+ if (options.launch) {
743
+ try {
744
+ await launchPiWalkthrough();
745
+ }
746
+ catch (error) {
747
+ errLine(error instanceof Error ? error.message : String(error));
748
+ errLine(finishSetupHint('pi'));
749
+ }
750
+ }
751
+ return undefined;
752
+ },
686
753
  };
687
754
  function requireNative(harness) {
688
755
  const client = NATIVE_BY_HARNESS.get(harness);
@@ -1,5 +1,5 @@
1
1
  import { CLI_VERSION } from '../lib/constants.js';
2
- import { checkAuthCall, checkCliVersion, checkConnectivity, checkCredential, checkHermesPlugin, checkRegistration, provesHarnessReach, verifyHarnessKey, } from '../lib/doctor-checks.js';
2
+ import { checkAuthCall, checkCliVersion, checkConnectivity, checkCredential, checkHermesPlugin, checkPiAdapter, checkRegistration, provesHarnessReach, verifyHarnessKey, } from '../lib/doctor-checks.js';
3
3
  import { DOCTOR_COULD_NOT_RUN, DOCTOR_INVALID_INPUT, DOCTOR_SCHEMA_VERSION, doctorReportSchema, exitCodeFor, renderPretty, verdictFor, } from '../lib/doctor-report.js';
4
4
  import { applyRepairs, repairsFor } from '../lib/doctor-repairs.js';
5
5
  import { couldNotRunPayload, telemetryKey, telemetryPayload } from '../lib/doctor-telemetry.js';
@@ -30,11 +30,13 @@ export async function runDoctor(options) {
30
30
  ]);
31
31
  const hermesStatus = statuses.find((status) => status.harness === 'hermes');
32
32
  const hermesPlugin = hermesStatus ? checkHermesPlugin(hermesStatus) : undefined;
33
+ const piStatus = statuses.find((status) => status.harness === 'pi');
33
34
  const checks = [
34
35
  checkCliVersion(),
35
36
  connectivity,
36
37
  ...statuses.map((status, i) => checkRegistration(status, options.mcpUrl, keyAuths[i], !credential.key)),
37
38
  ...(hermesPlugin ? [hermesPlugin.check] : []),
39
+ ...(piStatus ? [checkPiAdapter(piStatus)] : []),
38
40
  credential.check,
39
41
  checkAuthCall(cliAuthResult),
40
42
  ];
@@ -4,14 +4,64 @@ import { fetchContentGet } from '../lib/client.js';
4
4
  import { err, handleApiError, out, outLine } from '../lib/output.js';
5
5
  const MIN_PER_URL_TIMEOUT_MS = 1;
6
6
  const MAX_PER_URL_TIMEOUT_MS = 110000;
7
- function parsePerUrlTimeoutMs(value) {
8
- const parsed = Number(value);
9
- if (!Number.isInteger(parsed) ||
10
- parsed < MIN_PER_URL_TIMEOUT_MS ||
11
- parsed > MAX_PER_URL_TIMEOUT_MS) {
12
- throw new InvalidArgumentError(`must be an integer between ${MIN_PER_URL_TIMEOUT_MS} and ${MAX_PER_URL_TIMEOUT_MS}`);
7
+ const MIN_HIGHLIGHT_SNIPPETS = 1;
8
+ const MAX_HIGHLIGHT_SNIPPETS = 20;
9
+ const MAX_URLS = 10;
10
+ function integerInRange(min, max) {
11
+ return (value) => {
12
+ const parsed = Number(value);
13
+ if (!Number.isSafeInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
14
+ throw new InvalidArgumentError(max === undefined
15
+ ? 'must be a positive integer'
16
+ : `must be an integer between ${min} and ${max}`);
17
+ }
18
+ return parsed;
19
+ };
20
+ }
21
+ function validateHighlightsOptions(opts) {
22
+ const hasSubFlag = opts.maxSnippets !== undefined ||
23
+ opts.maxCharacters !== undefined ||
24
+ opts.includeFullPageText !== undefined;
25
+ if (opts.highlights === undefined) {
26
+ return hasSubFlag
27
+ ? '--max-snippets, --max-characters and --include-full-page-text require --highlights'
28
+ : null;
29
+ }
30
+ if (opts.highlights.trim() === '') {
31
+ return '--highlights requires a non-empty query';
13
32
  }
14
- return parsed;
33
+ if (opts.format !== undefined && opts.format !== 'markdown') {
34
+ return '--highlights requires --format markdown';
35
+ }
36
+ return null;
37
+ }
38
+ function buildHighlightsParams(opts) {
39
+ if (opts.highlights === undefined)
40
+ return undefined;
41
+ return {
42
+ query: opts.highlights,
43
+ ...(opts.maxSnippets !== undefined ? { max_snippets: opts.maxSnippets } : {}),
44
+ ...(opts.maxCharacters !== undefined ? { max_characters: opts.maxCharacters } : {}),
45
+ ...(opts.includeFullPageText !== undefined
46
+ ? { include_full_page_text: opts.includeFullPageText }
47
+ : {}),
48
+ };
49
+ }
50
+ function buildFetchParams(urls, opts) {
51
+ const highlights = buildHighlightsParams(opts);
52
+ return {
53
+ urls,
54
+ ...(opts.format !== undefined ? { format: opts.format } : {}),
55
+ ...(opts.links !== undefined ? { links: opts.links } : {}),
56
+ ...(opts.imageLinks !== undefined ? { image_links: opts.imageLinks } : {}),
57
+ ...(opts.perUrlTimeoutMs !== undefined ? { per_url_timeout_ms: opts.perUrlTimeoutMs } : {}),
58
+ ...(opts.ifNoneMatch !== undefined ? { if_none_match: opts.ifNoneMatch } : {}),
59
+ ...(opts.ifModifiedSince !== undefined ? { if_modified_since: opts.ifModifiedSince } : {}),
60
+ ...(opts.includeEtagAndLastModified !== undefined
61
+ ? { include_etag_and_last_modified: opts.includeEtagAndLastModified }
62
+ : {}),
63
+ ...(highlights !== undefined ? { highlights } : {}),
64
+ };
15
65
  }
16
66
  function printPrettyFetch(response) {
17
67
  outLine(`Results: ${response.results.length}`);
@@ -32,6 +82,12 @@ function printPrettyFetch(response) {
32
82
  outLine(` ETag: ${result.etag}`);
33
83
  if (result.last_modified)
34
84
  outLine(` Last-Modified: ${result.last_modified}`);
85
+ if (result.highlights && result.highlights.length > 0) {
86
+ outLine(' Highlights:');
87
+ result.highlights.forEach((snippet, index) => {
88
+ outLine(` ${index + 1}. ${snippet.text}`);
89
+ });
90
+ }
35
91
  outLine('');
36
92
  }
37
93
  }
@@ -57,12 +113,26 @@ export function registerFetch(program) {
57
113
  .option('--format <format>', 'Output format: markdown, html, or json')
58
114
  .option('--links', 'Include extracted links')
59
115
  .option('--image-links', 'Include extracted image links')
60
- .option('--per-url-timeout-ms <milliseconds>', 'Per-URL timeout budget in milliseconds', parsePerUrlTimeoutMs)
116
+ .option('--per-url-timeout-ms <milliseconds>', 'Per-URL timeout budget in milliseconds', integerInRange(MIN_PER_URL_TIMEOUT_MS, MAX_PER_URL_TIMEOUT_MS))
61
117
  .option('--if-none-match <etag>', 'ETag validator for a conditional GET (single URL only)')
62
118
  .option('--if-modified-since <http-date>', 'Last-Modified validator for a conditional GET (single URL only)')
63
119
  .option('--include-etag-and-last-modified', 'Include etag / last_modified validators on each result')
120
+ .option('--highlights <query>', '(beta) Return ranked verbatim snippets answering the query instead of full-page text (markdown only, max 10 URLs)')
121
+ .option('--max-snippets <n>', 'Maximum snippets per URL, 1-20 (default 5; requires --highlights)', integerInRange(MIN_HIGHLIGHT_SNIPPETS, MAX_HIGHLIGHT_SNIPPETS))
122
+ .option('--max-characters <n>', 'Maximum total characters across snippets (requires --highlights)', integerInRange(1))
123
+ .option('--include-full-page-text', 'Also return full-page text alongside snippets (requires --highlights)')
64
124
  .option('--pretty', 'Human-readable output')
65
125
  .action(async (urls, opts) => {
126
+ // Mirrors the SDK cap the raw post path no longer enforces.
127
+ if (urls.length > MAX_URLS) {
128
+ err({ error: `urls must contain at most ${MAX_URLS} items` });
129
+ process.exit(1);
130
+ }
131
+ const highlightsError = validateHighlightsOptions(opts);
132
+ if (highlightsError !== null) {
133
+ err({ error: highlightsError });
134
+ process.exit(1);
135
+ }
66
136
  if ((opts.ifNoneMatch !== undefined || opts.ifModifiedSince !== undefined) &&
67
137
  urls.length > 1) {
68
138
  err({
@@ -72,22 +142,7 @@ export function registerFetch(program) {
72
142
  }
73
143
  const apiKey = getApiKey();
74
144
  try {
75
- const response = await fetchContentGet({
76
- urls,
77
- ...(opts.format !== undefined ? { format: opts.format } : {}),
78
- ...(opts.links !== undefined ? { links: opts.links } : {}),
79
- ...(opts.imageLinks !== undefined ? { image_links: opts.imageLinks } : {}),
80
- ...(opts.perUrlTimeoutMs !== undefined
81
- ? { per_url_timeout_ms: opts.perUrlTimeoutMs }
82
- : {}),
83
- ...(opts.ifNoneMatch !== undefined ? { if_none_match: opts.ifNoneMatch } : {}),
84
- ...(opts.ifModifiedSince !== undefined
85
- ? { if_modified_since: opts.ifModifiedSince }
86
- : {}),
87
- ...(opts.includeEtagAndLastModified !== undefined
88
- ? { include_etag_and_last_modified: opts.includeEtagAndLastModified }
89
- : {}),
90
- }, apiKey);
145
+ const response = await fetchContentGet(buildFetchParams(urls, opts), apiKey);
91
146
  if (opts.pretty) {
92
147
  printPrettyFetch(response);
93
148
  }
@@ -1,5 +1,6 @@
1
- import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type FetchGetContentsParams, type FetchResponse, type BrowserSession, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse, type WalletResponse } from '@tiny-fish/sdk';
2
- import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from './types.js';
1
+ import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type BrowserSession, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse, type WalletResponse } from '@tiny-fish/sdk';
2
+ import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, CliFetchGetContentsParams, CookieRecord, ProfileCreateResponse, ProfileUploadResponse, RunStepsResponse, VaultConnectRequest, VaultConnectResponse, VaultConnectionsListResponse, VaultDisconnectResponse, VaultItemsListResponse, VaultItemsSyncResponse } from './types.js';
3
+ import { z } from 'zod';
3
4
  type ConnectMap = Record<string, {
4
5
  attempt_id: string;
5
6
  }> | undefined;
@@ -12,7 +13,116 @@ export declare function listRuns(opts: RunListParams, apiKey: string, timeout?:
12
13
  export declare function getRun(runId: string, apiKey: string): Promise<Run>;
13
14
  export declare function getRunSteps(runId: string, apiKey: string): Promise<RunStepsResponse>;
14
15
  export declare function searchQuery(params: SearchQueryParams, apiKey: string): Promise<SearchQueryResponse>;
15
- export declare function fetchContentGet(params: FetchGetContentsParams, apiKey: string): Promise<FetchResponse>;
16
+ declare const cliFetchResponseSchema: z.ZodObject<{
17
+ results: z.ZodArray<z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
18
+ url: z.ZodString;
19
+ final_url: z.ZodNullable<z.ZodString>;
20
+ title: z.ZodNullable<z.ZodString>;
21
+ description: z.ZodNullable<z.ZodString>;
22
+ language: z.ZodNullable<z.ZodString>;
23
+ author: z.ZodNullable<z.ZodString>;
24
+ published_date: z.ZodNullable<z.ZodString>;
25
+ links: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
+ image_links: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
+ latency_ms: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
28
+ not_modified: z.ZodOptional<z.ZodBoolean>;
29
+ etag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ last_modified: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ format: z.ZodLiteral<"markdown">;
32
+ text: z.ZodNullable<z.ZodString>;
33
+ }, z.core.$strip>, z.ZodObject<{
34
+ url: z.ZodString;
35
+ final_url: z.ZodNullable<z.ZodString>;
36
+ title: z.ZodNullable<z.ZodString>;
37
+ description: z.ZodNullable<z.ZodString>;
38
+ language: z.ZodNullable<z.ZodString>;
39
+ author: z.ZodNullable<z.ZodString>;
40
+ published_date: z.ZodNullable<z.ZodString>;
41
+ links: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
+ image_links: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
+ latency_ms: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
44
+ not_modified: z.ZodOptional<z.ZodBoolean>;
45
+ etag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
+ last_modified: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ format: z.ZodLiteral<"html">;
48
+ text: z.ZodNullable<z.ZodString>;
49
+ }, z.core.$strip>, z.ZodObject<{
50
+ url: z.ZodString;
51
+ final_url: z.ZodNullable<z.ZodString>;
52
+ title: z.ZodNullable<z.ZodString>;
53
+ description: z.ZodNullable<z.ZodString>;
54
+ language: z.ZodNullable<z.ZodString>;
55
+ author: z.ZodNullable<z.ZodString>;
56
+ published_date: z.ZodNullable<z.ZodString>;
57
+ links: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
+ image_links: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
+ latency_ms: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
60
+ not_modified: z.ZodOptional<z.ZodBoolean>;
61
+ etag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
62
+ last_modified: z.ZodOptional<z.ZodNullable<z.ZodString>>;
63
+ format: z.ZodLiteral<"json">;
64
+ text: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodType<string | number | boolean | {
65
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
66
+ } | (string | number | boolean | {
67
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
68
+ } | (string | number | boolean | {
69
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
70
+ } | (string | number | boolean | {
71
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
72
+ } | (string | number | boolean | {
73
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
74
+ } | (string | number | boolean | {
75
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
76
+ } | (string | number | boolean | {
77
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null;
78
+ } | (string | number | boolean | {
79
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null;
80
+ } | (string | number | boolean | {
81
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null;
82
+ } | (string | number | boolean | {
83
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null;
84
+ } | (string | number | boolean | {
85
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null;
86
+ } | (string | number | boolean | {
87
+ [key: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
88
+ } | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null, unknown, z.core.$ZodTypeInternals<string | number | boolean | {
89
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
90
+ } | (string | number | boolean | {
91
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
92
+ } | (string | number | boolean | {
93
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
94
+ } | (string | number | boolean | {
95
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
96
+ } | (string | number | boolean | {
97
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
98
+ } | (string | number | boolean | {
99
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
100
+ } | (string | number | boolean | {
101
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null;
102
+ } | (string | number | boolean | {
103
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null;
104
+ } | (string | number | boolean | {
105
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null;
106
+ } | (string | number | boolean | {
107
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null;
108
+ } | (string | number | boolean | {
109
+ [key: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null;
110
+ } | (string | number | boolean | {
111
+ [key: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
112
+ } | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null, unknown>>>>;
113
+ }, z.core.$strip>], "format">, z.ZodObject<{
114
+ highlights: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
115
+ text: z.ZodString;
116
+ rank: z.ZodNumber;
117
+ }, z.core.$strip>>>>;
118
+ }, z.core.$strip>>>;
119
+ errors: z.ZodArray<z.ZodObject<{
120
+ url: z.ZodString;
121
+ error: z.ZodString;
122
+ }, z.core.$strip>>;
123
+ }, z.core.$strip>;
124
+ export type CliFetchResponse = z.infer<typeof cliFetchResponseSchema>;
125
+ export declare function fetchContentGet(params: CliFetchGetContentsParams, apiKey: string): Promise<CliFetchResponse>;
16
126
  export declare function browserSessionCreate(params: CliBrowserSessionCreateParams, apiKey: string): Promise<BrowserSession>;
17
127
  export declare function cancelRun(runId: string, apiKey: string): Promise<CancelRunResponse>;
18
128
  export declare function getWallet(apiKey: string): Promise<WalletResponse>;
@@ -1,4 +1,4 @@
1
- import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from '@tiny-fish/sdk';
1
+ import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, fetchErrorSchema, fetchResultSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from '@tiny-fish/sdk';
2
2
  import { CONNECT_SOURCE, loadConfig } from './auth.js';
3
3
  import { API_URL_OVERRIDE, CLI_AGENT_IDENTITY, CLI_VERSION } from './constants.js';
4
4
  import { detectHarness, detectHumanInitiated } from './harness.js';
@@ -281,10 +281,21 @@ export function searchQuery(params, apiKey) {
281
281
  return searchQueryResponseSchema.parse(response);
282
282
  });
283
283
  }
284
+ const highlightSnippetSchema = z.object({
285
+ text: z.string(),
286
+ rank: z.number().int().min(1),
287
+ });
288
+ // Published SDK result schema strips `highlights`; intersect to keep it.
289
+ // nullish, not optional: a null must not fail the whole response parse.
290
+ const cliFetchResponseSchema = z.object({
291
+ results: z.array(z.intersection(fetchResultSchema, z.object({ highlights: z.array(highlightSnippetSchema).nullish() }))),
292
+ errors: z.array(fetchErrorSchema),
293
+ });
284
294
  export function fetchContentGet(params, apiKey) {
285
295
  return sdk(apiKey, async (client) => {
286
- const response = await client.fetch.getContents(params);
287
- return fetchResponseSchema.parse(response);
296
+ // TODO(PROD-4260): use client.fetch.getContents once #4786 lands in an SDK release.
297
+ const response = await client.post(client.productUrl('fetch'), { json: params });
298
+ return parseWithSchema(cliFetchResponseSchema, response, 'Invalid fetch response');
288
299
  });
289
300
  }
290
301
  export function browserSessionCreate(params, apiKey) {
@@ -9,7 +9,7 @@ const HIDDEN_ONCE_SOMETHING_WORKED = new Set([
9
9
  ]);
10
10
  /** A stale config dir is a detection signal with nothing behind it. */
11
11
  export function actionable(result) {
12
- return result.detected && result.outcome !== 'stale_config';
12
+ return (result.detected && result.outcome !== 'stale_config' && result.outcome !== 'needs_first_run');
13
13
  }
14
14
  function absentSummaryLine(result, uninstall) {
15
15
  const name = DISPLAY_NAMES[result.harness];
@@ -29,6 +29,9 @@ function skippedSummaryLine(result) {
29
29
  if (result.outcome === 'stale_config') {
30
30
  return `… ${name} — config at ${result.configPath} but no \`${HARNESS_COMMANDS[result.harness]}\` on PATH, skipped. Install ${name}, then run: ${result.fixCommand}`;
31
31
  }
32
+ if (result.outcome === 'needs_first_run') {
33
+ return `… ${name} — installed but never run, so ${result.configPath} does not exist yet, skipped. Run \`${HARNESS_COMMANDS[result.harness]}\` once, then: ${result.fixCommand}`;
34
+ }
32
35
  if (result.outcome === 'not_reached') {
33
36
  return `… ${name} — not reached, the run stopped early. Fix: ${result.fixCommand}`;
34
37
  }
@@ -3,6 +3,7 @@ import { NATIVE_BY_HARNESS, openclawSkillUninstall } from './connect-clients.js'
3
3
  import { ConnectInterruptedError, spawnRemoval } from './connect-runtime.js';
4
4
  import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
5
5
  import { planOmpWrite, removeOmpMcpServer } from './omp-config.js';
6
+ import { piMcpPath, planPiWrite, removePiMcpServer } from './pi-config.js';
6
7
  import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
7
8
  import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
8
9
  import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
@@ -14,6 +15,8 @@ export function planText(harness, mcpUrl, apiKey) {
14
15
  return planCursorWrite(mcpUrl, apiKey);
15
16
  if (harness === 'omp')
16
17
  return planOmpWrite(mcpUrl, apiKey);
18
+ if (harness === 'pi')
19
+ return planPiWrite(mcpUrl, apiKey);
17
20
  // Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
18
21
  if (harness === 'hermes' && apiKey) {
19
22
  return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
@@ -29,6 +32,9 @@ export function uninstallPlanText(harness) {
29
32
  if (harness === 'omp') {
30
33
  return "would remove the tinyfish entry from omp's mcp.json (located via `omp config path`)";
31
34
  }
35
+ if (harness === 'pi') {
36
+ return `would remove the tinyfish entry from ${piMcpPath()}`;
37
+ }
32
38
  if (!uninstallRuns(harness)) {
33
39
  return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
34
40
  }
@@ -62,7 +68,8 @@ function uninstallPointer(harness) {
62
68
  }
63
69
  /** `mcp remove` clears neither the key nor the backends we wrote. */
64
70
  function clearHermesLocalState() {
65
- const home = resolveHermesHome();
71
+ const resolved = resolveHermesHome();
72
+ const home = typeof resolved === 'string' ? resolved : undefined;
66
73
  const advisories = [clearHermesEnvKey(home), clearHermesBackends(home)].filter((advisory) => advisory !== undefined);
67
74
  return advisories.length > 0 ? advisories.join(' ') : undefined;
68
75
  }
@@ -186,6 +193,10 @@ const MCP_JSON_UNINSTALLS = {
186
193
  remove: removeOmpMcpServer,
187
194
  fix: "Fix omp's mcp.json (at `omp config path`) by hand, then re-run: tinyfish connect --all --uninstall",
188
195
  },
196
+ pi: {
197
+ remove: removePiMcpServer,
198
+ fix: `Fix ${piMcpPath()} by hand, then re-run: tinyfish connect --all --uninstall`,
199
+ },
189
200
  };
190
201
  function uninstallMcpJson(detection, spec) {
191
202
  let result;
@@ -1,6 +1,6 @@
1
- import { type Harness } from './harness-detect.js';
1
+ import { type Harness, type HarnessDetection } from './harness-detect.js';
2
2
  import { type VerifyDepth } from './verify.js';
3
- export type Outcome = 'not_detected' | 'not_installed' | 'stale_config' | 'installed' | 'failed' | 'harness_too_old' | 'interrupted' | 'auth_pending' | 'no_tty_auth_skip' | 'uninstalled' | 'uninstall_noop' | 'uninstall_pointer' | 'dry_run' | 'not_reached' | 'deselected';
3
+ export type Outcome = 'not_detected' | 'not_installed' | 'stale_config' | 'needs_first_run' | 'installed' | 'failed' | 'harness_too_old' | 'interrupted' | 'auth_pending' | 'no_tty_auth_skip' | 'uninstalled' | 'uninstall_noop' | 'uninstall_pointer' | 'dry_run' | 'not_reached' | 'deselected';
4
4
  export type HarnessResultBase = {
5
5
  harness: Harness;
6
6
  detected: boolean;
@@ -34,5 +34,7 @@ export interface ConnectAllOptions {
34
34
  /** Setup-page id riding in on --url; joins "copied the command" to this run. */
35
35
  attemptId?: string;
36
36
  }
37
+ /** The harness has never created its dir, so connecting would build one it does not own. */
38
+ export declare function needsFirstRun(detection: HarnessDetection): boolean;
37
39
  export declare function runConnectAll(opts: ConnectAllOptions): Promise<number>;
38
40
  export declare const FIRST_TASK_PROMPT: string;