feat: add inkreach CLI + inkreach-teable-ops skill
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
This commit is contained in:
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
"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;
|
||||
Reference in New Issue
Block a user