@blinkdotnew/cli 0.2.7 → 0.3.1

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/dist/cli.js CHANGED
@@ -478,7 +478,7 @@ Supported formats: mp3, wav, m4a, ogg
478
478
  if (isJsonMode()) return printJson(result);
479
479
  console.log(result?.text ?? result?.transcript ?? JSON.stringify(result));
480
480
  });
481
- ai.command("call <phone-number> <system-prompt>").description("Make an AI phone call to any number (US/International)").option("--voice <voice>", "Voice: openai:alloy | openai:nova | cartesia:sonic-english", "openai:alloy").option("--max-duration <seconds>", "Max call duration in seconds", "300").option("--no-wait", "Return call_id immediately without waiting for completion").addHelpText("after", `
481
+ ai.command("call <phone-number> <system-prompt>").description("Make an AI phone call to any number (US/International)").option("--voice <voice>", "Voice: openai:alloy | openai:nova | cartesia:sonic-english", "openai:alloy").option("--max-duration <seconds>", "Max call duration in seconds", "300").option("--no-wait", "Return call_id immediately without waiting for completion").option("--from <number>", "Phone number to call from (E.164 format, e.g. +14155551234). Uses primary number if omitted.").addHelpText("after", `
482
482
  Examples:
483
483
  $ blink ai call "+14155551234" "You are collecting a payment of $240 from John Smith. Be polite but firm."
484
484
  $ blink ai call "+14155551234" "Confirm John's appointment for tomorrow at 3pm" --voice openai:nova
@@ -496,6 +496,7 @@ Voices:
496
496
  cartesia:sonic-english Low-latency, natural
497
497
 
498
498
  Call is charged to your workspace credits after completion (~1 credit/min).
499
+ Use --from to specify which of your workspace numbers to call from.
499
500
  `).action(async (phoneNumber, systemPrompt, opts) => {
500
501
  requireToken();
501
502
  const result = await withSpinner(
@@ -505,13 +506,14 @@ Call is charged to your workspace credits after completion (~1 credit/min).
505
506
  phone_number: phoneNumber,
506
507
  system_prompt: systemPrompt,
507
508
  voice: opts.voice,
508
- max_duration_seconds: parseInt(opts.maxDuration)
509
+ max_duration_seconds: parseInt(opts.maxDuration),
510
+ ...opts.from ? { from_number: opts.from } : {}
509
511
  }
510
512
  })
511
513
  );
512
514
  if (isJsonMode()) return printJson(result);
513
515
  const callId = result?.call_id;
514
- if (opts.noWait) {
516
+ if (!opts.wait) {
515
517
  console.log(`Call initiated: ${callId}`);
516
518
  console.log(`Poll status: blink ai call-status ${callId}`);
517
519
  return;
@@ -884,9 +886,21 @@ Examples:
884
886
  for (const f of files) table.push([f.name, f.size ? `${(f.size / 1024).toFixed(1)} KB` : "-"]);
885
887
  console.log(table.toString());
886
888
  });
887
- storage.command("download <path> [output]").description("Download a file from storage").action(async (storagePath, output) => {
889
+ storage.command("download <arg1> [arg2] [output]").description("Download a file from storage \u2014 blink storage download <path> [output] OR blink storage download <proj> <path> [output]").action(async (arg1, arg2, output) => {
888
890
  requireToken();
889
- const projectId = requireProjectId();
891
+ let projectId;
892
+ let storagePath;
893
+ if (arg2 !== void 0 && !arg1.startsWith("proj_")) {
894
+ projectId = requireProjectId();
895
+ storagePath = arg1;
896
+ output = arg2;
897
+ } else if (arg2 !== void 0) {
898
+ projectId = requireProjectId(arg1);
899
+ storagePath = arg2;
900
+ } else {
901
+ projectId = requireProjectId();
902
+ storagePath = arg1;
903
+ }
890
904
  const result = await withSpinner(
891
905
  "Downloading...",
892
906
  () => resourcesRequest(`/api/storage/${projectId}/download`, { body: { path: storagePath } })
@@ -896,9 +910,17 @@ Examples:
896
910
  else writeFileSync4(outFile, Buffer.from(result?.data ?? "", "base64"));
897
911
  if (!isJsonMode()) console.log("Saved to " + outFile);
898
912
  });
899
- storage.command("delete <path>").description("Delete a file from storage").action(async (storagePath) => {
913
+ storage.command("delete <arg1> [arg2]").description("Delete a file from storage \u2014 blink storage delete <path> OR blink storage delete <proj> <path>").action(async (arg1, arg2) => {
900
914
  requireToken();
901
- const projectId = requireProjectId();
915
+ let projectId;
916
+ let storagePath;
917
+ if (arg2 !== void 0) {
918
+ projectId = requireProjectId(arg1);
919
+ storagePath = arg2;
920
+ } else {
921
+ projectId = requireProjectId();
922
+ storagePath = arg1;
923
+ }
902
924
  await withSpinner(
903
925
  "Deleting...",
904
926
  () => resourcesRequest(`/api/storage/${projectId}/remove`, { method: "DELETE", body: { path: storagePath } })
@@ -906,9 +928,17 @@ Examples:
906
928
  if (!isJsonMode()) console.log("Deleted: " + storagePath);
907
929
  else printJson({ status: "ok", path: storagePath });
908
930
  });
909
- storage.command("url <path>").description("Get public URL for a storage file").action(async (storagePath) => {
931
+ storage.command("url <arg1> [arg2]").description("Get public URL for a storage file \u2014 blink storage url <path> OR blink storage url <proj> <path>").action(async (arg1, arg2) => {
910
932
  requireToken();
911
- const projectId = requireProjectId();
933
+ let projectId;
934
+ let storagePath;
935
+ if (arg2 !== void 0) {
936
+ projectId = requireProjectId(arg1);
937
+ storagePath = arg2;
938
+ } else {
939
+ projectId = requireProjectId();
940
+ storagePath = arg1;
941
+ }
912
942
  const result = await resourcesRequest(`/api/storage/${projectId}/public-url`, { body: { path: storagePath } });
913
943
  if (isJsonMode()) return printJson(result);
914
944
  console.log(result?.url ?? result);
@@ -1227,7 +1257,7 @@ ${filtered.length} providers total. Connect at blink.new/settings?tab=connectors
1227
1257
  ${connected.length} provider(s) connected`));
