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
289 lines
11 KiB
JavaScript
289 lines
11 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.sqlCommand = exports.fireCommand = exports.runsCommand = exports.secretCommand = void 0;
|
|
/**
|
|
* `inkreach secret` — manage workflow automation secrets.
|
|
* `inkreach runs` — execution history.
|
|
* `inkreach fire` — manual trigger testing (button-click / webhook).
|
|
* `inkreach sql` — read-only SQL queries.
|
|
*/
|
|
const commander_1 = require("commander");
|
|
const ctx_1 = require("../lib/ctx");
|
|
const output_1 = require("../lib/output");
|
|
// ===================== secret =====================
|
|
function secretCommand() {
|
|
const cmd = new commander_1.Command('secret');
|
|
cmd.description('Manage automation script secrets');
|
|
cmd
|
|
.command('list <workflowId>')
|
|
.description('List secrets for a workflow. Values are never returned.')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const list = (await ctx.client.listSecrets(baseId, workflowId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No secrets found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'description', header: 'DESCRIPTION' },
|
|
], list.map((s) => ({
|
|
id: s.id,
|
|
name: (0, output_1.truncate)(s.name, 30),
|
|
description: (0, output_1.truncate)(s.description, 50),
|
|
})));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('set <workflowId>')
|
|
.description('Create a new secret')
|
|
.requiredOption('--name <name>', 'Secret name')
|
|
.requiredOption('--value <value>', 'Secret value')
|
|
.option('--description <desc>', 'Secret description')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const created = await ctx.client.createSecret(baseId, workflowId, {
|
|
name: opts.name,
|
|
value: opts.value,
|
|
description: opts.description,
|
|
});
|
|
const s = created;
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(created);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['id', s.id],
|
|
['name', s.name],
|
|
['', ''],
|
|
['note', 'Secret value is never returned. Update it if needed.'],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <workflowId> <secretId>')
|
|
.description('Update a secret')
|
|
.option('--name <name>', 'New name')
|
|
.option('--value <value>', 'New value')
|
|
.option('--description <desc>', 'New description')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [workflowId, secretId] = args;
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
await ctx.client.updateSecret(baseId, workflowId, secretId, {
|
|
name: opts.name,
|
|
value: opts.value,
|
|
description: opts.description,
|
|
});
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: secretId });
|
|
else
|
|
process.stdout.write(`✓ Secret ${secretId} updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete <workflowId> <secretId>')
|
|
.description('Delete a secret')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [workflowId, secretId] = args;
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
await ctx.client.deleteSecret(baseId, workflowId, secretId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: secretId });
|
|
else
|
|
process.stdout.write(`✓ Secret ${secretId} deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.secretCommand = secretCommand;
|
|
// ===================== runs =====================
|
|
function runsCommand() {
|
|
const cmd = new commander_1.Command('runs');
|
|
cmd.description('Execution history for automations');
|
|
cmd
|
|
.command('list <workflowId>')
|
|
.description('List execution history')
|
|
.option('--status <status>', 'Filter by status: failed, succeeded, or running')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
let list = (await ctx.client.listExecutions(baseId, workflowId));
|
|
if (opts.status) {
|
|
list = list.filter((e) => e.status === opts.status);
|
|
}
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No executions found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'status', header: 'STATUS' },
|
|
{ key: 'triggerType', header: 'TRIGGER' },
|
|
{ key: 'startedAt', header: 'STARTED' },
|
|
{ key: 'finishedAt', header: 'FINISHED' },
|
|
], list.map((e) => ({
|
|
id: e.id,
|
|
status: e.status,
|
|
triggerType: e.triggerType || '',
|
|
startedAt: (0, output_1.truncate)(e.startedAt, 25),
|
|
finishedAt: (0, output_1.truncate)(e.finishedAt, 25),
|
|
})));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get <workflowId> <executionId>')
|
|
.description('Get execution detail with node results')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [workflowId, executionId] = args;
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const exec = await ctx.client.getExecution(baseId, workflowId, executionId);
|
|
const ex = exec;
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(exec);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['id', ex.id],
|
|
['status', ex.status],
|
|
['trigger', ex.triggerType || ''],
|
|
['started', ex.startedAt || ''],
|
|
['finished', ex.finishedAt || ''],
|
|
]);
|
|
if (ex.nodeResults?.length) {
|
|
process.stdout.write('\nNODE RESULTS:\n');
|
|
(0, output_1.printTable)([
|
|
{ key: 'nodeId', header: 'NODE' },
|
|
{ key: 'status', header: 'STATUS' },
|
|
{ key: 'error', header: 'ERROR' },
|
|
], ex.nodeResults.map((r) => ({
|
|
nodeId: r.nodeId,
|
|
status: r.status,
|
|
error: (0, output_1.truncate)(r.error, 50),
|
|
})));
|
|
}
|
|
}
|
|
}));
|
|
cmd
|
|
.command('rerun <workflowId> <executionId>')
|
|
.description('Rerun a failed execution')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [workflowId, executionId] = args;
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const res = (await ctx.client.rerunExecution(baseId, workflowId, executionId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(res);
|
|
else {
|
|
const pairs = [
|
|
['executionId', executionId],
|
|
['status', 'rerun requested'],
|
|
];
|
|
if (res.id)
|
|
pairs.push(['newExecutionId', res.id]);
|
|
(0, output_1.printKv)(pairs);
|
|
}
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.runsCommand = runsCommand;
|
|
// ===================== fire =====================
|
|
function fireCommand() {
|
|
const cmd = new commander_1.Command('fire');
|
|
cmd.description('Manually fire triggers for testing');
|
|
cmd
|
|
.command('button-click <tableId> <recordId> <fieldId>')
|
|
.description('Simulate a button field click')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const [tableId, recordId, fieldId] = args;
|
|
const res = await ctx.client.fireButtonClick(tableId, recordId, fieldId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(res);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['runId', res.runId],
|
|
['tableId', res.tableId],
|
|
['fieldId', res.fieldId],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('webhook <baseId> <workflowId>')
|
|
.description('Simulate a webhook call')
|
|
.option('--body <json>', 'JSON body to send')
|
|
.option('--method <method>', 'HTTP method (informational only)', 'POST')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [baseId, workflowId] = args;
|
|
const opts = o;
|
|
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
|
const res = await ctx.client.fireWebhook(baseId, workflowId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(res);
|
|
else {
|
|
process.stdout.write(`✓ Webhook fired for workflow ${workflowId}\n`);
|
|
if (typeof res === 'object' && res !== null) {
|
|
(0, output_1.printJson)(res);
|
|
}
|
|
}
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.fireCommand = fireCommand;
|
|
// ===================== sql =====================
|
|
function sqlCommand() {
|
|
const cmd = new commander_1.Command('sql');
|
|
cmd.description('Execute a read-only SQL query (SELECT only)');
|
|
cmd.option('--base-id <id>', 'Base id (overrides default)');
|
|
cmd.argument('<sql>', 'SQL SELECT statement');
|
|
cmd.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const sql = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const rows = (await ctx.client.sql(baseId, sql));
|
|
if (ctx.json) {
|
|
(0, output_1.printJson)(rows);
|
|
}
|
|
else {
|
|
if (!rows.length) {
|
|
process.stdout.write('No rows returned.\n');
|
|
return;
|
|
}
|
|
const first = rows[0];
|
|
if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
|
|
const keys = Object.keys(first);
|
|
(0, output_1.printTable)(keys.map((k) => ({ key: k, header: k })), rows.map((r) => {
|
|
const obj = r;
|
|
const row = {};
|
|
for (const k of keys) {
|
|
row[k] = (0, output_1.truncate)(obj[k], 40);
|
|
}
|
|
return row;
|
|
}));
|
|
process.stdout.write(`\n${rows.length} row(s).\n`);
|
|
}
|
|
else {
|
|
rows.forEach((r) => process.stdout.write(JSON.stringify(r) + '\n'));
|
|
}
|
|
}
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.sqlCommand = sqlCommand;
|