@tapi-dev/sdk 0.1.10 → 0.1.11

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 CHANGED
@@ -101,6 +101,48 @@ const completedRun = await tapi.runs.wait(run.id);
101
101
  console.log(completedRun.status, completedRun.result);
102
102
  ```
103
103
 
104
+ ## API-Call Triggers
105
+
106
+ Triggers are scheduled calls to published website APIs. Keep trigger definitions
107
+ in the app repo and sync them after publishing the API catalog:
108
+
109
+ ```ts
110
+ // tapi.config.ts
111
+ export default {
112
+ triggers: {
113
+ nightlyBalance: {
114
+ apiRequest: "schwab.get_balance",
115
+ schedule: { cron: "0 9 * * MON-FRI" },
116
+ inputs: { accountId: "main" },
117
+ runtime: { profileRef: "perm_default" },
118
+ },
119
+ },
120
+ };
121
+ ```
122
+
123
+ ```bash
124
+ TAPI_API_KEY=tapi_project_key npx tapi triggers sync
125
+ ```
126
+
127
+ `triggers sync` upserts by trigger name for the current `.tapi/project.json`
128
+ project. Schedules support standard five-field cron strings or intervals as
129
+ numbers of seconds / strings like `30s`, `15m`, `2h`, and `1d`.
130
+
131
+ You can also manage triggers directly:
132
+
133
+ ```ts
134
+ await tapi.triggers.create({
135
+ name: "nightlyBalance",
136
+ apiRequest: "schwab.get_balance",
137
+ schedule: { cron: "0 9 * * MON-FRI" },
138
+ inputs: { accountId: "main" },
139
+ runtime: { profileRef: "perm_default" },
140
+ });
141
+
142
+ await tapi.triggers.fire("act_123");
143
+ await tapi.triggers.disable("act_123");
144
+ ```
145
+
104
146
  You can inspect the same input/output contract from the CLI:
105
147
 
106
148
  ```bash
package/dist/cli.d.ts CHANGED
@@ -64,8 +64,17 @@ interface StudioCliOptions {
64
64
  projectSlug?: string;
65
65
  workspaceMode: boolean;
66
66
  }
67
+ export interface ServiceStatus {
68
+ installed: boolean;
69
+ name?: string;
70
+ status?: string;
71
+ pathName?: string;
72
+ installedVersion?: string;
73
+ }
67
74
  export declare function runCli(argv?: string[]): Promise<number>;
68
75
  export declare function parseStudioOptions(args: string[]): StudioCliOptions;
76
+ export declare function installedServiceVersionFromPathName(pathName: string): string | undefined;
77
+ export declare function serviceNeedsInstall(status: ServiceStatus, manifest: ServiceReleaseManifest): boolean;
69
78
  export declare function getDefaultStudioCacheDir(): string;
70
79
  export declare function getDefaultServiceCacheDir(): string;
71
80
  export declare class CliWideEvent {
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import { basename, dirname, join, resolve } from "node:path";
10
10
  import { performance } from "node:perf_hooks";
11
11
  import { Readable } from "node:stream";
12
12
  import { pipeline } from "node:stream/promises";
13
- import { fileURLToPath } from "node:url";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import { TapiClient } from "./index.js";
15
15
  import { loadWorkspace, normalizeProjectValue, writeWorkspaceConfig, } from "./workspace.js";
16
16
  const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
@@ -88,6 +88,18 @@ export async function runCli(argv = process.argv.slice(2)) {
88
88
  printApisHelp();
89
89
  return 1;
90
90
  }
91
+ if (command === "triggers") {
92
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
93
+ printTriggersHelp();
94
+ return 0;
95
+ }
96
+ if (subcommand === "sync") {
97
+ return syncApiTriggers(rest);
98
+ }
99
+ console.error(`Unknown triggers command: ${subcommand}`);
100
+ printTriggersHelp();
101
+ return 1;
102
+ }
91
103
  if (command !== "studio") {
92
104
  console.error(`Unknown command: ${command}`);
93
105
  printHelp();
@@ -433,6 +445,298 @@ async function generateApiClient(args) {
433
445
  return 1;
434
446
  }
435
447
  }
