Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion n8n/credentials/TinyfishApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class TinyfishApi implements ICredentialType {

displayName = 'TinyFish Web Agent API';

icon = { light: 'file:tinyfish.svg', dark: 'file:tinyfish.svg' } as const;
icon = 'file:tinyfish.svg' as const;

documentationUrl = 'https://docs.mino.ai';

Expand Down
81 changes: 15 additions & 66 deletions n8n/nodes/Tinyfish/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import { NodeApiError, NodeOperationError } from 'n8n-workflow';

const API_BASE_URL = 'https://agent.tinyfish.ai';

const MAX_RETRIES = 3;
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);

/**
* Map known TinyFish API error codes to actionable user messages.
*/
Expand Down Expand Up @@ -45,33 +42,14 @@ function getActionableMessage(error: unknown): string | undefined {
return `Invalid input: ${message || 'Validation failed'}. Check your URL and goal parameters.`;
}
case 'RATE_LIMIT_EXCEEDED':
return 'Rate limit exceeded after multiple retries. Wait a few minutes and try again, or reduce request frequency.';
return 'Rate limit exceeded. Wait a few minutes and try again, or reduce request frequency.';
case 'INTERNAL_ERROR':
return `TinyFish server error: ${message || 'An unexpected error occurred'}. Retries exhausted — try again later.`;
return `TinyFish server error: ${message || 'An unexpected error occurred'}. Try again later.`;
default:
return undefined;
}
}

/**
* Check if an error has a retryable HTTP status code.
*/
function isRetryable(error: unknown): boolean {
const httpCode = (error as Record<string, unknown>)?.httpCode as number | undefined;
if (httpCode && RETRYABLE_STATUS_CODES.has(httpCode)) return true;

const cause = (error as Record<string, unknown>)?.cause as Record<string, unknown> | undefined;
const status = cause?.status as number | undefined;
return status !== undefined && RETRYABLE_STATUS_CODES.has(status);
}

