@tiny-fish/cli 0.12.1-next.139 → 0.12.1-next.143
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 +1 -1
- package/dist/commands/connect.js +5 -4
- package/dist/lib/connect-all.js +3 -3
- package/dist/lib/notice.d.ts +8 -0
- package/dist/lib/notice.js +53 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +5 -4
- package/package.json +3 -2
- package/dist/lib/version-marker.d.ts +0 -10
- package/dist/lib/version-marker.js +0 -109
package/README.md
CHANGED
|
@@ -263,7 +263,7 @@ tinyfish agent run "..." --url https://example.com --use-vault \
|
|
|
263
263
|
|
|
264
264
|
By default all commands output newline-delimited JSON to stdout — pipe-friendly for agents and scripts. Add `--pretty` for human-readable output.
|
|
265
265
|
|
|
266
|
-
Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically.
|
|
266
|
+
Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically. The server may also attach an advisory upgrade notice to stderr: a plain line in a terminal, and JSON (`{"notice":…,"level":…}`) in non-interactive use. The JSON message is composed locally from the notice level, never from server-sent prose. It never changes stdout or the exit code.
|
|
267
267
|
|
|
268
268
|
`--output-schema` and `--output-schema-file` are mutually exclusive. Both inputs must parse to a top-level JSON object. The CLI only validates that outer shape locally; the API performs full schema validation and may return errors such as `INVALID_INPUT` or feature-flag access failures.
|
|
269
269
|
|
package/dist/commands/connect.js
CHANGED
|
@@ -6,10 +6,10 @@ import { CLI_VERSION } from "../lib/constants.js";
|
|
|
6
6
|
import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig } from "../lib/cursor-config.js";
|
|
7
7
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
8
8
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
9
|
+
import { captureNotice, emitNotice } from "../lib/notice.js";
|
|
9
10
|
import { errLine } from "../lib/output.js";
|
|
10
11
|
import { telemetryDisabled, telemetryHeaders } from "../lib/setup-telemetry.js";
|
|
11
12
|
import { verifyMcpAuth } from "../lib/verify.js";
|
|
12
|
-
import { recordInstalledVersion } from "../lib/version-marker.js";
|
|
13
13
|
export const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
|
|
14
14
|
const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
|
|
15
15
|
const SKILL_INSTALL_TIMEOUT_MS = 120_000;
|
|
@@ -254,6 +254,8 @@ function createConnectTelemetry(mcpUrl, client, apiKey) {
|
|
|
254
254
|
redirect: "error",
|
|
255
255
|
signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS),
|
|
256
256
|
});
|
|
257
|
+
// Before the ok branch so an error response still yields a notice.
|
|
258
|
+
captureNotice(response.headers);
|
|
257
259
|
if (response.ok)
|
|
258
260
|
return;
|
|
259
261
|
if (response.status < 500) {
|
|
@@ -591,7 +593,6 @@ async function connectNativeMcpClient(client, options) {
|
|
|
591
593
|
// Persist once here (skill installed + authenticated) so both success paths, launch
|
|
592
594
|
// or not, record the install; buildConnectHeaders reads it on subsequent requests.
|
|
593
595
|
saveConnectContext(client.connectClient, telemetry.attemptId);
|
|
594
|
-
recordInstalledVersion();
|
|
595
596
|
errLine(UPGRADE_HINT);
|
|
596
597
|
if (!options.launch) {
|
|
597
598
|
errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
|
|
@@ -662,7 +663,6 @@ export async function connectOpenClaw(options) {
|
|
|
662
663
|
}
|
|
663
664
|
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
664
665
|
saveConnectContext("openclaw", telemetry.attemptId);
|
|
665
|
-
recordInstalledVersion();
|
|
666
666
|
errLine(UPGRADE_HINT);
|
|
667
667
|
if (!options.launch) {
|
|
668
668
|
errLine("TinyFish is connected. Open OpenClaw to start using it.");
|
|
@@ -706,7 +706,6 @@ export async function connectCursor(options) {
|
|
|
706
706
|
}
|
|
707
707
|
telemetry.track("checkpoint", { phase: "registered" });
|
|
708
708
|
saveConnectContext("cursor", telemetry.attemptId);
|
|
709
|
-
recordInstalledVersion();
|
|
710
709
|
if (resolvedKey) {
|
|
711
710
|
// Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
|
|
712
711
|
const verify = await verifyMcpAuth(resolvedKey);
|
|
@@ -841,5 +840,7 @@ export function registerConnect(program) {
|
|
|
841
840
|
client +
|
|
842
841
|
". Supported clients: claude-code, codex, cursor, hermes, openclaw");
|
|
843
842
|
}
|
|
843
|
+
// Deferred to here so the notice lands after install output, not interleaved with it.
|
|
844
|
+
emitNotice();
|
|
844
845
|
});
|
|
845
846
|
}
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -4,11 +4,11 @@ import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectO
|
|
|
4
4
|
import { loadConfig, validateKeyFormat } from "./auth.js";
|
|
5
5
|
import { detectInstalledHarnesses, FIVE_HARNESSES, } from "./harness-detect.js";
|
|
6
6
|
import { detectHumanInitiated } from "./harness.js";
|
|
7
|
+
import { emitNotice } from "./notice.js";
|
|
7
8
|
import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
|
|
8
9
|
import { errLine, outLine } from "./output.js";
|
|
9
10
|
import { sendSetupCompleted } from "./setup-telemetry.js";
|
|
10
11
|
import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
|
|
11
|
-
import { printUpdateNoticeIfDrifted, recordInstalledVersion } from "./version-marker.js";
|
|
12
12
|
const DISPLAY_NAMES = {
|
|
13
13
|
"claude-code": "Claude Code",
|
|
14
14
|
codex: "Codex",
|
|
@@ -153,7 +153,6 @@ async function processHarness(detection, opts, prompt) {
|
|
|
153
153
|
if (!interruptedBefore && process.exitCode === 130) {
|
|
154
154
|
return { ...base, outcome: "interrupted", fixCommand: `tinyfish connect ${harness}` };
|
|
155
155
|
}
|
|
156
|
-
recordInstalledVersion();
|
|
157
156
|
const verify = await verifyHarness(harness, opts.mcpUrl, resolvedApiKey(opts.apiKey));
|
|
158
157
|
return {
|
|
159
158
|
...base,
|
|
@@ -292,7 +291,8 @@ export async function runConnectAll(opts) {
|
|
|
292
291
|
verify_ok: r.verifyOk ?? null,
|
|
293
292
|
}));
|
|
294
293
|
await sendSetupCompleted(opts.mcpUrl, harnessTelemetry, resolvedApiKey(opts.apiKey));
|
|
295
|
-
|
|
294
|
+
// After the summary and before the launch, which is where the user is reading.
|
|
295
|
+
emitNotice();
|
|
296
296
|
}
|
|
297
297
|
const exitCode = computeExitCode(results);
|
|
298
298
|
const interrupted = results.some((r) => r.outcome === "interrupted");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Test-only: the capture slot and the once-per-invocation latch are module state. */
|
|
2
|
+
export declare function resetNoticeState(): void;
|
|
3
|
+
/** Records the first notice seen; printing is deferred so it lands at the end of a command. */
|
|
4
|
+
export declare function captureNotice(headers: Headers): void;
|
|
5
|
+
/** Advisory only, never changes stdout, the exit code, or control flow. */
|
|
6
|
+
export declare function emitNotice(): void;
|
|
7
|
+
/** For callers where the response is the end of the command, such as one-shot API calls. */
|
|
8
|
+
export declare function surfaceNotice(headers: Headers): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Surfaces the advisory X-TF-Notice header the server attaches to outdated clients (PF-3301).
|
|
2
|
+
import { CLI_VERSION } from "./constants.js";
|
|
3
|
+
import { detectHumanInitiated } from "./harness.js";
|
|
4
|
+
import { err, errLine, sanitizeLine } from "./output.js";
|
|
5
|
+
const NOTICE_HEADER = "x-tf-notice";
|
|
6
|
+
const LEVEL_HEADER = "x-tf-notice-level";
|
|
7
|
+
const MAX_LENGTH = 300;
|
|
8
|
+
const VALID_LEVELS = new Set(["required", "recommended"]);
|
|
9
|
+
let captured;
|
|
10
|
+
let printed = false;
|
|
11
|
+
/** Test-only: the capture slot and the once-per-invocation latch are module state. */
|
|
12
|
+
export function resetNoticeState() {
|
|
13
|
+
captured = undefined;
|
|
14
|
+
printed = false;
|
|
15
|
+
}
|
|
16
|
+
/** Records the first notice seen; printing is deferred so it lands at the end of a command. */
|
|
17
|
+
export function captureNotice(headers) {
|
|
18
|
+
if (captured)
|
|
19
|
+
return;
|
|
20
|
+
if (typeof headers?.get !== "function")
|
|
21
|
+
return;
|
|
22
|
+
const message = sanitizeLine(headers.get(NOTICE_HEADER) ?? "")
|
|
23
|
+
.trim()
|
|
24
|
+
.slice(0, MAX_LENGTH);
|
|
25
|
+
if (!message)
|
|
26
|
+
return;
|
|
27
|
+
const rawLevel = headers.get(LEVEL_HEADER)?.trim().toLowerCase();
|
|
28
|
+
// Spread rather than `level: undefined`, which exactOptionalPropertyTypes rejects.
|
|
29
|
+
captured = { message, ...(rawLevel && VALID_LEVELS.has(rawLevel) ? { level: rawLevel } : {}) };
|
|
30
|
+
}
|
|
31
|
+
/** Built here, not by the server, so nothing it sends can reach an agent's context as instructions. */
|
|
32
|
+
function agentMessage(level) {
|
|
33
|
+
return level === "required"
|
|
34
|
+
? `This TinyFish CLI (${CLI_VERSION}) is below the required minimum. Run: tinyfish upgrade`
|
|
35
|
+
: `A TinyFish CLI update is available (you have ${CLI_VERSION}). Run: tinyfish upgrade`;
|
|
36
|
+
}
|
|
37
|
+
/** Advisory only, never changes stdout, the exit code, or control flow. */
|
|
38
|
+
export function emitNotice() {
|
|
39
|
+
if (printed || !captured)
|
|
40
|
+
return;
|
|
41
|
+
printed = true;
|
|
42
|
+
const { message, level } = captured;
|
|
43
|
+
if (detectHumanInitiated()) {
|
|
44
|
+
errLine(message);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
err({ notice: agentMessage(level), ...(level ? { level } : {}) });
|
|
48
|
+
}
|
|
49
|
+
/** For callers where the response is the end of the command, such as one-shot API calls. */
|
|
50
|
+
export function surfaceNotice(headers) {
|
|
51
|
+
captureNotice(headers);
|
|
52
|
+
emitNotice();
|
|
53
|
+
}
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare class ApiError extends Error {
|
|
|
4
4
|
constructor(status: number, message: string, code?: string | undefined);
|
|
5
5
|
}
|
|
6
6
|
export declare function out(data: unknown): void;
|
|
7
|
+
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
8
|
+
export declare function sanitizeLine(text: string): string;
|
|
7
9
|
export declare function outLine(line: string): void;
|
|
8
10
|
export declare function errLine(line: string): void;
|
|
9
11
|
export declare function err(data: unknown): void;
|
package/dist/lib/output.js
CHANGED
|
@@ -12,20 +12,21 @@ export class ApiError extends Error {
|
|
|
12
12
|
export function out(data) {
|
|
13
13
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
16
|
+
export function sanitizeLine(text) {
|
|
16
17
|
// eslint-disable-next-line no-control-regex
|
|
17
18
|
return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
18
19
|
}
|
|
19
20
|
export function outLine(line) {
|
|
20
|
-
process.stdout.write(
|
|
21
|
+
process.stdout.write(sanitizeLine(line) + "\n");
|
|
21
22
|
}
|
|
22
23
|
export function errLine(line) {
|
|
23
|
-
process.stderr.write(
|
|
24
|
+
process.stderr.write(sanitizeLine(line) + "\n");
|
|
24
25
|
}
|
|
25
26
|
export function err(data) {
|
|
26
27
|
const payload = typeof data === "string" ? { error: data } : data;
|
|
27
28
|
const serialized = JSON.stringify(payload) ?? "undefined";
|
|
28
|
-
process.stderr.write(
|
|
29
|
+
process.stderr.write(sanitizeLine(serialized) + "\n");
|
|
29
30
|
}
|
|
30
31
|
export function handleApiError(e) {
|
|
31
32
|
if (e instanceof ApiError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.12.1-next.
|
|
3
|
+
"version": "0.12.1-next.143",
|
|
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.
|
|
35
|
+
"@tiny-fish/sdk": "^0.3.0",
|
|
36
36
|
"chrome-cookies-secure": "^3.0.2",
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
38
|
"cross-spawn": "^7.0.6",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"brace-expansion": "^5.0.9",
|
|
57
57
|
"ip-address": ">=10.3.1",
|
|
58
58
|
"postcss": ">=8.5.23",
|
|
59
|
+
"nanoid": "^3.3.17",
|
|
59
60
|
"tar": "^7.5.21"
|
|
60
61
|
},
|
|
61
62
|
"engines": {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** Install-time, offline-safe; best-effort — never fails a finished install. */
|
|
2
|
-
export declare function recordInstalledVersion(): void;
|
|
3
|
-
interface UpdateCheckResult {
|
|
4
|
-
updateAvailable: boolean;
|
|
5
|
-
latest?: string;
|
|
6
|
-
}
|
|
7
|
-
/** Throttled npm-currency check; never auto-mutates the install. */
|
|
8
|
-
export declare function checkForCliUpdate(now?: number): Promise<UpdateCheckResult>;
|
|
9
|
-
export declare function printUpdateNoticeIfDrifted(): Promise<void>;
|
|
10
|
-
export {};
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { configDir } from "./auth.js";
|
|
5
|
-
import { CLI_VERSION } from "./constants.js";
|
|
6
|
-
import { errLine } from "./output.js";
|
|
7
|
-
import { telemetryDisabled } from "./setup-telemetry.js";
|
|
8
|
-
const MARKER_FILE = "skill-version-marker.json";
|
|
9
|
-
const THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
10
|
-
const CHECK_TIMEOUT_MS = 5_000;
|
|
11
|
-
// npm is the only currency signal with a publisher behind it: the cookbook repo has no
|
|
12
|
-
// releases and no release automation, so a tag check would report "up to date" forever.
|
|
13
|
-
const REGISTRY_URL = "https://registry.npmjs.org/@tiny-fish/cli/latest";
|
|
14
|
-
const markerSchema = z.object({
|
|
15
|
-
last_checked_at: z.number().optional(),
|
|
16
|
-
last_known_latest: z.string().optional(),
|
|
17
|
-
});
|
|
18
|
-
function markerPath() {
|
|
19
|
-
return path.join(configDir(), MARKER_FILE);
|
|
20
|
-
}
|
|
21
|
-
function readMarker() {
|
|
22
|
-
try {
|
|
23
|
-
const parsed = markerSchema.safeParse(JSON.parse(fs.readFileSync(markerPath(), "utf8")));
|
|
24
|
-
return parsed.success ? parsed.data : undefined;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function writeMarker(state) {
|
|
31
|
-
fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
32
|
-
const tmpPath = `${markerPath()}.tmp-${process.pid}`;
|
|
33
|
-
fs.writeFileSync(tmpPath, JSON.stringify(state), { mode: 0o600 });
|
|
34
|
-
fs.renameSync(tmpPath, markerPath());
|
|
35
|
-
}
|
|
36
|
-
/** Install-time, offline-safe; best-effort — never fails a finished install. */
|
|
37
|
-
export function recordInstalledVersion() {
|
|
38
|
-
try {
|
|
39
|
-
writeMarker({ ...readMarker() });
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// marker is a nice-to-have
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const SEMVER = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
|
46
|
-
/** Strictly newer; false on unparsable input. */
|
|
47
|
-
function isNewer(current, latest) {
|
|
48
|
-
const c = SEMVER.exec(current);
|
|
49
|
-
const l = SEMVER.exec(latest);
|
|
50
|
-
if (!c || !l)
|
|
51
|
-
return false;
|
|
52
|
-
for (let i = 1; i <= 3; i++) {
|
|
53
|
-
const cn = Number(c[i]);
|
|
54
|
-
const ln = Number(l[i]);
|
|
55
|
-
if (ln !== cn)
|
|
56
|
-
return ln > cn;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
/** Throttled npm-currency check; never auto-mutates the install. */
|
|
61
|
-
export async function checkForCliUpdate(now = Date.now()) {
|
|
62
|
-
const marker = readMarker();
|
|
63
|
-
// No marker means the CLI never installed anything — nothing to nag about.
|
|
64
|
-
if (!marker || telemetryDisabled())
|
|
65
|
-
return { updateAvailable: false };
|
|
66
|
-
const cached = () => ({
|
|
67
|
-
updateAvailable: marker.last_known_latest !== undefined && isNewer(CLI_VERSION, marker.last_known_latest),
|
|
68
|
-
...(marker.last_known_latest !== undefined ? { latest: marker.last_known_latest } : {}),
|
|
69
|
-
});
|
|
70
|
-
// Time-based only — failed fetch must not stall every invocation.
|
|
71
|
-
if (marker.last_checked_at !== undefined && now - marker.last_checked_at < THROTTLE_MS) {
|
|
72
|
-
return cached();
|
|
73
|
-
}
|
|
74
|
-
let latest;
|
|
75
|
-
try {
|
|
76
|
-
const response = await fetch(REGISTRY_URL, {
|
|
77
|
-
headers: { accept: "application/json" },
|
|
78
|
-
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
79
|
-
});
|
|
80
|
-
if (response.ok) {
|
|
81
|
-
const body = (await response.json());
|
|
82
|
-
if (typeof body.version === "string")
|
|
83
|
-
latest = body.version;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
// Unreachable registry — don't fail setup, don't erase the cache.
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
writeMarker({
|
|
91
|
-
...marker,
|
|
92
|
-
last_checked_at: now,
|
|
93
|
-
last_known_latest: latest ?? marker.last_known_latest,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// best-effort
|
|
98
|
-
}
|
|
99
|
-
// Fall back to the cache so consecutive invocations can't disagree.
|
|
100
|
-
if (!latest)
|
|
101
|
-
return cached();
|
|
102
|
-
return { updateAvailable: isNewer(CLI_VERSION, latest), latest };
|
|
103
|
-
}
|
|
104
|
-
export async function printUpdateNoticeIfDrifted() {
|
|
105
|
-
const { updateAvailable } = await checkForCliUpdate();
|
|
106
|
-
if (updateAvailable) {
|
|
107
|
-
errLine("a newer TinyFish CLI is available — re-run `npx @tiny-fish/cli connect --all`");
|
|
108
|
-
}
|
|
109
|
-
}
|