1228
1258
  }
1229
1259
  });
1230
- 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", `
1260
+ connector.command("exec <provider> <endpoint> [method-or-params] [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", `
1231
1261
  <endpoint> is the API path relative to the provider's base URL, OR a GraphQL query string for Linear.
1232
1262
 
1233
1263
  Examples (REST):
@@ -1260,22 +1290,35 @@ Provider base URLs used:
1260
1290
  jira https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/
1261
1291
  shopify https://{shop}.myshopify.com/admin/api/2024-10/
1262
1292
  ... run \`blink connector providers\` for all 38 providers
1263
- `).action(async (provider, endpoint, paramsArg, opts) => {
1293
+ `).action(async (provider, endpoint, arg1, arg2, opts) => {
1264
1294
  requireToken();
1295
+ const HTTP_METHODS = /^(GET|POST|PUT|PATCH|DELETE|HEAD)$/i;
1296
+ let httpMethod = opts.method;
1265
1297
  let params = {};
1266
- if (paramsArg) {
1267
- try {
1268
- params = JSON.parse(paramsArg);
1269
- } catch {
1270
- params = {};
1298
+ if (arg1) {
1299
+ if (HTTP_METHODS.test(arg1)) {
1300
+ httpMethod = arg1.toUpperCase();
1301
+ if (arg2) {
1302
+ try {
1303
+ params = JSON.parse(arg2);
1304
+ } catch {
1305
+ params = {};
1306
+ }
1307
+ }
1308
+ } else {
1309
+ try {
1310
+ params = JSON.parse(arg1);
1311
+ } catch {
1312
+ params = {};
1313
+ }
1271
1314
  }
1272
1315
  }
1273
1316
  const result = await withSpinner(
1274
- `${opts.method} ${provider}${endpoint}...`,
1317
+ `${httpMethod} ${provider}${endpoint}...`,
1275
1318
  () => resourcesRequest(`/v1/connectors/${provider}/execute`, {
1276
1319
  body: {
1277
1320
  method: endpoint,
1278
- http_method: opts.method,
1321
+ http_method: httpMethod,
1279
1322
  params,
1280
1323
  ...opts.account ? { account_id: opts.account } : {}
1281
1324
  }
@@ -1380,6 +1423,30 @@ Examples:
1380
1423
  console.log(chalk7.green("\u2713 Post published"));
1381
1424
  if (data?.id) console.log(chalk7.dim(` URN: ${data.id}`));
1382
1425
  });
1426
+ li.command("upload-media <media-url>").description("Upload an image or video to LinkedIn storage, returns asset URN for use in posts").option("--type <type>", "Media type: image | video (default: image)", "image").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1427
+ Returns an asset URN to use when composing a post with media via blink connector exec.
1428
+
1429
+ Examples:
1430
+ $ blink linkedin upload-media https://example.com/photo.jpg
1431
+ $ blink linkedin upload-media https://example.com/demo.mp4 --type video
1432
+ $ ASSET_URN=$(blink linkedin upload-media https://example.com/photo.jpg --json | python3 -c "import json,sys; print(json.load(sys.stdin)['asset_urn'])")
1433
+ `).action(async (mediaUrl, opts) => {
1434
+ requireToken();
1435
+ const agentId = requireAgentId(opts.agent);
1436
+ const result = await withSpinner(
1437
+ `Uploading ${opts.type}...`,
1438
+ () => resourcesRequest("/v1/connectors/linkedin/upload-media", {
1439
+ body: { media_url: mediaUrl, media_type: opts.type },
1440
+ headers: { "x-blink-agent-id": agentId }
1441
+ })
1442
+ );
1443
+ if (isJsonMode()) return printJson(result?.data ?? result);
1444
+ const assetUrn = result?.data?.asset_urn;
1445
+ if (assetUrn) {
1446
+ console.log(chalk7.green("\u2713 Upload complete"));
1447
+ console.log(chalk7.dim(` Asset URN: ${assetUrn}`));
1448
+ }
1449
+ });
1383
1450
  li.command("delete <postUrn>").description("Delete one of your LinkedIn posts").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1384
1451
  <postUrn> is the LinkedIn post URN returned when the post was created.
1385
1452
  e.g. urn:li:ugcPost:1234567890
@@ -1399,6 +1466,145 @@ Examples:
1399
1466
  });
