Skip to content

Commit b408c6d

Browse files
committed
Add integrations CLI and workflows step-delete
Integrations ------------ - integrations list / get <provider>: catalog browsing - integrations connections list [--provider]: list active connections - integrations connect <provider>: API-key connection (Claude, OpenAI, etc.). Prompts for the key (hidden) by default, accepts --api-key, supports --user-connection for user-scoped connections. - integrations oauth status <provider>: shows OAuth client setup state - integrations oauth configure <provider>: stores OAuth client_id + secret for a tenant. --use-social-sign-in flags it for social login. - integrations oauth callback-url: prints the dashboard callback URL users need to add to their OAuth app's redirect URLs (the URL is also surfaced by oauth status / shown after a successful configure). - integrations oauth connect <provider>: full browser OAuth dance — binds a local listener on http://localhost:8745/callback, opens the browser, captures the code, exchanges for a connection. - integrations test <id> / enable <id> / disable <id> / disconnect <id>: connection lifecycle. - integrations execute <provider> <operation>: runs an integration operation (e.g. claude generate-text). Inputs via repeated --input k=v or --inputs '<json>'. Output is the response's content field, or full JSON with --json. Workflows --------- - workflows step-delete <workflow_id> <step_id>: deletes a step. Detects inbound links and warns before the delete. If the API rejects with a 500 (FK constraint when other steps still reference the target), translates the error into a clear message listing the offending steps and the step-link commands needed to fix them. Tests ----- - tests/IntegrationsTests.cs: 17 tests covering all integration client methods, snake_case wire format on POST/PUT bodies (the bug we hit during development), and IntegrationsCallbackUrl / DashboardUrl derivation. - tests/AnythinkClientExtendedTests.cs: DeleteWorkflowStepAsync happy path plus 500 propagation for the FK-violation case. - All 211 tests pass. Verified end-to-end against staging: - Slack OAuth dance: configure -> connect -> connection created with bot token - Slack send-message via integrations execute (response received in channel) - step-delete: deleted 3 orphans on workflow 36 (workout_completed_points) using the new command. Verified the FK-violation error translation by attempting to delete a step with inbound links.
1 parent feece9b commit b408c6d

8 files changed

Lines changed: 1687 additions & 1 deletion

File tree

