@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 +231 -18
- package/package.json +1 -1
- package/src/cli.ts +8 -0
- package/src/commands/ai.ts +4 -1
- package/src/commands/connector.ts +22 -7
- package/src/commands/linkedin.ts +30 -0
- package/src/commands/phone.ts +177 -0
- package/src/commands/storage.ts +28 -12
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.
|
|
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 <
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
`${
|
|
1317
|
+
`${httpMethod} ${provider}${endpoint}...`,
|
|
1275
1318
|
() => resourcesRequest(`/v1/connectors/${provider}/execute`, {
|
|
1276
1319
|
body: {
|
|
1277
1320
|
method: endpoint,
|
|
1278
|
-
http_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
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)')
|
package/src/commands/ai.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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:
|
|
216
|
+
http_method: httpMethod,
|
|
202
217
|
params,
|
|
203
218
|
...(opts.account ? { account_id: opts.account } : {}),
|
|
204
219
|
}
|
package/src/commands/linkedin.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/storage.ts
CHANGED
|
@@ -80,11 +80,21 @@ Examples:
|
|
|
80
80
|
console.log(table.toString())
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
storage.command('download <
|
|
84
|
-
.description('Download a file from storage')
|
|
85
|
-
.action(async (
|
|
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
|
-
|
|
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 <
|
|
98
|
-
.description('Delete a file from storage')
|
|
99
|
-
.action(async (
|
|
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
|
-
|
|
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 <
|
|
110
|
-
.description('Get public URL for a storage file')
|
|
111
|
-
.action(async (
|
|
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
|
-
|
|
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)
|