1400
1467
  }
1401
1468
 
1469
+ // src/commands/phone.ts
1470
+ function formatPhone(num) {
1471
+ const m = num.match(/^\+1(\d{3})(\d{3})(\d{4})$/);
1472
+ return m ? `+1 (${m[1]}) ${m[2]}-${m[3]}` : num;
1473
+ }
1474
+ function statusDot(status) {
1475
+ return status === "active" ? "\u25CF" : status === "grace" ? "\u26A1" : "\u25CB";
1476
+ }
1477
+ function nextCharge(lastCharged) {
1478
+ if (!lastCharged) return "";
1479
+ const d = new Date(new Date(lastCharged).getTime() + 30 * 864e5);
1480
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
1481
+ }
1482
+ function printRecords(records) {
1483
+ if (!records.length) {
1484
+ console.log("No phone numbers. Run: blink phone buy");
1485
+ return;
1486
+ }
1487
+ records.forEach((r, i) => {
1488
+ const label = r.label ? ` ${r.label}` : "";
1489
+ const primary = i === 0 && r.status === "active" ? " \u2605 primary" : "";
1490
+ const next = nextCharge(r.last_charged_at);
1491
+ const charge = next ? ` Next charge ${next}` : "";
1492
+ console.log(`${statusDot(r.status)} ${formatPhone(r.phone_number)}${label}${primary}`);
1493
+ console.log(` ${r.id} ${r.country}${r.area_code ? ` \xB7 ${r.area_code}` : ""}${charge}`);
1494
+ });
1495
+ }
1496
+ function registerPhoneCommands(program2) {
1497
+ const phone = program2.command("phone").description("Manage workspace phone numbers for AI calling").addHelpText("after", `
1498
+ Commands:
1499
+ list List all workspace phone numbers
1500
+ buy Provision a new phone number
1501
+ label Update a number's label
1502
+ release Release (cancel) a phone number
1503
+
1504
+ Examples:
1505
+ $ blink phone list
1506
+ $ blink phone buy --label "Sales" --country US --area-code 415
1507
+ $ blink phone buy --label "Support"
1508
+ $ blink phone label wpn_abc123 "Support line"
1509
+ $ blink phone release wpn_abc123
1510
+
1511
+ Numbers cost 10 credits/month each. First charge is immediate on buy.
1512
+ Primary number (oldest active) is used by default for \`blink ai call\`.
1513
+ Use \`blink ai call --from +1XXXXXXXXXX\` to specify a different number.
1514
+ `);
1515
+ phone.action(async () => {
1516
+ requireToken();
1517
+ const records = await withSpinner(
1518
+ "Fetching phone numbers...",
1519
+ () => resourcesRequest("/api/v1/phone-numbers")
1520
+ );
1521
+ if (isJsonMode()) return printJson(records);
1522
+ printRecords(records);
1523
+ });
1524
+ phone.command("list").description("List all workspace phone numbers").addHelpText("after", `
1525
+ Examples:
1526
+ $ blink phone list
1527
+ $ blink phone list --json
1528
+ `).action(async () => {
1529
+ requireToken();
1530
+ const records = await withSpinner(
1531
+ "Fetching phone numbers...",
1532
+ () => resourcesRequest("/api/v1/phone-numbers")
1533
+ );
1534
+ if (isJsonMode()) return printJson(records);
1535
+ printRecords(records);
1536
+ });
1537
+ phone.command("buy").description("Provision a new phone number (10 credits/month)").option("--label <label>", 'Label for this number, e.g. "Sales" or "Support"').option("--country <code>", "Country code: US, GB, CA, AU", "US").option("--area-code <code>", "Preferred area code (US/CA only, e.g. 415)").addHelpText("after", `
1538
+ Examples:
1539
+ $ blink phone buy
1540
+ $ blink phone buy --label "Sales" --area-code 415
1541
+ $ blink phone buy --label "UK Support" --country GB
1542
+ $ blink phone buy --json | jq '.phone_number'
1543
+
1544
+ 10 credits charged immediately, then monthly on anniversary.
1545
+ `).action(async (opts) => {
1546
+ requireToken();
1547
+ const result = await withSpinner(
1548
+ "Provisioning phone number...",
1549
+ () => resourcesRequest("/api/v1/phone-numbers", {
1550
+ body: {
1551
+ label: opts.label || void 0,
1552
+ country: opts.country,
1553
+ area_code: opts.areaCode || void 0
1554
+ }
1555
+ })
1556
+ );
1557
+ if (isJsonMode()) return printJson(result);
1558
+ console.log(`\u2713 Phone number provisioned`);
1559
+ console.log(` Number ${formatPhone(result.phone_number)}`);
1560
+ if (result.label) console.log(` Label ${result.label}`);
1561
+ console.log(` Country ${result.country}${result.area_code ? ` \xB7 Area ${result.area_code}` : ""}`);
1562
+ console.log(` ID ${result.id}`);
1563
+ console.log(` Billing 10 credits/month`);
1564
+ console.log();
1565
+ console.log(`Use it: blink ai call "+1..." "Your task" --from "${result.phone_number}"`);
1566
+ });
1567
+ phone.command("label <id> <label>").description("Set or update the label for a phone number").addHelpText("after", `
1568
+ Examples:
1569
+ $ blink phone label wpn_abc123 "Sales"
1570
+ $ blink phone label wpn_abc123 "" Clear label
1571
+ `).action(async (id, label) => {
1572
+ requireToken();
1573
+ const result = await withSpinner(
1574
+ "Updating label...",
1575
+ () => resourcesRequest(`/api/v1/phone-numbers/${id}`, {
1576
+ method: "PATCH",
1577
+ body: { label }
1578
+ })
1579
+ );
1580
+ if (isJsonMode()) return printJson(result);
1581
+ console.log(`\u2713 Label updated: ${formatPhone(result.phone_number)} \u2192 "${result.label ?? ""}"`);
1582
+ });
1583
+ phone.command("release <id>").description("Release a phone number (permanent)").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", `
1584
+ Examples:
1585
+ $ blink phone release wpn_abc123
1586
+ $ blink phone release wpn_abc123 --yes
1587
+
1588
+ The number is permanently returned to the carrier pool. This action cannot be undone.
1589
+ `).action(async (id, opts) => {
1590
+ requireToken();
1591
+ if (!opts.yes && !isJsonMode()) {
1592
+ const { confirm } = await import("@clack/prompts");
1593
+ const yes = await confirm({ message: `Release ${id}? This cannot be undone.` });
1594
+ if (!yes) {
1595
+ console.log("Cancelled.");
1596
+ return;
1597
+ }
1598
+ }
1599
+ await withSpinner(
1600
+ "Releasing phone number...",
1601
+ () => resourcesRequest(`/api/v1/phone-numbers/${id}`, { method: "DELETE" })
1602
+ );
1603
+ if (isJsonMode()) return printJson({ success: true, id });
1604
+ console.log(`\u2713 Phone number ${id} released`);
1605
+ });
1606
+ }
1607
+
1402
1608
  // src/lib/api-app.ts
1403
1609
  var BASE_URL2 = process.env.BLINK_APP_URL ?? "https://blink.new";
1404
1610
  async function appRequest(path, opts = {}) {
@@ -1942,6 +2148,12 @@ Realtime / RAG / Notify:
1942
2148
  $ blink rag search "how does billing work" --ai
1943
2149
  $ blink notify email user@example.com "Subject" "Body"
1944
2150
 
2151
+ Phone Numbers (10 credits/month per number):
2152
+ $ blink phone list List all workspace phone numbers
2153
+ $ blink phone buy --label Sales Buy a new number (US, UK, CA, AU)
2154
+ $ blink phone label <id> Sales Update label
2155
+ $ blink phone release <id> Release a number
2156
+
1945
2157
  Connectors (38 OAuth providers \u2014 GitHub, Notion, Slack, Stripe, Shopify, Jira, Linear, and more):
1946
2158
  $ blink connector providers List all 38 providers
1947
2159
  $ blink connector status Show all connected accounts
@@ -2003,6 +2215,7 @@ registerRagCommands(program);
2003
2215
  registerNotifyCommands(program);
2004
2216
  registerConnectorCommands(program);
2005
2217
  registerLinkedInCommands(program);
2218
+ registerPhoneCommands(program);
2006
2219
  program.command("use <project_id>").description("Set active project for this shell session (alternative to blink link)").option("--export", "Output a shell export statement \u2014 use with eval to actually set it").addHelpText("after", `
2007
2220
  Examples:
2008
2221
  $ blink use proj_xxx Shows the export command to run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/cli",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
4
4
  "description": "Blink platform CLI — deploy apps, manage databases, generate AI content",
5
5
  "bin": {
6
6
  "blink": "dist/cli.js"
package/src/cli.ts CHANGED
@@ -10,6 +10,7 @@ import { registerRagCommands } from './commands/rag.js'
10
10
  import { registerNotifyCommands } from './commands/notify.js'
11
11
  import { registerConnectorCommands } from './commands/connector.js'
12
12
  import { registerLinkedInCommands } from './commands/linkedin.js'
13
+ import { registerPhoneCommands } from './commands/phone.js'
13
14
  import { registerDeployCommands } from './commands/deploy.js'
14
15
  import { registerProjectCommands } from './commands/project.js'
15
16
  import { registerAuthCommands } from './commands/auth.js'
@@ -79,6 +80,12 @@ Realtime / RAG / Notify:
79
80
  $ blink rag search "how does billing work" --ai
80
81
  $ blink notify email user@example.com "Subject" "Body"
81
82
 
83
+ Phone Numbers (10 credits/month per number):
84
+ $ blink phone list List all workspace phone numbers
85
+ $ blink phone buy --label Sales Buy a new number (US, UK, CA, AU)
86
+ $ blink phone label <id> Sales Update label
87
+ $ blink phone release <id> Release a number
88
+
82
89
  Connectors (38 OAuth providers — GitHub, Notion, Slack, Stripe, Shopify, Jira, Linear, and more):
83
90
  $ blink connector providers List all 38 providers
84
91
  $ blink connector status Show all connected accounts
@@ -142,6 +149,7 @@ registerRagCommands(program)
142
149
  registerNotifyCommands(program)
143
150
  registerConnectorCommands(program)
144
151
  registerLinkedInCommands(program)
152
+ registerPhoneCommands(program)
145
153
 
146
154
  program.command('use <project_id>')
147
155
  .description('Set active project for this shell session (alternative to blink link)')
@@ -290,6 +290,7 @@ Supported formats: mp3, wav, m4a, ogg
290
290
  .option('--voice <voice>', 'Voice: openai:alloy | openai:nova | cartesia:sonic-english', 'openai:alloy')
291
291
  .option('--max-duration <seconds>', 'Max call duration in seconds', '300')
292
292
  .option('--no-wait', 'Return call_id immediately without waiting for completion')
293
+ .option('--from <number>', 'Phone number to call from (E.164 format, e.g. +14155551234). Uses primary number if omitted.')
293
294
  .addHelpText('after', `
294
295
  Examples:
295
296
  $ blink ai call "+14155551234" "You are collecting a payment of $240 from John Smith. Be polite but firm."
@@ -308,6 +309,7 @@ Voices:
308
309
  cartesia:sonic-english Low-latency, natural
309
310
 
310
311
  Call is charged to your workspace credits after completion (~1 credit/min).
312
+ Use --from to specify which of your workspace numbers to call from.
311
313
  `)
312
314
  .action(async (phoneNumber, systemPrompt, opts) => {
313
315
  requireToken()
@@ -318,6 +320,7 @@ Call is charged to your workspace credits after completion (~1 credit/min).
318
320
  system_prompt: systemPrompt,
319
321
  voice: opts.voice,
320
322
  max_duration_seconds: parseInt(opts.maxDuration),
323
+ ...(opts.from ? { from_number: opts.from } : {}),
321
324
  },
322
325
  })
323
326
  )
