@tiny-fish/cli 0.13.1-next.147 → 0.13.1-next.149

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.
@@ -3,7 +3,7 @@ export declare const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
3
3
  export declare const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
4
4
  export declare const DEFAULT_ONBOARDING_PROMPT: string;
5
5
  export declare const OPENCLAW_ONBOARDING_PROMPT: string;
6
- export type AgentClient = "claude-code" | "codex" | "cursor" | "hermes" | "openclaw";
6
+ export type AgentClient = "claude-code" | "codex" | "cursor" | "hermes" | "openclaw" | "opencode";
7
7
  /** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
8
8
  export declare class ConnectInterruptedError extends Error {
9
9
  }
@@ -32,6 +32,11 @@ export declare function connectHermes(options: {
32
32
  mcpUrl: string;
33
33
  launch: boolean;
34
34
  }): Promise<void>;
35
+ export declare function connectOpencode(options: {
36
+ apiKey?: string;
37
+ mcpUrl: string;
38
+ launch: boolean;
39
+ }): Promise<void>;
35
40
  export declare function connectOpenClaw(options: {
36
41
  mcpUrl: string;
37
42
  launch: boolean;
@@ -24,6 +24,13 @@ const HERMES_OAUTH_UNAVAILABLE_MESSAGE = "This Hermes installation does not supp
24
24
  "retry, or add TinyFish manually with `hermes mcp add`.";
25
25
  const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = "This OpenClaw installation does not support global skill installation. Update OpenClaw and " +
26
26
  "retry.";
27
+ const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = "This OpenCode installation does not support one-command MCP setup (`opencode mcp add --url`). " +
28
+ "Update OpenCode and retry, or add TinyFish manually with `opencode mcp add`.";
29
+ // OpenCode is model-agnostic (unlike Claude Code / Codex), so a fresh install can default to a
30
+ // model with no tool support — TinyFish runs entirely on tool calls, so it would fail there.
31
+ const OPENCODE_MODEL_NOTE = "Note: TinyFish runs on tool calls, so OpenCode needs a model that supports tool use. Image-only " +
32
+ 'models (e.g. Nano Banana Pro) will show "No endpoints found that support tool use" — switch ' +
33
+ "OpenCode's model if the walkthrough can't start.";
27
34
  const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
28
35
  const TINYFISH_CLI_PACKAGE = "@tiny-fish/cli@latest";
29
36
  export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
@@ -217,6 +224,40 @@ const HERMES = {
217
224
  addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl, "--auth", "oauth"],
218
225
  launchWalkthrough: launchHermesWalkthrough,
219
226
  };
227
+ // OpenCode's TUI takes a positional as a project directory, so a bare `opencode "<prompt>"`
228
+ // would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
229
+ // (opencode.ai/docs/cli), so the onboarding guide fires just like the other agents.
230
+ function launchOpencode() {
231
+ const result = spawn.sync("opencode", ["--prompt", DEFAULT_ONBOARDING_PROMPT], {
232
+ stdio: "inherit",
233
+ });
234
+ if (result.error) {
235
+ throw new Error("Could not launch OpenCode", { cause: result.error });
236
+ }
237
+ if (result.signal === "SIGINT" || result.signal === "SIGTERM")
238
+ return;
239
+ if (result.status !== 0) {
240
+ throw new Error(`OpenCode exited with status ${result.status ?? "unknown"}`);
241
+ }
242
+ }
243
+ const OPENCODE = {
244
+ command: "opencode",
245
+ connectClient: "opencode",
246
+ skillAgent: "opencode",
247
+ displayName: "OpenCode",
248
+ supportCheck: {
249
+ args: ["mcp", "add", "--help"],
250
+ patterns: [/--url(?:\s|$)/m],
251
+ unavailableMessage: OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE,
252
+ },
253
+ // `opencode mcp add` upserts by name and there is no `opencode mcp remove`, so no cleanup step.
254
+ removals: [],
255
+ // OpenCode writes ~/.config/opencode/opencode.jsonc itself; `mcp auth` is the separate OAuth step.
256
+ addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl],
257
+ loginArgs: ["mcp", "auth", "tinyfish"],
258
+ launchWalkthrough: launchOpencode,
259
+ postConnectNote: OPENCODE_MODEL_NOTE,
260
+ };
220
261
  const OPENCLAW = {
221
262
  command: "openclaw",
222
263
  displayName: "OpenClaw",
@@ -237,12 +278,12 @@ function storedApiKey() {
237
278
  function createConnectTelemetry(mcpUrl, client, apiKey) {
238
279
  const attemptId = randomUUID();
239
280
  const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
240
- const telemetryKey = apiKey || process.env["TINYFISH_API_KEY"] || storedApiKey();
241
- const headers = telemetryHeaders(endpoint, telemetryKey);
242
281
  const pending = [];
243
282
  async function deliver(body) {
244
283
  if (telemetryDisabled())
245
284
  return;
285
+ // Per event: `tinyfish auth login` writes the config from a subprocess mid-connect.
286
+ const headers = telemetryHeaders(endpoint, apiKey || process.env["TINYFISH_API_KEY"] || storedApiKey());
246
287
  // One retry: stale keep-alive sockets eat post-install POSTs. 5xx retries;
247
288
  // 4xx never will (schema mismatch, e.g. new stages hitting an old server).
248
289
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -380,7 +421,7 @@ const SKILL_INSTALL_FAILURE_PATTERN = /Failed to install/;
380
421
  export function updateWebSkill() {
381
422
  // One entry per `tinyfish connect <client>`, persisted by PF-3169.
382
423
  const connected = loadConfig().connect ?? {};
383
- const skillAgents = [CLAUDE_CODE, CODEX, HERMES]
424
+ const skillAgents = [CLAUDE_CODE, CODEX, HERMES, OPENCODE]
384
425
  .filter((client) => connected[client.connectClient])
385
426
  .map((client) => client.skillAgent);
386
427
  const hasOpenClaw = Boolean(connected["openclaw"]);
@@ -594,6 +635,8 @@ async function connectNativeMcpClient(client, options) {
594
635
  // or not, record the install; buildConnectHeaders reads it on subsequent requests.
595
636
  saveConnectContext(client.connectClient, telemetry.attemptId);
596
637
  errLine(UPGRADE_HINT);
638
+ if (client.postConnectNote)
639
+ errLine(client.postConnectNote);
597
640
  if (!options.launch) {
598
641
  errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
599
642
  settle(state, telemetry, "completed");
@@ -614,6 +657,9 @@ export async function connectCodex(options) {
614
657
  export async function connectHermes(options) {
615
658
  await connectNativeMcpClient(HERMES, options);
616
659
  }
660
+ export async function connectOpencode(options) {
661
+ await connectNativeMcpClient(OPENCODE, options);
662
+ }
617
663
  export async function connectOpenClaw(options) {
618
664
  const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw");
619
665
  const state = { stage: "prerequisite_check", settled: false };
@@ -749,6 +795,9 @@ export function launchAgent(client) {
749
795
  else if (client === "hermes") {
750
796
  launchNativeMcpClient(HERMES);
751
797
  }
798
+ else if (client === "opencode") {
799
+ launchNativeMcpClient(OPENCODE);
800
+ }
752
801
  else if (client === "openclaw") {
753
802
  launchOpenClawWalkthrough();
754
803
  }
@@ -765,7 +814,7 @@ export function registerConnect(program) {
765
814
  program
766
815
  .command("connect")
767
816
  .description("Connect TinyFish to an AI agent")
768
- .argument("[client]", "Agent client to connect (claude-code, codex, cursor, hermes, or openclaw)")
817
+ .argument("[client]", "Agent client to connect (claude-code, codex, cursor, hermes, openclaw, or opencode)")
769
818
  .option("--all", "Detect and connect every supported harness found on this machine")
770
819
  .option("--dry-run", "Print planned writes per harness without touching anything (--all only)")
771
820
  .option("--uninstall", "Remove TinyFish entries written by connect --all")
@@ -826,6 +875,9 @@ export function registerConnect(program) {
826
875
  else if (client === "hermes") {
827
876
  await connectHermes(connectOptions);
828
877
  }
878
+ else if (client === "opencode") {
879
+ await connectOpencode(connectOptions);
880
+ }
829
881
  else if (client === "openclaw") {
830
882
  await connectOpenClaw({ mcpUrl: connectOptions.mcpUrl, launch: connectOptions.launch });
831
883
  }
@@ -840,7 +892,7 @@ export function registerConnect(program) {
840
892
  else {
841
893
  throw new Error("Unsupported client: " +
842
894
  client +
843
- ". Supported clients: claude-code, codex, cursor, hermes, openclaw");
895
+ ". Supported clients: claude-code, codex, cursor, hermes, openclaw, opencode");
844
896
  }
845
897
  // Deferred to here so the notice lands after install output, not interleaved with it.
846
898
  emitNotice();
@@ -3,7 +3,7 @@ import { getApiKey, saveConfig } from "../lib/auth.js";
3
3
  import { fetchContentGet, searchQuery } from "../lib/client.js";
4
4
  import { BASE_URL } from "../lib/constants.js";
5
5
  import { errLine, handleApiError, outLine } from "../lib/output.js";
6
- import { connectClaudeCode, connectCodex, connectHermes, connectOpenClaw, launchAgent, } from "./connect.js";
6
+ import { connectClaudeCode, connectCodex, connectHermes, connectOpenClaw, connectOpencode, launchAgent, } from "./connect.js";
7
7
  const EXAMPLE_QUERIES = [
8
8
  "What are recent changes in the AI dev stack? Give me the top 5 changes that matter, why they matter, and any action needed.",
9
9
  "Find a new-grad software engineering role posted in the last 24 hours and fetch the job description.",
@@ -14,6 +14,7 @@ const AGENTS = [
14
14
  { id: "codex", label: "Codex", connect: connectCodex },
15
15
  { id: "hermes", label: "Hermes", connect: connectHermes },
16
16
  { id: "openclaw", label: "OpenClaw", connect: connectOpenClaw },
17
+ { id: "opencode", label: "OpenCode", connect: connectOpencode },
17
18
  ];
18
19
  function cleanFetchedContent(text) {
19
20
  const cleaned = text
@@ -208,7 +209,7 @@ export function registerOnboard(program) {
208
209
  .argument("[query]", "A real topic to research")
209
210
  .addHelpText("after", "\nExamples:\n" +
210
211
  ' tinyfish onboard "What changed in the AI dev stack this week?"\n' +
211
- "\nAgents: claude-code, codex, hermes, openclaw.\n" +
212
+ "\nAgents: claude-code, codex, hermes, openclaw, opencode.\n" +
212
213
  "Run `tinyfish connect <client> --launch` later to add one directly.")
213
214
  .action(async (query) => {
214
215
  const apiKey = getApiKey();
@@ -21,6 +21,10 @@ const HARNESS_FINGERPRINTS = [
21
21
  // prefix so we catch a Codex run regardless of which of its vars are set.
22
22
  { name: "codex", matches: (env) => hasVarWithPrefix(env, "CODEX_") },
23
23
  { name: "hermes", matches: (env) => hasVarWithPrefix(env, "HERMES_") },
24
+ // OpenCode sets a bare `OPENCODE` env var globally at startup (opencode#9292), inherited by
25
+ // the commands it shells out. Exact key, not a prefix — `OPENCODE_*` are user config inputs.
26
+ // Sourced from OpenCode's docs, not yet confirmed against scripts/harness-probe.
27
+ { name: "opencode", matches: (env) => Boolean(env["OPENCODE"]) },
24
28
  {
25
29
  name: "claude-code",
26
30
  matches: (env) => env["CLAUDECODE"] === "1" || Boolean(env["CLAUDE_CODE_ENTRYPOINT"]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.13.1-next.147",
3
+ "version": "0.13.1-next.149",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {