n8n-nodes-ethos 0.1.0

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 ADDED
@@ -0,0 +1,64 @@
1
+ # n8n-nodes-ethos
2
+
3
+ An [n8n](https://n8n.io) community node for [ETHOS](https://ethos.so) — drop autonomous browser workflows into any n8n pipeline using a plain-English goal.
4
+
5
+ ## Installation
6
+
7
+ 1. In n8n, open **Settings → Community Nodes**.
8
+ 2. Click **Install**.
9
+ 3. Enter `n8n-nodes-ethos` and confirm.
10
+ 4. After install, restart n8n if prompted.
11
+
12
+ ## Credentials
13
+
14
+ The node uses an **ETHOS API** credential with two fields:
15
+
16
+ | Field | Required | Description |
17
+ |------------|----------|-------------------------------------------------------------------------|
18
+ | API Key | Yes | Your ETHOS API key (starts with `ethos_sk_`). Get one at https://ethos.so/dashboard/api-keys. |
19
+ | Base URL | No | Defaults to `https://api.ethos.so`. Override only for self-hosted ETHOS. |
20
+
21
+ ## Node inputs
22
+
23
+ | Name | Type | Default | Description |
24
+ |----------------|----------|-----------|------------------------------------------------------|
25
+ | Goal | string | — | Plain-English browser task (10–2000 chars) |
26
+ | Execution Mode | options | headless | Only `headless` is supported via API today |
27
+ | Poll Interval | number | 3000 | (Additional Options) Milliseconds between polls |
28
+ | Timeout | number | 120 | (Additional Options) Maximum wait time, in seconds |
29
+
30
+ ## Node outputs
31
+
32
+ Two output branches:
33
+
34
+ | Branch | When | Payload |
35
+ |-----------|-------------------------------------------------------------|----------------------------------------------------------------------|
36
+ | Success | Workflow status is `completed` | `{ workflowId, status, result, taskCount, tasksDone }` |
37
+ | Error | Status is `failed`, timed out, or hit an HTTP error | `{ workflowId, status, error }` |
38
+
39
+ The Error branch is used instead of throwing, so downstream nodes can react.
40
+
41
+ ### Error messages
42
+
43
+ - `Invalid ETHOS API key` — 401 from the API
44
+ - `Quota exceeded` — 429 from the API
45
+ - `Workflow timed out after Xs` — exceeded the configured timeout
46
+ - Other HTTP errors are retried up to twice with a 2-second backoff before being routed to the Error branch
47
+
48
+ ## Example
49
+
50
+ **Goal:**
51
+
52
+ > Get the top 5 trending repositories from github.com/trending with name, stars, and description
53
+
54
+ Drop the ETHOS node after a trigger, paste the goal, connect the Success branch to your data sink (Slack, Sheets, Notion, etc.), and the Error branch to a notification node.
55
+
56
+ ## Limitations
57
+
58
+ - Headless execution only — extension mode is coming soon.
59
+ - Maximum timeout is bounded by your n8n execution timeout; the node default cap is 120 seconds.
60
+ - Each node execution submits and waits for one workflow; for high throughput, fan out with the n8n **Split In Batches** node.
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class EthosNode implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EthosNode = void 0;
4
+ const TERMINAL_STATUSES = new Set(['completed', 'failed']);
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ function getStatusCode(err) {
9
+ if (typeof err !== 'object' || err === null)
10
+ return undefined;
11
+ const e = err;
12
+ if (typeof e.statusCode === 'number')
13
+ return e.statusCode;
14
+ if (typeof e.httpCode === 'string') {
15
+ const parsed = parseInt(e.httpCode, 10);
16
+ if (!Number.isNaN(parsed))
17
+ return parsed;
18
+ }
19
+ const response = e.response;
20
+ if (response && typeof response.statusCode === 'number')
21
+ return response.statusCode;
22
+ if (response && typeof response.status === 'number')
23
+ return response.status;
24
+ return undefined;
25
+ }
26
+ function getErrorMessage(err) {
27
+ if (err instanceof Error)
28
+ return err.message;
29
+ if (typeof err === 'string')
30
+ return err;
31
+ return 'Unknown error';
32
+ }
33
+ class EthosNode {
34
+ constructor() {
35
+ this.description = {
36
+ displayName: 'ETHOS',
37
+ name: 'ethosNode',
38
+ icon: 'file:ethos.svg',
39
+ group: ['transform'],
40
+ version: 1,
41
+ subtitle: '={{$parameter["goal"]}}',
42
+ description: 'Run an ETHOS autonomous browser workflow from a plain-English goal',
43
+ defaults: {
44
+ name: 'ETHOS',
45
+ },
46
+ inputs: ['main'],
47
+ outputs: ['main', 'main'],
48
+ outputNames: ['Success', 'Error'],
49
+ credentials: [
50
+ {
51
+ name: 'ethosApi',
52
+ required: true,
53
+ },
54
+ ],
55
+ properties: [
56
+ {
57
+ displayName: 'Goal',
58
+ name: 'goal',
59
+ type: 'string',
60
+ typeOptions: {
61
+ rows: 4,
62
+ },
63
+ default: '',
64
+ required: true,
65
+ placeholder: 'Get the top 5 trending repositories from github.com/trending',
66
+ description: 'Plain-English browser task (10–2000 chars)',
67
+ },
68
+ {
69
+ displayName: 'Execution Mode',
70
+ name: 'executionMode',
71
+ type: 'options',
72
+ options: [
73
+ {
74
+ name: 'Headless',
75
+ value: 'headless',
76
+ },
77
+ ],
78
+ default: 'headless',
79
+ description: 'How ETHOS executes the workflow. Only headless is supported via API today.',
80
+ },
81
+ {
82
+ displayName: 'Additional Options',
83
+ name: 'additionalOptions',
84
+ type: 'collection',
85
+ placeholder: 'Add Option',
86
+ default: {},
87
+ options: [
88
+ {
89
+ displayName: 'Poll Interval (ms)',
90
+ name: 'pollInterval',
91
+ type: 'number',
92
+ typeOptions: {
93
+ minValue: 1000,
94
+ },
95
+ default: 3000,
96
+ description: 'How often to poll for workflow status, in milliseconds',
97
+ },
98
+ {
99
+ displayName: 'Timeout (Seconds)',
100
+ name: 'timeout',
101
+ type: 'number',
102
+ typeOptions: {
103
+ minValue: 10,
104
+ },
105
+ default: 120,
106
+ description: 'Maximum time to wait for the workflow to complete, in seconds',
107
+ },
108
+ ],
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ async execute() {
114
+ const items = this.getInputData();
115
+ const successOut = [];
116
+ const errorOut = [];
117
+ const credentials = (await this.getCredentials('ethosApi'));
118
+ const baseUrl = (credentials.baseUrl || 'https://api.ethos.so').replace(/\/+$/, '');
119
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
120
+ const goal = this.getNodeParameter('goal', itemIndex);
121
+ const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {});
122
+ const pollInterval = Math.max(1000, additionalOptions.pollInterval ?? 3000);
123
+ const timeoutSeconds = Math.max(10, additionalOptions.timeout ?? 120);
124
+ let workflowId = '';
125
+ try {
126
+ const submitOptions = {
127
+ method: 'POST',
128
+ url: `${baseUrl}/api/v1/workflow`,
129
+ headers: {
130
+ Authorization: `Bearer ${credentials.apiKey}`,
131
+ 'Content-Type': 'application/json',
132
+ },
133
+ body: {
134
+ goal,
135
+ execution_mode: 'headless',
136
+ },
137
+ json: true,
138
+ };
139
+ const submitResp = await requestWithRetry.call(this, submitOptions);
140
+ const submitted = submitResp;
141
+ workflowId = submitted.id;
142
+ const deadline = Date.now() + timeoutSeconds * 1000;
143
+ let finalStatus;
144
+ while (Date.now() < deadline) {
145
+ await sleep(pollInterval);
146
+ const pollOptions = {
147
+ method: 'GET',
148
+ url: `${baseUrl}/api/v1/workflow/${workflowId}`,
149
+ headers: {
150
+ Authorization: `Bearer ${credentials.apiKey}`,
151
+ },
152
+ json: true,
153
+ };
154
+ const pollResp = (await requestWithRetry.call(this, pollOptions));
155
+ if (TERMINAL_STATUSES.has(pollResp.status)) {
156
+ finalStatus = pollResp;
157
+ break;
158
+ }
159
+ }
160
+ if (!finalStatus) {
161
+ errorOut.push({
162
+ json: {
163
+ workflowId,
164
+ status: 'timeout',
165
+ error: `Workflow timed out after ${timeoutSeconds}s`,
166
+ },
167
+ pairedItem: { item: itemIndex },
168
+ });
169
+ continue;
170
+ }
171
+ if (finalStatus.status === 'completed') {
172
+ successOut.push({
173
+ json: {
174
+ workflowId: finalStatus.id,
175
+ status: finalStatus.status,
176
+ result: finalStatus.result,
177
+ taskCount: finalStatus.task_count,
178
+ tasksDone: finalStatus.tasks_done,
179
+ },
180
+ pairedItem: { item: itemIndex },
181
+ });
182
+ }
183
+ else {
184
+ errorOut.push({
185
+ json: {
186
+ workflowId: finalStatus.id,
187
+ status: finalStatus.status,
188
+ error: finalStatus.error ?? 'Workflow failed',
189
+ },
190
+ pairedItem: { item: itemIndex },
191
+ });
192
+ }
193
+ }
194
+ catch (err) {
195
+ const statusCode = getStatusCode(err);
196
+ let message;
197
+ if (statusCode === 401) {
198
+ message = 'Invalid ETHOS API key';
199
+ }
200
+ else if (statusCode === 429) {
201
+ message = 'Quota exceeded';
202
+ }
203
+ else {
204
+ message = getErrorMessage(err);
205
+ }
206
+ errorOut.push({
207
+ json: {
208
+ workflowId,
209
+ status: 'error',
210
+ error: message,
211
+ },
212
+ pairedItem: { item: itemIndex },
213
+ });
214
+ }
215
+ }
216
+ return [successOut, errorOut];
217
+ }
218
+ }
219
+ exports.EthosNode = EthosNode;
220
+ async function requestWithRetry(options) {
221
+ const maxAttempts = 3;
222
+ let lastErr;
223
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
224
+ try {
225
+ return await this.helpers.httpRequest(options);
226
+ }
227
+ catch (err) {
228
+ lastErr = err;
229
+ const statusCode = getStatusCode(err);
230
+ if (statusCode === 401 || statusCode === 429) {
231
+ throw err;
232
+ }
233
+ const isRetryable = statusCode === undefined || (statusCode >= 500 && statusCode < 600);
234
+ if (!isRetryable || attempt === maxAttempts) {
235
+ throw err;
236
+ }
237
+ await sleep(2000);
238
+ }
239
+ }
240
+ throw lastErr;
241
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "node": "n8n-nodes-ethos.ethosNode",
3
+ "nodeVersion": "1.0",
4
+ "codexVersion": "1.0",
5
+ "categories": ["AI", "Productivity"],
6
+ "resources": {
7
+ "credentialDocumentation": [
8
+ {
9
+ "url": "https://ethos.so/docs/api"
10
+ }
11
+ ],
12
+ "primaryDocumentation": [
13
+ {
14
+ "url": "https://ethos.so/docs"
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,9 @@
1
+ import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class EthosApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EthosApi = void 0;
4
+ class EthosApi {
5
+ constructor() {
6
+ this.name = 'ethosApi';
7
+ this.displayName = 'ETHOS API';
8
+ this.documentationUrl = 'https://ethos.so/docs';
9
+ this.properties = [
10
+ {
11
+ displayName: 'API Key',
12
+ name: 'apiKey',
13
+ type: 'string',
14
+ typeOptions: { password: true },
15
+ default: '',
16
+ required: true,
17
+ description: 'Your ETHOS API key (starts with ethos_sk_)',
18
+ },
19
+ {
20
+ displayName: 'Base URL',
21
+ name: 'baseUrl',
22
+ type: 'string',
23
+ default: 'https://api.ethos.so',
24
+ description: 'Override only if you are self-hosting ETHOS',
25
+ },
26
+ ];
27
+ this.authenticate = {
28
+ type: 'generic',
29
+ properties: {
30
+ headers: {
31
+ Authorization: '=Bearer {{$credentials.apiKey}}',
32
+ },
33
+ },
34
+ };
35
+ this.test = {
36
+ request: {
37
+ baseURL: '={{$credentials.baseUrl}}',
38
+ url: '/api/v1/health',
39
+ method: 'GET',
40
+ },
41
+ };
42
+ }
43
+ }
44
+ exports.EthosApi = EthosApi;
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = {};
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "n8n-nodes-ethos",
3
+ "version": "0.1.0",
4
+ "description": "n8n community node for ETHOS browser automation",
5
+ "keywords": [
6
+ "n8n-community-node-package"
7
+ ],
8
+ "license": "MIT",
9
+ "author": "ETHOS",
10
+ "files": [
11
+ "dist/**/*",
12
+ "README.md"
13
+ ],
14
+ "n8n": {
15
+ "n8nNodesApiVersion": 1,
16
+ "credentials": [
17
+ "dist/credentials/EthosApi.credentials.js"
18
+ ],
19
+ "nodes": [
20
+ "dist/EthosNode.node.js"
21
+ ]
22
+ },
23
+ "main": "index.js",
24
+ "scripts": {
25
+ "build": "tsc && cp src/EthosNode.node.json dist/",
26
+ "dev": "tsc --watch"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.0.0",
30
+ "@types/node": "^18.0.0",
31
+ "n8n-workflow": "^1.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "n8n-workflow": ">=0.5.0"
35
+ }
36
+ }