@@ -325,7 +328,7 @@ Call is charged to your workspace credits after completion (~1 credit/min).
325
328
 
326
329
  const callId = result?.call_id as string
327
330
 
328
- if (opts.noWait) {
331
+ if (!opts.wait) { // Commander --no-wait sets opts.wait=false (not opts.noWait)
329
332
  console.log(`Call initiated: ${callId}`)
330
333
  console.log(`Poll status: blink ai call-status ${callId}`)
331
334
  return
@@ -149,8 +149,12 @@ Use --account <id> if you have multiple linked accounts for the same provider.
149
149
  }
150
150
  })
151
151
 
152
- // blink connector exec <provider> <endpoint> [params]
153
- connector.command('exec <provider> <endpoint> [params]')
152
+ // blink connector exec <provider> <endpoint> [method-or-params] [params]
153
+ // Supports both patterns:
154
+ // blink connector exec github /user/repos GET
155
+ // blink connector exec notion /search POST '{"query":"notes"}'
156
+ // blink connector exec notion /search '{"query":"notes"}'
157
+ connector.command('exec <provider> <endpoint> [method-or-params] [params]')
154
158
  .description('Execute a call on a connected OAuth provider')
155
159
  .option('--account <id>', 'Specific account ID (if you have multiple accounts)')
