@tiny-fish/cli 0.13.1-next.145 → 0.13.1-next.148

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",
@@ -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 };
@@ -686,7 +732,9 @@ export async function connectCursor(options) {
686
732
  state.stage = "authentication";
687
733
  ensureCliAuthenticated("cursor", options.apiKey);
688
734
  telemetry.track("checkpoint", { phase: "authenticated" });
689
- const resolvedKey = options.apiKey ?? process.env["TINYFISH_API_KEY"] ?? loadConfig().api_key;
735
+ // loadConfig() is unvalidated on read — a hand-edited key must not reach mcp.json.
736
+ const storedKey = options.apiKey ?? process.env["TINYFISH_API_KEY"] ?? loadConfig().api_key;
737
+ const resolvedKey = storedKey && validateKeyFormat(storedKey) ? storedKey : undefined;
690
738
  state.stage = "registration";
691
739
  errLine("Adding TinyFish to Cursor...");
692
740
  const mcpUrl = new URL(options.mcpUrl);
@@ -747,6 +795,9 @@ export function launchAgent(client) {
747
795
  else if (client === "hermes") {
748
796
  launchNativeMcpClient(HERMES);
749
797
  }
798
+ else if (client === "opencode") {
799
+ launchNativeMcpClient(OPENCODE);
800
+ }
750
801
  else if (client === "openclaw") {
751
802
  launchOpenClawWalkthrough();
752
803
  }
@@ -763,7 +814,7 @@ export function registerConnect(program) {
763
814
  program
764
815
  .command("connect")
765
816
  .description("Connect TinyFish to an AI agent")
766
- .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)")
767
818
  .option("--all", "Detect and connect every supported harness found on this machine")
768
819
  .option("--dry-run", "Print planned writes per harness without touching anything (--all only)")
769
820
  .option("--uninstall", "Remove TinyFish entries written by connect --all")
@@ -808,8 +859,8 @@ export function registerConnect(program) {
808
859
  process.exitCode = exitCode;
809
860
  return;
810
861
  }
811
- if (options.dryRun || options.uninstall) {
812
- throw new Error("--dry-run and --uninstall are only supported with --all.");
862
+ if (options.dryRun || options.uninstall || options.skipLaunch) {
863
+ throw new Error("--dry-run, --uninstall and --skip-launch are only supported with --all.");
813
864
  }
814
865
  if (!client) {
815
866
  throw new Error("Pass a client (claude-code, codex, cursor, hermes, openclaw) or --all.");
@@ -824,6 +875,9 @@ export function registerConnect(program) {
824
875
  else if (client === "hermes") {
825
876
  await connectHermes(connectOptions);
826
877
  }
878
+ else if (client === "opencode") {
879
+ await connectOpencode(connectOptions);
880
+ }
827
881
  else if (client === "openclaw") {
828
882
  await connectOpenClaw({ mcpUrl: connectOptions.mcpUrl, launch: connectOptions.launch });
829
883
  }
@@ -838,7 +892,7 @@ export function registerConnect(program) {
838
892
  else {
839
893
  throw new Error("Unsupported client: " +
840
894
  client +
841
- ". Supported clients: claude-code, codex, cursor, hermes, openclaw");
895
+ ". Supported clients: claude-code, codex, cursor, hermes, openclaw, opencode");
842
896
  }
843
897
  // Deferred to here so the notice lands after install output, not interleaved with it.
844
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.145",
3
+ "version": "0.13.1-next.148",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {