@blinkdotnew/cli 0.3.0 → 0.3.2

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
@@ -886,9 +886,21 @@ Examples:
886
886
  for (const f of files) table.push([f.name, f.size ? `${(f.size / 1024).toFixed(1)} KB` : "-"]);
887
887
  console.log(table.toString());
888
888
  });
889
- 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) => {
890
890
  requireToken();
891
- 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
+ }
892
904
  const result = await withSpinner(
893
905
  "Downloading...",
894
906
  () => resourcesRequest(`/api/storage/${projectId}/download`, { body: { path: storagePath } })
@@ -898,9 +910,17 @@ Examples:
898
910
  else writeFileSync4(outFile, Buffer.from(result?.data ?? "", "base64"));
899
911
  if (!isJsonMode()) console.log("Saved to " + outFile);
900
912
  });
901
- 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) => {
902
914
  requireToken();
903
- 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
+ }
904
924
  await withSpinner(
905
925
  "Deleting...",
906
926
  () => resourcesRequest(`/api/storage/${projectId}/remove`, { method: "DELETE", body: { path: storagePath } })
@@ -908,9 +928,17 @@ Examples:
908
928
  if (!isJsonMode()) console.log("Deleted: " + storagePath);
909
929
  else printJson({ status: "ok", path: storagePath });
910
930
  });
911
- 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) => {
912
932
  requireToken();
913
- 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
+ }
914
942
  const result = await resourcesRequest(`/api/storage/${projectId}/public-url`, { body: { path: storagePath } });
915
943
  if (isJsonMode()) return printJson(result);
916
944
  console.log(result?.url ?? result);
@@ -1320,30 +1348,40 @@ async function getPersonId(agentId) {
1320
1348
  return id;
1321
1349
  }
