@superglue/cli 1.1.7 → 1.1.9

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "superglue",
3
3
  "description": "Build, test, and manage superglue tools via the sg CLI. Use when working with superglue tools, systems, APIs, SDKs, or the sg command.",
4
- "version": "1.1.7",
4
+ "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "superglue",
7
7
  "email": "hello@superglue.cloud"
@@ -9,12 +9,5 @@
9
9
  "homepage": "https://superglue.cloud",
10
10
  "repository": "https://github.com/superglue-ai/cli",
11
11
  "license": "MIT",
12
- "keywords": [
13
- "systems",
14
- "integration",
15
- "cli",
16
- "tools",
17
- "automation",
18
- "api"
19
- ]
12
+ "keywords": ["systems", "integration", "cli", "tools", "automation", "api"]
20
13
  }
package/dist/cli.js CHANGED
@@ -2390,7 +2390,7 @@ var require_templates = __commonJS({
2390
2390
  "use strict";
2391
2391
  Object.defineProperty(exports2, "__esModule", { value: true });
2392
2392
  exports2.systemOptions = exports2.systems = void 0;
2393
- exports2.findTemplateForSystem = findTemplateForSystem3;
2393
+ exports2.findTemplateForSystem = findTemplateForSystem4;
2394
2394
  exports2.uniqueKeywords = uniqueKeywords;
2395
2395
  exports2.enrichWithTemplate = enrichWithTemplate;
2396
2396
  exports2.getOAuthConfig = getOAuthConfig;
@@ -2407,6 +2407,15 @@ var require_templates = __commonJS({
2407
2407
  preferredAuthType: "apikey",
2408
2408
  keywords: ["database", "sql", "postgres", "postgresql", "api key", "tables"]
2409
2409
  },
2410
+ redis_direct: {
2411
+ name: "redis_direct",
2412
+ apiUrl: "redis://<<username>>:<<password>>@<<host>>:<<port>>/<<database>>",
2413
+ regex: "^.*(rediss?://).*$",
2414
+ icon: "redis",
2415
+ docsUrl: "https://redis.io/docs/latest/commands/",
2416
+ preferredAuthType: "apikey",
2417
+ keywords: ["database", "cache", "redis", "key-value", "nosql", "api key"]
2418
+ },
2410
2419
  stripe: {
2411
2420
  name: "stripe",
2412
2421
  apiUrl: "https://api.stripe.com",
@@ -4784,8 +4793,146 @@ HEADERS: { "Authorization": "Bearer <<token>>", "Content-Type": "application/jso
4784
4793
  Tenant-Specific Endpoints: Multi-tenant apps need tenant ID in OAuth URLs (/04a63d67.../oauth2/v2.0/authorize) instead of /common endpoint
4785
4794
  Credentials Needed: Application (client) ID + Client Secret (generated under Certificates & secrets - copy the Value immediately, not the Secret ID)
4786
4795
  API Permissions: Add Microsoft Graph permissions (e.g., Sites.ReadWrite.All) under API permissions, then grant admin consent if you have admin rights
4787
- Scopes Must Include: Always add offline_access scope to get refresh tokens for long-term access without re-authentication
4796
+ Scopes Must Include: Always add offline_access scope to get refresh tokens for long-term access without re-authentication
4788
4797
  `
4798
+ },
4799
+ dynamics365_sales: {
4800
+ name: "dynamics365_sales",
4801
+ apiUrl: "https://<<org>>.crm<<region_number>>.dynamics.com/api/data/v9.2",
4802
+ regex: "^.*(crm\\d*\\.dynamics\\.com|dynamicscrm).*$",
4803
+ icon: "lucide:square-percent",
4804
+ docsUrl: "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api",
4805
+ preferredAuthType: "oauth",
4806
+ oauth: {
4807
+ authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
4808
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
4809
+ scopes: "https://<<org>>.crm<<region_number>>.dynamics.com/user_impersonation offline_access",
4810
+ grant_type: "authorization_code"
4811
+ },
4812
+ keywords: [
4813
+ "accounts",
4814
+ "contacts",
4815
+ "leads",
4816
+ "opportunities",
4817
+ "quotes",
4818
+ "salesorders",
4819
+ "salesorderdetails",
4820
+ "products",
4821
+ "incidents",
4822
+ "campaigns",
4823
+ "invoices",
4824
+ "competitors",
4825
+ "teams",
4826
+ "systemusers",
4827
+ "businessunits",
4828
+ "odata",
4829
+ "dynamics",
4830
+ "crm",
4831
+ "dynamics 365 sales",
4832
+ "dataverse",
4833
+ "oauth"
4834
+ ],
4835
+ systemSpecificInstructions: `Dynamics 365 Sales \u2014 Dataverse Web API v9.2
4836
+
4837
+ REQUIRED HEADERS (in addition to Authorization):
4838
+ - OData-MaxVersion: 4.0
4839
+ - OData-Version: 4.0
4840
+ - If-None-Match: null (recommended \u2014 prevents stale cached data, especially on $expand queries)
4841
+
4842
+ QUERY BEHAVIOR:
4843
+ - $skip is NOT supported. Use @odata.nextLink for pagination (default page size 5000, control with Prefer: odata.maxpagesize=<n>).
4844
+ - PAGINATION IN SUPERGLUE: Use cursorBased pagination with cursorPath "@odata.nextLink":
4845
+ pagination: { type: "cursorBased", pageSize: "5000", cursorPath: "@odata.nextLink" }
4846
+ The cursor is a full URL \u2014 use <<cursor>> as the step URL (set the initial URL via a "cursor" input variable).
4847
+ - $apply is supported for aggregations.
4848
+ - Lookup fields (foreign keys) are named _<field>_value \u2014 e.g. _parentaccountid_value, _customerid_value.
4849
+ - To get formatted/display values (e.g. option set labels, currency formatting), add header: Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"
4850
+ - FetchXML: GET /api/data/v9.2/<entity>?fetchXml=<url_encoded_xml> for complex aggregations and outer joins that OData $expand cannot express.
4851
+ - Max URL length is 32KB. For long queries, wrap in a $batch POST request (raises limit to 64KB).
4852
+ - Max individual OData URL segment is 260 chars \u2014 use parameter aliases (@p1=value) to shorten segments.
4853
+
4854
+ WRITE SPECIFICS:
4855
+ - PATCH doubles as an upsert when the target record doesn't exist (by ID or alternate key). Use If-Match: * to prevent accidental creates. Use If-None-Match: * to prevent accidental updates.
4856
+ - Setting lookups on write: use @odata.bind \u2014 e.g. "parentaccountid@odata.bind": "/accounts(<guid>)"
4857
+ - Disassociating a lookup: set the navigation property to null (without @odata.bind), e.g. "parentcustomerid_account": null
4858
+
4859
+ RATE LIMITS (Service Protection):
4860
+ - 429 Too Many Requests returned when limits exceeded. Always implement Retry-After handling.
4861
+ - Per-user limits per 5-min window: 6,000 requests, 20 min combined execution time, 52+ concurrent requests.
4862
+ - These are per web server \u2014 actual capacity varies by environment.
4863
+
4864
+ DISCOVERING CUSTOM ENTITIES:
4865
+ GET /api/data/v9.2/EntityDefinitions?$filter=IsCustomEntity eq true&$select=LogicalName,DisplayName
4866
+ `
4867
+ },
4868
+ dynamics365_business_central: {
4869
+ name: "dynamics365_business_central",
4870
+ apiUrl: "https://api.businesscentral.dynamics.com/v2.0/<<environment>>/api/v2.0",
4871
+ regex: "^.*(businesscentral\\.dynamics\\.com|business[\\s\\-]?central).*$",
4872
+ icon: "lucide:landmark",
4873
+ docsUrl: "https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/",
4874
+ preferredAuthType: "oauth",
4875
+ oauth: {
4876
+ authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
4877
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
4878
+ scopes: "https://api.businesscentral.dynamics.com/.default offline_access",
4879
+ grant_type: "authorization_code"
4880
+ },
4881
+ keywords: [
4882
+ "customers",
4883
+ "vendors",
4884
+ "items",
4885
+ "salesOrders",
4886
+ "salesInvoices",
4887
+ "salesQuotes",
4888
+ "purchaseOrders",
4889
+ "purchaseInvoices",
4890
+ "generalLedgerEntries",
4891
+ "journals",
4892
+ "accounts",
4893
+ "employees",
4894
+ "dimensions",
4895
+ "warehouses",
4896
+ "inventory",
4897
+ "odata",
4898
+ "dynamics",
4899
+ "erp",
4900
+ "business central",
4901
+ "finance",
4902
+ "oauth"
4903
+ ],
4904
+ systemSpecificInstructions: `Dynamics 365 Business Central \u2014 API v2.0
4905
+
4906
+ COMPANY SCOPING: All entities are scoped under a company. List companies first: GET /api/v2.0/companies, then access entities at /companies(<id>)/<entity>.
4907
+
4908
+ WRITE SPECIFICS:
4909
+ - Updates REQUIRE an If-Match header with the entity's @odata.etag value \u2014 omitting it will fail.
4910
+ - Do NOT insert child records belonging to the same parent in parallel \u2014 causes locks. Use $batch to serialize them.
4911
+ - Transactional $batch: add Isolation: snapshot header for all-or-nothing batch operations. Max 100 operations per $batch.
4912
+
4913
+ PAGINATION: Uses @odata.nextLink (a full URL for the next page). In superglue, use offsetBased pagination with $top and $skip since BC supports $skip:
4914
+ pagination: { type: "offsetBased", pageSize: "1000" }
4915
+ queryParams: { "$top": "<<limit>>", "$skip": "<<offset>>" }
4916
+
4917
+ QUERY LIMITS:
4918
+ - Max page size: 20,000 entities per response (returns 413 if exceeded).
4919
+ - Max request body size: 350 MB.
4920
+ - Request timeout: 8 minutes \u2014 long-running requests get 408 or 504.
4921
+ - Use Data-Access-Intent: ReadOnly header on GET requests that don't need latest data (routes to read replica, reduces load).
4922
+
4923
+ RATE LIMITS:
4924
+ - 429 Too Many Requests when limits exceeded. Always implement Retry-After handling.
4925
+ - Per-user: 6,000 requests per 5-min sliding window, 5 concurrent requests, 100 max connections.
4926
+
4927
+ PERFORMANCE GOTCHAS:
4928
+ - Avoid temp-table-based API pages with >100 records \u2014 no caching, poor pagination.
4929
+ - Calculated/complex fields on API pages are expensive. Prefer stored values.
4930
+ - API pages/queries are up to 10x faster than SOAP endpoints \u2014 always prefer API v2.0.
4931
+
4932
+ WEBHOOKS: Supports up to 200 webhook subscriptions for entity change notifications via /subscriptions.
4933
+
4934
+ CUSTOM APIs: Publishers expose custom API pages at /api/{publisher}/{group}/{version}/companies(<id>)/<endpoint>, NOT under /api/v2.0/.
4935
+ `
4789
4936
  },
4790
4937
  redis: {
4791
4938
  name: "redis",
@@ -5409,7 +5556,7 @@ EXAMPLE WORKFLOWS:
5409
5556
  icon: system.icon || "default"
5410
5557
  }))
5411
5558
  ];
5412
- function findTemplateForSystem3(system) {
5559
+ function findTemplateForSystem4(system) {
5413
5560
  if (system.templateName && exports2.systems[system.templateName]) {
5414
5561
  return { key: system.templateName, template: exports2.systems[system.templateName] };
5415
5562
  }
@@ -5451,7 +5598,7 @@ EXAMPLE WORKFLOWS:
5451
5598
  return [...new Set(keywords)];
5452
5599
  }
5453
5600
  function enrichWithTemplate(input) {
5454
- const match = findTemplateForSystem3(input);
5601
+ const match = findTemplateForSystem4(input);
5455
5602
  if (!match) {
5456
5603
  return input;
5457
5604
  }
@@ -5488,7 +5635,7 @@ EXAMPLE WORKFLOWS:
5488
5635
  tokenContentType: creds.tokenContentType,
5489
5636
  extraHeaders
5490
5637
  };
5491
- const match = findTemplateForSystem3(system);
5638
+ const match = findTemplateForSystem4(system);
5492
5639
  const templateOAuth = match?.template.oauth;
5493
5640
  return {
5494
5641
  tokenAuthMethod: storedConfig.tokenAuthMethod ?? templateOAuth?.tokenAuthMethod,
@@ -5500,7 +5647,7 @@ EXAMPLE WORKFLOWS:
5500
5647
  if (system.credentials?.token_url) {
5501
5648
  return system.credentials.token_url;
5502
5649
  }
5503
- const match = findTemplateForSystem3(system);
5650
+ const match = findTemplateForSystem4(system);
5504
5651
  if (match?.template.oauth?.tokenUrl) {
5505
5652
  return match.template.oauth.tokenUrl;
5506
5653
  }
@@ -19977,6 +20124,8 @@ var require_utils = __commonJS({
19977
20124
  var getConnectionProtocol2 = (url) => {
19978
20125
  if (url.startsWith("postgres://") || url.startsWith("postgresql://"))
19979
20126
  return "postgres";
20127
+ if (url.startsWith("redis://") || url.startsWith("rediss://"))
20128
+ return "redis";
19980
20129
  if (url.startsWith("ftp://") || url.startsWith("ftps://") || url.startsWith("sftp://"))
19981
20130
  return "sftp";
19982
20131
  if (url.startsWith("smb://"))
@@ -21785,7 +21934,7 @@ var import_node_util = require("util");
21785
21934
  // package.json
21786
21935
  var package_default = {
21787
21936
  name: "@superglue/cli",
21788
- version: "1.1.7",
21937
+ version: "1.1.9",
21789
21938
  bin: {
21790
21939
  sg: "./dist/cli.js"
21791
21940
  },
@@ -22310,61 +22459,92 @@ async function choose(message, options, defaultIndex = 0) {
22310
22459
 
22311
22460
  // src/commands/init.ts
22312
22461
  function registerInitCommand(program2) {
22313
- program2.command("init").description("Set up superglue CLI configuration").action(async () => {
22314
- banner();
22315
- console.log(` ${c.bold}Welcome to the superglue CLI setup!${c.reset}`);
22316
- console.log(` ${c.dim}Let's get you connected in a few steps.${c.reset}
22462
+ program2.command("init").description("Set up superglue CLI configuration").option("--api-key <key>", "API key (skips interactive prompt)").option("--endpoint <url>", "API endpoint", "https://api.superglue.cloud").option("--web-endpoint <url>", "Web endpoint for OAuth callbacks").option("--output-mode <mode>", "Output mode: stdout or stdout+file", "stdout").option("--output-dir <dir>", "Output directory for stdout+file mode", ".superglue/output").option("--global", "Save config globally (~/.superglue/) instead of locally").action(async (opts) => {
22463
+ const isNonInteractive = opts.apiKey || !process.stdin.isTTY;
22464
+ let apiKey;
22465
+ let endpoint;
22466
+ let webEndpoint;
22467
+ let outputMode;
22468
+ let outputDir;
22469
+ let preferLocal;
22470
+ if (isNonInteractive) {
22471
+ apiKey = opts.apiKey || process.env.SUPERGLUE_API_KEY;
22472
+ if (!apiKey) {
22473
+ error("API key is required (--api-key or SUPERGLUE_API_KEY)");
22474
+ process.exit(1);
22475
+ }
22476
+ endpoint = opts.endpoint;
22477
+ const defaultWeb = endpoint.replace(/:3002\b/, ":3001").replace(/api\.superglue/, "app.superglue");
22478
+ webEndpoint = opts.webEndpoint || defaultWeb;
22479
+ outputMode = opts.outputMode === "stdout+file" ? "stdout+file" : "stdout";
22480
+ outputDir = opts.outputDir;
22481
+ preferLocal = !opts.global;
22482
+ const spin = spinner("Verifying connection...");
22483
+ try {
22484
+ const client = new import_shared2.SuperglueClient({ apiKey, apiEndpoint: endpoint });
22485
+ await client.listSystems(1);
22486
+ spin.stop(`${c.green}\u2713${c.reset} Connected to ${c.bold}${endpoint}${c.reset}`);
22487
+ } catch (err) {
22488
+ spin.stop(`${c.red}\u2717${c.reset} Connection failed`);
22489
+ error(err.message);
22490
+ process.exit(1);
22491
+ }
22492
+ } else {
22493
+ banner();
22494
+ console.log(` ${c.bold}Welcome to the superglue CLI setup!${c.reset}`);
22495
+ console.log(` ${c.dim}Let's get you connected in a few steps.${c.reset}
22317
22496
  `);
22318
- heading("Authentication");
22319
- console.log(
22320
- ` ${c.dim}Get your API key at${c.reset} ${link("https://app.superglue.cloud/admin?view=api-keys")}`
22321
- );
22322
- console.log("");
22323
- const apiKey = await promptHidden(" API Key");
22324
- if (!apiKey) {
22325
- error("API key is required");
22326
- process.exit(1);
22327
- }
22328
- const endpoint = await prompt(" API Endpoint", "https://api.superglue.cloud");
22329
- const defaultWeb = endpoint.replace(/:3002\b/, ":3001").replace(/api\.superglue/, "app.superglue");
22330
- const webEndpoint = await prompt(" Web Endpoint (for OAuth callbacks)", defaultWeb);
22331
- const spin = spinner("Verifying connection...");
22332
- try {
22333
- const client = new import_shared2.SuperglueClient({ apiKey, apiEndpoint: endpoint });
22334
- await client.listSystems(1);
22335
- spin.stop(`${c.green}\u2713${c.reset} Connected to ${c.bold}${endpoint}${c.reset}`);
22336
- } catch (err) {
22337
- spin.stop(`${c.red}\u2717${c.reset} Connection failed`);
22338
- error(err.message);
22339
- process.exit(1);
22497
+ heading("Authentication");
22498
+ console.log(
22499
+ ` ${c.dim}Get your API key at${c.reset} ${link("https://app.superglue.cloud/admin?view=api-keys")}`
22500
+ );
22501
+ console.log("");
22502
+ apiKey = await promptHidden(" API Key");
22503
+ if (!apiKey) {
22504
+ error("API key is required");
22505
+ process.exit(1);
22506
+ }
22507
+ endpoint = await prompt(" API Endpoint", opts.endpoint);
22508
+ const defaultWeb = endpoint.replace(/:3002\b/, ":3001").replace(/api\.superglue/, "app.superglue");
22509
+ webEndpoint = await prompt(" Web Endpoint (for OAuth callbacks)", defaultWeb);
22510
+ const spin = spinner("Verifying connection...");
22511
+ try {
22512
+ const client = new import_shared2.SuperglueClient({ apiKey, apiEndpoint: endpoint });
22513
+ await client.listSystems(1);
22514
+ spin.stop(`${c.green}\u2713${c.reset} Connected to ${c.bold}${endpoint}${c.reset}`);
22515
+ } catch (err) {
22516
+ spin.stop(`${c.red}\u2717${c.reset} Connection failed`);
22517
+ error(err.message);
22518
+ process.exit(1);
22519
+ }
22520
+ heading("Output Preferences");
22521
+ const outputModeIdx = await choose(
22522
+ "Output mode",
22523
+ [
22524
+ "stdout only \u2014 print results to terminal",
22525
+ "stdout + file \u2014 also save results as JSON files"
22526
+ ],
22527
+ 0
22528
+ );
22529
+ outputMode = outputModeIdx === 1 ? "stdout+file" : "stdout";
22530
+ outputDir = ".superglue/output";
22531
+ if (outputMode === "stdout+file") {
22532
+ outputDir = await prompt(" Output directory", ".superglue/output");
22533
+ }
22534
+ heading("Config Location");
22535
+ const homeDir = os2.homedir();
22536
+ const localPath = path2.join(process.cwd(), ".superglue");
22537
+ const globalPath = path2.join(homeDir, ".superglue");
22538
+ const configLocationIdx = await choose(
22539
+ "Where to save config?",
22540
+ [
22541
+ `Project (${localPath}) \u2014 config stays with this project`,
22542
+ `Global (${globalPath}) \u2014 shared across all projects`
22543
+ ],
22544
+ 0
22545
+ );
22546
+ preferLocal = configLocationIdx === 0;
22340
22547
  }
22341
- heading("Output Preferences");
22342
- const outputModeIdx = await choose(
22343
- "Output mode",
22344
- [
22345
- "stdout only \u2014 print results to terminal",
22346
- "stdout + file \u2014 also save results as JSON files"
22347
- ],
22348
- 0
22349
- );
22350
- const outputMode = outputModeIdx === 1 ? "stdout+file" : "stdout";
22351
- let outputDir = ".superglue/output";
22352
- if (outputMode === "stdout+file") {
22353
- outputDir = await prompt(" Output directory", ".superglue/output");
22354
- }
22355
- heading("Config Location");
22356
- const homeDir = os2.homedir();
22357
- const localPath = path2.join(process.cwd(), ".superglue");
22358
- const globalPath = path2.join(homeDir, ".superglue");
22359
- const configLocationIdx = await choose(
22360
- "Where to save config?",
22361
- [
22362
- `Project (${localPath}) \u2014 config stays with this project`,
22363
- `Global (${globalPath}) \u2014 shared across all projects`
22364
- ],
22365
- 0
22366
- );
22367
- const preferLocal = configLocationIdx === 0;
22368
22548
  const config = {
22369
22549
  apiKey,
22370
22550
  endpoint,
@@ -22935,12 +23115,21 @@ async function collectSensitiveCredentials(fields) {
22935
23115
  return creds;
22936
23116
  }
22937
23117
  function registerCreateCommand(parent, getContext2) {
22938
- parent.command("create").description("Create a new system").option("--config <file>", "JSON config file").option("--id <id>", "System ID (derived from name if omitted)").option("--name <name>", "Human-readable name (required)").option("--url <url>", "API URL").option("--template <id>", "Template ID").option("--instructions <text>", "Specific instructions").option("--credentials <json>", "Non-sensitive credentials JSON").option("--sensitive-credentials <fields>", "Comma-separated sensitive credential field names").option("--docs-url <url>", "Documentation URL to scrape").option("--openapi-url <url>", "OpenAPI spec URL").addOption(
23118
+ parent.command("create").description("Create a new system").option("--config <file>", "JSON config file").option("--id <id>", "System ID (derived from name if omitted)").option("--name <name>", "Human-readable name (required)").option("--url <url>", "API URL").option(
23119
+ "--template <id>",
23120
+ "Template ID \u2014 auto-fills URL, OAuth config, and credentials. Auto-detected from URL if omitted."
23121
+ ).option("--instructions <text>", "Specific instructions").option("--credentials <json>", "Non-sensitive credentials JSON").option("--sensitive-credentials <fields>", "Comma-separated sensitive credential field names").option("--docs-url <url>", "Documentation URL to scrape").option("--openapi-url <url>", "OpenAPI spec URL").addOption(
22939
23122
  new import_commander.Option("--env <environment>", "Environment: dev or prod (default: prod)").choices([
22940
23123
  "dev",
22941
23124
  "prod"
22942
23125
  ])
22943
- ).action(async (opts) => {
23126
+ ).addHelpText("after", () => {
23127
+ const oauthTemplates = Object.entries(import_shared6.systems).filter(([, t]) => t.oauth).map(([key, t]) => ` ${key.padEnd(24)} ${t.name}`).join("\n");
23128
+ return `
23129
+ OAuth templates (use with --template or auto-detected from URL):
23130
+ ${oauthTemplates}
23131
+ `;
23132
+ }).action(async (opts) => {
22944
23133
  const { client } = getContext2();
22945
23134
  let systemInput;
22946
23135
  if (opts.config) {
@@ -22973,14 +23162,34 @@ function registerCreateCommand(parent, getContext2) {
22973
23162
  environment: opts.env === "dev" || opts.env === "prod" ? opts.env : void 0
22974
23163
  };
22975
23164
  }
22976
- if (opts.template) {
22977
- const template = import_shared6.systems[opts.template];
23165
+ let templateKey = opts.template;
23166
+ let template;
23167
+ if (templateKey) {
23168
+ template = import_shared6.systems[templateKey];
22978
23169
  if (!template) {
22979
23170
  error(
22980
- `Template not found: ${opts.template}. Available: ${Object.keys(import_shared6.systems).join(", ")}`
23171
+ `Template not found: ${templateKey}. Available: ${Object.keys(import_shared6.systems).join(", ")}`
22981
23172
  );
22982
23173
  process.exit(1);
22983
23174
  }
23175
+ } else if (systemInput.url || systemInput.name || systemInput.id) {
23176
+ const match = (0, import_shared6.findTemplateForSystem)({
23177
+ url: systemInput.url,
23178
+ name: systemInput.name,
23179
+ id: systemInput.id
23180
+ });
23181
+ if (match) {
23182
+ templateKey = match.key;
23183
+ template = match.template;
23184
+ if (process.stderr.isTTY) {
23185
+ process.stderr.write(
23186
+ `${c.dim}Auto-detected template: ${c.bold}${templateKey}${c.reset}${c.dim} (use --template to override)${c.reset}
23187
+ `
23188
+ );
23189
+ }
23190
+ }
23191
+ }
23192
+ if (template && templateKey) {
22984
23193
  const oauthCreds = {};
22985
23194
  if (template.oauth) {
22986
23195
  if (template.oauth.authUrl) oauthCreds.auth_url = template.oauth.authUrl;
@@ -22992,7 +23201,7 @@ function registerCreateCommand(parent, getContext2) {
22992
23201
  ...systemInput,
22993
23202
  name: systemInput.name || template.name,
22994
23203
  url: systemInput.url || template.apiUrl,
22995
- templateName: opts.template,
23204
+ templateName: templateKey,
22996
23205
  credentials: { ...oauthCreds, ...systemInput.credentials }
22997
23206
  };
22998
23207
  }
@@ -23031,12 +23240,14 @@ function registerCreateCommand(parent, getContext2) {
23031
23240
  if (process.argv.includes("--json") || !process.stdout.isTTY) {
23032
23241
  output({
23033
23242
  success: true,
23034
- system: { id: result.id, name: result.name, url: result.url }
23243
+ system: { id: result.id, name: result.name, url: result.url },
23244
+ ...templateKey ? { template: templateKey } : {}
23035
23245
  });
23036
23246
  } else {
23037
23247
  success(`System created: ${c.bold}${result.id}${c.reset}`, {
23038
23248
  name: result.name,
23039
- url: result.url
23249
+ url: result.url,
23250
+ ...templateKey ? { template: templateKey } : {}
23040
23251
  });
23041
23252
  }
23042
23253
  } catch (err) {
@@ -23334,6 +23545,7 @@ function registerDocsCommand(parent, getContext2) {
23334
23545
 
23335
23546
  // src/commands/system/oauth.ts
23336
23547
  var import_node_child_process2 = require("child_process");
23548
+ var import_node_crypto = __toESM(require("crypto"));
23337
23549
  var import_shared9 = __toESM(require_dist2());
23338
23550
  function openBrowser(url) {
23339
23551
  const platform = process.platform;
@@ -23449,6 +23661,21 @@ function registerOAuthCommand(parent, getContext2) {
23449
23661
  process.exit(1);
23450
23662
  }
23451
23663
  const encryptedApiKey = (0, import_shared9.encryptCliApiKey)(config.apiKey, opts.systemId, encryptionSecret);
23664
+ const usingTemplate = Boolean(templateMatch?.key);
23665
+ let clientCredentialsUid;
23666
+ if (!usingTemplate && system.credentials?.client_secret && clientId) {
23667
+ clientCredentialsUid = import_node_crypto.default.randomUUID();
23668
+ try {
23669
+ await client.cacheOauthClientCredentials({
23670
+ clientCredentialsUid,
23671
+ clientId,
23672
+ clientSecret: system.credentials.client_secret
23673
+ });
23674
+ } catch (err) {
23675
+ error(`Failed to cache OAuth credentials: ${err.message}`);
23676
+ process.exit(1);
23677
+ }
23678
+ }
23452
23679
  const redirectUri = `${config.webEndpoint.replace(/\/$/, "")}/api/auth/callback`;
23453
23680
  const state = {
23454
23681
  systemId: opts.systemId,
@@ -23458,6 +23685,7 @@ function registerOAuthCommand(parent, getContext2) {
23458
23685
  token_url: tokenUrl,
23459
23686
  clientId,
23460
23687
  ...templateMatch ? { templateId: templateMatch.key } : {},
23688
+ ...clientCredentialsUid ? { client_credentials_uid: clientCredentialsUid } : {},
23461
23689
  scopes: opts.scopes,
23462
23690
  cliApiKey: encryptedApiKey
23463
23691
  };
@@ -235,21 +235,26 @@ sg system call --url "postgres://<<my_db_host>>:5432/mydb" \
235
235
  --system-id my_db \
236
236
  --body '{"query":"SELECT * FROM users LIMIT 5"}'
237
237
 
238
+ # Redis command
239
+ sg system call --url "redis://<<my_redis_host>>:6379/0" \
240
+ --system-id my_redis \
241
+ --body '{"command":"HGETALL","args":["user:123"]}'
242
+
238
243
  # File server (SFTP)
239
244
  sg system call --url "sftp://<<my_sftp_host>>:22/data" \
240
245
  --system-id my_sftp \
241
246
  --body '{"operation":"list","path":"/"}'
242
247
  ```
243
248
 
244
- | Flag | Description |
245
- | ------------------- | --------------------------------------------------------- |
246
- | `--url <url>` | Full URL with protocol (http/https/postgres/sftp/ftp/smb) |
247
- | `--system-id <id>` | System ID for credential injection |
248
- | `--method <method>` | HTTP method (GET, POST, PUT, DELETE, PATCH) |
249
- | `--headers <json>` | HTTP headers JSON with credential placeholders |
250
- | `--body <string>` | Request body (JSON string) |
251
- | `--env <env>` | Environment: `dev` or `prod` |
252
- | `--file <key=path>` | File references (repeatable) |
249
+ | Flag | Description |
250
+ | ------------------- | ---------------------------------------------------------------------- |
251
+ | `--url <url>` | Full URL with protocol (http/https/postgres/redis/rediss/sftp/ftp/smb) |
252
+ | `--system-id <id>` | System ID for credential injection |
253
+ | `--method <method>` | HTTP method (GET, POST, PUT, DELETE, PATCH) |
254
+ | `--headers <json>` | HTTP headers JSON with credential placeholders |
255
+ | `--body <string>` | Request body (JSON string) |
256
+ | `--env <env>` | Environment: `dev` or `prod` |
257
+ | `--file <key=path>` | File references (repeatable) |
253
258
 
254
259
  **`sg system search-docs`** — Search system documentation.
255
260
 
@@ -366,15 +371,26 @@ All step config fields support `<<variable>>` placeholders:
366
371
  | -------------------------- | --------------------------- |
367
372
  | `<<fieldName>>` | Payload field or credential |
368
373
  | `<<systemId_credKey>>` | System credential |
374
+ | `<<sg_auth_email>>` | Email of authenticated user |
369
375
  | `<<(sourceData) => expr>>` | JavaScript expression |
370
376
 
371
377
  **CRITICAL**: Simple `<<varName>>` only works for **top-level keys**. No dots, no nesting.
372
378
 
373
- - VALID: `<<userId>>`, `<<currentItem>>`, `<<stripe_api_key>>`
379
+ - VALID: `<<userId>>`, `<<currentItem>>`, `<<stripe_api_key>>`, `<<sg_auth_email>>`
374
380
  - INVALID: `<<currentItem.id>>`, `<<sourceData.userId>>` — these FAIL at runtime
375
381
 
376
382
  For nested access, use arrow functions: `<<(sourceData) => sourceData.currentItem.id>>`
377
383
 
384
+ ### Context Variables
385
+
386
+ Context variables provide information about the execution environment:
387
+
388
+ - `<<sg_auth_email>>` — Email of the authenticated user who triggered the tool
389
+
390
+ **Use case**: Personalize tool outputs based on who runs the tool (e.g., user-specific permissions, account lookups, audit logging).
391
+
392
+ **Important**: `sg_auth_email` is NOT available in scheduled executions. Tools using `<<sg_auth_email>>` will fail if run via scheduler.
393
+
378
394
  ### sourceData Object
379
395
 
380
396
  `sourceData` is the cumulative state available to all JS expressions (data selectors, transforms, variable expressions):
@@ -442,7 +458,7 @@ Empty arrays are valid — the step simply skips execution.
442
458
 
443
459
  ### Two Step Types
444
460
 
445
- **Request step** (HTTP, Postgres, FTP/SFTP/SMB) — makes an API call:
461
+ **Request step** (HTTP, Postgres, Redis, FTP/SFTP/SMB) — makes an API call:
446
462
 
447
463
  ```json
448
464
  {
@@ -534,9 +550,31 @@ Templates auto-populate endpoints and OAuth config. Use `sg system find` to disc
534
550
 
535
551
  ### OAuth Flow
536
552
 
537
- 1. `sg system create --name "Gmail" --url https://gmail.googleapis.com --credentials '{"auth_url":"...","token_url":"..."}' --sensitive-credentials client_id,client_secret`
538
- 2. `sg system oauth --system-id gmail --scopes "..."`
539
- 3. User authenticates in browser → tokens saved automatically
553
+ **IMPORTANT:** For OAuth systems, always use `--template` when creating the system. Templates auto-configure OAuth URLs, token exchange settings, and scopes. Without a template, OAuth will likely fail or require manual credential setup.
554
+
555
+ If you don't know the template ID, the CLI auto-detects it from the URL — but using `--template` explicitly is more reliable.
556
+
557
+ **Step 1: Create the system with template and credentials**
558
+
559
+ ```bash
560
+ # Best: explicit template (auto-fills URL and OAuth config)
561
+ sg system create --name "Gmail" --template gmail \
562
+ --credentials '{"client_id":"...","client_secret":"..."}'
563
+
564
+ # Also works: URL-based auto-detection (template inferred from googleapis.com)
565
+ sg system create --name "Gmail" --url https://gmail.googleapis.com \
566
+ --credentials '{"client_id":"...","client_secret":"..."}'
567
+ ```
568
+
569
+ **Step 2: Authenticate via OAuth**
570
+
571
+ ```bash
572
+ sg system oauth --system-id gmail --scopes "https://www.googleapis.com/auth/gmail.readonly"
573
+ ```
574
+
575
+ **Step 3:** User authenticates in browser → tokens saved automatically
576
+
577
+ **Common OAuth templates:** gmail, google_drive, google_calendar, google_sheets, slack, salesforce, github, jira, confluence, notion, airtable, hubspot, shopify, dropbox, zoom, microsoft_365
540
578
 
541
579
  ### System IDs
542
580
 
@@ -608,6 +646,7 @@ For detailed documentation on specific topics, read these files in the `referenc
608
646
  | ------------------------------------- | ------------------------------------------------------------------------------------------------ |
609
647
  | `references/integration.md` | **READ THIS** when deploying tools to production - SDK usage, REST API, webhooks, error handling |
610
648
  | `references/databases.md` | Building tools that query PostgreSQL/MySQL databases |
649
+ | `references/redis.md` | Building tools that interact with Redis (commands, pipelines, data types) |
611
650
  | `references/file-servers.md` | Building tools that interact with FTP/SFTP/SMB file servers |
612
651
  | `references/transforms-and-output.md` | Complex data transformations, output shaping, JS sandbox constraints |
613
652