/**
* Sleep for a given number of milliseconds.
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
* Make an authenticated request to the TinyFish API.
* Retries on 429/5xx with exponential backoff (max 3 retries).
Expand All @@ -96,35 +74,21 @@ export async function tinyfishApiRequest(
requestOptions.body = body;
}

let lastError: unknown;

for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
return (await this.helpers.httpRequestWithAuthentication.call(
this,
'tinyfishApi',
requestOptions,
)) as IDataObject;
} catch (error) {
lastError = error;

if (attempt < MAX_RETRIES && isRetryable(error)) {
await sleep(Math.pow(2, attempt) * 1000);
continue;
}

const actionableMessage = getActionableMessage(error);
if (actionableMessage) {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: actionableMessage,
});
}
throw new NodeApiError(this.getNode(), error as JsonObject);
try {
return (await this.helpers.httpRequestWithAuthentication.call(
this,
'tinyfishApi',
requestOptions,
)) as IDataObject;
} catch (error) {
const actionableMessage = getActionableMessage(error);
if (actionableMessage) {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: actionableMessage,
});
}
throw new NodeApiError(this.getNode(), error as JsonObject);
}

// Should not reach here, but TypeScript needs a return path
throw new NodeApiError(this.getNode(), lastError as JsonObject);
}

/**
Expand Down Expand Up @@ -164,13 +128,10 @@ export function buildAutomationPayload(
export async function consumeSseStream(
this: IExecuteFunctions,
payload: IDataObject,
timeoutMs: number,
): Promise<IDataObject> {
const credentials = await this.getCredentials('tinyfishApi');
const apiKey = credentials.apiKey as string;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
let lastProgress = '';

try {
Expand All @@ -181,7 +142,6 @@ export async function consumeSseStream(
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: controller.signal,
});

if (!response.ok) {
Expand Down Expand Up @@ -261,17 +221,6 @@ export async function consumeSseStream(

return finalResult;
} catch (error) {
if ((error as Error).name === 'AbortError') {
const progressHint = lastProgress
? ` Last progress: "${lastProgress}".`
: '';
throw new NodeOperationError(
this.getNode(),
`Automation timed out after ${Math.round(timeoutMs / 1000)} seconds.${progressHint} Try increasing the timeout or simplifying the goal.`,
);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
7 changes: 0 additions & 7 deletions n8n/nodes/Tinyfish/Tinyfish.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,17 @@ export class Tinyfish implements INodeType {

if (operation === 'runSse') {
const payload = buildAutomationPayload.call(this, i);
const options = this.getNodeParameter('options', i, {}) as IDataObject;
const timeoutSeconds = (options.timeout as number) || 300;
responseData = await consumeSseStream.call(
this,
payload,
timeoutSeconds * 1000,
);
} else if (operation === 'runSync') {
const payload = buildAutomationPayload.call(this, i);
const options = this.getNodeParameter('options', i, {}) as IDataObject;
const timeoutSeconds = (options.timeout as number) || 300;
responseData = await tinyfishApiRequest.call(
this,
'POST',
'/v1/automation/run',
payload,
{},
{ timeout: timeoutSeconds * 1000 },
);
} else if (operation === 'runAsync') {
const payload = buildAutomationPayload.call(this, i);
Expand Down
12 changes: 0 additions & 12 deletions n8n/nodes/Tinyfish/TinyfishDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,6 @@ export const runFields: INodeProperties[] = [
{ name: 'United States', value: 'US' },
],
},
{
displayName: 'Timeout (Seconds)',
name: 'timeout',
type: 'number',
default: 300,
description:
'Maximum time to wait for automation to complete (30-600s). Most tasks complete within 60-120 seconds. Increase for complex multi-step workflows.',
typeOptions: {
minValue: 30,
maxValue: 600,
},
},
],
},
];
Expand Down
4 changes: 2 additions & 2 deletions n8n/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions n8n/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "n8n-nodes-tinyfish",
"version": "0.1.5",
"version": "0.1.7",
"description": "n8n community node for TinyFish Web Agent - AI-powered web automation using natural language",
"license": "MIT",
"homepage": "https://docs.mino.ai",
Expand All @@ -19,8 +19,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git",
"directory": "n8n"
"url": "https://github.com/tinyfish-io/tinyfish-web-agent-integrations/tree/main/n8n"
},
Comment on lines 20 to 23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

npm package.json repository field format requirements GitHub URL

💡 Result:

In package.json, the repository field can be either:

  1. An object (recommended) with type and url (optionally directory for monorepos):
{
  "repository": {
    "type": "git",
    "url": "git+https://github.com/OWNER/REPO.git"
  }
}

The url should be a VCS-cloneable URL (for machines), not a browser “project page” URL. [1]

  1. A shorthand string (supported, but npm may normalize it to the object form on publish and warn):
{ "repository": "github:OWNER/REPO" }

or even:

{ "repository": "OWNER/REPO" }

npm supports shorthands for GitHub/Gist/Bitbucket/GitLab similar to npm install syntax. [1]

If you’re publishing to GitHub Packages, GitHub notes the repository field should match your GitHub repo URL (example given as https://github.com/my-org/test.git). [2]

Sources: [1] npm Docs package.jsonrepository field, formats, shorthands, normalization, directory option; [2] GitHub Docs on npm registry + repository field matching GitHub repo URL.


🏁 Script executed:

# Check the actual current state of the n8n package.json file
find . -name "package.json" -path "*/n8n/*" -type f | head -5

Repository: tinyfish-io/tinyfish-web-agent-integrations

Length of output: 105


🏁 Script executed:

# Look at the n8n/package.json file directly
cat n8n/package.json | head -30

Repository: tinyfish-io/tinyfish-web-agent-integrations

Length of output: 956


Invalid repository URL format for npm package.json.

The repository.url must be a Git-cloneable URL, not a GitHub web UI URL. The current value https://github.com/tinyfish-io/tinyfish-web-agent-integrations/tree/main/n8n is a browser path and will fail when npm attempts to resolve the repository. For monorepo subdirectories, use the base repository .git URL with the directory field:

🛠️ Proposed fix
"repository": {
  "type": "git",
-  "url": "https://github.com/tinyfish-io/tinyfish-web-agent-integrations/tree/main/n8n"
+  "url": "https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git",
+  "directory": "n8n"
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@n8n/package.json` around lines 20 - 23, The repository.url in package.json is
a GitHub web UI path; change it to a cloneable Git URL (e.g.,
git+https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git) and add
a repository.directory field set to "n8n" so npm can resolve the monorepo
subpackage; update the repository object (repository.url and add
repository.directory) in package.json to use the .git clone URL and the
subdirectory name.

"main": "dist/nodes/Tinyfish/Tinyfish.node.js",
"scripts": {
Expand Down