156
160
  .option('--method <method>', 'HTTP method: GET | POST | PUT | PATCH | DELETE (default: POST)', 'POST')
@@ -188,17 +192,28 @@ Provider base URLs used:
188
192
  shopify https://{shop}.myshopify.com/admin/api/2024-10/
189
193
  ... run \`blink connector providers\` for all 38 providers
190
194
  `)
191
- .action(async (provider: string, endpoint: string, paramsArg: string | undefined, opts) => {
195
+ .action(async (provider: string, endpoint: string, arg1: string | undefined, arg2: string | undefined, opts) => {
192
196
  requireToken()
197
+ const HTTP_METHODS = /^(GET|POST|PUT|PATCH|DELETE|HEAD)$/i
198
+ let httpMethod = opts.method
193
199
  let params: Record<string, unknown> = {}
194
- if (paramsArg) {
195
- try { params = JSON.parse(paramsArg) } catch { params = {} }
200
+
201
+ if (arg1) {
202
+ if (HTTP_METHODS.test(arg1)) {
203
+ // Pattern: exec provider endpoint GET ['{"key":"val"}']
204
+ httpMethod = arg1.toUpperCase()
205
+ if (arg2) { try { params = JSON.parse(arg2) } catch { params = {} } }
206
+ } else {
207
+ // Pattern: exec provider endpoint '{"key":"val"}'
208
+ try { params = JSON.parse(arg1) } catch { params = {} }
209
+ }
196
210
  }
197
- const result = await withSpinner(`${opts.method} ${provider}${endpoint}...`, () =>
211
+
212
+ const result = await withSpinner(`${httpMethod} ${provider}${endpoint}...`, () =>
198
213
  resourcesRequest(`/v1/connectors/${provider}/execute`, {
199
214
  body: {
200
215
  method: endpoint,
201
- http_method: opts.method,
216
+ http_method: httpMethod,
202
217
  params,
203
218
  ...(opts.account ? { account_id: opts.account } : {}),
204
219
  }
@@ -117,6 +117,36 @@ Examples:
117
117
  if (data?.id) console.log(chalk.dim(` URN: ${data.id}`))
118
118
  })
119
119
 
120
+ // blink linkedin upload-media <media-url>
121
+ li.command('upload-media <media-url>')
122
+ .description('Upload an image or video to LinkedIn storage, returns asset URN for use in posts')
123
+ .option('--type <type>', 'Media type: image | video (default: image)', 'image')
124
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
125
+ .addHelpText('after', `
126
+ Returns an asset URN to use when composing a post with media via blink connector exec.
127
+
128
+ Examples:
129
+ $ blink linkedin upload-media https://example.com/photo.jpg
130
+ $ blink linkedin upload-media https://example.com/demo.mp4 --type video
131
+ $ ASSET_URN=$(blink linkedin upload-media https://example.com/photo.jpg --json | python3 -c "import json,sys; print(json.load(sys.stdin)['asset_urn'])")
132
+ `)
133
+ .action(async (mediaUrl: string, opts) => {
134
+ requireToken()
135
+ const agentId = requireAgentId(opts.agent)
136
+ const result = await withSpinner(`Uploading ${opts.type}...`, () =>
137
+ resourcesRequest('/v1/connectors/linkedin/upload-media', {
138
+ body: { media_url: mediaUrl, media_type: opts.type },
139
+ headers: { 'x-blink-agent-id': agentId },
140
+ })
141
+ )
142
+ if (isJsonMode()) return printJson(result?.data ?? result)
143
+ const assetUrn = result?.data?.asset_urn
144
+ if (assetUrn) {
145
+ console.log(chalk.green('✓ Upload complete'))
146
+ console.log(chalk.dim(` Asset URN: ${assetUrn}`))
147
+ }
148
+ })
149
+
120
150
  // blink linkedin delete <postUrn>
121
151
  li.command('delete <postUrn>')
122
152
  .description('Delete one of your LinkedIn posts')
@@ -0,0 +1,177 @@
1
+ import { Command } from 'commander'
2
+ import { resourcesRequest } from '../lib/api-resources.js'
3
+ import { requireToken } from '../lib/auth.js'
4
+ import { printJson, isJsonMode, withSpinner } from '../lib/output.js'
5
+
6
+ interface PhoneRecord {
7
+ id: string
8
+ phone_number: string
9
+ label?: string | null
10
+ area_code?: string | null
11
+ country: string
12
+ status: string
13
+ last_charged_at?: string | null
14
+ created_at: string
15
+ }
16
+
17
+ function formatPhone(num: string): string {
18
+ const m = num.match(/^\+1(\d{3})(\d{3})(\d{4})$/)
19
+ return m ? `+1 (${m[1]}) ${m[2]}-${m[3]}` : num
20
+ }
21
+
22
+ function statusDot(status: string): string {
23
+ return status === 'active' ? '●' : status === 'grace' ? '⚡' : '○'
24
+ }
25
+
26
+ function nextCharge(lastCharged?: string | null): string {
27
+ if (!lastCharged) return ''
28
+ const d = new Date(new Date(lastCharged).getTime() + 30 * 86_400_000)
29
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
30
+ }
31
+
32
+ function printRecords(records: PhoneRecord[]) {
33
+ if (!records.length) {
34
+ console.log('No phone numbers. Run: blink phone buy')
35
+ return
36
+ }
37
+ records.forEach((r, i) => {
38
+ const label = r.label ? ` ${r.label}` : ''
39
+ const primary = i === 0 && r.status === 'active' ? ' ★ primary' : ''
40
+ const next = nextCharge(r.last_charged_at)
41
+ const charge = next ? ` Next charge ${next}` : ''
42
+ console.log(`${statusDot(r.status)} ${formatPhone(r.phone_number)}${label}${primary}`)
43
+ console.log(` ${r.id} ${r.country}${r.area_code ? ` · ${r.area_code}` : ''}${charge}`)
44
+ })
45
+ }
46
+
47
+ export function registerPhoneCommands(program: Command) {
48
+ const phone = program.command('phone')
49
+ .description('Manage workspace phone numbers for AI calling')
50
+ .addHelpText('after', `
51
+ Commands:
52
+ list List all workspace phone numbers
53
+ buy Provision a new phone number
54
+ label Update a number's label
55
+ release Release (cancel) a phone number
56
+
57
+ Examples:
58
+ $ blink phone list
59
+ $ blink phone buy --label "Sales" --country US --area-code 415
60
+ $ blink phone buy --label "Support"
61
+ $ blink phone label wpn_abc123 "Support line"
62
+ $ blink phone release wpn_abc123
63
+
64
+ Numbers cost 10 credits/month each. First charge is immediate on buy.
65
+ Primary number (oldest active) is used by default for \`blink ai call\`.
66
+ Use \`blink ai call --from +1XXXXXXXXXX\` to specify a different number.
67
+ `)
68
+
69
+ // Default: list
70
+ phone.action(async () => {
71
+ requireToken()
72
+ const records = await withSpinner('Fetching phone numbers...', () =>
73
+ resourcesRequest('/api/v1/phone-numbers')
74
+ ) as PhoneRecord[]
75
+ if (isJsonMode()) return printJson(records)
76
+ printRecords(records)
77
+ })
78
+
79
+ // blink phone list
80
+ phone.command('list')
81
+ .description('List all workspace phone numbers')
82
+ .addHelpText('after', `
83
+ Examples:
84
+ $ blink phone list
85
+ $ blink phone list --json
86
+ `)
87
+ .action(async () => {
88
+ requireToken()
89
+ const records = await withSpinner('Fetching phone numbers...', () =>
90
+ resourcesRequest('/api/v1/phone-numbers')
91
+ ) as PhoneRecord[]
92
+ if (isJsonMode()) return printJson(records)
93
+ printRecords(records)
94
+ })
95
+
96
+ // blink phone buy
97
+ phone.command('buy')
98
+ .description('Provision a new phone number (10 credits/month)')
99
+ .option('--label <label>', 'Label for this number, e.g. "Sales" or "Support"')
100
+ .option('--country <code>', 'Country code: US, GB, CA, AU', 'US')
101
+ .option('--area-code <code>', 'Preferred area code (US/CA only, e.g. 415)')
102
+ .addHelpText('after', `
103
+ Examples:
104
+ $ blink phone buy
105
+ $ blink phone buy --label "Sales" --area-code 415
106
+ $ blink phone buy --label "UK Support" --country GB
107
+ $ blink phone buy --json | jq '.phone_number'
108
+
109
+ 10 credits charged immediately, then monthly on anniversary.
110
+ `)
111
+ .action(async (opts) => {
112
+ requireToken()
113
+ const result = await withSpinner('Provisioning phone number...', () =>
114
+ resourcesRequest('/api/v1/phone-numbers', {
115
+ body: {
116
+ label: opts.label || undefined,
117
+ country: opts.country,
118
+ area_code: opts.areaCode || undefined,
119
+ },
120
+ })
121
+ ) as PhoneRecord
122
+ if (isJsonMode()) return printJson(result)
123
+ console.log(`✓ Phone number provisioned`)
124
+ console.log(` Number ${formatPhone(result.phone_number)}`)
125
+ if (result.label) console.log(` Label ${result.label}`)
126
+ console.log(` Country ${result.country}${result.area_code ? ` · Area ${result.area_code}` : ''}`)
127
+ console.log(` ID ${result.id}`)
128
+ console.log(` Billing 10 credits/month`)
129
+ console.log()
130
+ console.log(`Use it: blink ai call "+1..." "Your task" --from "${result.phone_number}"`)
131
+ })
132
+
133
+ // blink phone label <id> <label>
134
+ phone.command('label <id> <label>')
135
+ .description('Set or update the label for a phone number')
136
+ .addHelpText('after', `
137
+ Examples:
138
+ $ blink phone label wpn_abc123 "Sales"
139
+ $ blink phone label wpn_abc123 "" Clear label
140
+ `)
141
+ .action(async (id: string, label: string) => {
142
+ requireToken()
143
+ const result = await withSpinner('Updating label...', () =>
144
+ resourcesRequest(`/api/v1/phone-numbers/${id}`, {
145
+ method: 'PATCH',
146
+ body: { label },
147
+ })
148
+ ) as PhoneRecord
149
+ if (isJsonMode()) return printJson(result)
150
+ console.log(`✓ Label updated: ${formatPhone(result.phone_number)} → "${result.label ?? ''}"`)
151
+ })
152
+
153
+ // blink phone release <id>
154
+ phone.command('release <id>')
155
+ .description('Release a phone number (permanent)')
156
+ .option('-y, --yes', 'Skip confirmation prompt')
157
+ .addHelpText('after', `
158
+ Examples:
159
+ $ blink phone release wpn_abc123
160
+ $ blink phone release wpn_abc123 --yes
161
+
162
+ The number is permanently returned to the carrier pool. This action cannot be undone.
163
+ `)
164
+ .action(async (id: string, opts) => {
165
+ requireToken()
166
+ if (!opts.yes && !isJsonMode()) {
167
+ const { confirm } = await import('@clack/prompts')
168
+ const yes = await confirm({ message: `Release ${id}? This cannot be undone.` })
169
+ if (!yes) { console.log('Cancelled.'); return }
170
+ }
171
+ await withSpinner('Releasing phone number...', () =>
172
+ resourcesRequest(`/api/v1/phone-numbers/${id}`, { method: 'DELETE' })
173
+ )
174
+ if (isJsonMode()) return printJson({ success: true, id })
175
+ console.log(`✓ Phone number ${id} released`)
176
+ })
177
+ }
@@ -80,11 +80,21 @@ Examples:
80
80
  console.log(table.toString())
81
81
  })
82
82
 
83
- storage.command('download <path> [output]')
84
- .description('Download a file from storage')
85
- .action(async (storagePath: string, output: string | undefined) => {
83
+ storage.command('download <arg1> [arg2] [output]')
84
+ .description('Download a file from storage — blink storage download <path> [output] OR blink storage download <proj> <path> [output]')
85
+ .action(async (arg1: string, arg2: string | undefined, output: string | undefined) => {
86
86
  requireToken()
87
- const projectId = requireProjectId()
87
+ let projectId: string
88
+ let storagePath: string
89
+ if (arg2 !== undefined && !arg1.startsWith('proj_')) {
90
+ // blink storage download <path> <output> (no project_id)
91
+ projectId = requireProjectId(); storagePath = arg1; output = arg2
92
+ } else if (arg2 !== undefined) {
93
+ // blink storage download proj_xxx <path> [output]
94
+ projectId = requireProjectId(arg1); storagePath = arg2
95
+ } else {
96
+ projectId = requireProjectId(); storagePath = arg1
97
+ }
88
98
  const result = await withSpinner('Downloading...', () =>
89
99
  resourcesRequest(`/api/storage/${projectId}/download`, { body: { path: storagePath } })
90
100
  )
@@ -94,11 +104,14 @@ Examples:
94
104
  if (!isJsonMode()) console.log('Saved to ' + outFile)
95
105
  })
96
106
 
97
- storage.command('delete <path>')
98
- .description('Delete a file from storage')
99
- .action(async (storagePath: string) => {
107
+ storage.command('delete <arg1> [arg2]')
108
+ .description('Delete a file from storage — blink storage delete <path> OR blink storage delete <proj> <path>')
109
+ .action(async (arg1: string, arg2: string | undefined) => {
100
110
  requireToken()
101
- const projectId = requireProjectId()
111
+ let projectId: string
112
+ let storagePath: string
113
+ if (arg2 !== undefined) { projectId = requireProjectId(arg1); storagePath = arg2 }
114
+ else { projectId = requireProjectId(); storagePath = arg1 }
102
115
  await withSpinner('Deleting...', () =>
103
116
  resourcesRequest(`/api/storage/${projectId}/remove`, { method: 'DELETE', body: { path: storagePath } })
104
117
  )
@@ -106,11 +119,14 @@ Examples:
106
119
  else printJson({ status: 'ok', path: storagePath })
107
120
  })
108
121
 
109
- storage.command('url <path>')
110
- .description('Get public URL for a storage file')
111
- .action(async (storagePath: string) => {
122
+ storage.command('url <arg1> [arg2]')
123
+ .description('Get public URL for a storage file — blink storage url <path> OR blink storage url <proj> <path>')
124
+ .action(async (arg1: string, arg2: string | undefined) => {
112
125
  requireToken()
113
- const projectId = requireProjectId()
126
+ let projectId: string
127
+ let storagePath: string
128
+ if (arg2 !== undefined) { projectId = requireProjectId(arg1); storagePath = arg2 }
129
+ else { projectId = requireProjectId(); storagePath = arg1 }
114
130
  const result = await resourcesRequest(`/api/storage/${projectId}/public-url`, { body: { path: storagePath } })
115
131
  if (isJsonMode()) return printJson(result)
116
132
  console.log(result?.url ?? result)