A self-built CLI and companion agent skill for operating a deployed
inkreach-teable instance — configuring tables, fields, records, views, and
the project's OWN automation engine.
Why not the official 'teable' CLI:
inkreach-teable's automation is self-built with a different contract:
- conditions (not filter), fieldIds (not watchFieldIds)
- recordCondition (not recordMatchesConditions)
- webhook route /api/webhook/{baseId}/{wfId} (not the /base//workflow/ form)
- scheduledTime backend honors only timing.type + timing.interval
The official CLI targets the upstream contract and 404s/misconfigures here.
Table/field/record/view APIs ARE upstream-compatible, but automation is not.
inkreach CLI (packages/inkreach-cli/):
- Node.js + commander, zero extra runtime deps
- Uses node:http (NOT fetch — Node 24 fetch drops Authorization header)
- Commands: auth (login/status/token), base/table/field/record/view,
workflow (list/get/create/update/delete/activate/deactivate/test),
trigger set (8 types, builds correct config keys), action add/update/delete
(6 types), secret, runs (list/get/rerun), fire (button-click/webhook), sql
- --json mode for agent parsing; GET->modify->PUT whole-graph for node edits
- Verified end-to-end: create workflow -> set webhook trigger -> add script
action -> activate -> fire webhook -> runs list (succeeded)
inkreach-teable-ops skill (.agents/skills/inkreach-teable-ops/):
- SKILL.md entry: module map, routing rules, 11 key constraints (the real
contract keys), execution rules, error recovery
- guides/: cli-reference, automation-guide, tables-fields, instance-access,
development (build/restart, known gotchas)
- api-reference/: trigger-types (8 triggers, exact config), action-types
(6 actions, sandbox globals), field-types (21 types incl. Button)
- metadata.json + install.sh
125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.describeError = exports.request = exports.ApiError = void 0;
|
|
/**
|
|
* HTTP request helper using node:http / node:https.
|
|
*
|
|
* IMPORTANT: we deliberately avoid global fetch() on Node.js — the Node 24
|
|
* undici fetch drops the Authorization header in some redirect/proxy scenarios
|
|
* (confirmed bug in this project). Using the built-in http(s) module keeps the
|
|
* Authorization: Bearer header intact end-to-end.
|
|
*/
|
|
const http = __importStar(require("node:http"));
|
|
const https = __importStar(require("node:https"));
|
|
class ApiError extends Error {
|
|
status;
|
|
body;
|
|
constructor(status, message, body) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
this.body = body;
|
|
}
|
|
}
|
|
exports.ApiError = ApiError;
|
|
async function request(baseUrl, pathName, options = {}) {
|
|
const url = new URL(pathName.startsWith('http') ? pathName : pathName.replace(/^\//, ''), baseUrl.endsWith('/') ? baseUrl : baseUrl + '/');
|
|
const isHttps = url.protocol === 'https:';
|
|
const lib = isHttps ? https : http;
|
|
const method = options.method || 'GET';
|
|
const headers = {
|
|
Accept: 'application/json',
|
|
...(options.headers || {}),
|
|
};
|
|
let bodyData;
|
|
if (options.body !== undefined && options.body !== null) {
|
|
bodyData = JSON.stringify(options.body);
|
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
headers['Content-Length'] = Buffer.byteLength(bodyData).toString();
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const req = lib.request({
|
|
hostname: url.hostname,
|
|
port: url.port || (isHttps ? 443 : 80),
|
|
path: url.pathname + url.search,
|
|
method,
|
|
headers,
|
|
}, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
const buf = Buffer.concat(chunks);
|
|
const text = buf.toString('utf8');
|
|
let data = text;
|
|
const ct = res.headers['content-type'] || '';
|
|
if (ct.includes('application/json') || text.trim().startsWith('{') || text.trim().startsWith('[')) {
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
}
|
|
catch {
|
|
data = text;
|
|
}
|
|
}
|
|
const status = res.statusCode || 200;
|
|
if (status >= 200 && status < 300) {
|
|
resolve({ status, headers: res.headers, data: data });
|
|
}
|
|
else {
|
|
const errBody = data;
|
|
const msg = (errBody && typeof errBody === 'object' && errBody.error?.message) ||
|
|
(typeof errBody === 'object' && errBody.message) ||
|
|
`HTTP ${status}`;
|
|
reject(new ApiError(status, String(msg), errBody));
|
|
}
|
|
});
|
|
});
|
|
if (options.signal) {
|
|
options.signal.addEventListener('abort', () => req.destroy(new Error('aborted')));
|
|
}
|
|
req.on('error', reject);
|
|
if (bodyData !== undefined)
|
|
req.write(bodyData);
|
|
req.end();
|
|
});
|
|
}
|
|
exports.request = request;
|
|
/** Extract `{ code, message }` from an ApiError body for display. */
|
|
function describeError(err) {
|
|
if (err instanceof ApiError) {
|
|
const b = err.body;
|
|
return {
|
|
status: err.status,
|
|
code: b?.error?.code || `HTTP_${err.status}`,
|
|
message: b?.error?.message || err.message,
|
|
};
|
|
}
|
|
if (err instanceof Error) {
|
|
return { status: 0, code: 'ERROR', message: err.message };
|
|
}
|
|
return { status: 0, code: 'UNKNOWN', message: String(err) };
|
|
}
|
|
exports.describeError = describeError;
|