1322
1350
  function registerLinkedInCommands(program2) {
1323
- const li = program2.command("linkedin").description("LinkedIn connector \u2014 publish posts and manage your profile").addHelpText("after", `
1351
+ const li = program2.command("linkedin").description("LinkedIn connector \u2014 publish posts, comment, react, and manage your profile").addHelpText("after", `
1324
1352
  LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
1325
1353
  Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
1326
1354
 
1327
1355
  What works today (w_member_social scope):
1328
1356
  \u2705 blink linkedin me Show your LinkedIn profile
1329
- \u2705 blink linkedin post "Excited to announce..." Publish a text post
1357
+ \u2705 blink linkedin post "text" Publish a text post
1330
1358
  \u2705 blink linkedin delete <postUrn> Delete one of your posts
1359
+ \u2705 blink linkedin like <postUrn> Like a post
1360
+ \u2705 blink linkedin unlike <postUrn> Unlike a post
1361
+ \u2705 blink linkedin comment <postUrn> "text" Add a comment
1362
+
1363
+ For feed reading, search, and profiles \u2014 use scripts/lk.py (requires cookies):
1364
+ python3 scripts/lk.py feed -n 10 Browse your LinkedIn feed
1365
+ python3 scripts/lk.py search "query" Search people
1366
+ python3 scripts/lk.py profile <id> View a profile
1367
+ python3 scripts/lk.py messages Check messages
1368
+ See SKILL.md for cookie setup instructions.
1331
1369
 
1332
- Not yet available (requires LinkedIn Partner API approval):
1333
- \u2717 Reading posts, comments, likes (needs r_member_social \u2014 restricted scope)
1334
- \u2717 Adding comments (needs Community Management API)
1370
+ Post URNs: use the URN returned by "blink linkedin post --json" or extract from
1371
+ a LinkedIn post URL: https://linkedin.com/feed/update/urn:li:activity:123...
1335
1372
 
1336
1373
  Examples:
1337
1374
  $ blink linkedin me
1338
- $ blink linkedin post "Our product just launched!"
1339
- $ blink linkedin post "Team update" --visibility CONNECTIONS
1340
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
1375
+ $ blink linkedin post "Our product just launched! \u{1F680}"
1376
+ $ blink linkedin like "urn:li:share:1234567890"
1377
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
1378
+ $ blink linkedin delete "urn:li:share:1234567890"
1341
1379
  `);
1342
1380
  li.command("me").description("Show your LinkedIn profile (name, ID, email)").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1343
1381
  Examples:
1344
1382
  $ blink linkedin me
1345
1383
  $ blink linkedin me --json
1346
- $ blink linkedin me --agent clw_xxx
1384
+ $ PERSON_ID=$(blink linkedin me --json | jq -r .sub)
1347
1385
  `).action(async (opts) => {
1348
1386
  requireToken();
1349
1387
  const agentId = requireAgentId(opts.agent);
@@ -1360,10 +1398,12 @@ Examples:
1360
1398
  if (data?.email) console.log(` ${chalk7.dim("Email:")} ${data.email}`);
1361
1399
  });
1362
1400
  li.command("post <text>").description("Publish a text post to LinkedIn").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").option("--visibility <vis>", "PUBLIC | CONNECTIONS (default: PUBLIC)", "PUBLIC").addHelpText("after", `
1401
+ Returns the post URN which you can pass to like/comment/delete.
1402
+
1363
1403
  Examples:
1364
1404
  $ blink linkedin post "Excited to share our latest update!"
1365
- $ blink linkedin post "Internal team update" --visibility CONNECTIONS
1366
- $ blink linkedin post "Hello LinkedIn" --json
1405
+ $ blink linkedin post "Internal update" --visibility CONNECTIONS
1406
+ $ POST_URN=$(blink linkedin post "Hello LinkedIn" --json | jq -r .id)
1367
1407
  `).action(async (text, opts) => {
1368
1408
  requireToken();
1369
1409
  const agentId = requireAgentId(opts.agent);
@@ -1395,46 +1435,227 @@ Examples:
1395
1435
  console.log(chalk7.green("\u2713 Post published"));
1396
1436
  if (data?.id) console.log(chalk7.dim(` URN: ${data.id}`));
1397
1437
  });
1398
- 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", `
1399
- Returns an asset URN to use when composing a post with media via blink connector exec.
1400
-
1438
+ li.command("delete <postUrn>").description("Delete one of your LinkedIn posts").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1401
1439
  Examples:
1402
- $ blink linkedin upload-media https://example.com/photo.jpg
1403
- $ blink linkedin upload-media https://example.com/demo.mp4 --type video
1404
- $ ASSET_URN=$(blink linkedin upload-media https://example.com/photo.jpg --json | python3 -c "import json,sys; print(json.load(sys.stdin)['asset_urn'])")
1405
- `).action(async (mediaUrl, opts) => {
1440
+ $ blink linkedin delete "urn:li:share:1234567890"
1441
+ `).action(async (postUrn, opts) => {
1406
1442
  requireToken();
1407
1443
  const agentId = requireAgentId(opts.agent);
1408
- const result = await withSpinner(
1409
- `Uploading ${opts.type}...`,
1410
- () => resourcesRequest("/v1/connectors/linkedin/upload-media", {
1411
- body: { media_url: mediaUrl, media_type: opts.type },
1412
- headers: { "x-blink-agent-id": agentId }
1413
- })
1444
+ const encoded = encodeURIComponent(postUrn);
1445
+ await withSpinner(
1446
+ "Deleting post...",
1447
+ () => liExec(`ugcPosts/${encoded}`, "DELETE", {}, agentId)
1414
1448
  );
1415
- if (isJsonMode()) return printJson(result?.data ?? result);
1416
- const assetUrn = result?.data?.asset_urn;
1417
- if (assetUrn) {
1418
- console.log(chalk7.green("\u2713 Upload complete"));
1419
- console.log(chalk7.dim(` Asset URN: ${assetUrn}`));
1420
- }
1449
+ if (isJsonMode()) return printJson({ deleted: true, urn: postUrn });
1450
+ console.log(chalk7.green("\u2713 Post deleted"));
1421
1451
  });
1422
- li.command("delete <postUrn>").description("Delete one of your LinkedIn posts").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1423
- <postUrn> is the LinkedIn post URN returned when the post was created.
1424
- e.g. urn:li:ugcPost:1234567890
1452
+ li.command("like <postUrn>").description("Like a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1453
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
1454
+ LinkedIn feed URLs contain the activity URN: linkedin.com/feed/update/urn:li:activity:123
1425
1455
 
1426
1456
  Examples:
1427
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
1457
+ $ blink linkedin like "urn:li:share:1234567890"
1458
+ $ blink linkedin like "urn:li:activity:1234567890"
1428
1459
  `).action(async (postUrn, opts) => {
1429
1460
  requireToken();
1430
1461
  const agentId = requireAgentId(opts.agent);
1462
+ const personId = await withSpinner(
1463
+ "Resolving LinkedIn identity...",
1464
+ () => getPersonId(agentId)
1465
+ );
1431
1466
  const encoded = encodeURIComponent(postUrn);
1467
+ const data = await withSpinner(
1468
+ "Liking post...",
1469
+ () => liExec(`v2/socialActions/${encoded}/likes`, "POST", {
1470
+ actor: `urn:li:person:${personId}`
1471
+ }, agentId)
1472
+ );
1473
+ if (isJsonMode()) return printJson(data);
1474
+ console.log(chalk7.green("\u2713 Post liked"));
1475
+ if (data?.["$URN"]) console.log(chalk7.dim(` Like URN: ${data["$URN"]}`));
1476
+ });
1477
+ li.command("unlike <postUrn>").description("Unlike a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1478
+ Examples:
1479
+ $ blink linkedin unlike "urn:li:share:1234567890"
1480
+ `).action(async (postUrn, opts) => {
1481
+ requireToken();
1482
+ const agentId = requireAgentId(opts.agent);
1483
+ const personId = await withSpinner(
1484
+ "Resolving LinkedIn identity...",
1485
+ () => getPersonId(agentId)
1486
+ );
1487
+ const encodedPost = encodeURIComponent(postUrn);
1488
+ const encodedPerson = encodeURIComponent(`urn:li:person:${personId}`);
1432
1489
  await withSpinner(
1433
- "Deleting post...",
1434
- () => liExec(`ugcPosts/${encoded}`, "DELETE", {}, agentId)
1490
+ "Unliking post...",
1491
+ () => liExec(`v2/socialActions/${encodedPost}/likes/${encodedPerson}`, "DELETE", {}, agentId)
1435
1492
  );
1436
- if (isJsonMode()) return printJson({ deleted: true, urn: postUrn });
1437
- console.log(chalk7.green("\u2713 Post deleted"));
1493
+ if (isJsonMode()) return printJson({ unliked: true });
1494
+ console.log(chalk7.green("\u2713 Post unliked"));
1495
+ });
1496
+ li.command("comment <postUrn> <text>").description("Add a comment to a LinkedIn post").option("--agent <id>", "Agent ID (defaults to BLINK_AGENT_ID)").addHelpText("after", `
1497
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
1498
+
1499
+ Examples:
1500
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
1501
+ $ blink linkedin comment "urn:li:activity:1234567890" "Thanks for sharing"
1502
+ `).action(async (postUrn, text, opts) => {
1503
+ requireToken();
1504
+ const agentId = requireAgentId(opts.agent);
1505
+ const personId = await withSpinner(
1506
+ "Resolving LinkedIn identity...",
1507
+ () => getPersonId(agentId)
1508
+ );
1509
+ const encoded = encodeURIComponent(postUrn);
1510
+ const data = await withSpinner(
1511
+ "Adding comment...",
1512
+ () => liExec(`v2/socialActions/${encoded}/comments`, "POST", {
1513
+ actor: `urn:li:person:${personId}`,
1514
+ message: { text }
1515
+ }, agentId)
1516
+ );
1517
+ if (isJsonMode()) return printJson(data);
1518
+ console.log(chalk7.green("\u2713 Comment added"));
1519
+ if (data?.id) console.log(chalk7.dim(` Comment ID: ${data.id}`));
1520
+ });
1521
+ }
1522
+
1523
+ // src/commands/phone.ts
1524
+ function formatPhone(num) {
1525
+ const m = num.match(/^\+1(\d{3})(\d{3})(\d{4})$/);
1526
+ return m ? `+1 (${m[1]}) ${m[2]}-${m[3]}` : num;
1527
+ }
1528
+ function statusDot(status) {
1529
+ return status === "active" ? "\u25CF" : status === "grace" ? "\u26A1" : "\u25CB";
1530
+ }
1531
+ function nextCharge(lastCharged) {
1532
+ if (!lastCharged) return "";
1533
+ const d = new Date(new Date(lastCharged).getTime() + 30 * 864e5);
1534
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
1535
+ }
1536
+ function printRecords(records) {
1537
+ if (!records.length) {
1538
+ console.log("No phone numbers. Run: blink phone buy");
1539
+ return;
1540
+ }
1541
+ records.forEach((r, i) => {
1542
+ const label = r.label ? ` ${r.label}` : "";
1543
+ const primary = i === 0 && r.status === "active" ? " \u2605 primary" : "";
1544
+ const next = nextCharge(r.last_charged_at);
1545
+ const charge = next ? ` Next charge ${next}` : "";
1546
+ console.log(`${statusDot(r.status)} ${formatPhone(r.phone_number)}${label}${primary}`);
1547
+ console.log(` ${r.id} ${r.country}${r.area_code ? ` \xB7 ${r.area_code}` : ""}${charge}`);
1548
+ });
1549
+ }
1550
+ function registerPhoneCommands(program2) {
1551
+ const phone = program2.command("phone").description("Manage workspace phone numbers for AI calling").addHelpText("after", `
1552
+ Commands:
1553
+ list List all workspace phone numbers
1554
+ buy Provision a new phone number
1555
+ label Update a number's label
1556
+ release Release (cancel) a phone number
1557
+
1558
+ Examples:
1559
+ $ blink phone list
1560
+ $ blink phone buy --label "Sales" --country US --area-code 415
1561
+ $ blink phone buy --label "Support"
1562
+ $ blink phone label wpn_abc123 "Support line"
1563
+ $ blink phone release wpn_abc123
1564
+
1565
+ Numbers cost 10 credits/month each. First charge is immediate on buy.
1566
+ Primary number (oldest active) is used by default for \`blink ai call\`.
1567
+ Use \`blink ai call --from +1XXXXXXXXXX\` to specify a different number.
1568
+ `);
1569
+ phone.action(async () => {
1570
+ requireToken();
1571
+ const records = await withSpinner(
1572
+ "Fetching phone numbers...",
1573
+ () => resourcesRequest("/api/v1/phone-numbers")
1574
+ );
1575
+ if (isJsonMode()) return printJson(records);
1576
+ printRecords(records);
1577
+ });
1578
+ phone.command("list").description("List all workspace phone numbers").addHelpText("after", `
1579
+ Examples:
1580
+ $ blink phone list
1581
+ $ blink phone list --json
1582
+ `).action(async () => {
1583
+ requireToken();
1584
+ const records = await withSpinner(
1585
+ "Fetching phone numbers...",
1586
+ () => resourcesRequest("/api/v1/phone-numbers")
1587
+ );
1588
+ if (isJsonMode()) return printJson(records);
1589
+ printRecords(records);
1590
+ });
1591
+ 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", `
1592
+ Examples:
1593
+ $ blink phone buy
1594
+ $ blink phone buy --label "Sales" --area-code 415
1595
+ $ blink phone buy --label "UK Support" --country GB
1596
+ $ blink phone buy --json | jq '.phone_number'
1597
+
1598
+ 10 credits charged immediately, then monthly on anniversary.
1599
+ `).action(async (opts) => {
1600
+ requireToken();
1601
+ const result = await withSpinner(
1602
+ "Provisioning phone number...",
1603
+ () => resourcesRequest("/api/v1/phone-numbers", {
1604
+ body: {
1605
+ label: opts.label || void 0,
1606
+ country: opts.country,
1607
+ area_code: opts.areaCode || void 0
1608
+ }
1609
+ })
1610
+ );
1611
+ if (isJsonMode()) return printJson(result);
1612
+ console.log(`\u2713 Phone number provisioned`);
1613
+ console.log(` Number ${formatPhone(result.phone_number)}`);
1614
+ if (result.label) console.log(` Label ${result.label}`);
1615
+ console.log(` Country ${result.country}${result.area_code ? ` \xB7 Area ${result.area_code}` : ""}`);
1616
+ console.log(` ID ${result.id}`);
1617
+ console.log(` Billing 10 credits/month`);
1618
+ console.log();
1619
+ console.log(`Use it: blink ai call "+1..." "Your task" --from "${result.phone_number}"`);
1620
+ });
1621
+ phone.command("label <id> <label>").description("Set or update the label for a phone number").addHelpText("after", `
1622
+ Examples:
1623
+ $ blink phone label wpn_abc123 "Sales"
1624
+ $ blink phone label wpn_abc123 "" Clear label
1625
+ `).action(async (id, label) => {
1626
+ requireToken();
1627
+ const result = await withSpinner(
1628
+ "Updating label...",
1629
+ () => resourcesRequest(`/api/v1/phone-numbers/${id}`, {
1630
+ method: "PATCH",
1631
+ body: { label }
1632
+ })
1633
+ );
1634
+ if (isJsonMode()) return printJson(result);
1635
+ console.log(`\u2713 Label updated: ${formatPhone(result.phone_number)} \u2192 "${result.label ?? ""}"`);
1636
+ });
1637
+ phone.command("release <id>").description("Release a phone number (permanent)").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", `
1638
+ Examples:
1639
+ $ blink phone release wpn_abc123
1640
+ $ blink phone release wpn_abc123 --yes
1641
+
1642
+ The number is permanently returned to the carrier pool. This action cannot be undone.
1643
+ `).action(async (id, opts) => {
1644
+ requireToken();
1645
+ if (!opts.yes && !isJsonMode()) {
1646
+ const { confirm } = await import("@clack/prompts");
1647
+ const yes = await confirm({ message: `Release ${id}? This cannot be undone.` });
1648
+ if (!yes) {
1649
+ console.log("Cancelled.");
1650
+ return;
1651
+ }
1652
+ }
1653
+ await withSpinner(
1654
+ "Releasing phone number...",
1655
+ () => resourcesRequest(`/api/v1/phone-numbers/${id}`, { method: "DELETE" })
1656
+ );
1657
+ if (isJsonMode()) return printJson({ success: true, id });
1658
+ console.log(`\u2713 Phone number ${id} released`);
1438
1659
  });
1439
1660
  }
1440
1661
 
@@ -1981,6 +2202,12 @@ Realtime / RAG / Notify:
1981
2202
  $ blink rag search "how does billing work" --ai
1982
2203
  $ blink notify email user@example.com "Subject" "Body"
1983
2204
 
2205
+ Phone Numbers (10 credits/month per number):
2206
+ $ blink phone list List all workspace phone numbers
2207
+ $ blink phone buy --label Sales Buy a new number (US, UK, CA, AU)
2208
+ $ blink phone label <id> Sales Update label
2209
+ $ blink phone release <id> Release a number
2210
+
1984
2211
  Connectors (38 OAuth providers \u2014 GitHub, Notion, Slack, Stripe, Shopify, Jira, Linear, and more):
1985
2212
  $ blink connector providers List all 38 providers
1986
2213
  $ blink connector status Show all connected accounts
@@ -2042,6 +2269,7 @@ registerRagCommands(program);
2042
2269
  registerNotifyCommands(program);
2043
2270
  registerConnectorCommands(program);
2044
2271
  registerLinkedInCommands(program);
2272
+ registerPhoneCommands(program);
2045
2273
  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", `
2046
2274
  Examples:
2047
2275
  $ 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.3.0",
3
+ "version": "0.3.2",
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)')
@@ -30,25 +30,35 @@ async function getPersonId(agentId: string): Promise<string> {
30
30
 
31
31
  export function registerLinkedInCommands(program: Command) {
32
32
  const li = program.command('linkedin')
33
- .description('LinkedIn connector — publish posts and manage your profile')
33
+ .description('LinkedIn connector — publish posts, comment, react, and manage your profile')
34
34
  .addHelpText('after', `
35
35
  LinkedIn must be linked to your agent via the Integrations tab at blink.new/claw.
36
36
  Agent ID defaults to BLINK_AGENT_ID (automatically set on Claw Fly machines).
37
37
 
38
38
  What works today (w_member_social scope):
39
39
  ✅ blink linkedin me Show your LinkedIn profile
40
- ✅ blink linkedin post "Excited to announce..." Publish a text post
40
+ ✅ blink linkedin post "text" Publish a text post
41
41
  ✅ blink linkedin delete <postUrn> Delete one of your posts
42
+ ✅ blink linkedin like <postUrn> Like a post
43
+ ✅ blink linkedin unlike <postUrn> Unlike a post
44
+ ✅ blink linkedin comment <postUrn> "text" Add a comment
42
45
 
43
- Not yet available (requires LinkedIn Partner API approval):
44
- ✗ Reading posts, comments, likes (needs r_member_social restricted scope)
45
- ✗ Adding comments (needs Community Management API)
46
+ For feed reading, search, and profiles use scripts/lk.py (requires cookies):
47
+ python3 scripts/lk.py feed -n 10 Browse your LinkedIn feed
48
+ python3 scripts/lk.py search "query" Search people
49
+ python3 scripts/lk.py profile <id> View a profile
50
+ python3 scripts/lk.py messages Check messages
51
+ See SKILL.md for cookie setup instructions.
52
+
53
+ Post URNs: use the URN returned by "blink linkedin post --json" or extract from
54
+ a LinkedIn post URL: https://linkedin.com/feed/update/urn:li:activity:123...
46
55
 
47
56
  Examples:
48
57
  $ blink linkedin me
49
- $ blink linkedin post "Our product just launched!"
50
- $ blink linkedin post "Team update" --visibility CONNECTIONS
51
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
58
+ $ blink linkedin post "Our product just launched! 🚀"
59
+ $ blink linkedin like "urn:li:share:1234567890"
60
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
61
+ $ blink linkedin delete "urn:li:share:1234567890"
52
62
  `)
53
63
 
54
64
  // blink linkedin me
@@ -59,7 +69,7 @@ Examples:
59
69
  Examples:
60
70
  $ blink linkedin me
61
71
  $ blink linkedin me --json
62
- $ blink linkedin me --agent clw_xxx
72
+ $ PERSON_ID=$(blink linkedin me --json | jq -r .sub)
63
73
  `)
64
74
  .action(async (opts) => {
65
75
  requireToken()
@@ -82,10 +92,12 @@ Examples:
82
92
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
83
93
  .option('--visibility <vis>', 'PUBLIC | CONNECTIONS (default: PUBLIC)', 'PUBLIC')
84
94
  .addHelpText('after', `
95
+ Returns the post URN which you can pass to like/comment/delete.
96
+
85
97
  Examples:
86
98
  $ blink linkedin post "Excited to share our latest update!"
87
- $ blink linkedin post "Internal team update" --visibility CONNECTIONS
88
- $ blink linkedin post "Hello LinkedIn" --json
99
+ $ blink linkedin post "Internal update" --visibility CONNECTIONS
100
+ $ POST_URN=$(blink linkedin post "Hello LinkedIn" --json | jq -r .id)
89
101
  `)
90
102
  .action(async (text: string, opts) => {
91
103
  requireToken()
@@ -117,55 +129,103 @@ Examples:
117
129
  if (data?.id) console.log(chalk.dim(` URN: ${data.id}`))
118
130
  })
119
131
 
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')
132
+ // blink linkedin delete <postUrn>
133
+ li.command('delete <postUrn>')
134
+ .description('Delete one of your LinkedIn posts')
124
135
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
125
136
  .addHelpText('after', `
126
- Returns an asset URN to use when composing a post with media via blink connector exec.
127
-
128
137
  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'])")
138
+ $ blink linkedin delete "urn:li:share:1234567890"
132
139
  `)
133
- .action(async (mediaUrl: string, opts) => {
140
+ .action(async (postUrn: string, opts) => {
134
141
  requireToken()
135
142
  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
- })
143
+ const encoded = encodeURIComponent(postUrn)
144
+ await withSpinner('Deleting post...', () =>
145
+ liExec(`ugcPosts/${encoded}`, 'DELETE', {}, agentId)
141
146
  )
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
- }
147
+ if (isJsonMode()) return printJson({ deleted: true, urn: postUrn })
148
+ console.log(chalk.green('✓ Post deleted'))
148
149
  })
149
150
 
150
- // blink linkedin delete <postUrn>
151
- li.command('delete <postUrn>')
152
- .description('Delete one of your LinkedIn posts')
151
+ // blink linkedin like <postUrn>
152
+ li.command('like <postUrn>')
153
+ .description('Like a LinkedIn post')
153
154
  .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
154
155
  .addHelpText('after', `
155
- <postUrn> is the LinkedIn post URN returned when the post was created.
156
- e.g. urn:li:ugcPost:1234567890
156
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
157
+ LinkedIn feed URLs contain the activity URN: linkedin.com/feed/update/urn:li:activity:123
157
158
 
158
159
  Examples:
159
- $ blink linkedin delete "urn:li:ugcPost:1234567890"
160
+ $ blink linkedin like "urn:li:share:1234567890"
161
+ $ blink linkedin like "urn:li:activity:1234567890"
160
162
  `)
161
163
  .action(async (postUrn: string, opts) => {
162
164
  requireToken()
163
165
  const agentId = requireAgentId(opts.agent)
166
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
167
+ getPersonId(agentId)
168
+ )
164
169
  const encoded = encodeURIComponent(postUrn)
165
- await withSpinner('Deleting post...', () =>
166
- liExec(`ugcPosts/${encoded}`, 'DELETE', {}, agentId)
170
+ const data = await withSpinner('Liking post...', () =>
171
+ liExec(`v2/socialActions/${encoded}/likes`, 'POST', {
172
+ actor: `urn:li:person:${personId}`,
173
+ }, agentId)
167
174
  )
168
- if (isJsonMode()) return printJson({ deleted: true, urn: postUrn })
169
- console.log(chalk.green('✓ Post deleted'))
175
+ if (isJsonMode()) return printJson(data)
176
+ console.log(chalk.green('✓ Post liked'))
177
+ if (data?.['$URN']) console.log(chalk.dim(` Like URN: ${data['$URN']}`))
178
+ })
179
+
180
+ // blink linkedin unlike <postUrn>
181
+ li.command('unlike <postUrn>')
182
+ .description('Unlike a LinkedIn post')
183
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
184
+ .addHelpText('after', `
185
+ Examples:
186
+ $ blink linkedin unlike "urn:li:share:1234567890"
187
+ `)
188
+ .action(async (postUrn: string, opts) => {
189
+ requireToken()
190
+ const agentId = requireAgentId(opts.agent)
191
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
192
+ getPersonId(agentId)
193
+ )
194
+ const encodedPost = encodeURIComponent(postUrn)
195
+ const encodedPerson = encodeURIComponent(`urn:li:person:${personId}`)
196
+ await withSpinner('Unliking post...', () =>
197
+ liExec(`v2/socialActions/${encodedPost}/likes/${encodedPerson}`, 'DELETE', {}, agentId)
198
+ )
199
+ if (isJsonMode()) return printJson({ unliked: true })
200
+ console.log(chalk.green('✓ Post unliked'))
201
+ })
202
+
203
+ // blink linkedin comment <postUrn> "text"
204
+ li.command('comment <postUrn> <text>')
205
+ .description('Add a comment to a LinkedIn post')
206
+ .option('--agent <id>', 'Agent ID (defaults to BLINK_AGENT_ID)')
207
+ .addHelpText('after', `
208
+ <postUrn> accepts urn:li:share:, urn:li:ugcPost:, or urn:li:activity: URNs.
209
+
210
+ Examples:
211
+ $ blink linkedin comment "urn:li:share:1234567890" "Great post!"
212
+ $ blink linkedin comment "urn:li:activity:1234567890" "Thanks for sharing"
213
+ `)
214
+ .action(async (postUrn: string, text: string, opts) => {
215
+ requireToken()
216
+ const agentId = requireAgentId(opts.agent)
217
+ const personId = await withSpinner('Resolving LinkedIn identity...', () =>
218
+ getPersonId(agentId)
219
+ )
220
+ const encoded = encodeURIComponent(postUrn)
221
+ const data = await withSpinner('Adding comment...', () =>
222
+ liExec(`v2/socialActions/${encoded}/comments`, 'POST', {
223
+ actor: `urn:li:person:${personId}`,
224
+ message: { text },
225
+ }, agentId)
226
+ )
227
+ if (isJsonMode()) return printJson(data)
228
+ console.log(chalk.green('✓ Comment added'))
229
+ if (data?.id) console.log(chalk.dim(` Comment ID: ${data.id}`))
170
230
  })
171
231
  }
@@ -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)