448
+ function parseTriggerSyncOptions(args) {
449
+ const workspace = loadWorkspace();
450
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
451
+ let apiKey = envString("TAPI_API_KEY") || "";
452
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
453
+ let configPath = "";
454
+ const root = workspace?.root || process.cwd();
455
+ for (let index = 0; index < args.length; index += 1) {
456
+ const arg = args[index];
457
+ if (!arg)
458
+ continue;
459
+ if (arg === "--api-base-url" || arg === "--server") {
460
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
461
+ continue;
462
+ }
463
+ if (arg.startsWith("--api-base-url=")) {
464
+ apiBaseUrl = arg.slice("--api-base-url=".length);
465
+ continue;
466
+ }
467
+ if (arg.startsWith("--server=")) {
468
+ apiBaseUrl = arg.slice("--server=".length);
469
+ continue;
470
+ }
471
+ if (arg === "--api-key") {
472
+ apiKey = requireOptionValue(args, ++index, "--api-key");
473
+ continue;
474
+ }
475
+ if (arg.startsWith("--api-key=")) {
476
+ apiKey = arg.slice("--api-key=".length);
477
+ continue;
478
+ }
479
+ if (arg === "--project") {
480
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
481
+ continue;
482
+ }
483
+ if (arg.startsWith("--project=")) {
484
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
485
+ continue;
486
+ }
487
+ if (arg === "--config") {
488
+ configPath = requireOptionValue(args, ++index, "--config");
489
+ continue;
490
+ }
491
+ if (arg.startsWith("--config=")) {
492
+ configPath = arg.slice("--config=".length);
493
+ continue;
494
+ }
495
+ throw new Error(`Unknown triggers option: ${arg}`);
496
+ }
497
+ if (!apiKey) {
498
+ throw new Error("triggers sync requires --api-key or TAPI_API_KEY.");
499
+ }
500
+ if (!projectId) {
501
+ throw new Error("triggers sync requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
502
+ }
503
+ return {
504
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
505
+ apiKey,
506
+ projectId,
507
+ configPath: configPath ? resolve(root, configPath) : defaultTriggerConfigPath(root),
508
+ };
509
+ }
510
+ async function syncApiTriggers(args) {
511
+ try {
512
+ const options = parseTriggerSyncOptions(args);
513
+ const triggers = await readApiTriggerConfig(options.configPath);
514
+ if (triggers.length === 0) {
515
+ throw new Error(`No Tapi API triggers found in ${options.configPath}.`);
516
+ }
517
+ const client = new TapiClient({
518
+ baseUrl: options.apiBaseUrl,
519
+ apiKey: options.apiKey,
520
+ projectId: options.projectId,
521
+ });
522
+ for (const trigger of triggers) {
523
+ await client.triggers.create(trigger);
524
+ }
525
+ console.log(`Synced ${triggers.length} Tapi API trigger(s) from ${options.configPath}.`);
526
+ return 0;
527
+ }
528
+ catch (error) {
529
+ console.error(formatError(error));
530
+ return 1;
531
+ }
532
+ }
533
+ function defaultTriggerConfigPath(root) {
534
+ for (const name of ["tapi.config.ts", "tapi.config.mjs", "tapi.config.js", "tapi.config.cjs", "tapi.config.json"]) {
535
+ const path = join(root, name);
536
+ if (existsSync(path)) {
537
+ return path;
538
+ }
539
+ }
540
+ return join(root, "tapi.config.json");
541
+ }
542
+ async function readApiTriggerConfig(configPath) {
543
+ if (!existsSync(configPath)) {
544
+ throw new Error(`Tapi trigger config not found at ${configPath}. Create tapi.config.json or pass --config.`);
545
+ }
546
+ const payload = await loadTriggerConfigPayload(configPath);
547
+ return normalizeTriggerConfig(payload, configPath);
548
+ }
549
+ async function loadTriggerConfigPayload(configPath) {
550
+ if (configPath.endsWith(".json")) {
551
+ return JSON.parse(readFileSync(configPath, "utf8"));
552
+ }
553
+ if (configPath.endsWith(".ts")) {
554
+ return evaluateTypeScriptTriggerConfig(configPath);
555
+ }
556
+ if (configPath.endsWith(".js") || configPath.endsWith(".mjs") || configPath.endsWith(".cjs")) {
557
+ const moduleUrl = pathToFileURL(configPath).href;
558
+ const module = await import(moduleUrl);
559
+ return module.default ?? module;
560
+ }
561
+ throw new Error("Tapi trigger config must be a .json, .js, .mjs, .cjs, or simple .ts file.");
562
+ }
563
+ function evaluateTypeScriptTriggerConfig(configPath) {
564
+ let source = readFileSync(configPath, "utf8");
565
+ source = source.replace(/^\s*import\s+type\s+[\s\S]*?;\s*$/gm, "");
566
+ source = source.replace(/^\s*import\s+[\s\S]*?;\s*$/gm, "");
567
+ const marker = "export default";
568
+ const markerIndex = source.indexOf(marker);
569
+ if (markerIndex < 0) {
570
+ throw new Error("tapi.config.ts must use `export default { ... }`.");
571
+ }
572
+ let expression = source.slice(markerIndex + marker.length).trim();
573
+ if (expression.endsWith(";")) {
574
+ expression = expression.slice(0, -1).trim();
575
+ }
576
+ expression = expression.replace(/\s+satisfies\s+[\s\S]+$/m, "").trim();
577
+ expression = expression.replace(/\s+as\s+const\s*$/m, "").trim();
578
+ try {
579
+ return Function("process", `"use strict"; return (${expression});`)(process);
580
+ }
581
+ catch (error) {
582
+ throw new Error(`Could not evaluate ${configPath}: ${formatError(error)}. Use a simple export default object or switch to tapi.config.js/json.`);
583
+ }
584
+ }
585
+ function normalizeTriggerConfig(payload, configPath) {
586
+ const rawTriggers = isRecord(payload) && "triggers" in payload ? payload.triggers : payload;
587
+ if (Array.isArray(rawTriggers)) {
588
+ return rawTriggers.map((entry, index) => normalizeTriggerEntry(entry, index, configPath));
589
+ }
590
+ if (isRecord(rawTriggers)) {
591
+ return Object.entries(rawTriggers).map(([name, entry], index) => {
592
+ if (!isRecord(entry)) {
593
+ throw new Error(`Trigger '${name}' in ${configPath} must be an object.`);
594
+ }
595
+ return normalizeTriggerEntry({ name, ...entry }, index, configPath);
596
+ });
597
+ }
598
+ throw new Error(`Tapi trigger config at ${configPath} must contain a triggers array or object.`);
599
+ }
600
+ function normalizeTriggerEntry(entry, index, configPath) {
601
+ if (!isRecord(entry)) {
602
+ throw new Error(`Trigger #${index + 1} in ${configPath} must be an object.`);
603
+ }
604
+ const name = stringField(entry.name, `Trigger #${index + 1} name`);
605
+ const apiRequest = optionalStringField(entry.apiRequest, "apiRequest") ??
606
+ optionalStringField(entry.api, "api") ??
607
+ apiRequestFromParts(entry.apiName, entry.requestKey);
608
+ if (!apiRequest) {
609
+ throw new Error(`Trigger '${name}' requires apiRequest, api, or apiName/requestKey.`);
610
+ }
611
+ if (!apiRequest.includes(".")) {
612
+ throw new Error(`Trigger '${name}' apiRequest must be formatted as '<apiName>.<requestKey>'.`);
613
+ }
614
+ const schedule = normalizeTriggerSchedule(entry.schedule, entry.interval, name);
615
+ const request = {
616
+ name,
617
+ apiRequest,
618
+ ...(entry.enabled === undefined ? {} : { enabled: booleanField(entry.enabled, `Trigger '${name}' enabled`) }),
619
+ ...(schedule ? { schedule } : {}),
620
+ ...(entry.inputs === undefined ? {} : { inputs: recordField(entry.inputs, `Trigger '${name}' inputs`) }),
621
+ ...(entry.runtime === undefined ? {} : { runtime: recordField(entry.runtime, `Trigger '${name}' runtime`) }),
622
+ ...(entry.runnerId === undefined ? {} : { runnerId: stringField(entry.runnerId, `Trigger '${name}' runnerId`) }),
623
+ ...(entry.priority === undefined ? {} : { priority: numberField(entry.priority, `Trigger '${name}' priority`) }),
624
+ ...(entry.site === undefined ? {} : { site: stringField(entry.site, `Trigger '${name}' site`) }),
625
+ };
626
+ return request;
627
+ }
628
+ function normalizeTriggerSchedule(scheduleValue, intervalValue, name) {
629
+ if (scheduleValue === undefined && intervalValue === undefined) {
630
+ return undefined;
631
+ }
632
+ if (typeof scheduleValue === "string" && looksLikeCron(scheduleValue)) {
633
+ return { cron: scheduleValue.trim() };
634
+ }
635
+ if (typeof scheduleValue === "number" || typeof scheduleValue === "string") {
636
+ return { intervalSeconds: parseDurationSeconds(scheduleValue, `Trigger '${name}' schedule`) };
637
+ }
638
+ if (scheduleValue !== undefined && !isRecord(scheduleValue)) {
639
+ throw new Error(`Trigger '${name}' schedule must be an object, number, or duration string.`);
640
+ }
641
+ const schedule = scheduleValue === undefined ? {} : { ...scheduleValue };
642
+ if (intervalValue !== undefined) {
643
+ if ("cron" in schedule) {
644
+ throw new Error(`Trigger '${name}' cannot define both cron and interval.`);
645
+ }
646
+ schedule.intervalSeconds = parseDurationSeconds(intervalValue, `Trigger '${name}' interval`);
647
+ }
648
+ else if ("interval" in schedule) {
649
+ if ("cron" in schedule) {
650
+ throw new Error(`Trigger '${name}' cannot define both cron and interval.`);
651
+ }
652
+ schedule.intervalSeconds = parseDurationSeconds(schedule.interval, `Trigger '${name}' schedule.interval`);
653
+ delete schedule.interval;
654
+ }
655
+ else if ("interval_seconds" in schedule) {
656
+ if ("cron" in schedule) {
657
+ throw new Error(`Trigger '${name}' cannot define both cron and interval_seconds.`);
658
+ }
659
+ schedule.intervalSeconds = parseDurationSeconds(schedule.interval_seconds, `Trigger '${name}' schedule.interval_seconds`);
660
+ delete schedule.interval_seconds;
661
+ }
662
+ else if ("intervalSeconds" in schedule) {
663
+ if ("cron" in schedule) {
664
+ throw new Error(`Trigger '${name}' cannot define both cron and intervalSeconds.`);
665
+ }
666
+ schedule.intervalSeconds = parseDurationSeconds(schedule.intervalSeconds, `Trigger '${name}' schedule.intervalSeconds`);
667
+ }
668
+ else if ("cron" in schedule) {
669
+ schedule.cron = stringField(schedule.cron, `Trigger '${name}' schedule.cron`);
670
+ }
671
+ if (!("intervalSeconds" in schedule) && !("cron" in schedule)) {
672
+ throw new Error(`Trigger '${name}' schedule requires intervalSeconds, interval, or cron.`);
673
+ }
674
+ return schedule;
675
+ }
676
+ function looksLikeCron(value) {
677
+ return value.trim().split(/\s+/).length === 5;
678
+ }
679
+ function parseDurationSeconds(value, fieldName) {
680
+ if (typeof value === "number") {
681
+ if (!Number.isFinite(value) || value <= 0) {
682
+ throw new Error(`${fieldName} must be a positive number of seconds.`);
683
+ }
684
+ return Math.round(value);
685
+ }
686
+ if (typeof value !== "string") {
687
+ throw new Error(`${fieldName} must be a number of seconds or a duration string like '15m'.`);
688
+ }
689
+ const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)?$/i);
690
+ if (!match) {
691
+ throw new Error(`${fieldName} must be a duration like '30s', '15m', '2h', or '1d'.`);
692
+ }
693
+ const amount = Number(match[1]);
694
+ const unit = (match[2] || "s").toLowerCase();
695
+ const multiplier = unit.startsWith("m") ? 60 :
696
+ unit.startsWith("h") ? 3600 :
697
+ unit.startsWith("d") ? 86400 :
698
+ 1;
699
+ const seconds = Math.round(amount * multiplier);
700
+ if (!Number.isFinite(seconds) || seconds <= 0) {
701
+ throw new Error(`${fieldName} must be greater than zero seconds.`);
702
+ }
703
+ return seconds;
704
+ }
705
+ function apiRequestFromParts(apiName, requestKey) {
706
+ const api = optionalStringField(apiName, "apiName");
707
+ const request = optionalStringField(requestKey, "requestKey");
708
+ return api && request ? `${api}.${request}` : undefined;
709
+ }
710
+ function stringField(value, fieldName) {
711
+ if (typeof value !== "string" || !value.trim()) {
712
+ throw new Error(`${fieldName} must be a non-empty string.`);
713
+ }
714
+ return value.trim();
715
+ }
716
+ function optionalStringField(value, fieldName) {
717
+ if (value === undefined || value === null || value === "") {
718
+ return undefined;
719
+ }
720
+ return stringField(value, fieldName);
721
+ }
722
+ function booleanField(value, fieldName) {
723
+ if (typeof value !== "boolean") {
724
+ throw new Error(`${fieldName} must be true or false.`);
725
+ }
726
+ return value;
727
+ }
728
+ function numberField(value, fieldName) {
729
+ if (typeof value !== "number" || !Number.isFinite(value)) {
730
+ throw new Error(`${fieldName} must be a finite number.`);
731
+ }
732
+ return value;
733
+ }
734
+ function recordField(value, fieldName) {
735
+ if (!isRecord(value)) {
736
+ throw new Error(`${fieldName} must be an object.`);
737
+ }
738
+ return value;
739
+ }
436
740
  async function fetchApiCatalog(options) {
437
741
  const client = new TapiClient({
438
742
  baseUrl: options.apiBaseUrl,
@@ -620,7 +924,7 @@ async function repairService(options) {
620
924
  }
621
925
  function servicePowerShell(action) {
622
926
  const serviceName = "tapi-service";
623
- const status = `$svc = Get-Service -Name '${serviceName}' -ErrorAction SilentlyContinue; if ($null -eq $svc) { [pscustomobject]@{ installed = $false; name = '${serviceName}'; status = 'not_installed' } | ConvertTo-Json -Compress; exit 0 }; [pscustomobject]@{ installed = $true; name = $svc.Name; status = $svc.Status.ToString() } | ConvertTo-Json -Compress`;
927
+ const status = `$svc = Get-Service -Name '${serviceName}' -ErrorAction SilentlyContinue; if ($null -eq $svc) { [pscustomobject]@{ installed = $false; name = '${serviceName}'; status = 'not_installed'; pathName = '' } | ConvertTo-Json -Compress; exit 0 }; $cim = Get-CimInstance Win32_Service -Filter "Name='${serviceName}'" -ErrorAction SilentlyContinue; $pathName = if ($null -ne $cim) { [string]$cim.PathName } else { '' }; [pscustomobject]@{ installed = $true; name = $svc.Name; status = $svc.Status.ToString(); pathName = $pathName } | ConvertTo-Json -Compress`;
624
928
  if (action === "status") {
625
929
  return status;
626
930
  }
@@ -649,6 +953,8 @@ async function getServiceStatus() {
649
953
  installed: Boolean(payload.installed),
650
954
  name: typeof payload.name === "string" ? payload.name : "tapi-service",
651
955
  status: typeof payload.status === "string" ? payload.status : "unknown",
956
+ pathName: typeof payload.pathName === "string" ? payload.pathName : "",
957
+ installedVersion: installedServiceVersionFromPathName(typeof payload.pathName === "string" ? payload.pathName : ""),
652
958
  };
653
959
  }
654
960
  catch {
@@ -665,6 +971,16 @@ async function ensureServiceReadyForStudio(options) {
665
971
  }
666
972
  return;
667
973
  }
974
+ const manifest = await fetchServiceManifestForEnsure(options);
975
+ if (manifest && serviceNeedsInstall(status, manifest)) {
976
+ const current = status.installedVersion || "unknown";
977
+ console.log(`Updating Tapi Service from ${current} to ${manifest.version}...`);
978
+ const code = await installService(options);
979
+ if (code !== 0) {
980
+ throw new Error("Tapi Service update failed.");
981
+ }
982
+ return;
983
+ }
668
984
  if (status.status !== "Running") {
669
985
  console.log("Starting Tapi Service...");
670
986
  const output = await runProcessCapture("powershell.exe", [
@@ -679,6 +995,66 @@ async function ensureServiceReadyForStudio(options) {
679
995
  }
680
996
  }
681
997
  }
998
+ async function fetchServiceManifestForEnsure(options) {
999
+ const event = await createCliWideEvent("tapi_cli.service_ensure", {
1000
+ sdk_version: sdkVersion,
1001
+ options: installEventOptions(options),
1002
+ });
1003
+ try {
1004
+ let installToken = options.installToken?.trim();
1005
+ if (!installToken) {
1006
+ await event.phase("auth.install_token.request.start", {
1007
+ apiBaseUrl: options.apiBaseUrl,
1008
+ channel: options.channel,
1009
+ });
1010
+ installToken = await obtainStudioInstallToken(options, event);
1011
+ await event.phase("auth.install_token.request.success", {
1012
+ apiBaseUrl: options.apiBaseUrl,
1013
+ channel: options.channel,
1014
+ });
1015
+ }
1016
+ await event.phase("service_manifest.fetch.start", {
1017
+ apiBaseUrl: options.apiBaseUrl,
1018
+ channel: options.channel,
1019
+ });
1020
+ const manifest = await fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken);
1021
+ ensureCompatibleServiceManifest(manifest);
1022
+ await event.finish(true, {
1023
+ manifest: serviceManifestEventSummary(manifest),
1024
+ });
1025
+ return manifest;
1026
+ }
1027
+ catch (error) {
1028
+ await event.finish(false, {
1029
+ error: errorDetails(error),
1030
+ });
1031
+ console.warn(`Could not check for Tapi Service updates: ${formatError(error)}`);
1032
+ return null;
1033
+ }
1034
+ }
1035
+ export function installedServiceVersionFromPathName(pathName) {
1036
+ const normalized = String(pathName || "").replace(/\\/g, "/");
1037
+ const parts = normalized
1038
+ .split("/")
1039
+ .map((part) => part.trim().replace(/^"+|"+$/g, ""))
1040
+ .filter(Boolean);
1041
+ const releasesIndex = parts.findIndex((part) => part.toLowerCase() === "releases");
1042
+ if (releasesIndex < 0 || releasesIndex + 1 >= parts.length) {
1043
+ return undefined;
1044
+ }
1045
+ const version = parts[releasesIndex + 1]?.replace(/^"+|"+$/g, "").trim();
1046
+ return version || undefined;
1047
+ }
1048
+ export function serviceNeedsInstall(status, manifest) {
1049
+ if (!status.installed) {
1050
+ return true;
1051
+ }
1052
+ const installedVersion = status.installedVersion || installedServiceVersionFromPathName(status.pathName || "");
1053
+ if (!installedVersion) {
1054
+ return true;
1055
+ }
1056
+ return installedVersion !== manifest.version;
1057
+ }
682
1058
  async function installService(options) {
683
1059
  const event = await createCliWideEvent("tapi_cli.service_install", {
684
1060
  sdk_version: sdkVersion,
@@ -2085,6 +2461,7 @@ Usage:
2085
2461
  tapi apis describe <namespace.operation>
2086
2462
  tapi apis sync
2087
2463
  tapi apis generate
2464
+ tapi triggers sync
2088
2465
  tapi publish
2089
2466
  tapi service status
2090
2467
  tapi doctor
@@ -2099,6 +2476,7 @@ Commands:
2099
2476
  apis describe Print a generated website API input/output contract
2100
2477
  apis sync Save the published API catalog to .tapi/generated
2101
2478
  apis generate Generate a TypeScript runtime wrapper from the catalog
2479
+ triggers sync Upsert API-call triggers from tapi.config
2102
2480
  publish Upload local .tapi API drafts and publish ready requests
2103
2481
  service Inspect or control the local Tapi Windows service
2104
2482
  doctor Alias for studio doctor
@@ -2121,6 +2499,32 @@ Options:
2121
2499
  --out <path> Generated TypeScript output path
2122
2500
  `);
2123
2501
  }
2502
+ function printTriggersHelp() {
2503
+ console.log(`Tapi API trigger commands
2504
+
2505
+ Usage:
2506
+ tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
2507
+
2508
+ Options:
2509
+ --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
2510
+ --api-base-url <url> Tapi API base URL
2511
+ --server <url> Alias for --api-base-url
2512
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
2513
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
2514
+
2515
+ Config:
2516
+ export default {
2517
+ triggers: {
2518
+ nightlyBalance: {
2519
+ apiRequest: "schwab.get_balance",
2520
+ interval: "1h",
2521
+ inputs: { accountId: "main" },
2522
+ runtime: { profileRef: "perm_default" },
2523
+ },
2524
+ },
2525
+ };
2526
+ `);
2527
+ }
2124
2528
  function printStudioHelp() {
2125
2529
  console.log(`Tapi Studio commands
2126
2530
 
package/dist/client.d.ts CHANGED
@@ -7,5 +7,7 @@ export declare class HttpClient {
7
7
  constructor(options: TapiClientOptions);
8
8
  get<T>(path: string): Promise<T>;
9
9
  post<T>(path: string, body?: unknown): Promise<T>;
10
+ patch<T>(path: string, body?: unknown): Promise<T>;
11
+ delete<T>(path: string): Promise<T>;
10
12
  private request;
11
13
  }
package/dist/client.js CHANGED
@@ -16,6 +16,12 @@ export class HttpClient {
16
16
  async post(path, body) {
17
17
  return this.request("POST", path, body);
18
18
  }
19
+ async patch(path, body) {
20
+ return this.request("PATCH", path, body);
21
+ }
22
+ async delete(path) {
23
+ return this.request("DELETE", path);
24
+ }
19
25
  async request(method, path, body) {
20
26
  const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
21
27
  method,
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { CatalogResource } from "./catalog.js";
3
3
  import { RunnersResource } from "./runners.js";
4
4
  import { RunsResource } from "./runs.js";
5
5
  import { RuntimeResource } from "./runtime.js";
6
+ import { TriggersResource } from "./triggers.js";
6
7
  import type { TapiClientOptions } from "./types.js";
7
8
  import { WebsiteApisResource } from "./website-apis.js";
8
9
  export declare class TapiClient {
@@ -11,6 +12,7 @@ export declare class TapiClient {
11
12
  readonly runners: RunnersResource;
12
13
  readonly runs: RunsResource;
13
14
  readonly runtime: RuntimeResource;
15
+ readonly triggers: TriggersResource;
14
16
  readonly websiteApis: WebsiteApisResource;
15
17
  constructor(options: TapiClientOptions);
16
18
  }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { CatalogResource } from "./catalog.js";
4
4
  import { RunnersResource } from "./runners.js";
5
5
  import { RunsResource } from "./runs.js";
6
6
  import { RuntimeResource } from "./runtime.js";
7
+ import { TriggersResource } from "./triggers.js";
7
8
  import { WebsiteApisResource } from "./website-apis.js";
8
9
  export class TapiClient {
9
10
  catalog;
@@ -11,6 +12,7 @@ export class TapiClient {
11
12
  runners;
12
13
  runs;
13
14
  runtime;
15
+ triggers;
14
16
  websiteApis;
15
17
  constructor(options) {
16
18
  const http = new HttpClient(options);
@@ -19,6 +21,7 @@ export class TapiClient {
19
21
  this.runners = new RunnersResource(http);
20
22
  this.runs = new RunsResource(http);
21
23
  this.runtime = new RuntimeResource(http, options);
24
+ this.triggers = new TriggersResource(http);
22
25
  this.websiteApis = new WebsiteApisResource(http);
23
26
  }
24
27
  }
@@ -0,0 +1,17 @@
1
+ import type { HttpClient } from "./client.js";
2
+ import type { WebsiteApiTrigger, WebsiteApiTriggerCreateRequest, WebsiteApiTriggerList, WebsiteApiTriggerRunResult, WebsiteApiTriggerUpdateRequest } from "./types.js";
3
+ export declare class TriggersResource {
4
+ private readonly http;
5
+ constructor(http: HttpClient);
6
+ list(): Promise<WebsiteApiTriggerList>;
7
+ create(request: WebsiteApiTriggerCreateRequest): Promise<WebsiteApiTrigger>;
8
+ get(triggerId: string): Promise<WebsiteApiTrigger>;
9
+ update(triggerId: string, request: WebsiteApiTriggerUpdateRequest): Promise<WebsiteApiTrigger>;
10
+ enable(triggerId: string): Promise<WebsiteApiTrigger>;
11
+ disable(triggerId: string): Promise<WebsiteApiTrigger>;
12
+ delete(triggerId: string): Promise<{
13
+ deleted: boolean;
14
+ id: string;
15
+ }>;
16
+ fire(triggerId: string): Promise<WebsiteApiTriggerRunResult>;
17
+ }
@@ -0,0 +1,30 @@
1
+ export class TriggersResource {
2
+ http;
3
+ constructor(http) {
4
+ this.http = http;
5
+ }
6
+ list() {
7
+ return this.http.get("/api/sdk/v1/website-api-triggers");
8
+ }
9
+ create(request) {
10
+ return this.http.post("/api/sdk/v1/website-api-triggers", request);
11
+ }
12
+ get(triggerId) {
13
+ return this.http.get(`/api/sdk/v1/website-api-triggers/${encodeURIComponent(triggerId)}`);
14
+ }
15
+ update(triggerId, request) {
16
+ return this.http.patch(`/api/sdk/v1/website-api-triggers/${encodeURIComponent(triggerId)}`, request);
17
+ }
18
+ enable(triggerId) {
19
+ return this.update(triggerId, { enabled: true });
20
+ }
21
+ disable(triggerId) {
22
+ return this.update(triggerId, { enabled: false });
23
+ }
24
+ delete(triggerId) {
25
+ return this.http.delete(`/api/sdk/v1/website-api-triggers/${encodeURIComponent(triggerId)}`);
26
+ }
27
+ fire(triggerId) {
28
+ return this.http.post(`/api/sdk/v1/website-api-triggers/${encodeURIComponent(triggerId)}/run`);
29
+ }
30
+ }
package/dist/types.d.ts CHANGED
@@ -162,6 +162,54 @@ export interface WebsiteApiOperation {
162
162
  outputSchema?: Record<string, unknown>;
163
163
  [key: string]: unknown;
164
164
  }
165
+ export interface WebsiteApiTriggerSchedule {
166
+ intervalSeconds?: number;
167
+ cron?: string;
168
+ [key: string]: unknown;
169
+ }
170
+ export interface WebsiteApiTriggerCreateRequest {
171
+ name: string;
172
+ apiRequest: string;
173
+ enabled?: boolean;
174
+ schedule?: WebsiteApiTriggerSchedule;
175
+ inputs?: Record<string, unknown>;
176
+ runtime?: RuntimeRunOptions;
177
+ runnerId?: string;
178
+ priority?: number;
179
+ site?: string;
180
+ }
181
+ export interface WebsiteApiTriggerUpdateRequest {
182
+ enabled: boolean;
183
+ }
184
+ export interface WebsiteApiTrigger {
185
+ id: string;
186
+ name: string;
187
+ apiRequest: string;
188
+ apiName: string;
189
+ requestKey: string;
190
+ site?: string;
191
+ enabled: boolean;
192
+ schedule?: WebsiteApiTriggerSchedule | Record<string, unknown>;
193
+ inputs?: Record<string, unknown>;
194
+ runtime?: RuntimeRunOptions | Record<string, unknown>;
195
+ runnerId?: string;
196
+ priority?: number;
197
+ lastRun?: TapiRun | Record<string, unknown> | null;
198
+ lastError?: string;
199
+ lastFiredAt?: string | null;
200
+ nextFireAt?: string | null;
201
+ createdAt?: string;
202
+ updatedAt?: string;
203
+ [key: string]: unknown;
204
+ }
205
+ export interface WebsiteApiTriggerList {
206
+ triggers: WebsiteApiTrigger[];
207
+ }
208
+ export interface WebsiteApiTriggerRunResult {
209
+ success: boolean;
210
+ trigger: WebsiteApiTrigger;
211
+ run: TapiRun | Record<string, unknown>;
212
+ }
165
213
  export interface TapiRun {
166
214
  id: string;
167
215
  status: RunStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",