@tanod-labs/piece-tanod 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanod
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
1
+ # @tanod-labs/piece-tanod
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Activepieces piece for [Tanod](https://tanod.dev/connect/) pay-per-call tools. MIT licensed.
4
+
5
+ No account or key is needed for the free tier: routes with a free daily allowance (OCR, page to Markdown, translate, email verify) are called with `X-Tanod-Free: 1`. The other routes need an x402 payment, which this piece does not make; once the allowance is used up or a route has none, the action fails with a "payment required" message and a link to https://tanod.dev/pricing. Use the n8n node or the MCP server for wallet payments.
6
+
7
+ ## Actions
8
+
9
+ | Action | Route | Price (USD) | Free daily allowance |
10
+ |---|---|---|---|
11
+ | Document to Markdown | /v1/docs/to-markdown | 0.005 | no |
12
+ | PDF Tables | /v1/pdf/tables | 0.01 | no |
13
+ | OCR Image | /v1/ocr | 0.0025 | yes |
14
+ | PDF to Word | /v1/pdf/to-docx | 0.01 | no |
15
+ | Office to PDF | /v1/docs/to-pdf | 0.01 | no |
16
+ | Page to Markdown | /v1/render | 0.002 (0.01 with JavaScript) | yes (static only) |
17
+ | Website Screenshot | /v1/web/screenshot | 0.005 | no |
18
+ | Speech to Text | /v1/audio/transcribe | 0.01 | no |
19
+ | Translate Text | /v1/text/translate | 0.003 | yes |
20
+ | Verify Email Address | /v1/email/verify | 0.001 | yes |
21
+
22
+ File inputs take either an uploaded File or a public File URL. File outputs (PDF to Word, Office to PDF, screenshot, PDF tables as CSV/XLSX) are stored with the Activepieces file service and returned as a URL.
23
+
24
+ ## Develop
25
+
26
+ ```
27
+ bash ops/capcpu.sh npm install
28
+ bash ops/capcpu.sh npm run build # tsc
29
+ bash ops/capcpu.sh npm test # mocked HTTP, no network
30
+ ```
31
+
32
+ ## Owner publish steps (about 3 minutes)
33
+
34
+ 1. `cd product/integrations/activepieces-piece-tanod`
35
+ 2. `npm login` (same npm account as the n8n node, owner item 121; skip if already logged in)
36
+ 3. `npm run build && npm test`
37
+ 4. `npm publish --access public`
38
+ 5. Activepieces cloud and self-hosted instances pick up public npm pieces named `@scope/piece-*`; to list it in the official piece catalog, submit the package to Activepieces separately (optional, not needed for installing it by name).
@@ -0,0 +1,2 @@
1
+ export declare const tanod: import("@activepieces/pieces-framework").Piece<undefined>;
2
+ export { actions } from './lib/actions';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.actions = exports.tanod = void 0;
4
+ const pieces_framework_1 = require("@activepieces/pieces-framework");
5
+ const shared_1 = require("@activepieces/shared");
6
+ const actions_1 = require("./lib/actions");
7
+ exports.tanod = (0, pieces_framework_1.createPiece)({
8
+ displayName: 'Tanod',
9
+ description: 'Pay-per-call document, web, audio, text and email tools. A free daily allowance per IP needs no account.',
10
+ logoUrl: 'https://tanod.dev/favicon.svg',
11
+ authors: ['aiexp1'],
12
+ categories: [shared_1.PieceCategory.CONTENT_AND_FILES],
13
+ auth: pieces_framework_1.PieceAuth.None(),
14
+ actions: actions_1.actions,
15
+ triggers: [],
16
+ });
17
+ var actions_2 = require("./lib/actions");
18
+ Object.defineProperty(exports, "actions", { enumerable: true, get: function () { return actions_2.actions; } });
@@ -0,0 +1,15 @@
1
+ type Ctx = {
2
+ propsValue: Record<string, any>;
3
+ files: {
4
+ write(a: {
5
+ fileName: string;
6
+ data: Buffer;
7
+ }): Promise<string>;
8
+ };
9
+ };
10
+ /** Put the file or URL into the request body. */
11
+ export declare function fileInput(p: Record<string, any>, field: string, withName: boolean): Record<string, unknown>;
12
+ /** Replace the base64 `file`/`files` in a reply with Activepieces file URLs. */
13
+ export declare function storeFiles(reply: Record<string, any>, ctx: Ctx): Promise<Record<string, any>>;
14
+ export declare const actions: import("@activepieces/pieces-framework").IAction<import("@activepieces/pieces-framework").PieceAuthProperty, Record<string, any>>[];
15
+ export {};
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.actions = void 0;
4
+ exports.fileInput = fileInput;
5
+ exports.storeFiles = storeFiles;
6
+ const pieces_framework_1 = require("@activepieces/pieces-framework");
7
+ const client_1 = require("./client");
8
+ /** Routes, request fields and prices follow https://tanod.dev/openapi.json (x-tanod-pricing). */
9
+ const urlProp = (required, description) => pieces_framework_1.Property.ShortText({ displayName: 'URL', description, required });
10
+ const fileProp = pieces_framework_1.Property.File({
11
+ displayName: 'File',
12
+ description: 'The file to process. Use this or a URL.',
13
+ required: false,
14
+ });
15
+ const fileUrlProp = pieces_framework_1.Property.ShortText({
16
+ displayName: 'File URL',
17
+ description: 'Public http(s) URL of the file. Use this or File.',
18
+ required: false,
19
+ });
20
+ /** Put the file or URL into the request body. */
21
+ function fileInput(p, field, withName) {
22
+ const f = p['file'];
23
+ if (f) {
24
+ const body = { [field]: f.base64 };
25
+ if (withName && f.filename)
26
+ body['filename'] = f.filename;
27
+ return body;
28
+ }
29
+ if (p['fileUrl'])
30
+ return { url: p['fileUrl'] };
31
+ throw new Error('Provide either a File or a File URL.');
32
+ }
33
+ /** Replace the base64 `file`/`files` in a reply with Activepieces file URLs. */
34
+ async function storeFiles(reply, ctx) {
35
+ const take = async (f) => {
36
+ if (!f || typeof f.data_base64 !== 'string')
37
+ return f;
38
+ const url = await ctx.files.write({ fileName: f.name || 'tanod-output', data: Buffer.from(f.data_base64, 'base64') });
39
+ const { data_base64: _drop, ...rest } = f;
40
+ return { ...rest, url };
41
+ };
42
+ const out = { ...reply };
43
+ if (out['file'] && typeof out['file'] === 'object')
44
+ out['file'] = await take(out['file']);
45
+ if (Array.isArray(out['files']))
46
+ out['files'] = await Promise.all(out['files'].map(take));
47
+ return out;
48
+ }
49
+ const act = (name, displayName, description, props, path, free, build, returnsFile = false) => (0, pieces_framework_1.createAction)({
50
+ name,
51
+ displayName,
52
+ description,
53
+ props,
54
+ async run(ctx) {
55
+ const body = build(ctx.propsValue);
56
+ if (returnsFile)
57
+ body['output'] = 'base64';
58
+ const reply = await (0, client_1.tanodCall)({ path, body, free });
59
+ return returnsFile ? storeFiles(reply, ctx) : reply;
60
+ },
61
+ });
62
+ const pages = pieces_framework_1.Property.ShortText({
63
+ displayName: 'Pages',
64
+ description: 'Pages to process (N, N-M, N-, -M, last; comma separated). Empty means all.',
65
+ required: false,
66
+ });
67
+ exports.actions = [
68
+ act('document_to_markdown', 'Document to Markdown', 'PDF, DOCX, XLSX, PPTX, EPUB, HTML or CSV to Markdown (USD 0.005 per call, no free allowance)', { file: fileProp, fileUrl: fileUrlProp }, '/v1/docs/to-markdown', false, (p) => fileInput(p, 'file_base64', true)),
69
+ act('pdf_tables', 'PDF Tables', 'Extract tables from a text PDF as JSON, CSV or XLSX (USD 0.01 per call, no free allowance)', {
70
+ file: fileProp, fileUrl: fileUrlProp, pages,
71
+ format: pieces_framework_1.Property.StaticDropdown({
72
+ displayName: 'Output Format', required: true, defaultValue: 'json',
73
+ options: { options: [{ label: 'JSON', value: 'json' }, { label: 'CSV (file)', value: 'csv' }, { label: 'XLSX (file)', value: 'xlsx' }] },
74
+ }),
75
+ }, '/v1/pdf/tables', false, (p) => {
76
+ const body = fileInput(p, 'file_base64', false);
77
+ if (p['pages'])
78
+ body['pages'] = p['pages'];
79
+ body['format'] = p['format'] ?? 'json';
80
+ body['output'] = 'base64';
81
+ return body;
82
+ }, true),
83
+ act('ocr_image', 'OCR Image', 'Extract text from an image URL with Tesseract (USD 0.0025 per call, free daily allowance)', {
84
+ url: urlProp(true, 'Public http(s) URL of the image'),
85
+ lang: pieces_framework_1.Property.ShortText({ displayName: 'Language', description: 'Tesseract language code (only eng is installed)', required: false, defaultValue: 'eng' }),
86
+ }, '/v1/ocr', true, (p) => ({ url: p['url'], lang: p['lang'] || 'eng' })),
87
+ act('pdf_to_word', 'PDF to Word', 'Convert a PDF to a .docx file (USD 0.01 per call, no free allowance)', { file: fileProp, fileUrl: fileUrlProp, pages }, '/v1/pdf/to-docx', false, (p) => {
88
+ const body = fileInput(p, 'pdf_base64', false);
89
+ if (p['pages'])
90
+ body['pages'] = p['pages'];
91
+ return body;
92
+ }, true),
93
+ act('office_to_pdf', 'Office to PDF', 'Convert DOCX, XLSX, PPTX, ODT, RTF or text to PDF (USD 0.01 per call, no free allowance)', { file: fileProp, fileUrl: fileUrlProp }, '/v1/docs/to-pdf', false, (p) => fileInput(p, 'file_base64', true), true),
94
+ act('page_to_markdown', 'Page to Markdown', 'Fetch a web page as clean Markdown (USD 0.002, USD 0.01 with JavaScript; free daily allowance on the static fetch)', {
95
+ url: urlProp(true, 'Public http(s) URL of the page'),
96
+ js: pieces_framework_1.Property.Checkbox({
97
+ displayName: 'Run JavaScript', required: false, defaultValue: false,
98
+ description: 'Render the page in a headless browser first. Costs USD 0.01 and has no free allowance.',
99
+ }),
100
+ }, '/v1/render', true, (p) => ({ url: p['url'], format: 'markdown', js: p['js'] === true })),
101
+ act('website_screenshot', 'Website Screenshot', 'Capture a web page as a PNG or JPEG image (USD 0.005 per call, no free allowance)', {
102
+ url: urlProp(true, 'Public http(s) URL of the page'),
103
+ format: pieces_framework_1.Property.StaticDropdown({
104
+ displayName: 'Image Format', required: true, defaultValue: 'png',
105
+ options: { options: [{ label: 'PNG', value: 'png' }, { label: 'JPEG', value: 'jpeg' }] },
106
+ }),
107
+ width: pieces_framework_1.Property.Number({ displayName: 'Width', description: '320 to 1920', required: false, defaultValue: 1280 }),
108
+ height: pieces_framework_1.Property.Number({ displayName: 'Height', description: '200 to 1600', required: false, defaultValue: 800 }),
109
+ fullPage: pieces_framework_1.Property.Checkbox({ displayName: 'Full Page', required: false, defaultValue: false }),
110
+ }, '/v1/web/screenshot', false, (p) => ({ url: p['url'], format: p['format'] ?? 'png', width: p['width'] ?? 1280, height: p['height'] ?? 800, full_page: p['fullPage'] === true }), true),
111
+ act('speech_to_text', 'Speech to Text', 'Transcribe an audio file, with SRT and WebVTT subtitles (USD 0.01 per call, no free allowance)', {
112
+ file: fileProp, fileUrl: fileUrlProp,
113
+ language: pieces_framework_1.Property.ShortText({ displayName: 'Spoken Language', description: 'ISO 639-1 code. Empty means detect automatically.', required: false }),
114
+ translateToEnglish: pieces_framework_1.Property.Checkbox({ displayName: 'Translate to English', required: false, defaultValue: false }),
115
+ }, '/v1/audio/transcribe', false, (p) => {
116
+ const body = fileInput(p, 'file_base64', false);
117
+ if (p['language'])
118
+ body['language'] = p['language'];
119
+ if (p['translateToEnglish'] === true)
120
+ body['translate_to_english'] = true;
121
+ return body;
122
+ }),
123
+ act('translate_text', 'Translate Text', 'Translate text between English and 14 languages (USD 0.003 per call, free daily allowance)', {
124
+ text: pieces_framework_1.Property.LongText({ displayName: 'Text', description: 'At most 5,000 characters', required: true }),
125
+ target: pieces_framework_1.Property.ShortText({ displayName: 'Target Language', description: 'en, es, fr, de, pt, it, nl, ru, zh, ja, ko, ar, hi, id or tl', required: true, defaultValue: 'es' }),
126
+ source: pieces_framework_1.Property.ShortText({ displayName: 'Source Language', description: 'Empty means detect automatically', required: false }),
127
+ }, '/v1/text/translate', true, (p) => {
128
+ const body = { text: p['text'], target: p['target'] };
129
+ if (p['source'])
130
+ body['source'] = p['source'];
131
+ return body;
132
+ }),
133
+ act('verify_email', 'Verify Email Address', 'Check an email address by syntax and DNS (MX), no mailbox probe (USD 0.001 per call, free daily allowance)', { email: pieces_framework_1.Property.ShortText({ displayName: 'Email Address', required: true }) }, '/v1/email/verify', true, (p) => ({ email: p['email'] })),
134
+ ];
@@ -0,0 +1,10 @@
1
+ export declare const BASE_URL = "https://tanod.dev";
2
+ export interface CallArgs {
3
+ path: string;
4
+ body: Record<string, unknown>;
5
+ free: boolean;
6
+ baseUrl?: string;
7
+ timeoutMs?: number;
8
+ }
9
+ /** POST a JSON body to a Tanod route. Sends X-Tanod-Free: 1 on routes with a free daily allowance. */
10
+ export declare function tanodCall(a: CallArgs): Promise<Record<string, any>>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASE_URL = void 0;
4
+ exports.tanodCall = tanodCall;
5
+ exports.BASE_URL = 'https://tanod.dev';
6
+ const HELP = 'Paid use beyond the free daily allowance needs an x402 payment; see https://tanod.dev/pricing';
7
+ /** POST a JSON body to a Tanod route. Sends X-Tanod-Free: 1 on routes with a free daily allowance. */
8
+ async function tanodCall(a) {
9
+ const headers = { 'content-type': 'application/json', accept: 'application/json' };
10
+ if (a.free)
11
+ headers['X-Tanod-Free'] = '1';
12
+ const res = await fetch((a.baseUrl ?? exports.BASE_URL) + a.path, {
13
+ method: 'POST',
14
+ headers,
15
+ body: JSON.stringify(a.body),
16
+ signal: AbortSignal.timeout(a.timeoutMs ?? 120_000),
17
+ });
18
+ const text = await res.text();
19
+ let data;
20
+ try {
21
+ data = JSON.parse(text);
22
+ }
23
+ catch {
24
+ data = { raw: text.slice(0, 500) };
25
+ }
26
+ if (res.status === 402) {
27
+ throw new Error(`Tanod payment required: the free daily allowance for this route is used up or does not apply. ${HELP}`);
28
+ }
29
+ if (!res.ok) {
30
+ const d = data?.detail?.message ?? data?.detail ?? data?.error ?? data?.message ?? data?.raw ?? JSON.stringify(data).slice(0, 300);
31
+ throw new Error(`Tanod ${res.status}: ${typeof d === 'string' ? d : JSON.stringify(d)}`);
32
+ }
33
+ return data;
34
+ }
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "@tanod-labs/piece-tanod",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Activepieces piece for Tanod pay-per-call tools: document to Markdown, PDF tables, OCR, PDF to Word, Office to PDF, web page to Markdown, screenshots, speech to text, translation and email verification. Free daily allowance, no account.",
5
+ "license": "MIT",
6
+ "homepage": "https://tanod.dev/connect/",
7
+ "keywords": ["activepieces", "tanod", "pdf", "ocr", "markdown", "x402", "screenshot", "transcription"],
8
+ "author": { "name": "Tanod", "email": "ops@tanod.dev" },
9
+ "main": "./dist/src/index.js",
10
+ "types": "./dist/src/index.d.ts",
11
+ "files": ["dist", "README.md", "LICENSE"],
12
+ "scripts": {
13
+ "build": "rm -rf dist && tsc -p tsconfig.json",
14
+ "test": "node --import tsx --test test/*.test.ts"
15
+ },
16
+ "dependencies": {
17
+ "@activepieces/pieces-framework": "0.32.0",
18
+ "@activepieces/shared": "0.95.1",
19
+ "tslib": "^2.6.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "tsx": "^4.19.0",
24
+ "typescript": "^5.6.0"
25
+ }
26
+ }