README.md

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ The official command-line interface for [Anythink](https://anythink.cloud) — t
3939
- [roles](#roles)
4040
- [api-keys](#api-keys)
4141
- [menus](#menus)
42+
- [integrations](#integrations)
4243
- [pay](#pay)
4344
- [oauth](#oauth)
4445
- [api](#api)
@@ -467,7 +468,6 @@ anythink api-keys revoke 42 --yes
467468

468469
---
469470

470-
471471
### menus
472472

473473
Manage dashboard sidebar menus in the active project. Menus control what entities appear in the Anythink dashboard and how they are grouped.
@@ -500,6 +500,87 @@ anythink menus add-item 250 badges --icon Award
500500

501501
---
502502

503+
### integrations
504+
505+
Manage integrations — both the catalog of available providers (Claude, OpenAI, Slack, Google, etc.) and the active connections that hold credentials for them.
506+
507+
```
508+
anythink integrations list List available providers
509+
anythink integrations get <provider> Show details and operations for one provider
510+
anythink integrations connections list [--provider <p>] List your active connections
511+
512+
anythink integrations connect <provider> Create an API-key connection (Claude, OpenAI, etc.)
513+
anythink integrations oauth status <provider> Show OAuth client setup status
514+
anythink integrations oauth configure <provider> Set the OAuth client ID + secret
515+
anythink integrations oauth connect <provider> Connect via the browser OAuth flow
516+
517+
anythink integrations test <connection-id> Test a connection
518+
anythink integrations enable <connection-id> Enable a connection
519+
anythink integrations disable <connection-id> Disable a connection
520+
anythink integrations disconnect <connection-id> Delete a connection
521+
522+
anythink integrations execute <provider> <operation> Run an operation on a connected provider
523+
```
524+
525+
**API-key providers — `integrations connect`**
526+
527+
| Flag | Description |
528+
| -------------------- | ---------------------------------------------------------------------------------------- |
529+
| `--api-key <key>` | API key for the provider. If omitted, you'll be prompted (input is hidden). |
530+
| `--name <name>` | Friendly name for this connection (default: `<provider> connection`) |
531+
| `--user-connection` | Make this a user-scoped connection (only the current user sees it). Default: tenant-wide. |
532+
533+
**OAuth providers — `integrations oauth connect`**
534+
535+
The CLI starts a local HTTP listener on `http://localhost:8745/callback`, opens your browser to the provider's authorisation URL, and exchanges the returned code for a connection — no copy/paste of auth codes required.
536+
537+
| Flag | Description |
538+
| -------------------- | ------------------------------------------------------------------------ |
539+
| `--name <name>` | Friendly name for this connection |
540+
| `--user-connection` | Make this a user-scoped connection |
541+
| `--port <n>` | Local callback port (default: `8745`) |
542+
| `--no-open` | Don't try to open the browser — just print the URL |
543+
| `--timeout <secs>` | How long to wait for the callback (default: `300`) |
544+
545+
OAuth credentials need to be set up once per provider before you can connect:
546+
547+
```bash
548+
anythink integrations oauth configure slack # prompts for client_id + secret (hidden)
549+
anythink integrations oauth connect slack --name main
550+
```
551+
552+
**Running operations — `integrations execute`**
553+
554+
| Flag | Description |
555+
| ------------------- | ----------------------------------------------------------------------------------- |
556+
| `--input <k=v>` | Input parameter as `key=value`. Repeatable. |
557+
| `--inputs <json>` | All inputs as a JSON object. |
558+
| `--json` | Print the full JSON response (default: just the `content` field if present). |
559+
560+
**Examples**
561+
562+
```bash
563+
# Browse what's available
564+
anythink integrations list
565+
anythink integrations get claude
566+
567+
# API-key flow (Claude, OpenAI)
568+
anythink integrations connect claude --name "main"
569+
anythink integrations execute claude generate-text --input "prompt=Tell me a haiku"
570+
571+
# OAuth flow (Slack, Google, GitHub)
572+
anythink integrations oauth configure slack
573+
anythink integrations oauth connect slack --name main
574+
575+
# Manage connections
576+
anythink integrations connections list
577+
anythink integrations test <connection-id>
578+
anythink integrations disable <connection-id>
579+
anythink integrations disconnect <connection-id> --yes
580+
```
581+
582+
---
583+
503584
### pay
504585

505586
Configure and manage Anythink Pay — the built-in Stripe Connect integration for accepting payments in your project.

src/Client/AnythinkClient.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ public class AnythinkClient : HttpApiClient
1212
public string BaseUrl { get; }
1313
private string _org;
1414

15+
/// <summary>
16+
/// Best-guess dashboard URL derived from the API URL. We assume the standard
17+
/// cloud convention of "api.{host}" → "{host}". For non-standard setups the
18+
/// caller should override this via --dashboard-url.
19+
/// </summary>
20+
public string DashboardUrl => BaseUrl.Replace("://api.", "://");
21+
22+
/// <summary>
23+
/// Where the dashboard hosts the generic integrations OAuth callback. The
24+
/// same URL is used for every provider — the dashboard differentiates by
25+
/// state. Users add this to the OAuth app's "redirect URLs" list.
26+
/// </summary>
27+
public string IntegrationsCallbackUrl => $"{DashboardUrl}/org/{OrgId}/settings/integrations/callback";
28+
1529
public AnythinkClient(string orgId, string baseUrl, string? token = null, string? apiKey = null)
1630
: base(token, apiKey)
1731
{
@@ -106,6 +120,9 @@ public Task TriggerWorkflowAsync(int id, object? payload = null)
106120

107121
public Task DeleteWorkflowAsync(int id) => DeleteAsync(_org + $"/workflows/{id}");
108122

123+
public Task DeleteWorkflowStepAsync(int workflowId, int stepId)
124+
=> DeleteAsync(_org + $"/workflows/{workflowId}/steps/{stepId}");
125+
109126
public async Task<PaginatedResult<WorkflowJob>> GetWorkflowJobsAsync(int workflowId, int page = 1, int pageSize = 10)
110127
=> (await GetAsync<PaginatedResult<WorkflowJob>>(_org + $"/workflows/{workflowId}/jobs?page={page}&pageSize={pageSize}"))
111128
?? new PaginatedResult<WorkflowJob>([], 0, null, false, page, pageSize);
@@ -275,6 +292,48 @@ public Task<ApiKeyResponse> CreateApiKeyAsync(CreateApiKeyRequest req)
275292
public Task RevokeApiKeyAsync(int apiKeyId)
276293
=> DeleteAsync(_org + $"/api-keys/{apiKeyId}");
277294

295+
// ── Integrations ──────────────────────────────────────────────────────────
296+
297+
public async Task<List<IntegrationDefinition>> GetIntegrationDefinitionsAsync()
298+
=> (await GetAsync<List<IntegrationDefinition>>(_org + "/integrations/definitions")) ?? [];
299+
300+
public async Task<IntegrationDefinition?> GetIntegrationDefinitionAsync(string provider)
301+
=> await GetAsync<IntegrationDefinition>(_org + $"/integrations/definitions/{provider}");
302+
303+
public async Task<List<IntegrationConnection>> GetIntegrationConnectionsAsync()
304+
=> (await GetAsync<List<IntegrationConnection>>(_org + "/integrations/connections")) ?? [];
305+
306+
public async Task<List<IntegrationConnection>> GetIntegrationConnectionsForProviderAsync(string provider)
307+
=> (await GetAsync<List<IntegrationConnection>>(_org + $"/integrations/definitions/{provider}/connections")) ?? [];
308+
309+
public Task<IntegrationConnection> CreateApiKeyConnectionAsync(CreateApiKeyConnectionRequest req)
310+
=> PostAsync<IntegrationConnection>(_org + "/integrations/connections/api-key", req);
311+
312+
public Task<IntegrationConnection?> UpdateIntegrationConnectionAsync(string connectionId, UpdateConnectionRequest req)
313+
=> PutAsync<IntegrationConnection>(_org + $"/integrations/connections/{connectionId}", req);
314+
315+
public Task DeleteIntegrationConnectionAsync(string connectionId)
316+
=> DeleteAsync(_org + $"/integrations/connections/{connectionId}");
317+
318+
public Task<TestConnectionResult> TestIntegrationConnectionAsync(string connectionId)
319+
=> PostAsync<TestConnectionResult>(_org + $"/integrations/connections/{connectionId}/test");
320+
321+
public Task<IntegrationOAuthSettings?> GetIntegrationOAuthSettingsAsync(string provider)
322+
=> GetAsync<IntegrationOAuthSettings>(_org + $"/integrations/definitions/{provider}/oauth");
323+
324+
public Task SetIntegrationOAuthSettingsAsync(string provider, SetOAuthSettingsRequest req)
325+
=> PutVoidAsync(_org + $"/integrations/definitions/{provider}/oauth", req);
326+
327+
public async Task<OAuthUrlResponse> GetIntegrationOAuthUrlAsync(string provider, string redirectUri)
328+
=> (await GetAsync<OAuthUrlResponse>(_org + $"/integrations/definitions/{provider}/oauth-url?redirectUri={Uri.EscapeDataString(redirectUri)}"))
329+
?? throw new AnythinkException("OAuth URL endpoint returned no response.", 0);
330+
331+
public Task<IntegrationConnection> CreateOAuthConnectionAsync(CreateConnectionRequest req)
332+
=> PostAsync<IntegrationConnection>(_org + "/integrations/connections", req);
333+
334+
public Task<JsonObject> ExecuteIntegrationAsync(string provider, ExecuteIntegrationRequest req)
335+
=> PostAsync<JsonObject>(_org + $"/integrations/definitions/{provider}/execute", req);
336+
278337
public Task<RoleResponse?> UpdateRoleWithPermissionsAsync(int roleId, UpdateRolePermissionsRequest req)
279338
=> PutAsync<RoleResponse>(_org + $"/roles/{roleId}", req);
280339

0 commit comments

Comments
 (0)