@blinkdotnew/cli 0.2.0 → 0.2.3
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 +18 -1
- package/dist/cli.js +277 -73
- package/package.json +1 -1
- package/src/cli.ts +13 -3
- package/src/commands/connector.ts +183 -34
- package/src/commands/web.ts +74 -0
package/README.md
CHANGED
|
@@ -36,6 +36,10 @@ blink db query "SELECT count(*) FROM users"
|
|
|
36
36
|
# Generate AI content
|
|
37
37
|
blink ai image "a glowing blink logo on dark background"
|
|
38
38
|
blink ai text "Summarize this article: ..."
|
|
39
|
+
|
|
40
|
+
# Scrape websites
|
|
41
|
+
blink scrape https://lovable.dev --extract "pricing tiers and costs"
|
|
42
|
+
blink scrape https://example.com --text
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
---
|
|
@@ -218,7 +222,7 @@ blink ai transcribe https://example.com/audio.mp3 --language en
|
|
|
218
222
|
|
|
219
223
|
---
|
|
220
224
|
|
|
221
|
-
### `blink fetch`
|
|
225
|
+
### `blink fetch`, `blink search`, `blink scrape` — Web & data
|
|
222
226
|
|
|
223
227
|
```bash
|
|
224
228
|
# Fetch any URL via Blink proxy (handles CORS, auth headers)
|
|
@@ -229,8 +233,21 @@ blink fetch https://api.example.com --header "X-API-Key: secret"
|
|
|
229
233
|
# Web search
|
|
230
234
|
blink search "latest AI news"
|
|
231
235
|
blink search "React Server Components" --count 10 --json
|
|
236
|
+
|
|
237
|
+
# Scrape web pages
|
|
238
|
+
blink scrape https://example.com # Raw response
|
|
239
|
+
blink scrape https://example.com --text # Clean text (strips HTML)
|
|
240
|
+
blink scrape https://example.com --extract "all prices" # AI-extract specific data
|
|
241
|
+
blink scrape https://news.ycombinator.com --extract "top 10 story titles and URLs"
|
|
242
|
+
blink scrape https://example.com --extract "contact email" --json
|
|
232
243
|
```
|
|
233
244
|
|
|
245
|
+
No project key needed — `blink scrape` uses your workspace key only.
|
|
246
|
+
|
|
247
|
+
- `--text` — strips all HTML tags, returns readable text
|
|
248
|
+
- `--extract <instructions>` — uses AI (Gemini Flash) to extract exactly what you ask for
|
|
249
|
+
- Combine with `--json` for `{ url, content }` or `{ url, extracted }` output
|
|
250
|
+
|
|
234
251
|
---
|
|
235
252
|
|
|
236
253
|
### `blink realtime` — Pub/sub
|
package/dist/cli.js
CHANGED
|
@@ -509,6 +509,65 @@ Examples:
|
|
|
509
509
|
if (typeof result === "string") console.log(result);
|
|
510
510
|
else console.log(JSON.stringify(result, null, 2));
|
|
511
511
|
});
|
|
512
|
+
program2.command("scrape <url>").description("Scrape a webpage \u2014 returns text content, optionally AI-extracted data").option("--extract <instructions>", 'What to extract using AI (e.g. "all prices and product names")').option("--text", "Strip HTML tags and return clean readable text").addHelpText("after", `
|
|
513
|
+
No project key needed \u2014 scrape runs through the Blink proxy using your workspace key.
|
|
514
|
+
|
|
515
|
+
Examples:
|
|
516
|
+
$ blink scrape https://example.com Get full page content
|
|
517
|
+
$ blink scrape https://example.com --text Clean text only (no HTML)
|
|
518
|
+
$ blink scrape https://example.com --extract "all prices" AI-extract specific data
|
|
519
|
+
$ blink scrape https://news.ycombinator.com --extract "top 10 story titles and URLs" --json
|
|
520
|
+
`).action(async (url, opts) => {
|
|
521
|
+
requireToken();
|
|
522
|
+
const response = await withSpinner(
|
|
523
|
+
"Fetching page...",
|
|
524
|
+
() => resourcesRequest("/api/v1/fetch", { body: { url } })
|
|
525
|
+
);
|
|
526
|
+
let content;
|
|
527
|
+
if (typeof response === "string") {
|
|
528
|
+
content = response;
|
|
529
|
+
} else if (typeof response?.body === "string") {
|
|
530
|
+
content = response.body;
|
|
531
|
+
} else {
|
|
532
|
+
content = JSON.stringify(response);
|
|
533
|
+
}
|
|
534
|
+
if (opts.extract) {
|
|
535
|
+
const extraction = await withSpinner(
|
|
536
|
+
`Extracting: ${opts.extract}...`,
|
|
537
|
+
() => resourcesRequest("/api/v1/ai/chat/completions", {
|
|
538
|
+
body: {
|
|
539
|
+
model: "google/gemini-3-flash",
|
|
540
|
+
messages: [
|
|
541
|
+
{
|
|
542
|
+
role: "system",
|
|
543
|
+
content: "You are a web scraping assistant. Extract exactly the requested information from the webpage content provided. Return only the extracted data, formatted clearly. No commentary."
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
role: "user",
|
|
547
|
+
content: `URL: ${url}
|
|
548
|
+
|
|
549
|
+
Webpage content:
|
|
550
|
+
${content.slice(0, 5e4)}
|
|
551
|
+
|
|
552
|
+
Extract: ${opts.extract}`
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
stream: false
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
const extracted = extraction?.choices?.[0]?.message?.content ?? "";
|
|
560
|
+
if (isJsonMode()) return printJson({ url, extracted, extract_instructions: opts.extract });
|
|
561
|
+
console.log(extracted);
|
|
562
|
+
} else if (opts.text) {
|
|
563
|
+
const text = content.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, " ").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/\s+/g, " ").trim();
|
|
564
|
+
if (isJsonMode()) return printJson({ url, text });
|
|
565
|
+
console.log(text);
|
|
566
|
+
} else {
|
|
567
|
+
if (isJsonMode()) return printJson({ url, content });
|
|
568
|
+
console.log(content);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
512
571
|
program2.command("search <query>").description("Web search \u2014 returns titles, URLs, and snippets").option("--count <n>", "Number of results to return (max 20)", "5").addHelpText("after", `
|
|
513
572
|
Examples:
|
|
514
573
|
$ blink search "latest AI news"
|
|
@@ -950,35 +1009,170 @@ The email is sent from your project's configured sender address (set in blink.ne
|
|
|
950
1009
|
}
|
|
951
1010
|
|
|
952
1011
|
// src/commands/connector.ts
|
|
1012
|
+
import chalk6 from "chalk";
|
|
1013
|
+
var PROVIDERS = [
|
|
1014
|
+
// Communication
|
|
1015
|
+
{ key: "slack", label: "Slack", category: "Communication" },
|
|
1016
|
+
{ key: "discord", label: "Discord", category: "Communication" },
|
|
1017
|
+
// Email & Calendar
|
|
1018
|
+
{ key: "google_gmail", label: "Gmail", category: "Email & Calendar" },
|
|
1019
|
+
{ key: "google_calendar", label: "Google Calendar", category: "Email & Calendar" },
|
|
1020
|
+
{ key: "microsoft_outlook", label: "Outlook", category: "Email & Calendar" },
|
|
1021
|
+
{ key: "microsoft_calendar", label: "Microsoft Calendar", category: "Email & Calendar" },
|
|
1022
|
+
// Files & Docs
|
|
1023
|
+
{ key: "google_drive", label: "Google Drive", category: "Files & Docs" },
|
|
1024
|
+
{ key: "google_docs", label: "Google Docs", category: "Files & Docs" },
|
|
1025
|
+
{ key: "google_sheets", label: "Google Sheets", category: "Files & Docs" },
|
|
1026
|
+
{ key: "google_slides", label: "Google Slides", category: "Files & Docs" },
|
|
1027
|
+
{ key: "microsoft_onedrive", label: "OneDrive", category: "Files & Docs" },
|
|
1028
|
+
{ key: "notion", label: "Notion", category: "Files & Docs" },
|
|
1029
|
+
// Dev Tools
|
|
1030
|
+
{ key: "github", label: "GitHub", category: "Dev Tools" },
|
|
1031
|
+
{ key: "linear", label: "Linear", category: "Dev Tools" },
|
|
1032
|
+
{ key: "jira", label: "Jira", category: "Dev Tools" },
|
|
1033
|
+
{ key: "vercel", label: "Vercel", category: "Dev Tools" },
|
|
1034
|
+
{ key: "figma", label: "Figma", category: "Dev Tools" },
|
|
1035
|
+
// CRM & Sales
|
|
1036
|
+
{ key: "hubspot", label: "HubSpot", category: "CRM & Sales" },
|
|
1037
|
+
{ key: "salesforce", label: "Salesforce", category: "CRM & Sales" },
|
|
1038
|
+
{ key: "pipedrive", label: "Pipedrive", category: "CRM & Sales" },
|
|
1039
|
+
{ key: "attio", label: "Attio", category: "CRM & Sales" },
|
|
1040
|
+
// Marketing
|
|
1041
|
+
{ key: "mailchimp", label: "Mailchimp", category: "Marketing" },
|
|
1042
|
+
{ key: "typeform", label: "Typeform", category: "Marketing" },
|
|
1043
|
+
// Data & Productivity
|
|
1044
|
+
{ key: "airtable", label: "Airtable", category: "Data & Productivity" },
|
|
1045
|
+
{ key: "asana", label: "Asana", category: "Data & Productivity" },
|
|
1046
|
+
{ key: "calendly", label: "Calendly", category: "Data & Productivity" },
|
|
1047
|
+
// Social & Creator
|
|
1048
|
+
{ key: "twitter", label: "X (Twitter)", category: "Social & Creator" },
|
|
1049
|
+
{ key: "instagram", label: "Instagram", category: "Social & Creator" },
|
|
1050
|
+
{ key: "tiktok", label: "TikTok", category: "Social & Creator" },
|
|
1051
|
+
{ key: "youtube", label: "YouTube", category: "Social & Creator" },
|
|
1052
|
+
{ key: "linkedin", label: "LinkedIn", category: "Social & Creator" },
|
|
1053
|
+
{ key: "reddit", label: "Reddit", category: "Social & Creator" },
|
|
1054
|
+
{ key: "loom", label: "Loom", category: "Social & Creator" },
|
|
1055
|
+
// Meetings
|
|
1056
|
+
{ key: "zoom", label: "Zoom", category: "Meetings" },
|
|
1057
|
+
{ key: "microsoft_teams", label: "Microsoft Teams", category: "Meetings" },
|
|
1058
|
+
// Commerce
|
|
1059
|
+
{ key: "stripe", label: "Stripe", category: "Commerce" },
|
|
1060
|
+
{ key: "shopify", label: "Shopify", category: "Commerce" },
|
|
1061
|
+
{ key: "etsy", label: "Etsy", category: "Commerce" }
|
|
1062
|
+
];
|
|
953
1063
|
function registerConnectorCommands(program2) {
|
|
954
|
-
const connector = program2.command("connector").description("Execute actions on connected OAuth apps (Notion, Slack,
|
|
955
|
-
Connectors are OAuth accounts linked to your workspace
|
|
956
|
-
Once linked, use blink connector exec to call their APIs without managing tokens yourself.
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
$ blink connector exec
|
|
964
|
-
$ blink connector exec
|
|
965
|
-
$ blink connector exec
|
|
966
|
-
$ blink connector exec
|
|
967
|
-
$ blink connector exec
|
|
968
|
-
$ blink connector exec
|
|
969
|
-
|
|
970
|
-
|
|
1064
|
+
const connector = program2.command("connector").description("Execute actions on connected OAuth apps (GitHub, Notion, Slack, Reddit, Google, and 30+ more)").addHelpText("after", `
|
|
1065
|
+
Connectors are OAuth accounts linked to your workspace at blink.new \u2192 Settings \u2192 Integrations.
|
|
1066
|
+
Once linked, use \`blink connector exec\` to call their APIs without managing tokens yourself.
|
|
1067
|
+
|
|
1068
|
+
Connect accounts at: https://blink.new/settings?tab=connectors
|
|
1069
|
+
|
|
1070
|
+
Run \`blink connector providers\` to see all 39 supported providers.
|
|
1071
|
+
|
|
1072
|
+
Quick examples:
|
|
1073
|
+
$ blink connector exec github /user/repos GET
|
|
1074
|
+
$ blink connector exec notion /search POST '{"query":"meeting notes"}'
|
|
1075
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"Hello"}'
|
|
1076
|
+
$ blink connector exec google_calendar /calendars/primary/events GET
|
|
1077
|
+
$ blink connector exec jira /issue POST '{"fields":{"project":{"key":"PROJ"},"summary":"Bug"}}'
|
|
1078
|
+
$ blink connector exec stripe /customers GET '{"limit":10}'
|
|
1079
|
+
$ blink connector exec shopify /orders.json GET
|
|
1080
|
+
$ blink connector exec linear '{ viewer { id name } }' POST # GraphQL
|
|
1081
|
+
|
|
1082
|
+
Use --method GET for reads, POST for writes (default: POST).
|
|
1083
|
+
Use --account <id> if you have multiple linked accounts for the same provider.
|
|
971
1084
|
`);
|
|
972
|
-
connector.command("
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
`
|
|
1085
|
+
connector.command("providers").description("List all supported OAuth connector providers").option("--category <cat>", "Filter by category").action((opts) => {
|
|
1086
|
+
const filtered = opts.category ? PROVIDERS.filter((p) => p.category.toLowerCase().includes(opts.category.toLowerCase())) : PROVIDERS;
|
|
1087
|
+
if (isJsonMode()) return printJson(filtered);
|
|
1088
|
+
const categories = [...new Set(filtered.map((p) => p.category))];
|
|
1089
|
+
for (const cat of categories) {
|
|
1090
|
+
console.log(chalk6.bold.cyan(`
|
|
1091
|
+
${cat}`));
|
|
1092
|
+
const catProviders = filtered.filter((p) => p.category === cat);
|
|
1093
|
+
for (const p of catProviders) {
|
|
1094
|
+
console.log(` ${chalk6.green(p.key.padEnd(22))} ${chalk6.dim(p.label)}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
console.log(chalk6.dim(`
|
|
1098
|
+
${filtered.length} providers total. Connect at blink.new/settings?tab=connectors`));
|
|
1099
|
+
});
|
|
1100
|
+
connector.command("status [provider]").description("Check connection status for a provider (or all providers)").option("--account <id>", "Check a specific account ID").action(async (provider, opts) => {
|
|
1101
|
+
requireToken();
|
|
1102
|
+
if (provider) {
|
|
1103
|
+
const result = await withSpinner(
|
|
1104
|
+
`Checking ${provider} status...`,
|
|
1105
|
+
() => resourcesRequest(`/v1/connectors/${provider}/status`, {
|
|
1106
|
+
method: "GET",
|
|
1107
|
+
...opts.account ? { query: { account_id: opts.account } } : {}
|
|
1108
|
+
})
|
|
1109
|
+
);
|
|
1110
|
+
if (isJsonMode()) return printJson(result);
|
|
1111
|
+
const d = result?.data ?? result;
|
|
1112
|
+
if (d?.connected) {
|
|
1113
|
+
console.log(chalk6.green("\u2713 Connected"));
|
|
1114
|
+
if (d.account_id) console.log(chalk6.dim(` Account: ${d.account_id}`));
|
|
1115
|
+
if (d.metadata?.email) console.log(chalk6.dim(` Email: ${d.metadata.email}`));
|
|
1116
|
+
if (d.metadata?.name) console.log(chalk6.dim(` Name: ${d.metadata.name}`));
|
|
1117
|
+
} else {
|
|
1118
|
+
console.log(chalk6.red("\u2717 Not connected"));
|
|
1119
|
+
console.log(chalk6.dim(` Connect at blink.new/settings?tab=connectors`));
|
|
1120
|
+
}
|
|
1121
|
+
} else {
|
|
1122
|
+
const result = await withSpinner(
|
|
1123
|
+
"Checking all connectors...",
|
|
1124
|
+
() => resourcesRequest("/v1/connectors/linked", { method: "GET" })
|
|
1125
|
+
);
|
|
1126
|
+
if (isJsonMode()) return printJson(result);
|
|
1127
|
+
const connected = result?.data ?? [];
|
|
1128
|
+
if (!connected.length) {
|
|
1129
|
+
console.log(chalk6.dim("No connectors linked. Connect at blink.new/settings?tab=connectors"));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const table = createTable(["Provider", "Accounts"]);
|
|
1133
|
+
for (const c of connected) {
|
|
1134
|
+
const accounts = c.accounts.map((a) => a.label || a.account_id).join(", ");
|
|
1135
|
+
table.push([chalk6.green(c.provider), accounts]);
|
|
1136
|
+
}
|
|
1137
|
+
console.log(table.toString());
|
|
1138
|
+
console.log(chalk6.dim(`
|
|
1139
|
+
${connected.length} provider(s) connected`));
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
connector.command("exec <provider> <endpoint> [params]").description("Execute a call on a connected OAuth provider").option("--account <id>", "Specific account ID (if you have multiple accounts)").option("--method <method>", "HTTP method: GET | POST | PUT | PATCH | DELETE (default: POST)", "POST").addHelpText("after", `
|
|
1143
|
+
<endpoint> is the API path relative to the provider's base URL, OR a GraphQL query string for Linear.
|
|
1144
|
+
|
|
1145
|
+
Examples (REST):
|
|
1146
|
+
$ blink connector exec github /user/repos GET
|
|
1147
|
+
$ blink connector exec github /repos/owner/repo/issues POST '{"title":"Bug","body":"..."}'
|
|
1148
|
+
$ blink connector exec notion /search POST '{"query":"meeting notes"}'
|
|
1149
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"Hello"}'
|
|
1150
|
+
$ blink connector exec google_drive /files GET '{"q":"mimeType=\\'application/pdf\\'"}'
|
|
1151
|
+
$ blink connector exec jira /search GET '{"jql":"assignee=currentUser()","maxResults":20}'
|
|
1152
|
+
$ blink connector exec stripe /customers GET '{"limit":5}'
|
|
1153
|
+
$ blink connector exec shopify /orders.json GET
|
|
1154
|
+
$ blink connector exec zoom /users/me/meetings GET
|
|
1155
|
+
$ blink connector exec github /user/repos GET --json | jq '.[].full_name'
|
|
1156
|
+
|
|
1157
|
+
Examples (GraphQL \u2014 Linear only):
|
|
1158
|
+
$ blink connector exec linear '{ viewer { id name email } }' POST
|
|
1159
|
+
$ blink connector exec linear 'mutation { issueCreate(input: { title: "Bug", teamId: "xxx" }) { issue { id } } }' POST
|
|
1160
|
+
|
|
1161
|
+
Multiple accounts:
|
|
1162
|
+
$ blink connector exec github /user/repos GET --account acct_xxx
|
|
1163
|
+
|
|
1164
|
+
Provider base URLs used:
|
|
1165
|
+
github https://api.github.com/
|
|
1166
|
+
stripe https://api.stripe.com/v1/
|
|
1167
|
+
notion https://api.notion.com/v1/
|
|
1168
|
+
slack https://slack.com/api/
|
|
1169
|
+
google_gmail https://gmail.googleapis.com/gmail/v1/
|
|
1170
|
+
google_drive https://www.googleapis.com/drive/v3/
|
|
1171
|
+
google_calendar https://www.googleapis.com/calendar/v3/
|
|
1172
|
+
jira https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/
|
|
1173
|
+
shopify https://{shop}.myshopify.com/admin/api/2024-10/
|
|
1174
|
+
... run \`blink connector providers\` for all 38 providers
|
|
1175
|
+
`).action(async (provider, endpoint, paramsArg, opts) => {
|
|
982
1176
|
requireToken();
|
|
983
1177
|
let params = {};
|
|
984
1178
|
if (paramsArg) {
|
|
@@ -989,10 +1183,10 @@ Examples:
|
|
|
989
1183
|
}
|
|
990
1184
|
}
|
|
991
1185
|
const result = await withSpinner(
|
|
992
|
-
|
|
1186
|
+
`${opts.method} ${provider}${endpoint}...`,
|
|
993
1187
|
() => resourcesRequest(`/v1/connectors/${provider}/execute`, {
|
|
994
1188
|
body: {
|
|
995
|
-
method:
|
|
1189
|
+
method: endpoint,
|
|
996
1190
|
http_method: opts.method,
|
|
997
1191
|
params,
|
|
998
1192
|
...opts.account ? { account_id: opts.account } : {}
|
|
@@ -1000,7 +1194,7 @@ Examples:
|
|
|
1000
1194
|
})
|
|
1001
1195
|
);
|
|
1002
1196
|
if (isJsonMode()) return printJson(result);
|
|
1003
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1197
|
+
console.log(JSON.stringify(result?.data ?? result, null, 2));
|
|
1004
1198
|
});
|
|
1005
1199
|
}
|
|
1006
1200
|
|
|
@@ -1042,7 +1236,7 @@ async function appRequest(path, opts = {}) {
|
|
|
1042
1236
|
init_project();
|
|
1043
1237
|
import { readdirSync, readFileSync as readFileSync8 } from "fs";
|
|
1044
1238
|
import { join as join3, relative } from "path";
|
|
1045
|
-
import
|
|
1239
|
+
import chalk7 from "chalk";
|
|
1046
1240
|
function collectFiles(dir) {
|
|
1047
1241
|
const files = [];
|
|
1048
1242
|
function walk(current) {
|
|
@@ -1115,7 +1309,7 @@ Project resolution:
|
|
|
1115
1309
|
const production = opts.prod === true;
|
|
1116
1310
|
if (!isJsonMode()) {
|
|
1117
1311
|
console.log();
|
|
1118
|
-
console.log(
|
|
1312
|
+
console.log(chalk7.bold(" Blink Deploy"));
|
|
1119
1313
|
console.log();
|
|
1120
1314
|
}
|
|
1121
1315
|
const files = await withSpinner(`Packaging ${buildDir}...`, async () => collectFiles(buildDir));
|
|
@@ -1169,7 +1363,7 @@ Project resolution:
|
|
|
1169
1363
|
|
|
1170
1364
|
// src/commands/project.ts
|
|
1171
1365
|
init_project();
|
|
1172
|
-
import
|
|
1366
|
+
import chalk8 from "chalk";
|
|
1173
1367
|
function registerProjectCommands(program2) {
|
|
1174
1368
|
const project = program2.command("project").description("Create, list, and delete Blink projects").addHelpText("after", `
|
|
1175
1369
|
Examples:
|
|
@@ -1198,8 +1392,8 @@ After creating a project, link it to your current directory:
|
|
|
1198
1392
|
);
|
|
1199
1393
|
if (isJsonMode()) return printJson(result);
|
|
1200
1394
|
const proj = result?.project ?? result;
|
|
1201
|
-
console.log(
|
|
1202
|
-
console.log(
|
|
1395
|
+
console.log(chalk8.green("\u2713") + ` Created: ${proj.id}`);
|
|
1396
|
+
console.log(chalk8.dim(" Run `blink link " + proj.id + "` to use it"));
|
|
1203
1397
|
});
|
|
1204
1398
|
project.command("delete <project_id>").description("Delete a project").option("--yes", "Skip confirmation").action(async (projectId, opts) => {
|
|
1205
1399
|
requireToken();
|
|
@@ -1242,7 +1436,7 @@ After linking, most commands work without specifying a project_id:
|
|
|
1242
1436
|
});
|
|
1243
1437
|
}
|
|
1244
1438
|
writeProjectConfig({ projectId: id });
|
|
1245
|
-
console.log(
|
|
1439
|
+
console.log(chalk8.green("\u2713") + " Linked to " + id);
|
|
1246
1440
|
});
|
|
1247
1441
|
program2.command("unlink").description("Remove project link from current directory").action(() => {
|
|
1248
1442
|
clearProjectConfig();
|
|
@@ -1254,29 +1448,29 @@ After linking, most commands work without specifying a project_id:
|
|
|
1254
1448
|
const agentId = resolveAgentId3();
|
|
1255
1449
|
const agentSource = process.env.BLINK_AGENT_ID ? "BLINK_AGENT_ID env" : process.env.BLINK_ACTIVE_AGENT ? "BLINK_ACTIVE_AGENT env" : null;
|
|
1256
1450
|
if (agentId) {
|
|
1257
|
-
console.log(
|
|
1451
|
+
console.log(chalk8.bold("Agent ") + agentId + chalk8.dim(" (" + agentSource + ")"));
|
|
1258
1452
|
} else {
|
|
1259
|
-
console.log(
|
|
1453
|
+
console.log(chalk8.dim("Agent not set (use: eval $(blink agent use clw_xxx --export))"));
|
|
1260
1454
|
}
|
|
1261
1455
|
if (config) {
|
|
1262
1456
|
const projectSource = process.env.BLINK_ACTIVE_PROJECT ? "BLINK_ACTIVE_PROJECT env" : ".blink/project.json";
|
|
1263
|
-
console.log(
|
|
1457
|
+
console.log(chalk8.bold("Project ") + config.projectId + chalk8.dim(" (" + projectSource + ")"));
|
|
1264
1458
|
} else if (process.env.BLINK_ACTIVE_PROJECT) {
|
|
1265
|
-
console.log(
|
|
1459
|
+
console.log(chalk8.bold("Project ") + process.env.BLINK_ACTIVE_PROJECT + chalk8.dim(" (BLINK_ACTIVE_PROJECT env)"));
|
|
1266
1460
|
} else {
|
|
1267
|
-
console.log(
|
|
1461
|
+
console.log(chalk8.dim("Project not linked (use: blink link or eval $(blink use proj_xxx --export))"));
|
|
1268
1462
|
}
|
|
1269
1463
|
const authSource = process.env.BLINK_API_KEY ? "BLINK_API_KEY env" : "~/.config/blink/config.toml";
|
|
1270
1464
|
const hasProjectKey = !!process.env.BLINK_PROJECT_KEY;
|
|
1271
|
-
console.log(
|
|
1465
|
+
console.log(chalk8.bold("Auth ") + authSource);
|
|
1272
1466
|
if (hasProjectKey) {
|
|
1273
|
-
console.log(
|
|
1467
|
+
console.log(chalk8.bold("ProjKey ") + "BLINK_PROJECT_KEY env" + chalk8.dim(" (used for db/storage/rag)"));
|
|
1274
1468
|
}
|
|
1275
1469
|
});
|
|
1276
1470
|
}
|
|
1277
1471
|
|
|
1278
1472
|
// src/commands/auth.ts
|
|
1279
|
-
import
|
|
1473
|
+
import chalk9 from "chalk";
|
|
1280
1474
|
function registerAuthCommands(program2) {
|
|
1281
1475
|
program2.command("login").description("Authenticate with your Blink API key").option("--interactive", "Prompt for API key (for headless/SSH/CI environments)").addHelpText("after", `
|
|
1282
1476
|
Get your API key at: blink.new \u2192 Settings \u2192 API Keys (starts with blnk_ak_)
|
|
@@ -1289,7 +1483,7 @@ In Blink Claw agents: BLINK_API_KEY is already set \u2014 login is not needed.
|
|
|
1289
1483
|
For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
1290
1484
|
`).action(async (opts) => {
|
|
1291
1485
|
if (process.env.BLINK_API_KEY && !opts.interactive) {
|
|
1292
|
-
console.log(
|
|
1486
|
+
console.log(chalk9.green("\u2713") + " Already authenticated via BLINK_API_KEY env var.");
|
|
1293
1487
|
return;
|
|
1294
1488
|
}
|
|
1295
1489
|
const { password } = await import("@clack/prompts");
|
|
@@ -1299,7 +1493,7 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
|
1299
1493
|
process.exit(1);
|
|
1300
1494
|
}
|
|
1301
1495
|
writeConfig({ api_key: apiKey });
|
|
1302
|
-
console.log(
|
|
1496
|
+
console.log(chalk9.green("\u2713") + " Saved to ~/.config/blink/config.toml");
|
|
1303
1497
|
});
|
|
1304
1498
|
program2.command("logout").description("Remove stored credentials").action(() => {
|
|
1305
1499
|
clearConfig();
|
|
@@ -1319,15 +1513,15 @@ For CI/GitHub Actions: set BLINK_API_KEY as a secret, skip login entirely.
|
|
|
1319
1513
|
});
|
|
1320
1514
|
}
|
|
1321
1515
|
const source = process.env.BLINK_API_KEY === token ? "BLINK_API_KEY env" : "~/.config/blink/config.toml";
|
|
1322
|
-
console.log(
|
|
1323
|
-
console.log(
|
|
1324
|
-
console.log(
|
|
1516
|
+
console.log(chalk9.green("\u2713") + " Authenticated");
|
|
1517
|
+
console.log(chalk9.bold("Key ") + token.slice(0, 20) + chalk9.dim("..."));
|
|
1518
|
+
console.log(chalk9.bold("Source ") + chalk9.dim(source));
|
|
1325
1519
|
});
|
|
1326
1520
|
}
|
|
1327
1521
|
|
|
1328
1522
|
// src/commands/agent.ts
|
|
1329
1523
|
init_agent();
|
|
1330
|
-
import
|
|
1524
|
+
import chalk10 from "chalk";
|
|
1331
1525
|
function registerAgentCommands(program2) {
|
|
1332
1526
|
const agent = program2.command("agent").description("Manage Blink Claw agents in your workspace").addHelpText("after", `
|
|
1333
1527
|
Examples:
|
|
@@ -1347,7 +1541,7 @@ Agent ID resolution for all agent/secrets commands (priority: high \u2192 low):
|
|
|
1347
1541
|
if (isJsonMode()) return printJson(result);
|
|
1348
1542
|
const agents = Array.isArray(result) ? result : result?.agents ?? [];
|
|
1349
1543
|
if (!agents.length) {
|
|
1350
|
-
console.log(
|
|
1544
|
+
console.log(chalk10.dim("No agents found. Deploy one at blink.new/claw"));
|
|
1351
1545
|
return;
|
|
1352
1546
|
}
|
|
1353
1547
|
const table = createTable(["ID", "Name", "Status", "Size", "Model"]);
|
|
@@ -1370,9 +1564,9 @@ After setting, secrets commands use this agent automatically:
|
|
|
1370
1564
|
process.stdout.write(`export BLINK_ACTIVE_AGENT=${agentId}
|
|
1371
1565
|
`);
|
|
1372
1566
|
} else {
|
|
1373
|
-
console.log(
|
|
1374
|
-
console.log(
|
|
1375
|
-
console.log(
|
|
1567
|
+
console.log(chalk10.bold("Active agent: ") + agentId);
|
|
1568
|
+
console.log(chalk10.dim(`Run: export BLINK_ACTIVE_AGENT=${agentId}`));
|
|
1569
|
+
console.log(chalk10.dim(`Or: eval $(blink agent use ${agentId} --export)`));
|
|
1376
1570
|
}
|
|
1377
1571
|
});
|
|
1378
1572
|
agent.command("status [agent_id]").description("Show details for an agent").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID or BLINK_ACTIVE_AGENT)").addHelpText("after", `
|
|
@@ -1388,18 +1582,18 @@ Examples:
|
|
|
1388
1582
|
);
|
|
1389
1583
|
if (isJsonMode()) return printJson(result);
|
|
1390
1584
|
const a = result?.agent ?? result;
|
|
1391
|
-
console.log(
|
|
1392
|
-
console.log(
|
|
1393
|
-
console.log(
|
|
1394
|
-
console.log(
|
|
1395
|
-
console.log(
|
|
1396
|
-
if (a.fly_app_name) console.log(
|
|
1585
|
+
console.log(chalk10.bold("ID ") + a.id);
|
|
1586
|
+
console.log(chalk10.bold("Name ") + a.name);
|
|
1587
|
+
console.log(chalk10.bold("Status ") + a.status);
|
|
1588
|
+
console.log(chalk10.bold("Model ") + (a.model ?? "-"));
|
|
1589
|
+
console.log(chalk10.bold("Machine ") + (a.machine_size ?? "-"));
|
|
1590
|
+
if (a.fly_app_name) console.log(chalk10.bold("Fly App ") + a.fly_app_name);
|
|
1397
1591
|
});
|
|
1398
1592
|
}
|
|
1399
1593
|
|
|
1400
1594
|
// src/commands/secrets.ts
|
|
1401
1595
|
init_agent();
|
|
1402
|
-
import
|
|
1596
|
+
import chalk11 from "chalk";
|
|
1403
1597
|
function registerSecretsCommands(program2) {
|
|
1404
1598
|
const secrets = program2.command("secrets").description("Manage encrypted secrets vault for a Claw agent").addHelpText("after", `
|
|
1405
1599
|
Secrets are encrypted key-value pairs stored in the agent's vault.
|
|
@@ -1435,11 +1629,11 @@ Examples:
|
|
|
1435
1629
|
if (isJsonMode()) return printJson(result);
|
|
1436
1630
|
const keys = result?.secrets?.map((s) => s.key) ?? result?.keys ?? [];
|
|
1437
1631
|
if (!keys.length) {
|
|
1438
|
-
console.log(
|
|
1632
|
+
console.log(chalk11.dim("(no secrets set \u2014 use `blink secrets set KEY value`)"));
|
|
1439
1633
|
return;
|
|
1440
1634
|
}
|
|
1441
|
-
for (const k of keys) console.log(
|
|
1442
|
-
console.log(
|
|
1635
|
+
for (const k of keys) console.log(chalk11.bold(k));
|
|
1636
|
+
console.log(chalk11.dim(`
|
|
1443
1637
|
${keys.length} secret${keys.length === 1 ? "" : "s"} (values hidden)`));
|
|
1444
1638
|
});
|
|
1445
1639
|
secrets.command("set <key> <value>").description("Add or update a secret (stored encrypted, value never shown again)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID or BLINK_ACTIVE_AGENT)").addHelpText("after", `
|
|
@@ -1462,8 +1656,8 @@ After setting, the secret is available as $KEY_NAME in agent shell commands.
|
|
|
1462
1656
|
);
|
|
1463
1657
|
const normalised = key.toUpperCase();
|
|
1464
1658
|
if (!isJsonMode()) {
|
|
1465
|
-
console.log(
|
|
1466
|
-
console.log(
|
|
1659
|
+
console.log(chalk11.green("\u2713") + ` ${normalised} saved`);
|
|
1660
|
+
console.log(chalk11.dim(" Value hidden. Use $" + normalised + " in shell commands."));
|
|
1467
1661
|
} else {
|
|
1468
1662
|
printJson({ status: "ok", key: normalised, agent_id: agentId });
|
|
1469
1663
|
}
|
|
@@ -1539,15 +1733,25 @@ Web & Data:
|
|
|
1539
1733
|
$ blink search "latest AI news" --count 10
|
|
1540
1734
|
$ blink fetch https://api.github.com/users/octocat
|
|
1541
1735
|
$ blink fetch https://api.example.com --method POST --body '{"key":"val"}'
|
|
1736
|
+
$ blink scrape https://example.com --text Clean text (no HTML)
|
|
1737
|
+
$ blink scrape https://example.com --extract "prices" AI-extract specific data
|
|
1542
1738
|
|
|
1543
1739
|
Realtime / RAG / Notify:
|
|
1544
1740
|
$ blink realtime publish updates '{"type":"refresh"}'
|
|
1545
1741
|
$ blink rag search "how does billing work" --ai
|
|
1546
1742
|
$ blink notify email user@example.com "Subject" "Body"
|
|
1547
1743
|
|
|
1548
|
-
Connectors (Notion, Slack,
|
|
1549
|
-
$ blink connector
|
|
1550
|
-
$ blink connector
|
|
1744
|
+
Connectors (38 OAuth providers \u2014 GitHub, Notion, Slack, Stripe, Shopify, Jira, Linear, and more):
|
|
1745
|
+
$ blink connector providers List all 38 providers
|
|
1746
|
+
$ blink connector status Show all connected accounts
|
|
1747
|
+
$ blink connector status github Check a specific provider
|
|
1748
|
+
$ blink connector exec github /user/repos GET Call any REST endpoint
|
|
1749
|
+
$ blink connector exec notion /search POST '{"query":"notes"}' Notion search
|
|
1750
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"hi"}'
|
|
1751
|
+
$ blink connector exec stripe /customers GET '{"limit":5}'
|
|
1752
|
+
$ blink connector exec jira /search GET '{"jql":"assignee=currentUser()"}'
|
|
1753
|
+
$ blink connector exec linear '{ viewer { id name } }' POST GraphQL (Linear)
|
|
1754
|
+
Connect accounts at: blink.new/settings?tab=connectors
|
|
1551
1755
|
|
|
1552
1756
|
Agents (Claw \u2014 zero config on Fly machines, BLINK_AGENT_ID is already set):
|
|
1553
1757
|
$ blink agent list List all agents in workspace
|
|
@@ -1602,10 +1806,10 @@ After setting:
|
|
|
1602
1806
|
process.stdout.write(`export BLINK_ACTIVE_PROJECT=${projectId}
|
|
1603
1807
|
`);
|
|
1604
1808
|
} else {
|
|
1605
|
-
const { default:
|
|
1606
|
-
console.log(
|
|
1607
|
-
console.log(
|
|
1608
|
-
console.log(
|
|
1809
|
+
const { default: chalk12 } = await import("chalk");
|
|
1810
|
+
console.log(chalk12.bold("Active project: ") + projectId);
|
|
1811
|
+
console.log(chalk12.dim(`Run: export BLINK_ACTIVE_PROJECT=${projectId}`));
|
|
1812
|
+
console.log(chalk12.dim(`Or: eval $(blink use ${projectId} --export)`));
|
|
1609
1813
|
}
|
|
1610
1814
|
});
|
|
1611
1815
|
program.action(async () => {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -70,15 +70,25 @@ Web & Data:
|
|
|
70
70
|
$ blink search "latest AI news" --count 10
|
|
71
71
|
$ blink fetch https://api.github.com/users/octocat
|
|
72
72
|
$ blink fetch https://api.example.com --method POST --body '{"key":"val"}'
|
|
73
|
+
$ blink scrape https://example.com --text Clean text (no HTML)
|
|
74
|
+
$ blink scrape https://example.com --extract "prices" AI-extract specific data
|
|
73
75
|
|
|
74
76
|
Realtime / RAG / Notify:
|
|
75
77
|
$ blink realtime publish updates '{"type":"refresh"}'
|
|
76
78
|
$ blink rag search "how does billing work" --ai
|
|
77
79
|
$ blink notify email user@example.com "Subject" "Body"
|
|
78
80
|
|
|
79
|
-
Connectors (Notion, Slack,
|
|
80
|
-
$ blink connector
|
|
81
|
-
$ blink connector
|
|
81
|
+
Connectors (38 OAuth providers — GitHub, Notion, Slack, Stripe, Shopify, Jira, Linear, and more):
|
|
82
|
+
$ blink connector providers List all 38 providers
|
|
83
|
+
$ blink connector status Show all connected accounts
|
|
84
|
+
$ blink connector status github Check a specific provider
|
|
85
|
+
$ blink connector exec github /user/repos GET Call any REST endpoint
|
|
86
|
+
$ blink connector exec notion /search POST '{"query":"notes"}' Notion search
|
|
87
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"hi"}'
|
|
88
|
+
$ blink connector exec stripe /customers GET '{"limit":5}'
|
|
89
|
+
$ blink connector exec jira /search GET '{"jql":"assignee=currentUser()"}'
|
|
90
|
+
$ blink connector exec linear '{ viewer { id name } }' POST GraphQL (Linear)
|
|
91
|
+
Connect accounts at: blink.new/settings?tab=connectors
|
|
82
92
|
|
|
83
93
|
Agents (Claw — zero config on Fly machines, BLINK_AGENT_ID is already set):
|
|
84
94
|
$ blink agent list List all agents in workspace
|
|
@@ -1,54 +1,203 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { resourcesRequest } from '../lib/api-resources.js'
|
|
3
3
|
import { requireToken } from '../lib/auth.js'
|
|
4
|
-
import { printJson, isJsonMode, withSpinner } from '../lib/output.js'
|
|
4
|
+
import { printJson, isJsonMode, withSpinner, createTable } from '../lib/output.js'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
// All supported OAuth connector providers
|
|
8
|
+
const PROVIDERS = [
|
|
9
|
+
// Communication
|
|
10
|
+
{ key: 'slack', label: 'Slack', category: 'Communication' },
|
|
11
|
+
{ key: 'discord', label: 'Discord', category: 'Communication' },
|
|
12
|
+
// Email & Calendar
|
|
13
|
+
{ key: 'google_gmail', label: 'Gmail', category: 'Email & Calendar' },
|
|
14
|
+
{ key: 'google_calendar', label: 'Google Calendar', category: 'Email & Calendar' },
|
|
15
|
+
{ key: 'microsoft_outlook', label: 'Outlook', category: 'Email & Calendar' },
|
|
16
|
+
{ key: 'microsoft_calendar', label: 'Microsoft Calendar', category: 'Email & Calendar' },
|
|
17
|
+
// Files & Docs
|
|
18
|
+
{ key: 'google_drive', label: 'Google Drive', category: 'Files & Docs' },
|
|
19
|
+
{ key: 'google_docs', label: 'Google Docs', category: 'Files & Docs' },
|
|
20
|
+
{ key: 'google_sheets', label: 'Google Sheets', category: 'Files & Docs' },
|
|
21
|
+
{ key: 'google_slides', label: 'Google Slides', category: 'Files & Docs' },
|
|
22
|
+
{ key: 'microsoft_onedrive', label: 'OneDrive', category: 'Files & Docs' },
|
|
23
|
+
{ key: 'notion', label: 'Notion', category: 'Files & Docs' },
|
|
24
|
+
// Dev Tools
|
|
25
|
+
{ key: 'github', label: 'GitHub', category: 'Dev Tools' },
|
|
26
|
+
{ key: 'linear', label: 'Linear', category: 'Dev Tools' },
|
|
27
|
+
{ key: 'jira', label: 'Jira', category: 'Dev Tools' },
|
|
28
|
+
{ key: 'vercel', label: 'Vercel', category: 'Dev Tools' },
|
|
29
|
+
{ key: 'figma', label: 'Figma', category: 'Dev Tools' },
|
|
30
|
+
// CRM & Sales
|
|
31
|
+
{ key: 'hubspot', label: 'HubSpot', category: 'CRM & Sales' },
|
|
32
|
+
{ key: 'salesforce', label: 'Salesforce', category: 'CRM & Sales' },
|
|
33
|
+
{ key: 'pipedrive', label: 'Pipedrive', category: 'CRM & Sales' },
|
|
34
|
+
{ key: 'attio', label: 'Attio', category: 'CRM & Sales' },
|
|
35
|
+
// Marketing
|
|
36
|
+
{ key: 'mailchimp', label: 'Mailchimp', category: 'Marketing' },
|
|
37
|
+
{ key: 'typeform', label: 'Typeform', category: 'Marketing' },
|
|
38
|
+
// Data & Productivity
|
|
39
|
+
{ key: 'airtable', label: 'Airtable', category: 'Data & Productivity' },
|
|
40
|
+
{ key: 'asana', label: 'Asana', category: 'Data & Productivity' },
|
|
41
|
+
{ key: 'calendly', label: 'Calendly', category: 'Data & Productivity' },
|
|
42
|
+
// Social & Creator
|
|
43
|
+
{ key: 'twitter', label: 'X (Twitter)', category: 'Social & Creator' },
|
|
44
|
+
{ key: 'instagram', label: 'Instagram', category: 'Social & Creator' },
|
|
45
|
+
{ key: 'tiktok', label: 'TikTok', category: 'Social & Creator' },
|
|
46
|
+
{ key: 'youtube', label: 'YouTube', category: 'Social & Creator' },
|
|
47
|
+
{ key: 'linkedin', label: 'LinkedIn', category: 'Social & Creator' },
|
|
48
|
+
{ key: 'reddit', label: 'Reddit', category: 'Social & Creator' },
|
|
49
|
+
{ key: 'loom', label: 'Loom', category: 'Social & Creator' },
|
|
50
|
+
// Meetings
|
|
51
|
+
{ key: 'zoom', label: 'Zoom', category: 'Meetings' },
|
|
52
|
+
{ key: 'microsoft_teams', label: 'Microsoft Teams', category: 'Meetings' },
|
|
53
|
+
// Commerce
|
|
54
|
+
{ key: 'stripe', label: 'Stripe', category: 'Commerce' },
|
|
55
|
+
{ key: 'shopify', label: 'Shopify', category: 'Commerce' },
|
|
56
|
+
{ key: 'etsy', label: 'Etsy', category: 'Commerce' },
|
|
57
|
+
] as const
|
|
5
58
|
|
|
6
59
|
export function registerConnectorCommands(program: Command) {
|
|
7
60
|
const connector = program.command('connector')
|
|
8
|
-
.description('Execute actions on connected OAuth apps (Notion, Slack,
|
|
61
|
+
.description('Execute actions on connected OAuth apps (GitHub, Notion, Slack, Reddit, Google, and 30+ more)')
|
|
9
62
|
.addHelpText('after', `
|
|
10
|
-
Connectors are OAuth accounts linked to your workspace
|
|
11
|
-
Once linked, use blink connector exec to call their APIs without managing tokens yourself.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
$ blink connector exec
|
|
19
|
-
$ blink connector exec
|
|
20
|
-
$ blink connector exec
|
|
21
|
-
$ blink connector exec
|
|
22
|
-
$ blink connector exec
|
|
23
|
-
$ blink connector exec
|
|
24
|
-
|
|
25
|
-
|
|
63
|
+
Connectors are OAuth accounts linked to your workspace at blink.new → Settings → Integrations.
|
|
64
|
+
Once linked, use \`blink connector exec\` to call their APIs without managing tokens yourself.
|
|
65
|
+
|
|
66
|
+
Connect accounts at: https://blink.new/settings?tab=connectors
|
|
67
|
+
|
|
68
|
+
Run \`blink connector providers\` to see all 39 supported providers.
|
|
69
|
+
|
|
70
|
+
Quick examples:
|
|
71
|
+
$ blink connector exec github /user/repos GET
|
|
72
|
+
$ blink connector exec notion /search POST '{"query":"meeting notes"}'
|
|
73
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"Hello"}'
|
|
74
|
+
$ blink connector exec google_calendar /calendars/primary/events GET
|
|
75
|
+
$ blink connector exec jira /issue POST '{"fields":{"project":{"key":"PROJ"},"summary":"Bug"}}'
|
|
76
|
+
$ blink connector exec stripe /customers GET '{"limit":10}'
|
|
77
|
+
$ blink connector exec shopify /orders.json GET
|
|
78
|
+
$ blink connector exec linear '{ viewer { id name } }' POST # GraphQL
|
|
79
|
+
|
|
80
|
+
Use --method GET for reads, POST for writes (default: POST).
|
|
81
|
+
Use --account <id> if you have multiple linked accounts for the same provider.
|
|
26
82
|
`)
|
|
27
83
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
.option('--
|
|
84
|
+
// blink connector providers
|
|
85
|
+
connector.command('providers')
|
|
86
|
+
.description('List all supported OAuth connector providers')
|
|
87
|
+
.option('--category <cat>', 'Filter by category')
|
|
88
|
+
.action((opts) => {
|
|
89
|
+
const filtered = opts.category
|
|
90
|
+
? PROVIDERS.filter(p => p.category.toLowerCase().includes(opts.category.toLowerCase()))
|
|
91
|
+
: PROVIDERS
|
|
92
|
+
|
|
93
|
+
if (isJsonMode()) return printJson(filtered)
|
|
94
|
+
|
|
95
|
+
const categories = [...new Set(filtered.map(p => p.category))]
|
|
96
|
+
for (const cat of categories) {
|
|
97
|
+
console.log(chalk.bold.cyan(`\n${cat}`))
|
|
98
|
+
const catProviders = filtered.filter(p => p.category === cat)
|
|
99
|
+
for (const p of catProviders) {
|
|
100
|
+
console.log(` ${chalk.green(p.key.padEnd(22))} ${chalk.dim(p.label)}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log(chalk.dim(`\n${filtered.length} providers total. Connect at blink.new/settings?tab=connectors`))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// blink connector status [provider]
|
|
107
|
+
connector.command('status [provider]')
|
|
108
|
+
.description('Check connection status for a provider (or all providers)')
|
|
109
|
+
.option('--account <id>', 'Check a specific account ID')
|
|
110
|
+
.action(async (provider: string | undefined, opts) => {
|
|
111
|
+
requireToken()
|
|
112
|
+
if (provider) {
|
|
113
|
+
const result = await withSpinner(`Checking ${provider} status...`, () =>
|
|
114
|
+
resourcesRequest(`/v1/connectors/${provider}/status`, {
|
|
115
|
+
method: 'GET',
|
|
116
|
+
...(opts.account ? { query: { account_id: opts.account } } : {})
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
if (isJsonMode()) return printJson(result)
|
|
120
|
+
const d = result?.data ?? result
|
|
121
|
+
if (d?.connected) {
|
|
122
|
+
console.log(chalk.green('✓ Connected'))
|
|
123
|
+
if (d.account_id) console.log(chalk.dim(` Account: ${d.account_id}`))
|
|
124
|
+
if (d.metadata?.email) console.log(chalk.dim(` Email: ${d.metadata.email}`))
|
|
125
|
+
if (d.metadata?.name) console.log(chalk.dim(` Name: ${d.metadata.name}`))
|
|
126
|
+
} else {
|
|
127
|
+
console.log(chalk.red('✗ Not connected'))
|
|
128
|
+
console.log(chalk.dim(` Connect at blink.new/settings?tab=connectors`))
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// List all connected providers
|
|
132
|
+
const result = await withSpinner('Checking all connectors...', () =>
|
|
133
|
+
resourcesRequest('/v1/connectors/linked', { method: 'GET' })
|
|
134
|
+
)
|
|
135
|
+
if (isJsonMode()) return printJson(result)
|
|
136
|
+
const connected: Array<{ provider: string; accounts: Array<{ account_id: string; label: string }> }> =
|
|
137
|
+
result?.data ?? []
|
|
138
|
+
if (!connected.length) {
|
|
139
|
+
console.log(chalk.dim('No connectors linked. Connect at blink.new/settings?tab=connectors'))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
const table = createTable(['Provider', 'Accounts'])
|
|
143
|
+
for (const c of connected) {
|
|
144
|
+
const accounts = c.accounts.map(a => a.label || a.account_id).join(', ')
|
|
145
|
+
table.push([chalk.green(c.provider), accounts])
|
|
146
|
+
}
|
|
147
|
+
console.log(table.toString())
|
|
148
|
+
console.log(chalk.dim(`\n${connected.length} provider(s) connected`))
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// blink connector exec <provider> <endpoint> [params]
|
|
153
|
+
connector.command('exec <provider> <endpoint> [params]')
|
|
154
|
+
.description('Execute a call on a connected OAuth provider')
|
|
155
|
+
.option('--account <id>', 'Specific account ID (if you have multiple accounts)')
|
|
156
|
+
.option('--method <method>', 'HTTP method: GET | POST | PUT | PATCH | DELETE (default: POST)', 'POST')
|
|
32
157
|
.addHelpText('after', `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
$ blink connector exec
|
|
37
|
-
$ blink connector exec
|
|
38
|
-
$ blink connector exec
|
|
39
|
-
$ blink connector exec
|
|
40
|
-
$ blink connector exec
|
|
158
|
+
<endpoint> is the API path relative to the provider's base URL, OR a GraphQL query string for Linear.
|
|
159
|
+
|
|
160
|
+
Examples (REST):
|
|
161
|
+
$ blink connector exec github /user/repos GET
|
|
162
|
+
$ blink connector exec github /repos/owner/repo/issues POST '{"title":"Bug","body":"..."}'
|
|
163
|
+
$ blink connector exec notion /search POST '{"query":"meeting notes"}'
|
|
164
|
+
$ blink connector exec slack /chat.postMessage POST '{"channel":"C123","text":"Hello"}'
|
|
165
|
+
$ blink connector exec google_drive /files GET '{"q":"mimeType=\\'application/pdf\\'"}'
|
|
166
|
+
$ blink connector exec jira /search GET '{"jql":"assignee=currentUser()","maxResults":20}'
|
|
167
|
+
$ blink connector exec stripe /customers GET '{"limit":5}'
|
|
168
|
+
$ blink connector exec shopify /orders.json GET
|
|
169
|
+
$ blink connector exec zoom /users/me/meetings GET
|
|
170
|
+
$ blink connector exec github /user/repos GET --json | jq '.[].full_name'
|
|
171
|
+
|
|
172
|
+
Examples (GraphQL — Linear only):
|
|
173
|
+
$ blink connector exec linear '{ viewer { id name email } }' POST
|
|
174
|
+
$ blink connector exec linear 'mutation { issueCreate(input: { title: "Bug", teamId: "xxx" }) { issue { id } } }' POST
|
|
175
|
+
|
|
176
|
+
Multiple accounts:
|
|
177
|
+
$ blink connector exec github /user/repos GET --account acct_xxx
|
|
178
|
+
|
|
179
|
+
Provider base URLs used:
|
|
180
|
+
github https://api.github.com/
|
|
181
|
+
stripe https://api.stripe.com/v1/
|
|
182
|
+
notion https://api.notion.com/v1/
|
|
183
|
+
slack https://slack.com/api/
|
|
184
|
+
google_gmail https://gmail.googleapis.com/gmail/v1/
|
|
185
|
+
google_drive https://www.googleapis.com/drive/v3/
|
|
186
|
+
google_calendar https://www.googleapis.com/calendar/v3/
|
|
187
|
+
jira https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/
|
|
188
|
+
shopify https://{shop}.myshopify.com/admin/api/2024-10/
|
|
189
|
+
... run \`blink connector providers\` for all 38 providers
|
|
41
190
|
`)
|
|
42
|
-
.action(async (provider: string,
|
|
191
|
+
.action(async (provider: string, endpoint: string, paramsArg: string | undefined, opts) => {
|
|
43
192
|
requireToken()
|
|
44
193
|
let params: Record<string, unknown> = {}
|
|
45
194
|
if (paramsArg) {
|
|
46
195
|
try { params = JSON.parse(paramsArg) } catch { params = {} }
|
|
47
196
|
}
|
|
48
|
-
const result = await withSpinner(
|
|
197
|
+
const result = await withSpinner(`${opts.method} ${provider}${endpoint}...`, () =>
|
|
49
198
|
resourcesRequest(`/v1/connectors/${provider}/execute`, {
|
|
50
199
|
body: {
|
|
51
|
-
method:
|
|
200
|
+
method: endpoint,
|
|
52
201
|
http_method: opts.method,
|
|
53
202
|
params,
|
|
54
203
|
...(opts.account ? { account_id: opts.account } : {}),
|
|
@@ -56,6 +205,6 @@ Examples:
|
|
|
56
205
|
})
|
|
57
206
|
)
|
|
58
207
|
if (isJsonMode()) return printJson(result)
|
|
59
|
-
console.log(JSON.stringify(result, null, 2))
|
|
208
|
+
console.log(JSON.stringify(result?.data ?? result, null, 2))
|
|
60
209
|
})
|
|
61
210
|
}
|
package/src/commands/web.ts
CHANGED
|
@@ -40,6 +40,80 @@ Examples:
|
|
|
40
40
|
else console.log(JSON.stringify(result, null, 2))
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
program.command('scrape <url>')
|
|
44
|
+
.description('Scrape a webpage — returns text content, optionally AI-extracted data')
|
|
45
|
+
.option('--extract <instructions>', 'What to extract using AI (e.g. "all prices and product names")')
|
|
46
|
+
.option('--text', 'Strip HTML tags and return clean readable text')
|
|
47
|
+
.addHelpText('after', `
|
|
48
|
+
No project key needed — scrape runs through the Blink proxy using your workspace key.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
$ blink scrape https://example.com Get full page content
|
|
52
|
+
$ blink scrape https://example.com --text Clean text only (no HTML)
|
|
53
|
+
$ blink scrape https://example.com --extract "all prices" AI-extract specific data
|
|
54
|
+
$ blink scrape https://news.ycombinator.com --extract "top 10 story titles and URLs" --json
|
|
55
|
+
`)
|
|
56
|
+
.action(async (url, opts) => {
|
|
57
|
+
requireToken()
|
|
58
|
+
|
|
59
|
+
const response = await withSpinner('Fetching page...', () =>
|
|
60
|
+
resourcesRequest('/api/v1/fetch', { body: { url } })
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Extract the response body — fetch returns { status, contentType, body } or raw string
|
|
64
|
+
let content: string
|
|
65
|
+
if (typeof response === 'string') {
|
|
66
|
+
content = response
|
|
67
|
+
} else if (typeof response?.body === 'string') {
|
|
68
|
+
content = response.body
|
|
69
|
+
} else {
|
|
70
|
+
content = JSON.stringify(response)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (opts.extract) {
|
|
74
|
+
// AI extraction — uses workspace key, no project needed
|
|
75
|
+
const extraction = await withSpinner(`Extracting: ${opts.extract}...`, () =>
|
|
76
|
+
resourcesRequest('/api/v1/ai/chat/completions', {
|
|
77
|
+
body: {
|
|
78
|
+
model: 'google/gemini-3-flash',
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
role: 'system',
|
|
82
|
+
content: 'You are a web scraping assistant. Extract exactly the requested information from the webpage content provided. Return only the extracted data, formatted clearly. No commentary.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: `URL: ${url}\n\nWebpage content:\n${content.slice(0, 50000)}\n\nExtract: ${opts.extract}`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
stream: false,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
const extracted = extraction?.choices?.[0]?.message?.content ?? ''
|
|
94
|
+
if (isJsonMode()) return printJson({ url, extracted, extract_instructions: opts.extract })
|
|
95
|
+
console.log(extracted)
|
|
96
|
+
} else if (opts.text) {
|
|
97
|
+
// Strip HTML and return clean text
|
|
98
|
+
const text = content
|
|
99
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ' ')
|
|
100
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ' ')
|
|
101
|
+
.replace(/<[^>]+>/g, ' ')
|
|
102
|
+
.replace(/ /g, ' ')
|
|
103
|
+
.replace(/&/g, '&')
|
|
104
|
+
.replace(/</g, '<')
|
|
105
|
+
.replace(/>/g, '>')
|
|
106
|
+
.replace(/"/g, '"')
|
|
107
|
+
.replace(/\s+/g, ' ')
|
|
108
|
+
.trim()
|
|
109
|
+
if (isJsonMode()) return printJson({ url, text })
|
|
110
|
+
console.log(text)
|
|
111
|
+
} else {
|
|
112
|
+
if (isJsonMode()) return printJson({ url, content })
|
|
113
|
+
console.log(content)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
43
117
|
program.command('search <query>')
|
|
44
118
|
.description('Web search — returns titles, URLs, and snippets')
|
|
45
119
|
.option('--count <n>', 'Number of results to return (max 20)', '5')
|